forked from PeterCxy/OpenEUICC
Compare commits
12 commits
02ef31199b
...
14687acc73
Author | SHA1 | Date | |
---|---|---|---|
14687acc73 | |||
267332afa4 | |||
f046d40f2c | |||
80adac68c8 | |||
1ed5a4de38 | |||
4b842c4afe | |||
2517fc817e | |||
e48f9aa828 | |||
1ac683f9ab | |||
7834e0348a | |||
92d8f9079f | |||
d9d0cf2e75 |
21 changed files with 273 additions and 164 deletions
|
@ -10,7 +10,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
private var seService: SEService? = null
|
||||
|
||||
private suspend fun ensureSEService() {
|
||||
if (seService == null) {
|
||||
if (seService == null || !seService!!.isConnected) {
|
||||
seService = connectSEService(context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ import android.util.Log
|
|||
import im.angry.openeuicc.di.AppContainer
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
open class DefaultEuiccChannelManager(
|
||||
protected val appContainer: AppContainer,
|
||||
|
@ -87,21 +89,51 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
}
|
||||
|
||||
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
|
||||
runBlocking {
|
||||
override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
|
||||
for (card in uiccCards) {
|
||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
||||
return card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
||||
.ifEmpty { null }
|
||||
}
|
||||
return@runBlocking null
|
||||
return null
|
||||
}
|
||||
|
||||
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
|
||||
runBlocking {
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)
|
||||
}
|
||||
|
||||
override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||
withContext(Dispatchers.IO) {
|
||||
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
|
||||
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
|
||||
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
|
||||
findEuiccChannelByPort(physicalSlotId, portId)
|
||||
}
|
||||
|
||||
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
|
||||
// If there is already a valid channel, we close it proactively
|
||||
// Sometimes the current channel can linger on for a bit even after it should have become invalid
|
||||
channels.find { it.slotId == physicalSlotId && it.portId == portId }?.apply {
|
||||
if (valid) close()
|
||||
}
|
||||
|
||||
withTimeout(timeoutMillis) {
|
||||
while (true) {
|
||||
try {
|
||||
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
||||
// and recreate when needed
|
||||
val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId)!!
|
||||
check(channel.valid) { "Invalid channel" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms")
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,13 @@ interface EuiccChannelManager {
|
|||
*/
|
||||
suspend fun enumerateEuiccChannels()
|
||||
|
||||
/**
|
||||
* Wait for a slot + port to reconnect (i.e. become valid again)
|
||||
* If the port is currently valid, this function will return immediately.
|
||||
* On timeout, the caller can decide to either try again later, or alert the user with an error
|
||||
*/
|
||||
suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000)
|
||||
|
||||
/**
|
||||
* Returns the EuiccChannel corresponding to a **logical** slot
|
||||
*/
|
||||
|
@ -24,11 +31,13 @@ interface EuiccChannelManager {
|
|||
* Returns all EuiccChannels corresponding to a **physical** slot
|
||||
* Multiple channels are possible in the case of MEP
|
||||
*/
|
||||
suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>?
|
||||
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>?
|
||||
|
||||
/**
|
||||
* Returns the EuiccChannel corresponding to a **physical** slot and a port ID
|
||||
*/
|
||||
suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel?
|
||||
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,9 @@ class OmapiApduInterface(
|
|||
private lateinit var session: Session
|
||||
private lateinit var lastChannel: Channel
|
||||
|
||||
override val valid: Boolean
|
||||
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
||||
|
||||
override fun connect() {
|
||||
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession()
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import android.widget.ImageButton
|
|||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -26,10 +27,10 @@ import net.typeblog.lpac_jni.LocalProfileInfo
|
|||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Exception
|
||||
|
||||
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||
EuiccChannelFragmentMarker {
|
||||
|
@ -130,34 +131,50 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
fab.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (enable) {
|
||||
doEnableProfile(iccid)
|
||||
beginTrackedOperation {
|
||||
val res = if (enable) {
|
||||
channel.lpa.enableProfile(iccid)
|
||||
} else {
|
||||
doDisableProfile(iccid)
|
||||
channel.lpa.disableProfile(iccid)
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
Log.d(TAG, "Failed to enable / disable profile $iccid")
|
||||
Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
return@beginTrackedOperation false
|
||||
}
|
||||
|
||||
try {
|
||||
euiccChannelManager.waitForReconnect(slotId, portId, timeoutMillis = 30 * 1000)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setMessage(R.string.enable_disable_timeout)
|
||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
}
|
||||
setOnDismissListener { _ ->
|
||||
requireActivity().finish()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
return@beginTrackedOperation false
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
preferenceRepository.notificationEnableFlow.first()
|
||||
} else {
|
||||
preferenceRepository.notificationDisableFlow.first()
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
fab.isEnabled = true
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to enable / disable profile $iccid")
|
||||
Log.d(TAG, Log.getStackTraceString(e))
|
||||
fab.isEnabled = true
|
||||
Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doEnableProfile(iccid: String) =
|
||||
channel.lpa.beginOperation {
|
||||
channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) &&
|
||||
preferenceRepository.notificationEnableFlow.first()
|
||||
}
|
||||
|
||||
private suspend fun doDisableProfile(iccid: String) =
|
||||
channel.lpa.beginOperation {
|
||||
channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) &&
|
||||
preferenceRepository.notificationDisableFlow.first()
|
||||
}
|
||||
|
||||
protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
|
||||
popup.inflate(R.menu.profile_options)
|
||||
|
|
|
@ -82,7 +82,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun doDelete() = channel.lpa.beginOperation {
|
||||
private suspend fun doDelete() = beginTrackedOperation {
|
||||
channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
|
||||
preferenceRepository.notificationDeleteFlow.first()
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.app.Dialog
|
|||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.ProgressBar
|
||||
|
@ -131,7 +130,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
// Fetch remaining NVRAM
|
||||
val str = channel.lpa.euiccInfo2?.freeNvram?.also {
|
||||
freeNvram = it
|
||||
}?.let { Formatter.formatShortFileSize(requireContext(), it.toLong()) }
|
||||
}?.let { formatFreeSpace(it) }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space,
|
||||
|
@ -189,8 +188,18 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = channel.lpa.beginOperation {
|
||||
downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback {
|
||||
private suspend fun doDownloadProfile(
|
||||
server: String,
|
||||
code: String?,
|
||||
confirmationCode: String?,
|
||||
imei: String?
|
||||
) = beginTrackedOperation {
|
||||
channel.lpa.downloadProfile(
|
||||
server,
|
||||
code,
|
||||
imei,
|
||||
confirmationCode,
|
||||
object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
progress.isIndeterminate = false
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
private const val TAG = "EuiccChannelFragmentUtils"
|
||||
|
||||
interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
|
||||
|
||||
|
@ -28,6 +34,27 @@ val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
|
|||
get() =
|
||||
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
|
||||
|
||||
/*
|
||||
* Begin a "tracked" operation where notifications may be generated by the eSIM
|
||||
* Automatically handle any newly generated notification during the operation
|
||||
* if the function "op" returns true.
|
||||
*/
|
||||
suspend fun <T> T.beginTrackedOperation(op: suspend () -> Boolean) where T : Fragment, T : EuiccChannelFragmentMarker =
|
||||
withContext(Dispatchers.IO) {
|
||||
val latestSeq = channel.lpa.notifications.firstOrNull()?.seqNumber ?: 0
|
||||
Log.d(TAG, "Latest notification is $latestSeq before operation")
|
||||
if (op()) {
|
||||
Log.d(TAG, "Operation has requested notification handling")
|
||||
// Note that the exact instance of "channel" might have changed here if reconnected;
|
||||
// so we MUST use the automatic getter for "channel"
|
||||
channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
|
||||
Log.d(TAG, "Handling notification $it")
|
||||
channel.lpa.handleNotification(it.seqNumber)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Operation complete")
|
||||
}
|
||||
|
||||
interface EuiccProfilesChangedListener {
|
||||
fun onEuiccProfilesChanged()
|
||||
}
|
|
@ -20,3 +20,11 @@ fun ByteArray.encodeHex(): String {
|
|||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun formatFreeSpace(size: Int): String =
|
||||
// SIM cards probably won't have much more space anytime soon.
|
||||
if (size >= 1024) {
|
||||
"%.2f KiB".format(size.toDouble() / 1024)
|
||||
} else {
|
||||
"$size B"
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
<string name="delete">Delete</string>
|
||||
<string name="rename">Rename</string>
|
||||
|
||||
<string name="enable_disable_timeout">Timed out waiting for the eSIM chip to switch profiles. You might want to restart the application or even the phone.</string>
|
||||
|
||||
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
||||
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
|
||||
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
<string name="purchase_esim">Buy JMP eSIM Adapter</string>
|
||||
<string name="purchase_sim_url" translatable="false">https://jmp.chat/esim-adapter</string>
|
||||
<string name="pref_info_source_code_url" translatable="false">https://gitea.angry.im/jmp-sim/jmp-sim-manager</string>
|
||||
|
||||
<string name="enable_disable_timeout">Timed out waiting for the eSIM chip to switch profiles. Please manually refresh the eSIM adapter by going to SIM Toolkit, and select Tools -> Reboot.</string>
|
||||
</resources>
|
|
@ -15,7 +15,8 @@ open class UnprivilegedOpenEuiccApplication : OpenEuiccApplication() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, _ ->
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
e.printStackTrace()
|
||||
Intent(this, LogsActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
|
|
@ -16,12 +16,16 @@ fun getCompatibilityChecks(context: Context): List<CompatibilityCheck> =
|
|||
HasSystemFeaturesCheck(context),
|
||||
OmapiConnCheck(context),
|
||||
IsdrChannelAccessCheck(context),
|
||||
KnownBrokenCheck(context)
|
||||
KnownBrokenCheck(context),
|
||||
Verdict(context),
|
||||
)
|
||||
|
||||
inline fun <reified T: CompatibilityCheck> List<CompatibilityCheck>.findCheck(): T? =
|
||||
find { it.javaClass == T::class.java }?.let { it as T }
|
||||
|
||||
suspend fun List<CompatibilityCheck>.executeAll(callback: () -> Unit) = withContext(Dispatchers.IO) {
|
||||
forEach {
|
||||
it.run()
|
||||
it.run(this@executeAll)
|
||||
withContext(Dispatchers.Main) {
|
||||
callback()
|
||||
}
|
||||
|
@ -57,13 +61,13 @@ abstract class CompatibilityCheck(context: Context) {
|
|||
else -> defaultDescription
|
||||
}
|
||||
|
||||
protected abstract suspend fun doCheck(): State
|
||||
protected abstract suspend fun doCheck(allChecks: List<CompatibilityCheck>): State
|
||||
|
||||
suspend fun run() {
|
||||
suspend fun run(allChecks: List<CompatibilityCheck>) {
|
||||
state = State.IN_PROGRESS
|
||||
delay(200)
|
||||
state = try {
|
||||
doCheck()
|
||||
doCheck(allChecks)
|
||||
} catch (_: Exception) {
|
||||
State.FAILURE
|
||||
}
|
||||
|
@ -76,7 +80,7 @@ internal class HasSystemFeaturesCheck(private val context: Context): Compatibili
|
|||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_system_features_desc)
|
||||
|
||||
override suspend fun doCheck(): State {
|
||||
override suspend fun doCheck(allChecks: List<CompatibilityCheck>): State {
|
||||
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_system_features_no_telephony)
|
||||
return State.FAILURE
|
||||
|
@ -87,7 +91,7 @@ internal class HasSystemFeaturesCheck(private val context: Context): Compatibili
|
|||
PackageManager.FEATURE_SE_OMAPI_UICC
|
||||
)) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_system_features_no_omapi)
|
||||
return State.FAILURE
|
||||
return State.FAILURE_UNKNOWN
|
||||
}
|
||||
|
||||
return State.SUCCESS
|
||||
|
@ -100,7 +104,7 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck(
|
|||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_omapi_connectivity_desc)
|
||||
|
||||
override suspend fun doCheck(): State {
|
||||
override suspend fun doCheck(allChecks: List<CompatibilityCheck>): State {
|
||||
val seService = connectSEService(context)
|
||||
if (!seService.isConnected) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail)
|
||||
|
@ -132,7 +136,7 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili
|
|||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_isdr_channel_desc)
|
||||
|
||||
override suspend fun doCheck(): State {
|
||||
override suspend fun doCheck(allChecks: List<CompatibilityCheck>): State {
|
||||
val seService = connectSEService(context)
|
||||
val readers = seService.readers.filter { it.isSIM }
|
||||
if (readers.isEmpty()) {
|
||||
|
@ -200,10 +204,57 @@ internal class KnownBrokenCheck(private val context: Context): CompatibilityChec
|
|||
failureDescription = context.getString(R.string.compatibility_check_known_broken_fail)
|
||||
}
|
||||
|
||||
override suspend fun doCheck(): State =
|
||||
override suspend fun doCheck(allChecks: List<CompatibilityCheck>): State =
|
||||
if (Build.MANUFACTURER.lowercase() in BROKEN_MANUFACTURERS) {
|
||||
State.FAILURE
|
||||
} else {
|
||||
State.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
internal class Verdict(private val context: Context) : CompatibilityCheck(context) {
|
||||
override val title: String
|
||||
get() = context.getString(R.string.compatibility_check_verdict)
|
||||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_verdict_desc)
|
||||
|
||||
override suspend fun doCheck(allChecks: List<CompatibilityCheck>): State {
|
||||
if (allChecks.findCheck<KnownBrokenCheck>()?.state == State.FAILURE) {
|
||||
failureDescription = context.getString(
|
||||
R.string.compatibility_check_verdict_known_broken,
|
||||
context.getString(R.string.compatibility_check_verdict_fail_shared)
|
||||
)
|
||||
return State.FAILURE
|
||||
}
|
||||
|
||||
if (allChecks.findCheck<OmapiConnCheck>()?.state == State.SUCCESS &&
|
||||
allChecks.findCheck<IsdrChannelAccessCheck>()?.state == State.SUCCESS
|
||||
) {
|
||||
successDescription = context.getString(R.string.compatibility_check_verdict_ok)
|
||||
return State.SUCCESS
|
||||
}
|
||||
|
||||
if (allChecks.findCheck<OmapiConnCheck>()?.state == State.FAILURE_UNKNOWN ||
|
||||
allChecks.findCheck<IsdrChannelAccessCheck>()?.state == State.FAILURE_UNKNOWN
|
||||
) {
|
||||
// We are not sure because we can't fully check OMAPI
|
||||
// however we can guess based on feature flags
|
||||
// TODO: We probably need a "known-good" list for these devices as well?
|
||||
failureDescription = context.getString(
|
||||
if (allChecks.findCheck<HasSystemFeaturesCheck>()?.state == State.SUCCESS) {
|
||||
R.string.compatibility_check_verdict_unknown_likely_ok
|
||||
} else {
|
||||
R.string.compatibility_check_verdict_unknown_likely_fail
|
||||
},
|
||||
context.getString(R.string.compatibility_check_verdict_fail_shared)
|
||||
)
|
||||
return State.FAILURE_UNKNOWN
|
||||
}
|
||||
|
||||
failureDescription = context.getString(
|
||||
R.string.compatibility_check_verdict_unknown,
|
||||
context.getString(R.string.compatibility_check_verdict_fail_shared)
|
||||
)
|
||||
return State.FAILURE_UNKNOWN
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<string name="compatibility_check_system_features">System Features</string>
|
||||
<string name="compatibility_check_system_features_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string>
|
||||
<string name="compatibility_check_system_features_no_telephony">Your device has no telephony features.</string>
|
||||
<string name="compatibility_check_system_features_no_omapi">Your device has no support for accessing SIM cards via OMAPI. If you are using a custom ROM, consider contacting the developer to determine whether it is due to hardware or a missing feature declaration in the OS.</string>
|
||||
<string name="compatibility_check_system_features_no_omapi">Your device / system does not declare support for OMAPI. This could be due to missing support from hardware, or it could be simply due to a missing flag. See the following two checks to determine whether OMAPI is actually supported or not.</string>
|
||||
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
|
||||
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
|
||||
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string>
|
||||
|
@ -15,7 +15,15 @@
|
|||
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
|
||||
<string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string>
|
||||
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: %s.</string>
|
||||
<string name="compatibility_check_known_broken">Known Broken?</string>
|
||||
<string name="compatibility_check_known_broken">Not on the Known Broken List</string>
|
||||
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
|
||||
<string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string>
|
||||
<string name="compatibility_check_verdict">Verdict</string>
|
||||
<string name="compatibility_check_verdict_desc">Based on all previous checks, how likely is your device to be compatible with removable eSIMs?</string>
|
||||
<string name="compatibility_check_verdict_ok">You can likely use and manage removable eSIMs on this device.</string>
|
||||
<string name="compatibility_check_verdict_known_broken">Your device is known to be buggy when accessing removable eSIMs.\n%s</string>
|
||||
<string name="compatibility_check_verdict_unknown_likely_ok">We cannot determine whether removable eSIMs can be managed on your device. Your device does declare support for OMAPI, though, so it is slightly more likely that it will work.\n%s</string>
|
||||
<string name="compatibility_check_verdict_unknown_likely_fail">We cannot determine whether removable eSIMs can be managed on your device. Since your device does not declare support for OMAPI, it is more likely that managing removable eSIMs on this device is unsupported.\n%s</string>
|
||||
<string name="compatibility_check_verdict_unknown">We cannot determine whether removable eSIMs can be managed on your device.\n%s</string>
|
||||
<string name="compatibility_check_verdict_fail_shared">However, a removable eSIM that has already been loaded with an eSIM profile will still work; this verdict is only for management features via the app, such as switching and downloading profiles.</string>
|
||||
</resources>
|
|
@ -11,6 +11,11 @@ class TelephonyManagerApduInterface(
|
|||
): ApduInterface {
|
||||
private var lastChannel: Int = -1
|
||||
|
||||
override val valid: Boolean
|
||||
// TelephonyManager channels will never become truly "invalid",
|
||||
// just that transactions might return errors or nonsense
|
||||
get() = lastChannel != -1
|
||||
|
||||
override fun connect() {
|
||||
// Do nothing
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package im.angry.openeuicc.service
|
||||
|
||||
import android.os.Build
|
||||
import android.service.euicc.*
|
||||
import android.telephony.UiccSlotMapping
|
||||
import android.telephony.euicc.DownloadableSubscription
|
||||
|
@ -34,6 +35,10 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
|
|||
lpa.profiles.any { it.iccid == iccid }
|
||||
|
||||
private fun ensurePortIsMapped(slotId: Int, portId: Int) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
return
|
||||
}
|
||||
|
||||
val mappings = telephonyManager.simSlotMapping.toMutableList()
|
||||
|
||||
mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId }?.let {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
|
@ -11,6 +12,10 @@ class PrivilegedMainActivity : MainActivity() {
|
|||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.activity_main_privileged, menu)
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
menu.findItem(R.id.slot_mapping).isVisible = false
|
||||
}
|
||||
|
||||
if (tm.supportsDSDS) {
|
||||
val dsds = menu.findItem(R.id.dsds)
|
||||
dsds.isVisible = true
|
||||
|
|
|
@ -9,4 +9,11 @@ interface ApduInterface {
|
|||
fun logicalChannelOpen(aid: ByteArray): Int
|
||||
fun logicalChannelClose(handle: Int)
|
||||
fun transmit(tx: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Is this APDU connection still valid?
|
||||
* Note that even if this returns true, the underlying connection might be broken anyway;
|
||||
* callers should further check with the LPA to fully determine the validity of a channel
|
||||
*/
|
||||
val valid: Boolean
|
||||
}
|
|
@ -1,24 +1,7 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface LocalProfileAssistant {
|
||||
companion object {
|
||||
private const val TAG = "LocalProfileAssistant"
|
||||
}
|
||||
|
||||
val valid: Boolean
|
||||
get() = try {
|
||||
// If we can read both eID and profiles properly, we are likely looking at
|
||||
// a valid LocalProfileAssistant
|
||||
eID
|
||||
profiles
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
val profiles: List<LocalProfileInfo>
|
||||
val notifications: List<LocalProfileNotification>
|
||||
val eID: String
|
||||
|
@ -27,8 +10,8 @@ interface LocalProfileAssistant {
|
|||
|
||||
// All blocking functions in this class assume that they are executed on non-Main threads
|
||||
// The IO context in Kotlin's coroutine library is recommended.
|
||||
fun enableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean
|
||||
fun disableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean
|
||||
fun enableProfile(iccid: String): Boolean
|
||||
fun disableProfile(iccid: String): Boolean
|
||||
fun deleteProfile(iccid: String): Boolean
|
||||
|
||||
fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
|
||||
|
@ -37,24 +20,6 @@ interface LocalProfileAssistant {
|
|||
fun deleteNotification(seqNumber: Long): Boolean
|
||||
fun handleNotification(seqNumber: Long): Boolean
|
||||
|
||||
// Wraps an operation on the eSIM chip (any of the other blocking functions)
|
||||
// Handles notifications automatically after the operation, unless the lambda executing
|
||||
// the operation returns false, which inhibits automatic notification processing.
|
||||
// All code executed within are also wrapped automatically in the IO context.
|
||||
suspend fun beginOperation(op: suspend LocalProfileAssistant.() -> Boolean) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val latestSeq = notifications.firstOrNull()?.seqNumber ?: 0
|
||||
Log.d(TAG, "Latest notification is $latestSeq before operation")
|
||||
if (op(this@LocalProfileAssistant)) {
|
||||
Log.d(TAG, "Operation has requested notification handling")
|
||||
notifications.filter { it.seqNumber > latestSeq }.forEach {
|
||||
Log.d(TAG, "Handling notification $it")
|
||||
handleNotification(it.seqNumber)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Operation complete")
|
||||
}
|
||||
|
||||
fun setNickname(
|
||||
iccid: String, nickname: String
|
||||
): Boolean
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
package net.typeblog.lpac_jni.impl
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import net.typeblog.lpac_jni.LpacJni
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.EuiccInfo2
|
||||
|
@ -15,12 +12,13 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback
|
|||
|
||||
class LocalProfileAssistantImpl(
|
||||
private val apduInterface: ApduInterface,
|
||||
private val httpInterface: HttpInterface
|
||||
httpInterface: HttpInterface
|
||||
): LocalProfileAssistant {
|
||||
companion object {
|
||||
private const val TAG = "LocalProfileAssistantImpl"
|
||||
}
|
||||
|
||||
private var finalized = false
|
||||
private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface)
|
||||
init {
|
||||
if (LpacJni.euiccInit(contextHandle) < 0) {
|
||||
|
@ -31,46 +29,15 @@ class LocalProfileAssistantImpl(
|
|||
httpInterface.usePublicKeyIds(pkids)
|
||||
}
|
||||
|
||||
private fun tryReconnect(timeoutMillis: Long) = runBlocking {
|
||||
withTimeout(timeoutMillis) {
|
||||
try {
|
||||
LpacJni.euiccFini(contextHandle)
|
||||
LpacJni.destroyContext(contextHandle)
|
||||
contextHandle = -1
|
||||
override val valid: Boolean
|
||||
get() = !finalized && apduInterface.valid && try {
|
||||
// If we can read both eID and profiles properly, we are likely looking at
|
||||
// a valid LocalProfileAssistant
|
||||
eID
|
||||
profiles
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
apduInterface.disconnect()
|
||||
} catch (e: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
|
||||
try {
|
||||
apduInterface.connect()
|
||||
contextHandle = LpacJni.createContext(apduInterface, httpInterface)
|
||||
check(LpacJni.euiccInit(contextHandle) >= 0) { "Reconnect attempt failed" }
|
||||
// Validate that we can actually use the APDU channel by trying to read eID and profiles
|
||||
check(valid) { "Reconnected channel is invalid" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (contextHandle != -1L) {
|
||||
try {
|
||||
LpacJni.euiccFini(contextHandle)
|
||||
LpacJni.destroyContext(contextHandle)
|
||||
contextHandle = -1
|
||||
} catch (e: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
// continue retrying
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
override val profiles: List<LocalProfileInfo>
|
||||
|
@ -87,29 +54,11 @@ class LocalProfileAssistantImpl(
|
|||
override val euiccInfo2: EuiccInfo2?
|
||||
get() = LpacJni.es10cexGetEuiccInfo2(contextHandle)
|
||||
|
||||
override fun enableProfile(iccid: String, reconnectTimeout: Long): Boolean {
|
||||
val res = LpacJni.es10cEnableProfile(contextHandle, iccid) == 0
|
||||
if (reconnectTimeout > 0) {
|
||||
try {
|
||||
tryReconnect(reconnectTimeout)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
override fun enableProfile(iccid: String): Boolean =
|
||||
LpacJni.es10cEnableProfile(contextHandle, iccid) == 0
|
||||
|
||||
override fun disableProfile(iccid: String, reconnectTimeout: Long): Boolean {
|
||||
val res = LpacJni.es10cDisableProfile(contextHandle, iccid) == 0
|
||||
if (reconnectTimeout > 0) {
|
||||
try {
|
||||
tryReconnect(reconnectTimeout)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
override fun disableProfile(iccid: String): Boolean =
|
||||
LpacJni.es10cDisableProfile(contextHandle, iccid) == 0
|
||||
|
||||
override fun deleteProfile(iccid: String): Boolean {
|
||||
return LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
|
||||
|
@ -134,8 +83,12 @@ class LocalProfileAssistantImpl(
|
|||
return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun close() {
|
||||
if (!finalized) {
|
||||
LpacJni.euiccFini(contextHandle)
|
||||
LpacJni.destroyContext(contextHandle)
|
||||
finalized = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit dc09c3e668fc191694e73fede255a7a0a26f4988
|
||||
Subproject commit 0bb196977e7f2e276a1076734897ad8c48d9d5ca
|
Loading…
Add table
Reference in a new issue