ソースを参照

refactor: Wrap EuiccChannelManager in an Android Service instance

This allows MUCH better lifecycle control over EuiccChannelManager. We
no longer have to keep all opened APDU channels open until the
application is destroyed. Instead, they can be closed as long as no
component is bound to this Service instance.

A catch is that other long-running services must bind to this service
as-needed, otherwise a binding is going to keep the service always
alive. This only affects the EuiccService implementation, and a
suspending/blocking helper function is added to deal with this case.
Peter Cai 1 年間 前
コミット
59f3597874
19 ファイル変更256 行追加49 行削除
  1. 4 0
      app-common/src/main/AndroidManifest.xml
  2. 10 0
      app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManagerFactory.kt
  3. 7 0
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManagerFactory.kt
  4. 2 0
      app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt
  5. 6 0
      app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt
  6. 41 0
      app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt
  7. 48 0
      app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt
  8. 2 5
      app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt
  9. 7 7
      app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt
  10. 3 2
      app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt
  11. 2 1
      app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt
  12. 4 1
      app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt
  13. 1 5
      app-common/src/main/java/im/angry/openeuicc/util/Utils.kt
  14. 4 1
      app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt
  15. 10 0
      app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManagerFactory.kt
  16. 6 0
      app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt
  17. 67 25
      app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt
  18. 5 2
      app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt
  19. 27 0
      app/src/main/java/im/angry/openeuicc/util/PrivilegedUtils.kt

+ 4 - 0
app-common/src/main/AndroidManifest.xml

@@ -28,5 +28,9 @@
             android:name="com.journeyapps.barcodescanner.CaptureActivity"
             android:screenOrientation="fullSensor"
             tools:replace="screenOrientation" />
+
+        <service
+            android:name="im.angry.openeuicc.service.EuiccChannelManagerService"
+            android:exported="false" />
     </application>
 </manifest>

+ 10 - 0
app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManagerFactory.kt

@@ -0,0 +1,10 @@
+package im.angry.openeuicc.core
+
+import android.app.Service
+import im.angry.openeuicc.di.AppContainer
+
+class DefaultEuiccChannelManagerFactory(private val appContainer: AppContainer) :
+    EuiccChannelManagerFactory {
+    override fun createEuiccChannelManager(serviceContext: Service) =
+        DefaultEuiccChannelManager(appContainer, serviceContext)
+}

+ 7 - 0
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManagerFactory.kt

@@ -0,0 +1,7 @@
+package im.angry.openeuicc.core
+
+import android.app.Service
+
+interface EuiccChannelManagerFactory {
+    fun createEuiccChannelManager(serviceContext: Service): EuiccChannelManager
+}

+ 2 - 0
app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt

