Compare commits
2 commits
74f6313e52
...
451b17d0a5
Author | SHA1 | Date | |
---|---|---|---|
451b17d0a5 | |||
6784eae43b |
7 changed files with 103 additions and 45 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue