Compare commits

..

No commits in common. "14687acc7343e4e4f8ddb1b9dcf5a24f42e5491a" and "02ef31199b8a07028c1dc0249fc42c35d3d12aec" have entirely different histories.

21 changed files with 163 additions and 272 deletions

View file

@ -10,7 +10,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
private var seService: SEService? = null
private suspend fun ensureSEService() {
if (seService == null || !seService!!.isConnected) {
if (seService == null) {
seService = connectSEService(context)
}
}

View file

@ -6,12 +6,10 @@ 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,
@ -89,54 +87,24 @@ open class DefaultEuiccChannelManager(
}
}
override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { 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) }
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return@runBlocking null
}
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
runBlocking {
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")
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
delay(1000)
}
}
}
override suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) {

View file

@ -8,13 +8,6 @@ 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
*/
@ -31,13 +24,11 @@ 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?
/**

View file

@ -13,9 +13,6 @@ 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()
}

View file

@ -16,7 +16,6 @@ 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
@ -27,10 +26,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 {
@ -131,51 +130,35 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
fab.isEnabled = false
lifecycleScope.launch {
beginTrackedOperation {
val res = if (enable) {
channel.lpa.enableProfile(iccid)
} else {
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
}
try {
if (enable) {
preferenceRepository.notificationEnableFlow.first()
doEnableProfile(iccid)
} else {
preferenceRepository.notificationDisableFlow.first()
doDisableProfile(iccid)
}
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()
}
refresh()
fab.isEnabled = true
}
}
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)
if (profile.isEnabled) {

View file

@ -82,7 +82,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
}
}
private suspend fun doDelete() = beginTrackedOperation {
private suspend fun doDelete() = channel.lpa.beginOperation {
channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
preferenceRepository.notificationDeleteFlow.first()
}

View file

@ -5,6 +5,7 @@ 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
@ -130,7 +131,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
// Fetch remaining NVRAM
val str = channel.lpa.euiccInfo2?.freeNvram?.also {
freeNvram = it
}?.let { formatFreeSpace(it) }
}?.let { Formatter.formatShortFileSize(requireContext(), it.toLong()) }
withContext(Dispatchers.Main) {
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space,
@ -188,25 +189,15 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
}
}
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
progress.progress = state.progress
}
private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = channel.lpa.beginOperation {
downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback {
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
lifecycleScope.launch(Dispatchers.Main) {
progress.isIndeterminate = false
progress.progress = state.progress
}
})
}
})
// If we get here, we are successful
// Only send notifications if the user allowed us to

View file

@ -1,14 +1,8 @@
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
@ -34,27 +28,6 @@ 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()
}

View file

@ -19,12 +19,4 @@ fun ByteArray.encodeHex(): String {
sb.append(String.format("%02X", this[i]))
}
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"
}
}

View file

@ -16,8 +16,6 @@
<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>

View file

@ -5,6 +5,4 @@
<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>

View file

@ -15,8 +15,7 @@ open class UnprivilegedOpenEuiccApplication : OpenEuiccApplication() {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler { _, e ->
e.printStackTrace()
Thread.setDefaultUncaughtExceptionHandler { _, _ ->
Intent(this, LogsActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

View file

@ -16,16 +16,12 @@ fun getCompatibilityChecks(context: Context): List<CompatibilityCheck> =
HasSystemFeaturesCheck(context),
OmapiConnCheck(context),
IsdrChannelAccessCheck(context),
KnownBrokenCheck(context),
Verdict(context),
KnownBrokenCheck(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(this@executeAll)
it.run()
withContext(Dispatchers.Main) {
callback()
}
@ -61,13 +57,13 @@ abstract class CompatibilityCheck(context: Context) {
else -> defaultDescription
}
protected abstract suspend fun doCheck(allChecks: List<CompatibilityCheck>): State
protected abstract suspend fun doCheck(): State
suspend fun run(allChecks: List<CompatibilityCheck>) {
suspend fun run() {
state = State.IN_PROGRESS
delay(200)
state = try {
doCheck(allChecks)
doCheck()
} catch (_: Exception) {
State.FAILURE
}
@ -80,7 +76,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(allChecks: List<CompatibilityCheck>): State {
override suspend fun doCheck(): State {
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
failureDescription = context.getString(R.string.compatibility_check_system_features_no_telephony)
return State.FAILURE
@ -91,7 +87,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_UNKNOWN
return State.FAILURE
}
return State.SUCCESS
@ -104,7 +100,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(allChecks: List<CompatibilityCheck>): State {
override suspend fun doCheck(): State {
val seService = connectSEService(context)
if (!seService.isConnected) {
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail)
@ -136,7 +132,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(allChecks: List<CompatibilityCheck>): State {
override suspend fun doCheck(): State {
val seService = connectSEService(context)
val readers = seService.readers.filter { it.isSIM }
if (readers.isEmpty()) {
@ -204,57 +200,10 @@ internal class KnownBrokenCheck(private val context: Context): CompatibilityChec
failureDescription = context.getString(R.string.compatibility_check_known_broken_fail)
}
override suspend fun doCheck(allChecks: List<CompatibilityCheck>): State =
override suspend fun doCheck(): 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
}
}

View file

@ -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 / 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_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_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,15 +15,7 @@
<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">Not on the Known Broken List</string>
<string name="compatibility_check_known_broken">Known Broken?</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>

View file

@ -11,11 +11,6 @@ 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
}

View file

@ -1,6 +1,5 @@
package im.angry.openeuicc.service
import android.os.Build
import android.service.euicc.*
import android.telephony.UiccSlotMapping
import android.telephony.euicc.DownloadableSubscription
@ -35,10 +34,6 @@ 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 {

View file

@ -1,6 +1,5 @@
package im.angry.openeuicc.ui
import android.os.Build
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
@ -12,10 +11,6 @@ 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

View file

@ -9,11 +9,4 @@ 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
}

View file

@ -1,7 +1,24 @@
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
@ -10,8 +27,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): Boolean
fun disableProfile(iccid: String): Boolean
fun enableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean
fun disableProfile(iccid: String, reconnectTimeout: Long = 0): Boolean
fun deleteProfile(iccid: String): Boolean
fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
@ -20,6 +37,24 @@ 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

View file

@ -1,6 +1,9 @@
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
@ -12,13 +15,12 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback
class LocalProfileAssistantImpl(
private val apduInterface: ApduInterface,
httpInterface: HttpInterface
private val 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) {
@ -29,16 +31,47 @@ class LocalProfileAssistantImpl(
httpInterface.usePublicKeyIds(pkids)
}
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) {
false
private fun tryReconnect(timeoutMillis: Long) = runBlocking {
withTimeout(timeoutMillis) {
try {
LpacJni.euiccFini(contextHandle)
LpacJni.destroyContext(contextHandle)
contextHandle = -1
} 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)
}
}
}
}
override val profiles: List<LocalProfileInfo>
get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList()
@ -54,11 +87,29 @@ class LocalProfileAssistantImpl(
override val euiccInfo2: EuiccInfo2?
get() = LpacJni.es10cexGetEuiccInfo2(contextHandle)
override fun enableProfile(iccid: String): Boolean =
LpacJni.es10cEnableProfile(contextHandle, iccid) == 0
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 disableProfile(iccid: String): Boolean =
LpacJni.es10cDisableProfile(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 deleteProfile(iccid: String): Boolean {
return LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
@ -83,12 +134,8 @@ class LocalProfileAssistantImpl(
return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0
}
@Synchronized
override fun close() {
if (!finalized) {
LpacJni.euiccFini(contextHandle)
LpacJni.destroyContext(contextHandle)
finalized = true
}
LpacJni.euiccFini(contextHandle)
LpacJni.destroyContext(contextHandle)
}
}

@ -1 +1 @@
Subproject commit 0bb196977e7f2e276a1076734897ad8c48d9d5ca
Subproject commit dc09c3e668fc191694e73fede255a7a0a26f4988