ソースを参照

Move profile switching to use the new foreground task flow

Peter Cai 1 年間 前
コミット
479e0ff34a

+ 54 - 0
app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt

@@ -13,6 +13,7 @@ import im.angry.openeuicc.common.R
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -309,4 +310,57 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
                 preferenceRepository.notificationDeleteFlow.first()
             }
         }
+
+    class SwitchingProfilesRefreshException : Exception()
+
+    fun launchProfileSwitchTask(
+        slotId: Int,
+        portId: Int,
+        iccid: String,
+        enable: Boolean, // Enable or disable the profile indicated in iccid
+        reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers
+    ): Flow<ForegroundTaskState>? =
+        launchForegroundTask(
+            getString(R.string.task_profile_switch),
+            R.drawable.ic_task_switch
+        ) {
+            euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) {
+                val channel = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
+                val (res, refreshed) =
+                    if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
+                        // Sometimes, we *can* enable or disable the profile, but we cannot
+                        // send the refresh command to the modem because the profile somehow
+                        // makes the modem "busy". In this case, we can still switch by setting
+                        // refresh to false, but then the switch cannot take effect until the
+                        // user resets the modem manually by toggling airplane mode or rebooting.
+                        Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
+                    } else {
+                        Pair(true, true)
+                    }
+
+                if (!res) {
+                    throw RuntimeException("Could not switch profile")
+                }
+
+                if (!refreshed) {
+                    // We may have switched the profile, but we could not refresh. Tell the caller about this
+                    throw SwitchingProfilesRefreshException()
+                }
+
+                if (reconnectTimeoutMillis > 0) {
+                    // Add an unconditional delay first to account for any race condition between
+                    // the card sending the refresh command and the modem actually refreshing
+                    delay(reconnectTimeoutMillis / 10)
+
+                    // This throws TimeoutCancellationException if timed out
+                    euiccChannelManager.waitForReconnect(
+                        slotId,
+                        portId,
+                        reconnectTimeoutMillis / 10 * 9
+                    )
+                }
+
+                preferenceRepository.notificationSwitchFlow.first()
+            }
+        }
 }

+ 53 - 54
app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -30,12 +30,12 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 import com.google.android.material.floatingactionbutton.FloatingActionButton
 import net.typeblog.lpac_jni.LocalProfileInfo
 import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.service.EuiccChannelManagerService
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.last
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -176,67 +176,47 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
         }
     }
 
+    private suspend fun showSwitchFailureText() = withContext(Dispatchers.Main) {
+        Toast.makeText(
+            context,
+            R.string.toast_profile_enable_failed,
+            Toast.LENGTH_LONG
+        ).show()
+    }
+
     private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
         swipeRefresh.isRefreshing = true
         fab.isEnabled = false
 
         lifecycleScope.launch {
-            beginTrackedOperation {
-                val (res, refreshed) =
-                    if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
-                        // Sometimes, we *can* enable or disable the profile, but we cannot
-                        // send the refresh command to the modem because the profile somehow
-                        // makes the modem "busy". In this case, we can still switch by setting
-                        // refresh to false, but then the switch cannot take effect until the
-                        // user resets the modem manually by toggling airplane mode or rebooting.
-                        Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
-                    } else {
-                        Pair(true, true)
-                    }
+            ensureEuiccChannelManager()
+            euiccChannelManagerService.waitForForegroundTask()
 
-                if (!res) {
-                    Log.d(TAG, "Failed to enable / disable profile $iccid")
-                    withContext(Dispatchers.Main) {
-                        Toast.makeText(
-                            context,
-                            R.string.toast_profile_enable_failed,
-                            Toast.LENGTH_LONG
-                        ).show()
-                    }
-                    return@beginTrackedOperation false
+            val res = euiccChannelManagerService.launchProfileSwitchTask(
+                slotId,
+                portId,
+                iccid,
+                enable,
+                reconnectTimeoutMillis = if (isUsb) {
+                    0
+                } else {
+                    30 * 1000
                 }
+            )?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done
 
-                if (!refreshed && !isUsb) {
-                    withContext(Dispatchers.Main) {
-                        AlertDialog.Builder(requireContext()).apply {
-                            setMessage(R.string.switch_did_not_refresh)
-                            setPositiveButton(android.R.string.ok) { dialog, _ ->
-                                dialog.dismiss()
-                                requireActivity().finish()
-                            }
-                            setOnDismissListener { _ ->
-                                requireActivity().finish()
-                            }
-                            show()
-                        }
-                    }
-                    return@beginTrackedOperation true
-                }
+            if (res == null) {
+                showSwitchFailureText()
+                return@launch
+            }
 
-                if (!isUsb) {
-                    try {
-                        euiccChannelManager.waitForReconnect(
-                            slotId,
-                            portId,
-                            timeoutMillis = 30 * 1000
-                        )
-                    } catch (e: TimeoutCancellationException) {
+            when (res.error) {
+                null -> {}
+                is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
+                    // This is only really fatal for internal eSIMs
+                    if (!isUsb) {
                         withContext(Dispatchers.Main) {
-                            // Prevent this Fragment from being used again
-                            invalid = true
-                            // 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)
+                                setMessage(R.string.switch_did_not_refresh)
                                 setPositiveButton(android.R.string.ok) { dialog, _ ->
                                     dialog.dismiss()
                                     requireActivity().finish()
@@ -247,12 +227,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
                                 show()
                             }
                         }
-                        return@beginTrackedOperation false
                     }
                 }
 
-                preferenceRepository.notificationSwitchFlow.first()
+                is TimeoutCancellationException -> {
+                    withContext(Dispatchers.Main) {
+                        // Prevent this Fragment from being used again
+                        invalid = true
+                        // 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()
+                        }
+                    }
+                }
+
+                else -> showSwitchFailureText()
             }
+
             refresh()
             fab.isEnabled = true
         }

+ 5 - 0
app-common/src/main/res/drawable/ic_task_switch.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+    
+</vector>

+ 1 - 0
app-common/src/main/res/values/strings.xml

@@ -37,6 +37,7 @@
     <string name="task_profile_download">Downloading eSIM profile</string>
     <string name="task_profile_rename">Renaming eSIM profile</string>
     <string name="task_profile_delete">Deleting eSIM profile</string>
+    <string name="task_profile_switch">Switching eSIM profile</string>
 
     <string name="profile_download">New eSIM</string>
     <string name="profile_download_server">Server (RSP / SM-DP+)</string>