Compare commits

...

2 commits

Author SHA1 Message Date
451b17d0a5 refactor: Stop exitting the application when enabling/disabling profiles
..instead, attempt to reconnect the LPA. This is needed to properly
send notifications after the operation.
2024-01-01 20:03:44 -05:00
6784eae43b Refactor automatic notification handling
Sometimes there could be more than one notification queued for each
operation.
2024-01-01 18:17:20 -05:00
7 changed files with 103 additions and 45 deletions

View file

@ -127,7 +127,6 @@ open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfi
private fun enableOrDisableProfile(iccid: String, enable: Boolean) { private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
swipeRefresh.isEnabled = false
fab.isEnabled = false fab.isEnabled = false
lifecycleScope.launch { lifecycleScope.launch {
@ -137,34 +136,27 @@ open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfi
} else { } else {
doDisableProfile(iccid) doDisableProfile(iccid)
} }
Toast.makeText(context, R.string.toast_profile_enabled, Toast.LENGTH_LONG).show() refresh()
// The APDU channel will be invalid when the SIM reboots. For now, just exit the app fab.isEnabled = true
euiccChannelManager.invalidate()
requireActivity().finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "Failed to enable / disable profile $iccid") Log.d(TAG, "Failed to enable / disable profile $iccid")
Log.d(TAG, Log.getStackTraceString(e)) Log.d(TAG, Log.getStackTraceString(e))
fab.isEnabled = true fab.isEnabled = true
swipeRefresh.isEnabled = true
Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show()
} }
} }
} }
private suspend fun doEnableProfile(iccid: String) = private suspend fun doEnableProfile(iccid: String) =
withContext(Dispatchers.IO) { channel.lpa.beginOperation {
channel.lpa.enableProfile(iccid) channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) &&
if (preferenceRepository.notificationEnableFlow.first()) { preferenceRepository.notificationEnableFlow.first()
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Enable)
}
} }
private suspend fun doDisableProfile(iccid: String) = private suspend fun doDisableProfile(iccid: String) =
withContext(Dispatchers.IO) { channel.lpa.beginOperation {
channel.lpa.disableProfile(iccid) channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) &&
if (preferenceRepository.notificationDisableFlow.first()) { preferenceRepository.notificationDisableFlow.first()
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Disable)
}
} }
sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {

View file

@ -73,10 +73,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
} }
} }
private suspend fun doDelete() = withContext(Dispatchers.IO) { private suspend fun doDelete() = channel.lpa.beginOperation {
channel.lpa.deleteProfile(requireArguments().getString("iccid")!!) channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
if (preferenceRepository.notificationDeleteFlow.first()) { preferenceRepository.notificationDeleteFlow.first()
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Delete)
}
} }
} }

View file

@ -184,8 +184,8 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
} }
} }
private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = withContext(Dispatchers.IO) { private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = channel.lpa.beginOperation {
channel.lpa.downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback { downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback {
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
progress.isIndeterminate = false progress.isIndeterminate = false
@ -195,8 +195,7 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
}) })
// If we get here, we are successful // If we get here, we are successful
if (preferenceRepository.notificationDownloadFlow.first()) { // Only send notifications if the user allowed us to
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Install) preferenceRepository.notificationDownloadFlow.first()
}
} }
} }

View file

@ -16,7 +16,6 @@
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="rename">Rename</string> <string name="rename">Rename</string>
<string name="toast_profile_enabled">eSIM profile switched. Please wait for a while when the card is restarting.</string>
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</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> <string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>

View file

