Merge branch 'master' into refresh-after-switch

This commit is contained in:
septs 2025-03-08 18:20:01 +01:00
commit e69fcc8d17
17 changed files with 376 additions and 250 deletions

View file

@ -9,6 +9,7 @@ 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,
@ -20,12 +21,8 @@ class OmapiApduInterface(
}
private lateinit var session: Session
private val channels = arrayOf<Channel?>(
null,
null,
null,
null,
)
private val index = AtomicInteger(0)
private val channels = mutableMapOf<Int, Channel>()
override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@ -44,21 +41,20 @@ class OmapiApduInterface(
override fun logicalChannelOpen(aid: ByteArray): Int {
val channel = session.openLogicalChannel(aid)
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
val index = channels.indexOf(null)
check(index != -1) { "No free logical channel slots" }
synchronized(channels) { channels[index] = channel }
return index
val handle = index.incrementAndGet()
synchronized(channels) { channels[handle] = channel }
return handle
}
override fun logicalChannelClose(handle: Int) {
val channel = channels.getOrNull(handle)
val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" }
if (channel.isOpen) channel.close()
synchronized(channels) { channels[handle] = null }
synchronized(channels) { channels.remove(handle) }
}
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
val channel = channels.getOrNull(handle)
val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" }
if (runBlocking { verboseLoggingFlow.first() }) {

View file

@ -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()))

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
@ -111,6 +113,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)

View file

@ -20,4 +20,15 @@ data class ActivationCode(
)
}
}
override fun toString(): String {
val parts = arrayOf(
"1",
address,
matchingId ?: "",
oid ?: "",
if (confirmationCodeRequired) "1" else ""
)
return parts.joinToString("$").trimEnd('$')
}
}

View file

@ -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 <T> newInstanceEuicc(clazz: Class<T>, 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 <T> newInstanceEuicc(clazz: Class<T>, 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> T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("slotId")
val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("portId")
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
val <T> T.slotId: Int
where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt(FIELD_SLOT_ID)
val <T> T.portId: Int
where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt(FIELD_PORT_ID)
val <T> T.isUsb: Boolean
where T : Fragment, T : EuiccChannelFragmentMarker
get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
private fun <T> T.requireEuiccActivity(): BaseEuiccAccessActivity
where T : Fragment, T : OpenEuiccContextMarker =
requireActivity() as BaseEuiccAccessActivity
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
val <T> T.euiccChannelManager: EuiccChannelManager
where T : Fragment, T : OpenEuiccContextMarker
get() = requireEuiccActivity().euiccChannelManager
val <T> T.euiccChannelManagerService: EuiccChannelManagerService
where T : Fragment, T : OpenEuiccContextMarker
get() = requireEuiccActivity().euiccChannelManagerService
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R
where T : Fragment, T : EuiccChannelFragmentMarker {
ensureEuiccChannelManager()
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
}
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
requireEuiccActivity().euiccChannelManagerLoaded.await()
fun <T> 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()

View file

@ -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<EuiccVendor> = 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
)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -3,13 +3,17 @@
<string name="no_euicc">このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。</string>
<string name="no_profile">この eSIM にはプロファイルがありません。</string>
<string name="unknown">不明</string>
<string name="information_unavailable">情報なし</string>
<string name="information_unavailable">情報がありません</string>
<string name="help">ヘルプ</string>
<string name="reload">スロットを再読み込み</string>
<string name="channel_name_format">論理スロット %d</string>
<string name="enabled">有効済み</string>
<string name="disabled">無効済み</string>
<string name="provider">プロバイダー:</string>
<string name="profile_class">クラス:</string>
<string name="profile_class_testing">テスト中</string>
<string name="profile_class_provisioning">プロビジョニング</string>
<string name="profile_class_operational">稼働中</string>
<string name="enable">有効化</string>
<string name="disable">無効化</string>
<string name="delete">削除</string>
@ -17,8 +21,9 @@
<string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string>
<string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string>
<string name="toast_profile_enable_failed">新しい eSIM プロファイルに切り替えることができません。</string>
<string name="toast_profile_delete_confirm_text_mismatched">入力した確認用テキストは一致していません</string>
<string name="toast_profile_delete_confirm_text_mismatched">確認文字列が一致しません</string>
<string name="toast_iccid_copied">ICCID をクリップボードにコピーしました</string>
<string name="toast_sn_copied">シリアル番号をクリップボードにコピーしました</string>
<string name="toast_eid_copied">EID をクリップボードにコピーしました</string>
<string name="toast_atr_copied">ATR をクリップボードにコピーしました</string>
<string name="usb_permission">USB の権限を許可</string>
@ -40,13 +45,15 @@
<string name="profile_download_imei">IMEI (オプション)</string>
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
<string name="profile_download_no_lpa_string">クリップボードに LPA コードが見つかりません</string>
<string name="profile_download_incorrect_lpa_string">LPA コードを解析できません</string>
<string name="profile_download_incorrect_lpa_string_message">クリップボードまたは QR コードの内容を LPA コードとして解析できません</string>
<string name="profile_download_no_lpa_string">クリップボードに LPA コードがありません</string>
<string name="profile_download_required_confirmation_code">確認コードが必要です</string>
<string name="profile_download_required_confirmation_code_message">クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。</string>
<string name="profile_download_incorrect_lpa_string">解析できません</string>
<string name="profile_download_incorrect_lpa_string_message">QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。</string>
<string name="download_wizard">ダウンロードウィザード</string>
<string name="download_wizard_back">戻る</string>
<string name="download_wizard_next">次へ</string>
<string name="download_wizard_slot_removed">選択された SIM が取り外されました</string>
<string name="download_wizard_slot_removed">選択した SIM が削除されました</string>
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</string>
<string name="download_wizard_slot_type">タイプ:</string>
<string name="download_wizard_slot_type_removable">リムーバブル</string>
@ -76,12 +83,12 @@
<string name="download_wizard_diagnostics_last_apdu_response_fail">最終の APDU レスポンス (SIM) は失敗しました</string>
<string name="download_wizard_diagnostics_last_apdu_exception">最終の APDU 例外:</string>
<string name="download_wizard_diagnostics_save">保存</string>
<string name="download_wizard_diagnostics_file_template">%s のエラー診断</string>
<string name="logs_saved_message">ログは指定されたパスに保存しました。他のアプリにシェアしますか?</string>
<string name="download_wizard_diagnostics_file_template">「%s」での診断</string>
<string name="logs_saved_message">ログは共有したパスに保存されました。別のアプリで共有しますか?</string>
<string name="profile_rename_new_name">新しいニックネーム</string>
<string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできません</string>
<string name="profile_rename_too_long">ニックネームは 64 文字以内にしてください</string>
<string name="profile_rename_failure">ニックネームの変更で予期せぬエラーが発生しました</string>
<string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできませんでした</string>
<string name="profile_rename_too_long">ニックネームが 64 文字を超えています</string>
<string name="profile_rename_failure">プロファイルの名前変更時に不明なエラーが発生しました</string>
<string name="profile_delete_confirm">%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。</string>
<string name="profile_delete_confirm_input">削除を確認するには「%s」を入力してください</string>
<string name="profile_notifications">通知</string>
@ -98,16 +105,20 @@
<string name="euicc_info_activity_title">eUICC 情報 (%s)</string>
<string name="euicc_info_access_mode">アクセスモード</string>
<string name="euicc_info_removable">リムーバブル</string>
<string name="euicc_info_sku">製品名</string>
<string name="euicc_info_sn">製品シリアル番号</string>
<string name="euicc_info_bl_ver">製品ブートローダーバージョン</string>
<string name="euicc_info_fw_ver">製品ファームウェアバージョン</string>
<string name="euicc_info_sgp22_version">SGP.22 バージョン</string>
<string name="euicc_info_firmware_version">eUICC OS のバージョン</string>
<string name="euicc_info_firmware_version">eUICC OS バージョン</string>
<string name="euicc_info_globalplatform_version">グローバルプラットフォームのバージョン</string>
<string name="euicc_info_sas_accreditation_number">SAS 認定番号</string>
<string name="euicc_info_pp_version">Protected Profileのバージョン</string>
<string name="euicc_info_pp_version">保護されたプロファイルのバージョン</string>
<string name="euicc_info_free_nvram">NVRAM の空き容量 (eSIM プロファイルストレージ)</string>
<string name="euicc_info_ci_type">証明書発行者 (CI)</string>
<string name="euicc_info_ci_gsma_live">GSMA プロダクション CI</string>
<string name="euicc_info_ci_type">証明書発行者 (CI)</string>
<string name="euicc_info_ci_gsma_live">GSMA ライブ CI</string>
<string name="euicc_info_ci_gsma_test">GSMA テスト CI</string>
<string name="euicc_info_ci_unknown">未知の eSIM CI</string>
<string name="euicc_info_ci_unknown">不明な eSIM CI</string>
<string name="yes">はい</string>
<string name="no">いいえ</string>
<string name="logs_save">保存</string>
@ -116,34 +127,28 @@
<string name="developer_options_enabled">あなたは開発者になりました!</string>
<string name="pref_settings">設定</string>
<string name="pref_notifications">通知</string>
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。ここでは、どのタイプの通知を送信するのかを微調整できます。</string>
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。</string>
<string name="pref_notifications_download">ダウンロード</string>
<string name="pref_notifications_download_desc">プロファイル<i>ダウンロード済み</i>の通知を送信します</string>
<string name="pref_notifications_download_desc">プロファイル<i>ダウンロード中</i>の通知を送信します</string>
<string name="pref_notifications_delete">削除</string>
<string name="pref_notifications_delete_desc">プロファイル<i>削除済み</i>の通知を送信します</string>
<string name="pref_notifications_switch">切り替え</string>
<string name="pref_notifications_switch_desc">プロファイル<i>切り替え済み</i>の通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。</string>
<string name="pref_notifications_delete_desc">プロファイル<i>削除中</i>の通知を送信します</string>
<string name="pref_notifications_switch">切り替え</string>
<string name="pref_notifications_switch_desc">プロファイル<i>切り替え中</i>の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。</string>
<string name="pref_advanced">高度な設定</string>
<string name="pref_advanced_disable_safeguard_removable_esim">有効なプロファイルの無効化と削除を許可する</string>
<string name="pref_advanced_disable_safeguard_removable_esim">プロファイルの無効化と削除を許可</string>
<string name="pref_advanced_disable_safeguard_removable_esim_desc">デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと<i>時々</i>アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を<i>解除</i>します。</string>
<string name="pref_advanced_verbose_logging">詳細ログ</string>
<string name="pref_advanced_verbose_logging_desc">詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。</string>
<string name="pref_advanced_language">言語</string>
<string name="pref_advanced_language_desc">アプリの言語</string>
<string name="pref_advanced_logs">ログ</string>
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string>
<string name="pref_developer">開発者オプション</string>
<string name="pref_developer_unfiltered_profile_list">フィルタリングされていないプロファイル一覧を表示</string>
<string name="pref_developer_unfiltered_profile_list_desc">非運用のプロファイルも含めます</string>
<string name="pref_developer_ignore_tls_certificate">SM-DP+ TLS 証明書を無視する</string>
<string name="pref_developer_ignore_tls_certificate_desc">SM-DP+ TLS 証明書を無視して任意の RSP を許可します</string>
<string name="pref_developer_ignore_tls_certificate_desc">RSP サーバーで使用される TLS 証明書を受け入れます</string>
<string name="pref_info">情報</string>
<string name="pref_info_app_version">アプリバージョン</string>
<string name="pref_info_source_code">ソースコード</string>
<string name="pref_advanced_language">言語</string>
<string name="pref_advanced_language_desc">アプリの言語を選択</string>
<string name="pref_developer_unfiltered_profile_list">すべてのプロファイルを表示</string>
<string name="pref_developer_unfiltered_profile_list_desc">プロダクション以外のプロファイルも表示する</string>
<string name="profile_class">タイプ:</string>
<string name="profile_class_testing">テスティング</string>
<string name="profile_class_provisioning">準備中</string>
<string name="profile_class_operational">動作中</string>
<string name="profile_download_required_confirmation_code">要確認コード</string>
<string name="profile_download_required_confirmation_code_message">スキャンされた QR コード、又はクリップボードからペーストされた LPA コードには、確認コードの必要が表示されています。</string>
</resources>

View file

@ -3,4 +3,5 @@
<locale android:name="en-US" />
<locale android:name="ja" />
<locale android:name="zh-CN" />
<locale android:name="zh-TW" />
</locale-config>

View file

@ -1,7 +1,11 @@
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
@ -23,9 +27,29 @@ 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 {
isVisible = stk.isAvailable(slotId)
intent = stk.intent(slotId)
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 =
getApplicationLabel(getApplicationInfo(packageName, 0))

View file

@ -3,65 +3,84 @@ 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))
}
private val packageNames = buildSet {
addAll(slotSelection.map { it.packageName })
addAll(slots.values.flatten().map { it.packageName })
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 activities = packageNames.flatMap(::getActivities).toSet()
data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
private val packageNames: Iterable<String>
get() = components.map(ComponentName::getPackageName).toSet()
.filter(packageManager::isInstalledApp)
private val launchIntent by lazy {
packageNames.firstNotNullOfOrNull(::getLaunchIntent)
}
private val launchIntent: Intent?
get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
private fun getLaunchIntent(packageName: String) = try {
val pm = context.packageManager
pm.getLaunchIntentForPackage(packageName)
} catch (_: PackageManager.NameNotFoundException) {
null
}
private val activities: Iterable<ComponentName>
get() = packageNames.flatMap(packageManager::getActivities)
.filter(ActivityInfo::exported).map { ComponentName(it.packageName, it.name) }
private fun getActivities(packageName: String): List<ComponentName> {
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 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 getComponentNames(@ArrayRes id: Int) =
context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
fun isAvailable(slotId: Int) = when (slotId) {
-1 -> false
EuiccChannelManager.USB_CHANNEL_ID -> false
else -> intent(slotId) != null
}
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)
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()
}
companion object {
fun getDisabledPackageName(intent: Intent?): String? {
if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null
return intent.data?.schemeSpecificPart
}
return if (intent.component != null) intent else launchIntent
}
}
private fun isDisabledState(state: Int) = when (state) {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
else -> false
}
private fun PackageManager.isInstalledApp(packageName: String) = try {
getPackageInfo(packageName, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
}
private fun PackageManager.getActivities(packageName: String) =
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList()

View file

@ -2,33 +2,36 @@
<resources>
<string name="compatibility_check">互換性のチェック</string>
<string name="open_sim_toolkit">SIM ツールキットを開く</string>
<!-- Settings -->
<!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
<string name="toast_prompt_to_enable_sim_toolkit">「%s」アプリを有効化してください</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">システムの機能</string>
<string name="compatibility_check_system_features_desc">デバイスにリムーバブル eUICC カードの管理に必要なすべての機能が備わっているかどうか。例えば基本的な電話機能や OMAPI のサポートなど。</string>
<string name="compatibility_check_system_features_no_telephony">使用しているデバイスには電話機能がありません。</string>
<string name="compatibility_check_system_features_no_omapi">使用しているデバイスまたはシステムには OMAPI のサポートを宣言していません。これは、ハードウェアからのサポートが不足していることが原因の可能性があります。または、フラグが不足していることが原因の可能性もあります。OMAPI が実際にサポートされているかどうかを判断するには次の 2 つのチェック項目を参照してください。</string>
<string name="compatibility_check_omapi_connectivity">OMAPI の接続</string>
<string name="compatibility_check_omapi_connectivity_desc">使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可していますか?</string>
<string name="compatibility_check_omapi_connectivity_desc">使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可しているか否や。</string>
<string name="compatibility_check_omapi_connectivity_fail">OMAPI 経由で SIM カードのセキュアエレメントリーダーを検出できません。このデバイスに SIM を挿入していない場合は、SIM を挿入後にこのチェックを再試行してください。</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: <b>SIM%s</b>.</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: &lt;b&gt;SIM%s&lt;/b&gt;</string>
<string name="compatibility_check_isdr_channel">ISD-R チャネルアクセス</string>
<string name="compatibility_check_isdr_channel_desc">使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしていますか?</string>
<string name="compatibility_check_isdr_channel_desc">使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしているか否や。</string>
<string name="compatibility_check_isdr_channel_desc_unknown">OMAPI 経由での ISD-R アクセスがサポートされているかどうかを確認できません。まだ SIM カードが挿入されていない場合は、挿入した状態で再試行してください (どの SIM カードでも構いません)。</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: <b>SIM%s</b>.</string>
<string name="compatibility_check_known_broken">既知の破損リストに掲載されていない</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: &lt;b&gt;SIM%s&lt;/b&gt;</string>
<string name="compatibility_check_known_broken">既知の破損リストの記載されていない</string>
<string name="compatibility_check_known_broken_desc">取り外し可能な eSIM に関連するバグがデバイスに存在しないかを確認します。</string>
<string name="compatibility_check_known_broken_fail">おっと使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。</string>
<string name="compatibility_check_known_broken_fail">おっと...使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。</string>
<string name="compatibility_check_usb">USB カードリーダーのサポート</string>
<string name="compatibility_check_usb_desc">使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしていますか?</string>
<string name="compatibility_check_usb_desc">使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしているか否や。</string>
<string name="compatibility_check_usb_ok">このデバイスの標準 USB CCID リーダーを介して eSIM を管理できます (ここで他のチェック項目に失敗した場合でも)。カードリーダーを挿入し、このアプリを開いてこの方法で eSIM を管理できます。</string>
<string name="compatibility_check_usb_fail">使用しているデバイスは USB ホストとしての機能をサポートしていません。</string>
<string name="compatibility_check_verdict">判定 (USB 以外)</string>
<string name="compatibility_check_verdict_desc">これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどのくらいありますか?</string>
<string name="compatibility_check_verdict_desc">これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどの程度かについて</string>
<string name="compatibility_check_verdict_ok">このデバイスに挿入された取り外し可能な eSIM の使用および管理が使用できる可能性があります。</string>
<string name="compatibility_check_verdict_known_broken">挿入された取り外し可能な eSIM にアクセスするとデバイスにバグが発生することが知られています。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_ok">挿入された取り外し可能な eSIM が使用しているデバイスで管理できるかはわかりません。ただし、このデバイスは OMAPI のサポートを宣言しているため、動作する可能性はわずかに高くなります。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string>
<string name="compatibility_check_verdict_unknown">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
</resources>

View file

@ -9,6 +9,7 @@
<!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
<string name="toast_prompt_to_enable_sim_toolkit">Please ENABLE your \"%s\" application</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string>

View file

@ -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,