Skip to content

Commit 0fe59ee

Browse files
committed
2 parents bb9c881 + 88eb1ce commit 0fe59ee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+828
-331
lines changed

app-common/src/main/AndroidManifest.xml

+14-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,20 @@
3030
<activity
3131
android:exported="true"
3232
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
33-
android:label="@string/download_wizard" />
33+
android:label="@string/download_wizard">
34+
<intent-filter>
35+
<action android:name="android.intent.action.VIEW" />
36+
37+
<category android:name="android.intent.category.DEFAULT" />
38+
<category android:name="android.intent.category.BROWSABLE" />
39+
40+
<!-- Accepts URIs that begin with "lpa:" -->
41+
<!-- for example: "LPA:1$..." -->
42+
<!-- refs: https://www.iana.org/assignments/uri-schemes/prov/lpa -->
43+
<data android:scheme="lpa"/>
44+
<data android:sspPrefix="1$"/>
45+
</intent-filter>
46+
</activity>
3447

3548
<activity-alias
3649
android:exported="true"

app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt

+9-13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow
99
import kotlinx.coroutines.flow.first
1010
import kotlinx.coroutines.runBlocking
1111
import net.typeblog.lpac_jni.ApduInterface
12+
import java.util.concurrent.atomic.AtomicInteger
1213

1314
class OmapiApduInterface(
1415
private val service: SEService,
@@ -20,12 +21,8 @@ class OmapiApduInterface(
2021
}
2122

2223
private lateinit var session: Session
23-
private val channels = arrayOf<Channel?>(
24-
null,
25-
null,
26-
null,
27-
null,
28-
)
24+
private val index = AtomicInteger(0)
25+
private val channels = mutableMapOf<Int, Channel>()
2926

3027
override val valid: Boolean
3128
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@@ -44,21 +41,20 @@ class OmapiApduInterface(
4441
override fun logicalChannelOpen(aid: ByteArray): Int {
4542
val channel = session.openLogicalChannel(aid)
4643
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
47-
val index = channels.indexOf(null)
48-
check(index != -1) { "No free logical channel slots" }
49-
synchronized(channels) { channels[index] = channel }
50-
return index
44+
val handle = index.incrementAndGet()
45+
synchronized(channels) { channels[handle] = channel }
46+
return handle
5147
}
5248

5349
override fun logicalChannelClose(handle: Int) {
54-
val channel = channels.getOrNull(handle)
50+
val channel = channels[handle]
5551
check(channel != null) { "Invalid logical channel handle $handle" }
5652
if (channel.isOpen) channel.close()
57-
synchronized(channels) { channels[handle] = null }
53+
synchronized(channels) { channels.remove(handle) }
5854
}
5955

6056
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
61-
val channel = channels.getOrNull(handle)
57+
val channel = channels[handle]
6258
check(channel != null) { "Invalid logical channel handle $handle" }
6359

6460
if (runBlocking { verboseLoggingFlow.first() }) {

app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt

+11
Original file line numberDiff line numberDiff line change
@@ -495,4 +495,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
495495
preferenceRepository.notificationSwitchFlow.first()
496496
}
497497
}
498+
499+
fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
500+
launchForegroundTask(
501+
getString(R.string.task_euicc_memory_reset),
502+
getString(R.string.task_euicc_memory_reset_failure),
503+
R.drawable.ic_euicc_memory_reset
504+
) {
505+
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
506+
channel.lpa.euiccMemoryReset()
507+
}
508+
}
498509
}

app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt

+5-10
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import im.angry.openeuicc.common.R
2323
import im.angry.openeuicc.core.EuiccChannel
2424
import im.angry.openeuicc.core.EuiccChannelManager
2525
import im.angry.openeuicc.util.*
26-
import im.angry.openeuicc.vendored.getESTKmeInfo
27-
import im.angry.openeuicc.vendored.getSIMLinkVersion
2826
import kotlinx.coroutines.launch
2927
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
3028
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
@@ -104,14 +102,11 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
104102
add(Item(R.string.euicc_info_access_mode, channel.type))
105103
add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO)))
106104
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
107-
getESTKmeInfo(channel.apduInterface)?.let {
108-
add(Item(R.string.euicc_info_sku, it.skuName))
109-
add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied))
110-
add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion))
111-
add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion))
112-
}
113-
getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let {
114-
add(Item(R.string.euicc_info_sku, "9eSIM $it"))
105+
channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
106+
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
107+
vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it)) }
108+
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
109+
vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
115110
}
116111
channel.lpa.euiccInfo2.let { info ->
117112
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString()))

