Compare commits

...

10 commits

Author SHA1 Message Date
e69fcc8d17 Merge branch 'master' into refresh-after-switch 2025-03-08 18:20:01 +01:00
889b08767c Move eUICC vendor handling to an interface in util
...instead of ad-hoc functions
2025-03-08 11:34:26 -05:00
8243914588 feat: recent url sharing (#160)
![Screenshot_20250306-135020.png](/attachments/c2932882-72bd-4fb4-8955-e538d2dcd59c)

see https://developer.android.com/guide/components/activities/recents#url-sharing

Reviewed-on: PeterCxy/OpenEUICC#160
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:54:53 +01:00
2eabf719d0 refactor: EuiccChannelFragmentUtils (#164)
Reviewed-on: PeterCxy/OpenEUICC#164
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:52:19 +01:00
d068261ff9 improve stk menu handling (#162)
Reviewed-on: PeterCxy/OpenEUICC#162
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:48:32 +01:00
6a5d4b9288 Update Japanese (#159)
Crowdinで英語のXMLを同期、Stringsを比較しやすいように再成形済み。

Reviewed-on: PeterCxy/OpenEUICC#159
Co-authored-by: reindex <reindex@noreply.gitea.angry.im>
Co-committed-by: reindex <reindex@noreply.gitea.angry.im>
2025-03-08 16:47:43 +01:00
1313bfd24e fix: iccCloseLogicalChannelByPort method signature (#157)
fixes #154

Reviewed-on: PeterCxy/OpenEUICC#157
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:46:46 +01:00
99d9200c28 fix: omapi apdu interface (#152)
Reviewed-on: PeterCxy/OpenEUICC#152
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:19:16 +01:00
65c7f8de83 feat: prompt to enable disabled sim toolkit app (#153)
<video src="/attachments/fb9f210c-5960-4889-ba6a-dba4aa085a12" title="screen-20250305-130942.mp4" controls></video>

Reviewed-on: PeterCxy/OpenEUICC#153
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:18:56 +01:00
6c9063a761 chore: add zh-TW to locale-config (#155)
Reviewed-on: PeterCxy/OpenEUICC#155
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:18:03 +01:00
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.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import java.util.concurrent.atomic.AtomicInteger
class OmapiApduInterface( class OmapiApduInterface(
private val service: SEService, private val service: SEService,
@ -20,12 +21,8 @@ class OmapiApduInterface(
} }
private lateinit var session: Session private lateinit var session: Session
private val channels = arrayOf<Channel?>( private val index = AtomicInteger(0)
null, private val channels = mutableMapOf<Int, Channel>()
null,
null,
null,
)
override val valid: Boolean override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed) get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@ -44,21 +41,20 @@ class OmapiApduInterface(
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {
val channel = session.openLogicalChannel(aid) val channel = session.openLogicalChannel(aid)
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
val index = channels.indexOf(null) val handle = index.incrementAndGet()
check(index != -1) { "No free logical channel slots" } synchronized(channels) { channels[handle] = channel }
synchronized(channels) { channels[index] = channel } return handle
return index
} }
override fun logicalChannelClose(handle: Int) { override fun logicalChannelClose(handle: Int) {
val channel = channels.getOrNull(handle) val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" } check(channel != null) { "Invalid logical channel handle $handle" }
if (channel.isOpen) channel.close() if (channel.isOpen) channel.close()
synchronized(channels) { channels[handle] = null } synchronized(channels) { channels.remove(handle) }
} }
override fun transmit(handle: Int, tx: ByteArray): ByteArray { 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" } check(channel != null) { "Invalid logical channel handle $handle" }
if (runBlocking { verboseLoggingFlow.first() }) { 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.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import im.angry.openeuicc.vendored.getESTKmeInfo
import im.angry.openeuicc.vendored.getSIMLinkVersion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_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_access_mode, channel.type))
add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO))) 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)) add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
getESTKmeInfo(channel.apduInterface)?.let { channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
add(Item(R.string.euicc_info_sku, it.skuName)) vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied)) vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it)) }
add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion)) vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion)) vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
}
getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let {
add(Item(R.string.euicc_info_sku, "9eSIM $it"))
} }
channel.lpa.euiccInfo2.let { info -> channel.lpa.euiccInfo2.let { info ->
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString())) add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString()))

View file

@ -20,14 +20,11 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_ICCID = "iccid" private const val FIELD_ICCID = "iccid"
private const val FIELD_NAME = "name" private const val FIELD_NAME = "name"
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment { fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
instance.requireArguments().apply {
putString(FIELD_ICCID, iccid) putString(FIELD_ICCID, iccid)
putString(FIELD_NAME, name) putString(FIELD_NAME, name)
} }
return instance
}
} }
private val iccid by lazy { private val iccid by lazy {
@ -91,19 +88,12 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
requireParentFragment().lifecycleScope.launch { requireParentFragment().lifecycleScope.launch {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart { euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
if (parentFragment is EuiccProfilesChangedListener) { .onStart {
// Trigger a refresh in the parent fragment -- it should wait until parentFragment?.notifyEuiccProfilesChanged()
// any foreground task is completed before actually doing a refresh runCatching(::dismiss)
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
} }
.waitDone()
try {
dismiss()
} catch (e: IllegalStateException) {
// Ignored
}
}.waitDone()
} }
} }
} }

View file

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@ -18,15 +19,15 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
companion object { companion object {
private const val FIELD_ICCID = "iccid"
private const val FIELD_CURRENT_NAME = "currentName"
const val TAG = "ProfileRenameFragment" const val TAG = "ProfileRenameFragment"
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment { fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) =
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) {
instance.requireArguments().apply { putString(FIELD_ICCID, iccid)
putString("iccid", iccid) putString(FIELD_CURRENT_NAME, currentName)
putString("currentName", currentName)
}
return instance
} }
} }
@ -36,6 +37,14 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
private var renaming = false 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -54,7 +63,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName")) profileRenameNewName.editText!!.setText(currentName)
toolbar.apply { toolbar.apply {
setTitle(R.string.rename) setTitle(R.string.rename)
setNavigationOnClickListener { setNavigationOnClickListener {
@ -78,12 +87,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
} }
} }
private fun showErrorAndCancel(errorStrRes: Int) { private fun showErrorAndCancel(@StringRes resId: Int) {
Toast.makeText( Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).show()
requireContext(),
errorStrRes,
Toast.LENGTH_LONG
).show()
renaming = false renaming = false
progress.visibility = View.GONE progress.visibility = View.GONE
@ -94,17 +99,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
progress.isIndeterminate = true progress.isIndeterminate = true
progress.visibility = View.VISIBLE progress.visibility = View.VISIBLE
val newName = profileRenameNewName.editText!!.text.toString().trim()
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
val res = euiccChannelManagerService.launchProfileRenameTask( val response = euiccChannelManagerService
slotId, .launchProfileRenameTask(slotId, portId, iccid, newName).waitDone()
portId,
requireArguments().getString("iccid")!!,
profileRenameNewName.editText!!.text.toString().trim()
).waitDone()
when (res) { when (response) {
is LocalProfileAssistant.ProfileNameTooLongException -> { is LocalProfileAssistant.ProfileNameTooLongException -> {
showErrorAndCancel(R.string.profile_rename_too_long) showErrorAndCancel(R.string.profile_rename_too_long)
} }
@ -118,15 +121,9 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
} }
else -> { else -> {
if (parentFragment is EuiccProfilesChangedListener) { parentFragment?.notifyEuiccProfilesChanged()
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
try { runCatching(::dismiss)
dismiss()
} catch (e: IllegalStateException) {
// Ignored
}
} }
} }
} }

View file

