diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index f918494..b3f42b5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.ApduInterface -import java.util.concurrent.atomic.AtomicInteger class OmapiApduInterface( private val service: SEService, @@ -21,8 +20,12 @@ class OmapiApduInterface( } private lateinit var session: Session - private val index = AtomicInteger(0) - private val channels = mutableMapOf() + private val channels = arrayOf( + null, + null, + null, + null, + ) override val valid: Boolean get() = service.isConnected && (this::session.isInitialized && !session.isClosed) @@ -41,20 +44,21 @@ class OmapiApduInterface( override fun logicalChannelOpen(aid: ByteArray): Int { val channel = session.openLogicalChannel(aid) check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } - val handle = index.incrementAndGet() - synchronized(channels) { channels[handle] = channel } - return handle + val index = channels.indexOf(null) + check(index != -1) { "No free logical channel slots" } + synchronized(channels) { channels[index] = channel } + return index } override fun logicalChannelClose(handle: Int) { - val channel = channels[handle] + val channel = channels.getOrNull(handle) check(channel != null) { "Invalid logical channel handle $handle" } if (channel.isOpen) channel.close() - synchronized(channels) { channels.remove(handle) } + synchronized(channels) { channels[handle] = null } } override fun transmit(handle: Int, tx: ByteArray): ByteArray { - val channel = channels[handle] + val channel = channels.getOrNull(handle) check(channel != null) { "Invalid logical channel handle $handle" } if (runBlocking { verboseLoggingFlow.first() }) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index 4e499dc..528b232 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -23,6 +23,8 @@ import im.angry.openeuicc.common.R import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* +import im.angry.openeuicc.vendored.getESTKmeInfo +import im.angry.openeuicc.vendored.getSIMLinkVersion import kotlinx.coroutines.launch import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI @@ -102,11 +104,14 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { add(Item(R.string.euicc_info_access_mode, channel.type)) add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied)) - channel.tryParseEuiccVendorInfo()?.let { vendorInfo -> - vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) } - vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it)) } - vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) } - vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) } + getESTKmeInfo(channel.apduInterface)?.let { + add(Item(R.string.euicc_info_sku, it.skuName)) + add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied)) + add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion)) + add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion)) + } + getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let { + add(Item(R.string.euicc_info_sku, "9eSIM $it")) } channel.lpa.euiccInfo2.let { info -> add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString())) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 38d1bc6..7f82f22 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -20,10 +20,13 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { private const val FIELD_ICCID = "iccid" private const val FIELD_NAME = "name" - fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) = - newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) { + fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment { + val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) + instance.requireArguments().apply { putString(FIELD_ICCID, iccid) putString(FIELD_NAME, name) + } + return instance } } @@ -88,12 +91,19 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { requireParentFragment().lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid) - .onStart { - parentFragment?.notifyEuiccProfilesChanged() - runCatching(::dismiss) + euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart { + if (parentFragment is EuiccProfilesChangedListener) { + // Trigger a refresh in the parent fragment -- it should wait until + // any foreground task is completed before actually doing a refresh + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() } - .waitDone() + + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } + }.waitDone() } } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index c588254..25c5273 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -7,7 +7,6 @@ import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.Toast -import androidx.annotation.StringRes import androidx.appcompat.widget.Toolbar import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout @@ -19,16 +18,16 @@ import net.typeblog.lpac_jni.LocalProfileAssistant class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { companion object { - private const val FIELD_ICCID = "iccid" - private const val FIELD_CURRENT_NAME = "currentName" - const val TAG = "ProfileRenameFragment" - fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) = - newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) { - putString(FIELD_ICCID, iccid) - putString(FIELD_CURRENT_NAME, currentName) + fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment { + val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) + instance.requireArguments().apply { + putString("iccid", iccid) + putString("currentName", currentName) } + return instance + } } private lateinit var toolbar: Toolbar @@ -37,14 +36,6 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment private var renaming = false - private val iccid: String by lazy { - requireArguments().getString(FIELD_ICCID)!! - } - - private val currentName: String by lazy { - requireArguments().getString(FIELD_CURRENT_NAME)!! - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -63,7 +54,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - profileRenameNewName.editText!!.setText(currentName) + profileRenameNewName.editText!!.setText(requireArguments().getString("currentName")) toolbar.apply { setTitle(R.string.rename) setNavigationOnClickListener { @@ -87,8 +78,12 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment } } - private fun showErrorAndCancel(@StringRes resId: Int) { - Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).show() + private fun showErrorAndCancel(errorStrRes: Int) { + Toast.makeText( + requireContext(), + errorStrRes, + Toast.LENGTH_LONG + ).show() renaming = false progress.visibility = View.GONE @@ -99,15 +94,17 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment progress.isIndeterminate = true progress.visibility = View.VISIBLE - val newName = profileRenameNewName.editText!!.text.toString().trim() - lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - val response = euiccChannelManagerService - .launchProfileRenameTask(slotId, portId, iccid, newName).waitDone() + val res = euiccChannelManagerService.launchProfileRenameTask( + slotId, + portId, + requireArguments().getString("iccid")!!, + profileRenameNewName.editText!!.text.toString().trim() + ).waitDone() - when (response) { + when (res) { is LocalProfileAssistant.ProfileNameTooLongException -> { showErrorAndCancel(R.string.profile_rename_too_long) } @@ -121,9 +118,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment } else -> { - parentFragment?.notifyEuiccProfilesChanged() + if (parentFragment is EuiccProfilesChangedListener) { + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + } - runCatching(::dismiss) + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 8cab18b..e342dee 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -1,6 +1,5 @@ package im.angry.openeuicc.ui.wizard -import android.app.assist.AssistContent import android.os.Bundle import android.view.View import android.view.inputmethod.InputMethodManager @@ -9,7 +8,6 @@ import android.widget.ProgressBar import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.enableEdgeToEdge -import androidx.core.net.toUri import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -113,21 +111,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { } } - override fun onProvideAssistContent(outContent: AssistContent?) { - super.onProvideAssistContent(outContent) - outContent?.webUri = try { - val activationCode = ActivationCode( - state.smdp, - state.matchingId, - null, - state.confirmationCode != null, - ) - "LPA:$activationCode".toUri() - } catch (_: Exception) { - null - } - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt index c21e837..3aca0d6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt @@ -20,15 +20,4 @@ data class ActivationCode( ) } } - - override fun toString(): String { - val parts = arrayOf( - "1", - address, - matchingId ?: "", - oid ?: "", - if (confirmationCodeRequired) "1" else "" - ) - return parts.joinToString("$").trimEnd('$') - } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index b44bef8..3f3c4ee 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -7,65 +7,43 @@ import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.ui.BaseEuiccAccessActivity -private const val FIELD_SLOT_ID = "slotId" -private const val FIELD_PORT_ID = "portId" - -interface EuiccChannelFragmentMarker : OpenEuiccContextMarker - -private typealias BundleSetter = Bundle.() -> Unit +interface EuiccChannelFragmentMarker: OpenEuiccContextMarker // We must use extension functions because there is no way to add bounds to the type of "self" // in the definition of an interface, so the only way is to limit where the extension functions // can be applied. -fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T - where T : Fragment, T : EuiccChannelFragmentMarker = - clazz.getDeclaredConstructor().newInstance().apply { - arguments = Bundle() - arguments!!.putInt(FIELD_SLOT_ID, slotId) - arguments!!.putInt(FIELD_PORT_ID, portId) - arguments!!.addArguments() +fun newInstanceEuicc(clazz: Class, slotId: Int, portId: Int, addArguments: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccChannelFragmentMarker { + val instance = clazz.newInstance() + instance.arguments = Bundle().apply { + putInt("slotId", slotId) + putInt("portId", portId) + addArguments() } + return instance +} // Convenient methods to avoid using `channel` for these // `channel` requires that the channel actually exists in EuiccChannelManager, which is // not always the case during operations such as switching -val T.slotId: Int - where T : Fragment, T : EuiccChannelFragmentMarker - get() = requireArguments().getInt(FIELD_SLOT_ID) -val T.portId: Int - where T : Fragment, T : EuiccChannelFragmentMarker - get() = requireArguments().getInt(FIELD_PORT_ID) -val T.isUsb: Boolean - where T : Fragment, T : EuiccChannelFragmentMarker - get() = slotId == EuiccChannelManager.USB_CHANNEL_ID +val T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker + get() = requireArguments().getInt("slotId") +val T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker + get() = requireArguments().getInt("portId") +val T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker + get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID -private fun T.requireEuiccActivity(): BaseEuiccAccessActivity - where T : Fragment, T : OpenEuiccContextMarker = - requireActivity() as BaseEuiccAccessActivity +val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker + get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager +val T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker + get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService -val T.euiccChannelManager: EuiccChannelManager - where T : Fragment, T : OpenEuiccContextMarker - get() = requireEuiccActivity().euiccChannelManager - -val T.euiccChannelManagerService: EuiccChannelManagerService - where T : Fragment, T : OpenEuiccContextMarker - get() = requireEuiccActivity().euiccChannelManagerService - -suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R - where T : Fragment, T : EuiccChannelFragmentMarker { +suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker { ensureEuiccChannelManager() return euiccChannelManager.withEuiccChannel(slotId, portId, fn) } -suspend fun T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker = - requireEuiccActivity().euiccChannelManagerLoaded.await() - -fun T.notifyEuiccProfilesChanged() where T : Fragment { - if (this !is EuiccProfilesChangedListener) return - // Trigger a refresh in the parent fragment -- it should wait until - // any foreground task is completed before actually doing a refresh - this.onEuiccProfilesChanged() -} +suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker = + (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() interface EuiccProfilesChangedListener { fun onEuiccProfilesChanged() diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt b/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt deleted file mode 100644 index 529f9ee..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt +++ /dev/null @@ -1,112 +0,0 @@ -package im.angry.openeuicc.util - -import android.util.Log -import im.angry.openeuicc.core.ApduInterfaceAtrProvider -import im.angry.openeuicc.core.EuiccChannel -import net.typeblog.lpac_jni.Version - -data class EuiccVendorInfo( - val skuName: String?, - val serialNumber: String?, - val bootloaderVersion: String?, - val firmwareVersion: String?, -) - -private val EUICC_VENDORS: Array = arrayOf(EstkMe(), SimLink()) - -fun EuiccChannel.tryParseEuiccVendorInfo(): EuiccVendorInfo? { - EUICC_VENDORS.forEach { vendor -> - vendor.tryParseEuiccVendorInfo(this@tryParseEuiccVendorInfo)?.let { - return it - } - } - - return null -} - -interface EuiccVendor { - fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? -} - -private class EstkMe : EuiccVendor { - companion object { - private val PRODUCT_AID = "A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex() - private val PRODUCT_ATR_FPR = "estk.me".encodeToByteArray() - } - - private fun checkAtr(channel: EuiccChannel): Boolean { - val iface = channel.apduInterface - if (iface !is ApduInterfaceAtrProvider) return false - val atr = iface.atr ?: return false - for (index in atr.indices) { - if (atr.size - index < PRODUCT_ATR_FPR.size) break - if (atr.sliceArray(index until index + PRODUCT_ATR_FPR.size) - .contentEquals(PRODUCT_ATR_FPR) - ) return true - } - return false - } - - private fun decodeAsn1String(b: ByteArray): String? { - if (b.size < 2) return null - if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null - return b.sliceArray(0 until b.size - 2).decodeToString() - } - - override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? { - if (!checkAtr(channel)) return null - - val iface = channel.apduInterface - return try { - iface.withLogicalChannel(PRODUCT_AID) { transmit -> - fun invoke(p1: Byte) = - decodeAsn1String(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00))) - EuiccVendorInfo( - skuName = invoke(0x03), - serialNumber = invoke(0x00), - bootloaderVersion = invoke(0x01), - firmwareVersion = invoke(0x02), - ) - } - } catch (e: Exception) { - Log.d(TAG, "Failed to get ESTKmeInfo", e) - null - } - } -} - -private class SimLink : EuiccVendor { - companion object { - private val EID_PATTERN = Regex("^89044045(84|21)67274948") - } - - override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? { - val eid = channel.lpa.eID - val version = channel.lpa.euiccInfo2?.euiccFirmwareVersion - if (version == null || EID_PATTERN.find(eid, 0) == null) return null - val versionName = when { - // @formatter:off - version >= Version(37, 1, 41) -> "v3.1 (beta 1)" - version >= Version(36, 18, 5) -> "v3 (final)" - version >= Version(36, 17, 39) -> "v3 (beta)" - version >= Version(36, 17, 4) -> "v2s" - version >= Version(36, 9, 3) -> "v2.1" - version >= Version(36, 7, 2) -> "v2" - // @formatter:on - else -> null - } - - val skuName = if (versionName == null) { - "9eSIM" - } else { - "9eSIM $versionName" - } - - return EuiccVendorInfo( - skuName = skuName, - serialNumber = null, - bootloaderVersion = null, - firmwareVersion = null - ) - } -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt new file mode 100644 index 0000000..2282921 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt @@ -0,0 +1,49 @@ +package im.angry.openeuicc.vendored + +import android.util.Log +import im.angry.openeuicc.core.ApduInterfaceAtrProvider +import im.angry.openeuicc.util.TAG +import im.angry.openeuicc.util.decodeHex +import net.typeblog.lpac_jni.ApduInterface + +data class ESTKmeInfo( + val serialNumber: String?, + val bootloaderVersion: String?, + val firmwareVersion: String?, + val skuName: String?, +) + +fun isESTKmeATR(iface: ApduInterface): Boolean { + if (iface !is ApduInterfaceAtrProvider) return false + val atr = iface.atr ?: return false + val fpr = "estk.me".encodeToByteArray() + for (index in atr.indices) { + if (atr.size - index < fpr.size) break + if (atr.sliceArray(index until index + fpr.size).contentEquals(fpr)) return true + } + return false +} + +fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? { + if (!isESTKmeATR(iface)) return null + fun decode(b: ByteArray): String? { + if (b.size < 2) return null + if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null + return b.sliceArray(0 until b.size - 2).decodeToString() + } + return try { + iface.withLogicalChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) { transmit -> + fun invoke(p1: Byte) = decode(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00))) + ESTKmeInfo( + invoke(0x00), // serial number + invoke(0x01), // bootloader version + invoke(0x02), // firmware version + invoke(0x03), // sku name + ) + } + } catch (e: Exception) { + Log.d(TAG, "Failed to get ESTKmeInfo", e) + null + } +} + diff --git a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt new file mode 100644 index 0000000..506f16c --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt @@ -0,0 +1,20 @@ +package im.angry.openeuicc.vendored + +import net.typeblog.lpac_jni.Version + +private val prefix = Regex("^89044045(84|21)67274948") // SIMLink EID prefix + +fun getSIMLinkVersion(eid: String, version: Version?): String? { + if (version == null || prefix.find(eid, 0) == null) return null + return when { + // @formatter:off + version >= Version(37, 1, 41) -> "v3.1 (beta 1)" + version >= Version(36, 18, 5) -> "v3 (final)" + version >= Version(36, 17, 39) -> "v3 (beta)" + version >= Version(36, 17, 4) -> "v2s" + version >= Version(36, 9, 3) -> "v2.1" + version >= Version(36, 7, 2) -> "v2" + // @formatter:on + else -> null + } +} diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index e4969c1..1710a7d 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -3,17 +3,13 @@ このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。 この eSIM にはプロファイルがありません。 不明 - 情報がありません + 情報なし ヘルプ スロットを再読み込み 論理スロット %d 有効済み 無効済み プロバイダー: - クラス: - テスト中 - プロビジョニング - 稼働中 有効化 無効化 削除 @@ -21,9 +17,8 @@ eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 新しい eSIM プロファイルに切り替えることができません。 - 確認文字列が一致しません + 入力した確認用テキストは一致していません ICCID をクリップボードにコピーしました - シリアル番号をクリップボードにコピーしました EID をクリップボードにコピーしました ATR をクリップボードにコピーしました USB の権限を許可 @@ -45,15 +40,13 @@ IMEI (オプション) ダウンロードに失敗する可能性があります 残り容量が少ないため、ダウンロードに失敗する可能性があります。 - クリップボードに LPA コードがありません - 確認コードが必要です - クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。 - 解析できません - QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。 + クリップボードに LPA コードが見つかりません + LPA コードを解析できません + クリップボードまたは QR コードの内容を LPA コードとして解析できません ダウンロードウィザード 戻る 次へ - 選択した SIM が削除されました + 選択された SIM が取り外されました ダウンロードする eSIM を選択または確認: タイプ: リムーバブル @@ -83,12 +76,12 @@ 最終の APDU レスポンス (SIM) は失敗しました 最終の APDU 例外: 保存 - 「%s」での診断 - ログは共有したパスに保存されました。別のアプリで共有しますか? + %s のエラー診断 + ログは指定されたパスに保存しました。他のアプリにシェアしますか? 新しいニックネーム - ニックネームを UTF-8 にエンコードできませんでした - ニックネームが 64 文字を超えています - プロファイルの名前変更時に不明なエラーが発生しました + ニックネームを UTF-8 にエンコードできません + ニックネームは 64 文字以内にしてください + ニックネームの変更で予期せぬエラーが発生しました %s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。 削除を確認するには「%s」を入力してください 通知 @@ -105,20 +98,16 @@ eUICC 情報 (%s) アクセスモード リムーバブル - 製品名 - 製品シリアル番号 - 製品ブートローダーバージョン - 製品ファームウェアバージョン SGP.22 バージョン - eUICC OS バージョン + eUICC OS のバージョン グローバルプラットフォームのバージョン SAS 認定番号 - 保護されたプロファイルのバージョン + Protected Profileのバージョン NVRAM の空き容量 (eSIM プロファイルストレージ) - 証明書発行者 (CI) - GSMA ライブ CI + 証明書の発行者 (CI) + GSMA プロダクション CI GSMA テスト CI - 不明な eSIM CI + 未知の eSIM CI はい いいえ 保存 @@ -127,28 +116,34 @@ あなたは開発者になりました! 設定 通知 - eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。 + eSIM のプロファイル操作により、通信事業者に通知が送信されます。ここでは、どのタイプの通知を送信するのかを微調整できます。 ダウンロード - プロファイルをダウンロード中の通知を送信します + プロファイルのダウンロード済みの通知を送信します 削除 - プロファイルを削除中の通知を送信します - 切り替え中 - プロファイルを切り替え中の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。 + プロファイルの削除済みの通知を送信します + 切り替え + プロファイルの切り替え済みの通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。 高度な設定 - プロファイルの無効化と削除を許可 + 有効なプロファイルの無効化と削除を許可する デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。 詳細ログ 詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。 - 言語 - アプリの言語 ログ アプリの最新デバッグログを表示します 開発者オプション - フィルタリングされていないプロファイル一覧を表示 - 非運用のプロファイルも含めます SM-DP+ TLS 証明書を無視する - RSP サーバーで使用される TLS 証明書を受け入れます + SM-DP+ TLS 証明書を無視して任意の RSP を許可します 情報 アプリバージョン ソースコード + 言語 + アプリの言語を選択 + すべてのプロファイルを表示 + プロダクション以外のプロファイルも表示する + タイプ: + テスティング + 準備中 + 動作中 + 要確認コード + スキャンされた QR コード、又はクリップボードからペーストされた LPA コードには、確認コードの必要が表示されています。 diff --git a/app-common/src/main/res/xml/locale_config.xml b/app-common/src/main/res/xml/locale_config.xml index 6d7f076..e1a13f8 100644 --- a/app-common/src/main/res/xml/locale_config.xml +++ b/app-common/src/main/res/xml/locale_config.xml @@ -3,5 +3,4 @@ - \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt index 7cf300c..fad03fd 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt @@ -1,11 +1,7 @@ package im.angry.openeuicc.ui -import android.content.pm.PackageManager -import android.provider.Settings import android.view.Menu import android.view.MenuInflater -import android.view.MenuItem -import android.widget.Toast import im.angry.easyeuicc.R import im.angry.openeuicc.util.SIMToolkit import im.angry.openeuicc.util.newInstanceEuicc @@ -27,29 +23,9 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.fragment_sim_toolkit, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) menu.findItem(R.id.open_sim_toolkit).apply { - intent = stk[slotId]?.intent - isVisible = intent != null + isVisible = stk.isAvailable(slotId) + intent = stk.intent(slotId) } } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.open_sim_toolkit -> { - SIMToolkit.getDisabledPackageName(item.intent)?.also { packageName -> - val label = requireContext().packageManager.getApplicationLabel(packageName) - val message = getString(R.string.toast_prompt_to_enable_sim_toolkit, label) - Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() - } - super.onOptionsItemSelected(item) // handling intent - } - - else -> super.onOptionsItemSelected(item) - } -} - -private fun PackageManager.getApplicationLabel(packageName: String): CharSequence = - getApplicationLabel(getApplicationInfo(packageName, 0)) +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt index f58f76a..ced813a 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt @@ -3,84 +3,65 @@ package im.angry.openeuicc.util import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.pm.ActivityInfo import android.content.pm.PackageManager -import android.net.Uri -import android.provider.Settings import androidx.annotation.ArrayRes import im.angry.easyeuicc.R import im.angry.openeuicc.core.EuiccChannelManager class SIMToolkit(private val context: Context) { + private val slotSelection = getComponentNames(R.array.sim_toolkit_slot_selection) + private val slots = buildMap { - fun getComponentNames(@ArrayRes id: Int) = context.resources - .getStringArray(id).mapNotNull(ComponentName::unflattenFromString) - put(-1, getComponentNames(R.array.sim_toolkit_slot_selection)) put(0, getComponentNames(R.array.sim_toolkit_slot_1)) put(1, getComponentNames(R.array.sim_toolkit_slot_2)) } - operator fun get(slotId: Int): Slot? = when (slotId) { - -1, EuiccChannelManager.USB_CHANNEL_ID -> null - else -> Slot(context.packageManager, buildSet { - addAll(slots.getOrDefault(slotId, emptySet())) - addAll(slots.getOrDefault(-1, emptySet())) - }) + private val packageNames = buildSet { + addAll(slotSelection.map { it.packageName }) + addAll(slots.values.flatten().map { it.packageName }) } - data class Slot(private val packageManager: PackageManager, private val components: Set) { - private val packageNames: Iterable - get() = components.map(ComponentName::getPackageName).toSet() - .filter(packageManager::isInstalledApp) + private val activities = packageNames.flatMap(::getActivities).toSet() - private val launchIntent: Intent? - get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage) - - private val activities: Iterable - get() = packageNames.flatMap(packageManager::getActivities) - .filter(ActivityInfo::exported).map { ComponentName(it.packageName, it.name) } - - private fun getActivityIntent(): Intent? { - for (activity in activities) { - if (!components.contains(activity)) continue - if (isDisabledState(packageManager.getComponentEnabledSetting(activity))) continue - return Intent.makeMainActivity(activity) - } - return launchIntent - } - - private fun getDisabledPackageIntent(): Intent? { - val disabledPackageName = packageNames - .find { isDisabledState(packageManager.getApplicationEnabledSetting(it)) } - ?: return null - val uri = Uri.fromParts("package", disabledPackageName, null) - return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) - } - - val intent: Intent? - get() = getActivityIntent() ?: getDisabledPackageIntent() + private val launchIntent by lazy { + packageNames.firstNotNullOfOrNull(::getLaunchIntent) } - companion object { - fun getDisabledPackageName(intent: Intent?): String? { - if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null - return intent.data?.schemeSpecificPart + private fun getLaunchIntent(packageName: String) = try { + val pm = context.packageManager + pm.getLaunchIntentForPackage(packageName) + } catch (_: PackageManager.NameNotFoundException) { + null + } + + private fun getActivities(packageName: String): List { + return try { + val pm = context.packageManager + val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) + val activities = packageInfo.activities + if (activities.isNullOrEmpty()) return emptyList() + activities.filter { it.exported }.map { ComponentName(it.packageName, it.name) } + } catch (_: PackageManager.NameNotFoundException) { + emptyList() } } -} -private fun isDisabledState(state: Int) = when (state) { - PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true - PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true - else -> false -} + private fun getComponentNames(@ArrayRes id: Int) = + context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString) -private fun PackageManager.isInstalledApp(packageName: String) = try { - getPackageInfo(packageName, 0) - true -} catch (_: PackageManager.NameNotFoundException) { - false -} + fun isAvailable(slotId: Int) = when (slotId) { + -1 -> false + EuiccChannelManager.USB_CHANNEL_ID -> false + else -> intent(slotId) != null + } -private fun PackageManager.getActivities(packageName: String) = - getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList() + fun intent(slotId: Int): Intent? { + val components = slots.getOrDefault(slotId, emptySet()) + slotSelection + val intent = Intent(Intent.ACTION_MAIN, null).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + component = components.find(activities::contains) + addCategory(Intent.CATEGORY_LAUNCHER) + } + return if (intent.component != null) intent else launchIntent + } +} diff --git a/app-unpriv/src/main/res/values-ja/strings.xml b/app-unpriv/src/main/res/values-ja/strings.xml index 3a6851a..053a8d1 100644 --- a/app-unpriv/src/main/res/values-ja/strings.xml +++ b/app-unpriv/src/main/res/values-ja/strings.xml @@ -2,36 +2,33 @@ 互換性のチェック SIM ツールキットを開く - - - ARA-M SHA-1 をクリップボードにコピーしました - 「%s」アプリを有効化してください システムの機能 デバイスにリムーバブル eUICC カードの管理に必要なすべての機能が備わっているかどうか。例えば基本的な電話機能や OMAPI のサポートなど。 使用しているデバイスには電話機能がありません。 使用しているデバイスまたはシステムには OMAPI のサポートを宣言していません。これは、ハードウェアからのサポートが不足していることが原因の可能性があります。または、フラグが不足していることが原因の可能性もあります。OMAPI が実際にサポートされているかどうかを判断するには次の 2 つのチェック項目を参照してください。 OMAPI の接続 - 使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可しているか否や。 + 使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可していますか? OMAPI 経由で SIM カードのセキュアエレメントリーダーを検出できません。このデバイスに SIM を挿入していない場合は、SIM を挿入後にこのチェックを再試行してください。 - セキュアエレメントのアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: <b>SIM%s</b> + セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: SIM%s. ISD-R チャネルアクセス - 使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしているか否や。 + 使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしていますか? OMAPI 経由での ISD-R アクセスがサポートされているかどうかを確認できません。まだ SIM カードが挿入されていない場合は、挿入した状態で再試行してください (どの SIM カードでも構いません)。 - ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: <b>SIM%s</b> - 既知の破損リストの記載されていない + ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: SIM%s. + 既知の破損リストに掲載されていない 取り外し可能な eSIM に関連するバグがデバイスに存在しないかを確認します。 - おっと...使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。 + おっと…使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。 USB カードリーダーのサポート - 使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしているか否や。 + 使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしていますか? このデバイスの標準 USB CCID リーダーを介して eSIM を管理できます (ここで他のチェック項目に失敗した場合でも)。カードリーダーを挿入し、このアプリを開いてこの方法で eSIM を管理できます。 使用しているデバイスは USB ホストとしての機能をサポートしていません。 判定 (USB 以外) - これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどの程度かについて + これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどのくらいありますか? このデバイスに挿入された取り外し可能な eSIM の使用および管理が使用できる可能性があります。 挿入された取り外し可能な eSIM にアクセスするとデバイスにバグが発生することが知られています。\n%s 挿入された取り外し可能な eSIM が使用しているデバイスで管理できるかはわかりません。ただし、このデバイスは OMAPI のサポートを宣言しているため、動作する可能性はわずかに高くなります。\n%s 挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s 挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。 + ARA-M SHA-1 をクリップボードにコピーしました diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 43bf44f..afed295 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -9,7 +9,6 @@ ARA-M SHA-1 copied to clipboard - Please ENABLE your \"%s\" application System Features diff --git a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt index 0d42354..4203fea 100644 --- a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt +++ b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt @@ -69,13 +69,11 @@ fun TelephonyManager.iccOpenLogicalChannelByPort( ): IccOpenLogicalChannelResponse = iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse -fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int) { - iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) -} +fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean = + iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as Boolean -fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int) { - iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) -} +fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int): Boolean = + iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) as Boolean fun TelephonyManager.iccTransmitApduLogicalChannelBySlot( slotId: Int, channel: Int, cla: Int, instruction: Int,