app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

+34-19
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ import im.angry.openeuicc.util.*
3838
import kotlinx.coroutines.Dispatchers
3939
import kotlinx.coroutines.TimeoutCancellationException
4040
import kotlinx.coroutines.flow.StateFlow
41+
import kotlinx.coroutines.flow.first
4142
import kotlinx.coroutines.flow.stateIn
4243
import kotlinx.coroutines.launch
44+
import kotlinx.coroutines.runBlocking
4345
import kotlinx.coroutines.withContext
4446

4547
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
@@ -55,6 +57,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
5557
private lateinit var fab: FloatingActionButton
5658
private lateinit var profileList: RecyclerView
5759
private var logicalSlotId: Int = -1
60+
private lateinit var eid: String
5861

5962
private val adapter = EuiccProfileAdapter()
6063

@@ -131,31 +134,42 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
131134
inflater.inflate(R.menu.fragment_euicc, menu)
132135
}
133136

134-
override fun onOptionsItemSelected(item: MenuItem): Boolean =
135-
when (item.itemId) {
136-
R.id.show_notifications -> {
137-
if (logicalSlotId != -1) {
138-
Intent(requireContext(), NotificationsActivity::class.java).apply {
139-
putExtra("logicalSlotId", logicalSlotId)
140-
startActivity(this)
141-
}
142-
}
143-
true
137+
override fun onPrepareOptionsMenu(menu: Menu) {
138+
super.onPrepareOptionsMenu(menu)
139+
menu.findItem(R.id.show_notifications).isVisible =
140+
logicalSlotId != -1
141+
menu.findItem(R.id.euicc_info).isVisible =
142+
logicalSlotId != -1
143+
menu.findItem(R.id.euicc_memory_reset).isVisible =
144+
runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
145+
}
146+
147+
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
148+
R.id.show_notifications -> {
149+
Intent(requireContext(), NotificationsActivity::class.java).apply {
150+
putExtra("logicalSlotId", logicalSlotId)
151+
startActivity(this)
144152
}
153+
true
154+
}
145155

146-
R.id.euicc_info -> {
147-
if (logicalSlotId != -1) {
148-
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
149-
putExtra("logicalSlotId", logicalSlotId)
150-
startActivity(this)
151-
}
152-
}
153-
true
156+
R.id.euicc_info -> {
157+
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
158+
putExtra("logicalSlotId", logicalSlotId)
159+
startActivity(this)
154160
}
161+
true
162+
}
155163

156-
else -> super.onOptionsItemSelected(item)
164+
R.id.euicc_memory_reset -> {
165+
EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
166+
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
167+
true
157168
}
158169

170+
else -> super.onOptionsItemSelected(item)
171+
}
172+
159173
protected open suspend fun onCreateFooterViews(
160174
parent: ViewGroup,
161175
profiles: List<LocalProfileInfo>
@@ -192,6 +206,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
192206

193207
val profiles = withEuiccChannel { channel ->
194208
logicalSlotId = channel.logicalSlotId
209+
eid = channel.lpa.eID
195210
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
196211
if (unfilteredProfileListFlow.value)
197212
channel.lpa.profiles
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package im.angry.openeuicc.ui
2+
3+
import android.graphics.Typeface
4+
import android.os.Bundle
5+
import android.text.Editable
6+
import android.util.Log
7+
import android.widget.EditText
8+
import android.widget.Toast
9+
import androidx.appcompat.app.AlertDialog
10+
import androidx.fragment.app.DialogFragment
11+
import androidx.fragment.app.Fragment
12+
import androidx.lifecycle.lifecycleScope
13+
import im.angry.openeuicc.common.R
14+
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
15+
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
16+
import im.angry.openeuicc.util.EuiccProfilesChangedListener
17+
import im.angry.openeuicc.util.ensureEuiccChannelManager
18+
import im.angry.openeuicc.util.euiccChannelManagerService
19+
import im.angry.openeuicc.util.newInstanceEuicc
20+
import im.angry.openeuicc.util.notifyEuiccProfilesChanged
21+
import im.angry.openeuicc.util.portId
22+
import im.angry.openeuicc.util.slotId
23+
import kotlinx.coroutines.flow.onStart
24+
import kotlinx.coroutines.launch
25+
26+
class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
27+
companion object {
28+
const val TAG = "EuiccMemoryResetFragment"
29+
30+
private const val FIELD_EID = "eid"
31+
32+
fun newInstance(slotId: Int, portId: Int, eid: String) =
33+
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
34+
putString(FIELD_EID, eid)
35+
}
36+
}
37+
38+
private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! }
39+
40+
private val confirmText: String by lazy {
41+
getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8))
42+
}
43+
44+
private inline val isMatched: Boolean
45+
get() = editText.text.toString() == confirmText
46+
47+
private var confirmed = false
48+
49+
private var toast: Toast? = null
50+
set(value) {
51+
toast?.cancel()
52+
field = value
53+
value?.show()
54+
}
55+
56+
private val editText by lazy {
57+
EditText(requireContext()).apply {
58+
isLongClickable = false
59+
typeface = Typeface.MONOSPACE
60+
hint = Editable.Factory.getInstance()
61+
.newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText))
62+
}
63+
}
64+
65+
private inline val alertDialog: AlertDialog
66+
get() = requireDialog() as AlertDialog
67+
68+
override fun onCreateDialog(savedInstanceState: Bundle?) =
69+
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
70+
.setTitle(R.string.euicc_memory_reset_title)
71+
.setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText))
72+
.setView(editText)
73+
// Set listener to null to prevent auto closing
74+
.setNegativeButton(android.R.string.cancel, null)
75+
.setPositiveButton(R.string.euicc_memory_reset_invoke_button, null)
76+
.create()
77+
78+
override fun onResume() {
79+
super.onResume()
80+
alertDialog.setCanceledOnTouchOutside(false)
81+
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
82+
.setOnClickListener { if (!confirmed) confirmation() }
83+
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
84+
.setOnClickListener { if (!confirmed) dismiss() }
85+
}
86+
87+
private fun confirmation() {
88+
toast?.cancel()
89+
if (!isMatched) {
90+
Log.d(TAG, buildString {
91+
appendLine("User input is mismatch:")
92+
appendLine(editText.text)
93+
appendLine(confirmText)
94+
})
95+
val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched
96+
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
97+
return
98+
}
99+
confirmed = true
100+
preventUserAction()
101+
102+
requireParentFragment().lifecycleScope.launch {
103+
ensureEuiccChannelManager()
104+
euiccChannelManagerService.waitForForegroundTask()
105+
106+
euiccChannelManagerService.launchMemoryReset(slotId, portId)
107+
.onStart {
108+
parentFragment?.notifyEuiccProfilesChanged()
109+
110+
val resId = R.string.toast_euicc_memory_reset_finitshed
111+
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
112+
113+
runCatching(::dismiss)
114+
}
115+
.waitDone()
116+
}
117+
}
118+
119+
private fun preventUserAction() {
120+
editText.isEnabled = false
121+
alertDialog.setCancelable(false)
122+
alertDialog.setCanceledOnTouchOutside(false)
123+
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
124+
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
125+
}
126+
}

app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt

+7-17
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
2020
private const val FIELD_ICCID = "iccid"
2121
private const val FIELD_NAME = "name"
2222

23-
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
24-
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
25-
instance.requireArguments().apply {
23+
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
24+
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
2625
putString(FIELD_ICCID, iccid)
2726
putString(FIELD_NAME, name)
28-
}
29-
return instance
3027
}
3128
}
3229

@@ -91,19 +88,12 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
9188
requireParentFragment().lifecycleScope.launch {
9289
ensureEuiccChannelManager()
9390
euiccChannelManagerService.waitForForegroundTask()
94-
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
95-
if (parentFragment is EuiccProfilesChangedListener) {
96-
// Trigger a refresh in the parent fragment -- it should wait until
97-
// any foreground task is completed before actually doing a refresh
98-
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
99-
}
100-
101-
try {
102-
dismiss()
103-
} catch (e: IllegalStateException) {
104-
// Ignored
91+
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
92+
.onStart {
93+
parentFragment?.notifyEuiccProfilesChanged()
94+
runCatching(::dismiss)
10595
}
106-
}.waitDone()
96+
.waitDone()
10797
}
10898
}
10999
}

0 commit comments

Comments
 (0)