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 528b232..4e499dc 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,8 +23,6 @@ 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 @@ -104,14 +102,11 @@ 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)) - 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.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)) } } 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 7f82f22..38d1bc6 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,13 +20,10 @@ 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): ProfileDeleteFragment { - val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) - instance.requireArguments().apply { + fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) = + newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) { putString(FIELD_ICCID, iccid) putString(FIELD_NAME, name) - } - return instance } } @@ -91,19 +88,12 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { requireParentFragment().lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - 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() + euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid) + .onStart { + parentFragment?.notifyEuiccProfilesChanged() + runCatching(::dismiss) } - - try { - dismiss() - } catch (e: IllegalStateException) { - // Ignored - } - }.waitDone() + .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 25c5273..c588254 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,6 +7,7 @@ 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 @@ -18,16 +19,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): ProfileRenameFragment { - val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) - instance.requireArguments().apply { - putString("iccid", iccid) - putString("currentName", currentName) + 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) } - return instance - } } private lateinit var toolbar: Toolbar @@ -36,6 +37,14 @@ 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?, @@ -54,7 +63,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - profileRenameNewName.editText!!.setText(requireArguments().getString("currentName")) + profileRenameNewName.editText!!.setText(currentName) toolbar.apply { setTitle(R.string.rename) setNavigationOnClickListener { @@ -78,12 +87,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment } } - private fun showErrorAndCancel(errorStrRes: Int) { - Toast.makeText( - requireContext(), - errorStrRes, - Toast.LENGTH_LONG - ).show() + private fun showErrorAndCancel(@StringRes resId: Int) { + Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).show() renaming = false progress.visibility = View.GONE @@ -94,17 +99,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment progress.isIndeterminate = true progress.visibility = View.VISIBLE + val newName = profileRenameNewName.editText!!.text.toString().trim() + lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - val res = euiccChannelManagerService.launchProfileRenameTask( - slotId, - portId, - requireArguments().getString("iccid")!!, - profileRenameNewName.editText!!.text.toString().trim() - ).waitDone() + val response = euiccChannelManagerService + .launchProfileRenameTask(slotId, portId, iccid, newName).waitDone() - when (res) { + when (response) { is LocalProfileAssistant.ProfileNameTooLongException -> { showErrorAndCancel(R.string.profile_rename_too_long) } @@ -118,15 +121,9 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment } else -> { - if (parentFragment is EuiccProfilesChangedListener) { - (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() - } + parentFragment?.notifyEuiccProfilesChanged() - try { - dismiss() - } catch (e: IllegalStateException) { - // Ignored - } + runCatching(::dismiss) } } } 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 dc28cee..255036f 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,5 +1,6 @@ package im.angry.openeuicc.ui.wizard +import android.app.assist.AssistContent import android.os.Bundle import android.view.View import android.view.inputmethod.InputMethodManager @@ -8,6 +9,7 @@ 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 @@ -110,6 +112,21 @@ 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/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index 3f3c4ee..b44bef8 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,43 +7,65 @@ import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.ui.BaseEuiccAccessActivity -interface EuiccChannelFragmentMarker: OpenEuiccContextMarker +private const val FIELD_SLOT_ID = "slotId" +private const val FIELD_PORT_ID = "portId" + +interface EuiccChannelFragmentMarker : OpenEuiccContextMarker + +private typealias BundleSetter = Bundle.() -> Unit // 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: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccChannelFragmentMarker { - val instance = clazz.newInstance() - instance.arguments = Bundle().apply { - putInt("slotId", slotId) - putInt("portId", portId) - addArguments() +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() } - 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("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 +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.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 +private fun T.requireEuiccActivity(): BaseEuiccAccessActivity + where T : Fragment, T : OpenEuiccContextMarker = + requireActivity() as BaseEuiccAccessActivity -suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker { +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 { ensureEuiccChannelManager() return euiccChannelManager.withEuiccChannel(slotId, portId, fn) } -suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker = - (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() +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() +} 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 new file mode 100644 index 0000000..529f9ee --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/util/Vendors.kt @@ -0,0 +1,112 @@ +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 deleted file mode 100644 index 2282921..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/estkme.kt +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 506f16c..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/vendored/simlink.kt +++ /dev/null @@ -1,20 +0,0 @@ -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 1710a7d..e4969c1 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -3,13 +3,17 @@ このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。 この eSIM にはプロファイルがありません。 不明 - 情報なし + 情報がありません ヘルプ スロットを再読み込み 論理スロット %d 有効済み 無効済み プロバイダー: + クラス: + テスト中 + プロビジョニング + 稼働中 有効化 無効化 削除 @@ -17,8 +21,9 @@ eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 新しい eSIM プロファイルに切り替えることができません。 - 入力した確認用テキストは一致していません + 確認文字列が一致しません ICCID をクリップボードにコピーしました + シリアル番号をクリップボードにコピーしました EID をクリップボードにコピーしました ATR をクリップボードにコピーしました USB の権限を許可 @@ -40,13 +45,15 @@ IMEI (オプション) ダウンロードに失敗する可能性があります 残り容量が少ないため、ダウンロードに失敗する可能性があります。 - クリップボードに LPA コードが見つかりません - LPA コードを解析できません - クリップボードまたは QR コードの内容を LPA コードとして解析できません + クリップボードに LPA コードがありません + 確認コードが必要です + クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。 + 解析できません + QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。 ダウンロードウィザード 戻る 次へ - 選択された SIM が取り外されました + 選択した SIM が削除されました ダウンロードする eSIM を選択または確認: タイプ: リムーバブル @@ -76,12 +83,12 @@ 最終の APDU レスポンス (SIM) は失敗しました 最終の APDU 例外: 保存 - %s のエラー診断 - ログは指定されたパスに保存しました。他のアプリにシェアしますか? + 「%s」での診断 + ログは共有したパスに保存されました。別のアプリで共有しますか? 新しいニックネーム - ニックネームを UTF-8 にエンコードできません - ニックネームは 64 文字以内にしてください - ニックネームの変更で予期せぬエラーが発生しました + ニックネームを UTF-8 にエンコードできませんでした + ニックネームが 64 文字を超えています + プロファイルの名前変更時に不明なエラーが発生しました %s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。 削除を確認するには「%s」を入力してください 通知 @@ -98,16 +105,20 @@ 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 はい いいえ 保存 @@ -116,34 +127,28 @@ あなたは開発者になりました! 設定 通知 - eSIM のプロファイル操作により、通信事業者に通知が送信されます。ここでは、どのタイプの通知を送信するのかを微調整できます。 + eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。 ダウンロード - プロファイルのダウンロード済みの通知を送信します + プロファイルをダウンロード中の通知を送信します 削除 - プロファイルの削除済みの通知を送信します - 切り替え - プロファイルの切り替え済みの通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。 + プロファイルを削除中の通知を送信します + 切り替え中 + プロファイルを切り替え中の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。 高度な設定 - 有効なプロファイルの無効化と削除を許可する + プロファイルの無効化と削除を許可 デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと時々アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を解除します。 詳細ログ 詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。 + 言語 + アプリの言語 ログ アプリの最新デバッグログを表示します 開発者オプション + フィルタリングされていないプロファイル一覧を表示 + 非運用のプロファイルも含めます SM-DP+ TLS 証明書を無視する - SM-DP+ TLS 証明書を無視して任意の RSP を許可します + RSP サーバーで使用される TLS 証明書を受け入れます 情報 アプリバージョン ソースコード - 言語 - アプリの言語を選択 - すべてのプロファイルを表示 - プロダクション以外のプロファイルも表示する - タイプ: - テスティング - 準備中 - 動作中 - 要確認コード - スキャンされた QR コード、又はクリップボードからペーストされた LPA コードには、確認コードの必要が表示されています。 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 617cbec..7cf300c 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 @@ -4,6 +4,7 @@ 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 @@ -26,22 +27,28 @@ 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 { - val slot = stk[slotId] ?: return@apply - isVisible = slot.intent != null - setOnMenuItemClickListener { - val intent = slot.intent ?: return@setOnMenuItemClickListener false - if (intent.action == Settings.ACTION_APPLICATION_DETAILS_SETTINGS) { - val packageName = intent.data!!.schemeSpecificPart - val label = requireContext().packageManager.getApplicationLabel(packageName) - val message = requireContext().getString(R.string.toast_prompt_to_enable_sim_toolkit, label) - Toast.makeText(context, message, Toast.LENGTH_LONG).show() - } - startActivity(intent) - true - } + intent = stk[slotId]?.intent + isVisible = intent != null } } + + 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 = 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 99824ff..f58f76a 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 @@ -7,7 +7,6 @@ import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.net.Uri import android.provider.Settings -import android.widget.Toast import androidx.annotation.ArrayRes import im.angry.easyeuicc.R import im.angry.openeuicc.core.EuiccChannelManager @@ -32,9 +31,10 @@ class SIMToolkit(private val context: Context) { 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 launchIntent: Intent? - get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntent) + get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage) private val activities: Iterable get() = packageNames.flatMap(packageManager::getActivities) @@ -50,23 +50,23 @@ class SIMToolkit(private val context: Context) { } private fun getDisabledPackageIntent(): Intent? { - val disabledPackageName = packageNames.find { - try { - isDisabledState(packageManager.getApplicationEnabledSetting(it)) - } catch (_: IllegalArgumentException) { - false - } - } - if (disabledPackageName == null) return null - return Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", disabledPackageName, null) - ) + 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() } + + companion object { + fun getDisabledPackageName(intent: Intent?): String? { + if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null + return intent.data?.schemeSpecificPart + } + } } private fun isDisabledState(state: Int) = when (state) { @@ -75,15 +75,12 @@ private fun isDisabledState(state: Int) = when (state) { else -> false } -private fun PackageManager.getLaunchIntent(packageName: String) = try { - getLaunchIntentForPackage(packageName) +private fun PackageManager.isInstalledApp(packageName: String) = try { + getPackageInfo(packageName, 0) + true } catch (_: PackageManager.NameNotFoundException) { - null + false } -private fun PackageManager.getActivities(packageName: String) = try { - getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) - .activities?.toList() ?: emptyList() -} catch (_: PackageManager.NameNotFoundException) { - emptyList() -} +private fun PackageManager.getActivities(packageName: String) = + getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList() diff --git a/app-unpriv/src/main/res/values-ja/strings.xml b/app-unpriv/src/main/res/values-ja/strings.xml index 053a8d1..3a6851a 100644 --- a/app-unpriv/src/main/res/values-ja/strings.xml +++ b/app-unpriv/src/main/res/values-ja/strings.xml @@ -2,33 +2,36 @@ 互換性のチェック SIM ツールキットを開く + + + ARA-M SHA-1 をクリップボードにコピーしました + 「%s」アプリを有効化してください システムの機能 デバイスにリムーバブル eUICC カードの管理に必要なすべての機能が備わっているかどうか。例えば基本的な電話機能や OMAPI のサポートなど。 使用しているデバイスには電話機能がありません。 使用しているデバイスまたはシステムには OMAPI のサポートを宣言していません。これは、ハードウェアからのサポートが不足していることが原因の可能性があります。または、フラグが不足していることが原因の可能性もあります。OMAPI が実際にサポートされているかどうかを判断するには次の 2 つのチェック項目を参照してください。 OMAPI の接続 - 使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可していますか? + 使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可しているか否や。 OMAPI 経由で SIM カードのセキュアエレメントリーダーを検出できません。このデバイスに SIM を挿入していない場合は、SIM を挿入後にこのチェックを再試行してください。 - セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: SIM%s. + セキュアエレメントのアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: <b>SIM%s</b> ISD-R チャネルアクセス - 使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしていますか? + 使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしているか否や。 OMAPI 経由での ISD-R アクセスがサポートされているかどうかを確認できません。まだ SIM カードが挿入されていない場合は、挿入した状態で再試行してください (どの SIM カードでも構いません)。 - ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: SIM%s. - 既知の破損リストに掲載されていない + ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: <b>SIM%s</b> + 既知の破損リストの記載されていない 取り外し可能な 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/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 4203fea..0d42354 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,11 +69,13 @@ fun TelephonyManager.iccOpenLogicalChannelByPort( ): IccOpenLogicalChannelResponse = iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse -fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean = - iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as Boolean +fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int) { + iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) +} -fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int): Boolean = - iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) as Boolean +fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int) { + iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) +} fun TelephonyManager.iccTransmitApduLogicalChannelBySlot( slotId: Int, channel: Int, cla: Int, instruction: Int,