@@ -4,11 +4,13 @@ import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
 import im.angry.openeuicc.core.EuiccChannelFactory
 import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.core.EuiccChannelManagerFactory
 import im.angry.openeuicc.util.*
 
 interface AppContainer {
     val telephonyManager: TelephonyManager
     val euiccChannelManager: EuiccChannelManager
+    val euiccChannelManagerFactory: EuiccChannelManagerFactory
     val subscriptionManager: SubscriptionManager
     val preferenceRepository: PreferenceRepository
     val uiComponentFactory: UiComponentFactory

+ 6 - 0
app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt

@@ -5,7 +5,9 @@ import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
 import im.angry.openeuicc.core.DefaultEuiccChannelFactory
 import im.angry.openeuicc.core.DefaultEuiccChannelManager
+import im.angry.openeuicc.core.DefaultEuiccChannelManagerFactory
 import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.core.EuiccChannelManagerFactory
 import im.angry.openeuicc.util.*
 
 open class DefaultAppContainer(context: Context) : AppContainer {
@@ -17,6 +19,10 @@ open class DefaultAppContainer(context: Context) : AppContainer {
         DefaultEuiccChannelManager(this, context)
     }
 
+    override val euiccChannelManagerFactory: EuiccChannelManagerFactory by lazy {
+        DefaultEuiccChannelManagerFactory(this)
+    }
+
     override val subscriptionManager by lazy {
         context.getSystemService(SubscriptionManager::class.java)!!
     }

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

@@ -0,0 +1,41 @@
+package im.angry.openeuicc.service
+
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.util.*
+
+/**
+ * An Android Service wrapper for EuiccChannelManager.
+ * The purpose of this wrapper is mainly lifecycle-wise: having a Service allows the manager
+ * instance to have its own independent lifecycle. This way it can be created as requested and
+ * destroyed when no other components are bound to this service anymore.
+ * This behavior allows us to avoid keeping the APDU channels open at all times. For example,
+ * the EuiccService implementation should *only* bind to this service when it requires an
+ * instance of EuiccChannelManager. UI components can keep being bound to this service for
+ * their entire lifecycles, since the whole purpose of them is to expose the current state
+ * to the user.
+ */
+class EuiccChannelManagerService : Service(), OpenEuiccContextMarker {
+    inner class LocalBinder : Binder() {
+        val service = this@EuiccChannelManagerService
+    }
+
+    private val euiccChannelManagerDelegate = lazy {
+        appContainer.euiccChannelManagerFactory.createEuiccChannelManager(this)
+    }
+    val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
+
+    override fun onBind(intent: Intent?): IBinder = LocalBinder()
+
+    override fun onDestroy() {
+        super.onDestroy()
+        // This is the whole reason of the existence of this service:
+        // we can clean up opened channels when no one is using them
+        if (euiccChannelManagerDelegate.isInitialized()) {
+            euiccChannelManager.invalidate()
+        }
+    }
+}

+ 48 - 0
app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt

@@ -0,0 +1,48 @@
+package im.angry.openeuicc.ui
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Bundle
+import android.os.IBinder
+import androidx.appcompat.app.AppCompatActivity
+import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.service.EuiccChannelManagerService
+
+abstract class BaseEuiccAccessActivity : AppCompatActivity() {
+    lateinit var euiccChannelManager: EuiccChannelManager
+
+    private val euiccChannelManagerServiceConnection = object : ServiceConnection {
+        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+            euiccChannelManager =
+                (service!! as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager
+            onInit()
+        }
+
+        override fun onServiceDisconnected(name: ComponentName?) {
+            // These activities should never lose the EuiccChannelManagerService connection
+            finish()
+        }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        bindService(
+            Intent(this, EuiccChannelManagerService::class.java),
+            euiccChannelManagerServiceConnection,
+            Context.BIND_AUTO_CREATE
+        )
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        unbindService(euiccChannelManagerServiceConnection)
+    }
+
+    /**
+     * When called, euiccChannelManager is guaranteed to have been initialized
+     */
+    abstract fun onInit()
+}

+ 2 - 5
app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt

@@ -1,16 +1,13 @@
 package im.angry.openeuicc.ui
 
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
-class DirectProfileDownloadActivity : AppCompatActivity(), SlotSelectFragment.SlotSelectedListener, OpenEuiccContextMarker {
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
+class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragment.SlotSelectedListener, OpenEuiccContextMarker {
+    override fun onInit() {
         lifecycleScope.launch {
             withContext(Dispatchers.IO) {
                 euiccChannelManager.enumerateEuiccChannels()

+ 7 - 7
app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt

@@ -10,16 +10,14 @@ import android.view.View
 import android.widget.AdapterView
 import android.widget.ArrayAdapter
 import android.widget.Spinner
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
-open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
+open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     companion object {
         const val TAG = "MainActivity"
     }
@@ -45,10 +43,6 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
         tm = telephonyManager
 
         spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
-
-        lifecycleScope.launch {
-            init()
-        }
     }
 
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -94,6 +88,12 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
             else -> super.onOptionsItemSelected(item)
         }
 
+    override fun onInit() {
+        lifecycleScope.launch {
+            init()
+        }
+    }
+
     private suspend fun init() {
         withContext(Dispatchers.IO) {
             euiccChannelManager.enumerateEuiccChannels()

+ 3 - 2
app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt

@@ -12,7 +12,6 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
 import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.forEach
 import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.DividerItemDecoration
@@ -27,7 +26,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import net.typeblog.lpac_jni.LocalProfileNotification
 
-class NotificationsActivity: AppCompatActivity(), OpenEuiccContextMarker {
+class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     private lateinit var swipeRefresh: SwipeRefreshLayout
     private lateinit var notificationList: RecyclerView
     private val notificationAdapter = NotificationAdapter()
@@ -39,7 +38,9 @@ class NotificationsActivity: AppCompatActivity(), OpenEuiccContextMarker {
         setContentView(R.layout.activity_notifications)
         setSupportActionBar(requireViewById(R.id.toolbar))
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+    }
 
+    override fun onInit() {
         euiccChannel = euiccChannelManager
             .findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!!
 

+ 2 - 1
app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt

@@ -29,7 +29,8 @@ class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker
     private lateinit var toolbar: Toolbar
     private lateinit var spinner: Spinner
     private val channels: List<EuiccChannel> by lazy {
-        euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }
+        (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
+            .knownChannels.sortedBy { it.logicalSlotId }
     }
 
     override fun onCreateView(

+ 4 - 1
app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt

@@ -4,9 +4,10 @@ import android.os.Bundle
 import android.util.Log
 import androidx.fragment.app.Fragment
 import im.angry.openeuicc.core.EuiccChannel
+import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.ui.BaseEuiccAccessActivity
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
-import net.typeblog.lpac_jni.LocalProfileAssistant
 
 private const val TAG = "EuiccChannelFragmentUtils"
 
@@ -30,6 +31,8 @@ val <T> T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker
 val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
     get() = requireArguments().getInt("portId")
 
+val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
+    get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
 val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
     get() =
         euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!

+ 1 - 5
app-common/src/main/java/im/angry/openeuicc/util/Utils.kt

@@ -7,7 +7,6 @@ import android.telephony.TelephonyManager
 import androidx.fragment.app.Fragment
 import im.angry.openeuicc.OpenEuiccApplication
 import im.angry.openeuicc.core.EuiccChannel
-import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.di.AppContainer
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -15,7 +14,7 @@ import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import net.typeblog.lpac_jni.LocalProfileInfo
-import java.lang.RuntimeException
+import kotlin.RuntimeException
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
 import kotlin.coroutines.suspendCoroutine
@@ -52,9 +51,6 @@ interface OpenEuiccContextMarker {
     val appContainer: AppContainer
         get() = openEuiccApplication.appContainer
 
-    val euiccChannelManager: EuiccChannelManager
-        get() = appContainer.euiccChannelManager
-
     val telephonyManager: TelephonyManager
         get() = appContainer.telephonyManager
 }

+ 4 - 1
app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt

@@ -5,7 +5,10 @@ import im.angry.openeuicc.di.AppContainer
 import im.angry.openeuicc.util.*
 import java.lang.Exception
 
-class PrivilegedEuiccChannelManager(appContainer: AppContainer, context: Context) :
+class PrivilegedEuiccChannelManager(
+    appContainer: AppContainer,
+    context: Context
+) :
     DefaultEuiccChannelManager(appContainer, context) {
     override val uiccCards: Collection<UiccCardInfoCompat>
         get() = tm.uiccCardsInfoCompat

+ 10 - 0
app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManagerFactory.kt

@@ -0,0 +1,10 @@
+package im.angry.openeuicc.core
+
+import android.app.Service
+import im.angry.openeuicc.di.AppContainer
+
+class PrivilegedEuiccChannelManagerFactory(private val appContainer: AppContainer) :
+    EuiccChannelManagerFactory {
+    override fun createEuiccChannelManager(serviceContext: Service): EuiccChannelManager =
+        PrivilegedEuiccChannelManager(appContainer, serviceContext)
+}

+ 6 - 0
app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt

@@ -2,14 +2,20 @@ package im.angry.openeuicc.di
 
 import android.content.Context
 import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.core.EuiccChannelManagerFactory
 import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
 import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
+import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory
 
 class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
     override val euiccChannelManager: EuiccChannelManager by lazy {
         PrivilegedEuiccChannelManager(this, context)
     }
 
+    override val euiccChannelManagerFactory: EuiccChannelManagerFactory by lazy {
+        PrivilegedEuiccChannelManagerFactory(this)
+    }
+
     override val uiComponentFactory by lazy {
         PrivilegedUiComponentFactory()
     }

+ 67 - 25
app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt

@@ -1,5 +1,7 @@
 package im.angry.openeuicc.service
 
+import android.content.Context
+import android.content.Intent
 import android.os.Build
 import android.service.euicc.*
 import android.telephony.UiccSlotMapping
@@ -8,7 +10,9 @@ import android.telephony.euicc.EuiccInfo
 import android.util.Log
 import net.typeblog.lpac_jni.LocalProfileInfo
 import im.angry.openeuicc.core.EuiccChannel
+import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.util.*
+import kotlinx.coroutines.runBlocking
 import java.lang.IllegalStateException
 
 class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
@@ -31,17 +35,51 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
             telephonyManager.uiccCardsInfoCompat.firstOrNull { it.isEuicc }?.physicalSlotIndex == physicalSlotId
         }
 
-    private fun findChannel(physicalSlotId: Int): EuiccChannel? =
-        euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
+    private data class EuiccChannelManagerContext(
+        val euiccChannelManager: EuiccChannelManager
+    ) {
+        fun findChannel(physicalSlotId: Int): EuiccChannel? =
+            euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
+
+        fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
+            euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
+
+        fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
+            euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
+    }
 
-    private fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
-        euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
+    /**
+     * Bind to EuiccChannelManagerService, run the callback with a EuiccChannelManager instance,
+     * and then unbind after the callback is finished. All methods in this class that require access
+     * to a EuiccChannelManager should be wrapped inside this call.
+     *
+     * This ensures that we only spawn and connect to APDU channels when we absolutely need to,
+     * instead of keeping them open unnecessarily in the background at all times.
+     */
+    private inline fun <T> withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T {
+        val (binder, unbind) = runBlocking {
+            bindServiceSuspended(
+                Intent(
+                    this@OpenEuiccService,
+                    EuiccChannelManagerService::class.java
+                ), Context.BIND_AUTO_CREATE
+            )
+        }
 
-    private fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
-        euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
+        if (binder == null) {
+            throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting")
+        }
 
-    override fun onGetEid(slotId: Int): String? =
+        val ret =
+            EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn()
+
+        unbind()
+        return ret
+    }
+
+    override fun onGetEid(slotId: Int): String? = withEuiccChannelManager {
         findChannel(slotId)?.lpa?.eID
+    }
 
     // When two eSIM cards are present on one device, the Android settings UI
     // gets confused and sets the incorrect slotId for profiles from one of
@@ -124,7 +162,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
         return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf())
     }
 
-    override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult {
+    override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = withEuiccChannelManager {
         Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
         if (shouldIgnoreSlot(slotId)) {
             Log.i(TAG, "ignoring slot $slotId")
@@ -165,7 +203,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
         return EuiccInfo("Unknown") // TODO: Can we actually implement this?
     }
 
-    override fun onDeleteSubscription(slotId: Int, iccid: String): Int {
+    override fun onDeleteSubscription(slotId: Int, iccid: String): Int = withEuiccChannelManager {
         Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid")
         if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
 
@@ -212,7 +250,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
         portIndex: Int,
         iccid: String?,
         forceDeactivateSim: Boolean
-    ): Int {
+    ): Int = withEuiccChannelManager {
         Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim")
         if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
 
@@ -264,22 +302,26 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
         }
     }
 
-    override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int {
-        Log.i(TAG, "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname")
-        if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
-        val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
-        if (!channel.profileExists(iccid)) {
-            return RESULT_FIRST_USER
-        }
-        val success = channel.lpa
-            .setNickname(iccid, nickname!!)
-        appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
-        return if (success) {
-            RESULT_OK
-        } else {
-            RESULT_FIRST_USER
+    override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int =
+        withEuiccChannelManager {
+            Log.i(
+                TAG,
+                "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname"
+            )
+            if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
+            val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
+            if (!channel.profileExists(iccid)) {
+                return RESULT_FIRST_USER
+            }
+            val success = channel.lpa
+                .setNickname(iccid, nickname!!)
+            appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
+            return if (success) {
+                RESULT_OK
+            } else {
+                RESULT_FIRST_USER
+            }
         }
-    }
 
     @Deprecated("Deprecated in Java")
     override fun onEraseSubscriptions(slotId: Int): Int {

+ 5 - 2
app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt

@@ -96,14 +96,17 @@ class SlotMappingFragment: BaseMaterialDialogFragment(),
                 withContext(Dispatchers.IO) {
                     // Use the utility method from PrivilegedTelephonyUtils to ensure
                     // unmapped ports have all profiles disabled
-                    telephonyManager.updateSimSlotMapping(euiccChannelManager, adapter.mappings)
+                    telephonyManager.updateSimSlotMapping(
+                        (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager,
+                        adapter.mappings
+                    )
                 }
             } catch (e: Exception) {
                 Toast.makeText(requireContext(), R.string.slot_mapping_failure, Toast.LENGTH_LONG).show()
                 return@launch
             }
             Toast.makeText(requireContext(), R.string.slot_mapping_completed, Toast.LENGTH_LONG).show()
-            euiccChannelManager.invalidate()
+            (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager.invalidate()
             requireActivity().finish()
         }
     }

+ 27 - 0
app/src/main/java/im/angry/openeuicc/util/PrivilegedUtils.kt

@@ -0,0 +1,27 @@
+package im.angry.openeuicc.util
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import java.util.concurrent.Executors
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+suspend fun Context.bindServiceSuspended(intent: Intent, flags: Int): Pair<IBinder?, () -> Unit> =
+    suspendCoroutine { cont ->
+        var binder: IBinder?
+        val conn = object : ServiceConnection {
+            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+                binder = service
+                cont.resume(Pair(binder) { unbindService(this) })
+            }
+
+            override fun onServiceDisconnected(name: ComponentName?) {
+
+            }
+        }
+
+        bindService(intent, flags, Executors.newSingleThreadExecutor(), conn)
+    }