@ -1,5 +1,6 @@
package im.angry.openeuicc.ui.wizard package im.angry.openeuicc.ui.wizard
import android.app.assist.AssistContent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@ -8,6 +9,7 @@ import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.core.net.toUri
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding 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) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName) 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.service.EuiccChannelManagerService
import im.angry.openeuicc.ui.BaseEuiccAccessActivity 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" // 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 // in the definition of an interface, so the only way is to limit where the extension functions
// can be applied. // can be applied.
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccChannelFragmentMarker { fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
val instance = clazz.newInstance() where T : Fragment, T : EuiccChannelFragmentMarker =
instance.arguments = Bundle().apply { clazz.getDeclaredConstructor().newInstance().apply {
putInt("slotId", slotId) arguments = Bundle()
putInt("portId", portId) arguments!!.putInt(FIELD_SLOT_ID, slotId)
addArguments() arguments!!.putInt(FIELD_PORT_ID, portId)
arguments!!.addArguments()
} }
return instance
}
// Convenient methods to avoid using `channel` for these // Convenient methods to avoid using `channel` for these
// `channel` requires that the channel actually exists in EuiccChannelManager, which is // `channel` requires that the channel actually exists in EuiccChannelManager, which is
// not always the case during operations such as switching // not always the case during operations such as switching
val <T> T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker val <T> T.slotId: Int
get() = requireArguments().getInt("slotId") where T : Fragment, T : EuiccChannelFragmentMarker
val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker get() = requireArguments().getInt(FIELD_SLOT_ID)
get() = requireArguments().getInt("portId") val <T> T.portId: Int
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID 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 private fun <T> T.requireEuiccActivity(): BaseEuiccAccessActivity
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager where T : Fragment, T : OpenEuiccContextMarker =
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker requireActivity() as BaseEuiccAccessActivity
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
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() ensureEuiccChannelManager()
return euiccChannelManager.withEuiccChannel(slotId, portId, fn) return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
} }
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker = suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() 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 { interface EuiccProfilesChangedListener {
fun onEuiccProfilesChanged() 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_euicc">このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。</string>
<string name="no_profile">この eSIM にはプロファイルがありません。</string> <string name="no_profile">この eSIM にはプロファイルがありません。</string>
<string name="unknown">不明</string> <string name="unknown">不明</string>
<string name="information_unavailable">情報なし</string> <string name="information_unavailable">情報がありません</string>
<string name="help">ヘルプ</string> <string name="help">ヘルプ</string>
<string name="reload">スロットを再読み込み</string> <string name="reload">スロットを再読み込み</string>
<string name="channel_name_format">論理スロット %d</string> <string name="channel_name_format">論理スロット %d</string>
<string name="enabled">有効済み</string> <string name="enabled">有効済み</string>
<string name="disabled">無効済み</string> <string name="disabled">無効済み</string>
<string name="provider">プロバイダー:</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="enable">有効化</string>
<string name="disable">無効化</string> <string name="disable">無効化</string>
<string name="delete">削除</string> <string name="delete">削除</string>
@ -17,8 +21,9 @@
<string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string> <string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string>
<string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string> <string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string>
<string name="toast_profile_enable_failed">新しい eSIM プロファイルに切り替えることができません。</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_iccid_copied">ICCID をクリップボードにコピーしました</string>
<string name="toast_sn_copied">シリアル番号をクリップボードにコピーしました</string>
<string name="toast_eid_copied">EID をクリップボードにコピーしました</string> <string name="toast_eid_copied">EID をクリップボードにコピーしました</string>
<string name="toast_atr_copied">ATR をクリップボードにコピーしました</string> <string name="toast_atr_copied">ATR をクリップボードにコピーしました</string>
<string name="usb_permission">USB の権限を許可</string> <string name="usb_permission">USB の権限を許可</string>
@ -40,13 +45,15 @@
<string name="profile_download_imei">IMEI (オプション)</string> <string name="profile_download_imei">IMEI (オプション)</string>
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string> <string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string> <string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
<string name="profile_download_no_lpa_string">クリップボードに LPA コードが見つかりません</string> <string name="profile_download_no_lpa_string">クリップボードに LPA コードがありません</string>
<string name="profile_download_incorrect_lpa_string">LPA コードを解析できません</string> <string name="profile_download_required_confirmation_code">確認コードが必要です</string>
<string name="profile_download_incorrect_lpa_string_message">クリップボードまたは QR コードの内容を LPA コードとして解析できません</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">ダウンロードウィザード</string>
<string name="download_wizard_back">戻る</string> <string name="download_wizard_back">戻る</string>
<string name="download_wizard_next">次へ</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_select">ダウンロードする eSIM を選択または確認:</string>
<string name="download_wizard_slot_type">タイプ:</string> <string name="download_wizard_slot_type">タイプ:</string>
<string name="download_wizard_slot_type_removable">リムーバブル</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_response_fail">最終の APDU レスポンス (SIM) は失敗しました</string>
<string name="download_wizard_diagnostics_last_apdu_exception">最終の APDU 例外:</string> <string name="download_wizard_diagnostics_last_apdu_exception">最終の APDU 例外:</string>
<string name="download_wizard_diagnostics_save">保存</string> <string name="download_wizard_diagnostics_save">保存</string>
<string name="download_wizard_diagnostics_file_template">%s のエラー診断</string> <string name="download_wizard_diagnostics_file_template">「%s」での診断</string>
<string name="logs_saved_message">ログは指定されたパスに保存しました。他のアプリにシェアしますか?</string> <string name="logs_saved_message">ログは共有したパスに保存されました。別のアプリで共有しますか?</string>
<string name="profile_rename_new_name">新しいニックネーム</string> <string name="profile_rename_new_name">新しいニックネーム</string>
<string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできません</string> <string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできませんでした</string>
<string name="profile_rename_too_long">ニックネームは 64 文字以内にしてください</string> <string name="profile_rename_too_long">ニックネームが 64 文字を超えています</string>
<string name="profile_rename_failure">ニックネームの変更で予期せぬエラーが発生しました</string> <string name="profile_rename_failure">プロファイルの名前変更時に不明なエラーが発生しました</string>
<string name="profile_delete_confirm">%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。</string> <string name="profile_delete_confirm">%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。</string>
<string name="profile_delete_confirm_input">削除を確認するには「%s」を入力してください</string> <string name="profile_delete_confirm_input">削除を確認するには「%s」を入力してください</string>
<string name="profile_notifications">通知</string> <string name="profile_notifications">通知</string>
@ -98,16 +105,20 @@
<string name="euicc_info_activity_title">eUICC 情報 (%s)</string> <string name="euicc_info_activity_title">eUICC 情報 (%s)</string>
<string name="euicc_info_access_mode">アクセスモード</string> <string name="euicc_info_access_mode">アクセスモード</string>
<string name="euicc_info_removable">リムーバブル</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_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_globalplatform_version">グローバルプラットフォームのバージョン</string>
<string name="euicc_info_sas_accreditation_number">SAS 認定番号</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_free_nvram">NVRAM の空き容量 (eSIM プロファイルストレージ)</string>
<string name="euicc_info_ci_type">証明書発行者 (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_live">GSMA ライブ CI</string>
<string name="euicc_info_ci_gsma_test">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="yes">はい</string>
<string name="no">いいえ</string> <string name="no">いいえ</string>
<string name="logs_save">保存</string> <string name="logs_save">保存</string>
@ -116,34 +127,28 @@
<string name="developer_options_enabled">あなたは開発者になりました!</string> <string name="developer_options_enabled">あなたは開発者になりました!</string>
<string name="pref_settings">設定</string> <string name="pref_settings">設定</string>
<string name="pref_notifications">通知</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">ダウンロード</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">削除</string>
<string name="pref_notifications_delete_desc">プロファイル<i>削除済み</i>の通知を送信します</string> <string name="pref_notifications_delete_desc">プロファイル<i>削除中</i>の通知を送信します</string>
<string name="pref_notifications_switch">切り替え</string> <string name="pref_notifications_switch">切り替え</string>
<string name="pref_notifications_switch_desc">プロファイル<i>切り替え済み</i>の通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。</string> <string name="pref_notifications_switch_desc">プロファイル<i>切り替え中</i>の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。</string>
<string name="pref_advanced">高度な設定</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_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">詳細ログ</string>
<string name="pref_advanced_verbose_logging_desc">詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。</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">ログ</string>
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string> <string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string>
<string name="pref_developer">開発者オプション</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">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">情報</string>
<string name="pref_info_app_version">アプリバージョン</string> <string name="pref_info_app_version">アプリバージョン</string>
<string name="pref_info_source_code">ソースコード</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> </resources>

View file

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

View file

@ -1,7 +1,11 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.content.pm.PackageManager
import android.provider.Settings
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import im.angry.easyeuicc.R import im.angry.easyeuicc.R
import im.angry.openeuicc.util.SIMToolkit import im.angry.openeuicc.util.SIMToolkit
import im.angry.openeuicc.util.newInstanceEuicc import im.angry.openeuicc.util.newInstanceEuicc
@ -23,9 +27,29 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_sim_toolkit, menu) inflater.inflate(R.menu.fragment_sim_toolkit, menu)
menu.findItem(R.id.open_sim_toolkit).apply {
isVisible = stk.isAvailable(slotId)
intent = stk.intent(slotId)
} }
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.open_sim_toolkit).apply {
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.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.annotation.ArrayRes import androidx.annotation.ArrayRes
import im.angry.easyeuicc.R import im.angry.easyeuicc.R
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
class SIMToolkit(private val context: Context) { class SIMToolkit(private val context: Context) {
private val slotSelection = getComponentNames(R.array.sim_toolkit_slot_selection)
private val slots = buildMap { 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(0, getComponentNames(R.array.sim_toolkit_slot_1))
put(1, getComponentNames(R.array.sim_toolkit_slot_2)) put(1, getComponentNames(R.array.sim_toolkit_slot_2))
} }
private val packageNames = buildSet { operator fun get(slotId: Int): Slot? = when (slotId) {
addAll(slotSelection.map { it.packageName }) -1, EuiccChannelManager.USB_CHANNEL_ID -> null
addAll(slots.values.flatten().map { it.packageName }) 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 { private val launchIntent: Intent?
packageNames.firstNotNullOfOrNull(::getLaunchIntent) get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
private val activities: Iterable<ComponentName>
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 getLaunchIntent(packageName: String) = try { private fun getDisabledPackageIntent(): Intent? {
val pm = context.packageManager val disabledPackageName = packageNames
pm.getLaunchIntentForPackage(packageName) .find { isDisabledState(packageManager.getApplicationEnabledSetting(it)) }
} catch (_: PackageManager.NameNotFoundException) { ?: return null
null val uri = Uri.fromParts("package", disabledPackageName, null)
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
} }
private fun getActivities(packageName: String): List<ComponentName> { val intent: Intent?
return try { get() = getActivityIntent() ?: getDisabledPackageIntent()
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 getComponentNames(@ArrayRes id: Int) = companion object {
context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString) fun getDisabledPackageName(intent: Intent?): String? {
if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null
fun isAvailable(slotId: Int) = when (slotId) { return intent.data?.schemeSpecificPart
-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)
}
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> <resources>
<string name="compatibility_check">互換性のチェック</string> <string name="compatibility_check">互換性のチェック</string>
<string name="open_sim_toolkit">SIM ツールキットを開く</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 --> <!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">システムの機能</string> <string name="compatibility_check_system_features">システムの機能</string>
<string name="compatibility_check_system_features_desc">デバイスにリムーバブル eUICC カードの管理に必要なすべての機能が備わっているかどうか。例えば基本的な電話機能や OMAPI のサポートなど。</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_telephony">使用しているデバイスには電話機能がありません。</string>
<string name="compatibility_check_system_features_no_omapi">使用しているデバイスまたはシステムには OMAPI のサポートを宣言していません。これは、ハードウェアからのサポートが不足していることが原因の可能性があります。または、フラグが不足していることが原因の可能性もあります。OMAPI が実際にサポートされているかどうかを判断するには次の 2 つのチェック項目を参照してください。</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">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_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">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_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_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">既知の破損リストの記載されていない</string>
<string name="compatibility_check_known_broken_desc">取り外し可能な eSIM に関連するバグがデバイスに存在しないかを確認します。</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">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_ok">このデバイスの標準 USB CCID リーダーを介して eSIM を管理できます (ここで他のチェック項目に失敗した場合でも)。カードリーダーを挿入し、このアプリを開いてこの方法で eSIM を管理できます。</string>
<string name="compatibility_check_usb_fail">使用しているデバイスは USB ホストとしての機能をサポートしていません。</string> <string name="compatibility_check_usb_fail">使用しているデバイスは USB ホストとしての機能をサポートしていません。</string>
<string name="compatibility_check_verdict">判定 (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_ok">このデバイスに挿入された取り外し可能な eSIM の使用および管理が使用できる可能性があります。</string>
<string name="compatibility_check_verdict_known_broken">挿入された取り外し可能な eSIM にアクセスするとデバイスにバグが発生することが知られています。\n%s</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_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_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string>
<string name="compatibility_check_verdict_unknown">挿入された取り外し可能な 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="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
</resources> </resources>

View file

@ -9,6 +9,7 @@
<!-- Toast --> <!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string> <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 --> <!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string> <string name="compatibility_check_system_features">System Features</string>

View file

@ -69,11 +69,13 @@ fun TelephonyManager.iccOpenLogicalChannelByPort(
): IccOpenLogicalChannelResponse = ): IccOpenLogicalChannelResponse =
iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse
fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean = fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int) {
iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as Boolean iccCloseLogicalChannelBySlot.invoke(this, slotId, channel)
}
fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int): Boolean = fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int) {
iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) as Boolean iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel)
}
fun TelephonyManager.iccTransmitApduLogicalChannelBySlot( fun TelephonyManager.iccTransmitApduLogicalChannelBySlot(
slotId: Int, channel: Int, cla: Int, instruction: Int, slotId: Int, channel: Int, cla: Int, instruction: Int,