@ -20,6 +20,7 @@ class TelephonyManagerApduInterface(
override fun disconnect() { override fun disconnect() {
// Do nothing // Do nothing
lastChannel = -1
} }
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {

View file

@ -1,14 +1,24 @@
package net.typeblog.lpac_jni package net.typeblog.lpac_jni
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface LocalProfileAssistant { interface LocalProfileAssistant {
companion object {
private const val TAG = "LocalProfileAssistant"
}
val profiles: List<LocalProfileInfo> val profiles: List<LocalProfileInfo>
val notifications: List<LocalProfileNotification> val notifications: List<LocalProfileNotification>
val eID: String val eID: String
// Extended EuiccInfo for use with LUIs, containing information such as firmware version // Extended EuiccInfo for use with LUIs, containing information such as firmware version
val euiccInfo2: EuiccInfo2? val euiccInfo2: EuiccInfo2?
fun enableProfile(iccid: String): Boolean // All blocking functions in this class assume that they are executed on non-Main threads
fun disableProfile(iccid: String): Boolean // 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 deleteProfile(iccid: String): Boolean fun deleteProfile(iccid: String): Boolean
fun downloadProfile(smdp: String, matchingId: String?, imei: String?, fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
@ -16,9 +26,24 @@ interface LocalProfileAssistant {
fun deleteNotification(seqNumber: Long): Boolean fun deleteNotification(seqNumber: Long): Boolean
fun handleNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): Boolean
// Handle the latest entry of a particular type of notification
// Note that this is not guaranteed to always be reliable and no feedback will be provided on errors. // Wraps an operation on the eSIM chip (any of the other blocking functions)
fun handleLatestNotification(operation: LocalProfileNotification.Operation) // 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( fun setNickname(
iccid: String, nickname: String iccid: String, nickname: String

View file

@ -1,6 +1,9 @@
package net.typeblog.lpac_jni.impl package net.typeblog.lpac_jni.impl
import android.util.Log 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.LpacJni
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.EuiccInfo2 import net.typeblog.lpac_jni.EuiccInfo2
@ -11,20 +14,52 @@ import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.ProfileDownloadCallback
class LocalProfileAssistantImpl( class LocalProfileAssistantImpl(
apduInterface: ApduInterface, private val apduInterface: ApduInterface,
httpInterface: HttpInterface private val httpInterface: HttpInterface
): LocalProfileAssistant { ): LocalProfileAssistant {
companion object { companion object {
private const val TAG = "LocalProfileAssistantImpl" private const val TAG = "LocalProfileAssistantImpl"
} }
private val contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface)
init { init {
if (LpacJni.es10xInit(contextHandle) < 0) { if (LpacJni.es10xInit(contextHandle) < 0) {
throw IllegalArgumentException("Failed to initialize LPA") throw IllegalArgumentException("Failed to initialize LPA")
} }
} }
private fun tryReconnect(timeoutMillis: Long) = runBlocking {
withTimeout(timeoutMillis) {
try {
LpacJni.es10xFini(contextHandle)
LpacJni.destroyContext(contextHandle)
} catch (e: Exception) {
// Ignored
}
while (true) {
try {
apduInterface.disconnect()
} catch (e: Exception) {
// Ignored
}
try {
apduInterface.connect()
contextHandle = LpacJni.createContext(apduInterface, httpInterface)
val res = LpacJni.es10xInit(contextHandle)
Log.d(TAG, "$res")
check(res >= 0) { "Reconnect attempt failed" }
break
} catch (e: Exception) {
e.printStackTrace()
// continue retrying
delay(1000)
}
}
}
}
override val profiles: List<LocalProfileInfo> override val profiles: List<LocalProfileInfo>
get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList() get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList()
@ -39,12 +74,28 @@ class LocalProfileAssistantImpl(
override val euiccInfo2: EuiccInfo2? override val euiccInfo2: EuiccInfo2?
get() = LpacJni.es10cexGetEuiccInfo2(contextHandle) get() = LpacJni.es10cexGetEuiccInfo2(contextHandle)
override fun enableProfile(iccid: String): Boolean { override fun enableProfile(iccid: String, reconnectTimeout: Long): Boolean {
return LpacJni.es10cEnableProfile(contextHandle, iccid) == 0 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 { override fun disableProfile(iccid: String, reconnectTimeout: Long): Boolean {
return LpacJni.es10cDisableProfile(contextHandle, iccid) == 0 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 { override fun deleteProfile(iccid: String): Boolean {
@ -64,13 +115,6 @@ class LocalProfileAssistantImpl(
Log.d(TAG, "handleNotification $seqNumber = $it") Log.d(TAG, "handleNotification $seqNumber = $it")
} == 0 } == 0
override fun handleLatestNotification(operation: LocalProfileNotification.Operation) {
notifications.find { it.profileManagementOperation == operation }?.let {
Log.d(TAG, "handleLatestNotification: $it")
handleNotification(it.seqNumber)
}
}
override fun setNickname(iccid: String, nickname: String): Boolean { override fun setNickname(iccid: String, nickname: String): Boolean {
return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0
} }