Browse Source

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 year ago
parent
commit
59f3597874
19 changed files with 256 additions and 49 deletions
  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:name="com.journeyapps.barcodescanner.CaptureActivity"
             android:screenOrientation="fullSensor"
             android:screenOrientation="fullSensor"
             tools:replace="screenOrientation" />
             tools:replace="screenOrientation" />
+
+        <service
+            android:name="im.angry.openeuicc.service.EuiccChannelManagerService"
+            android:exported="false" />
     </application>
     </application>
 </manifest>
 </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 android.telephony.TelephonyManager
 import im.angry.openeuicc.core.EuiccChannelFactory
 import im.angry.openeuicc.core.EuiccChannelFactory
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.core.EuiccChannelManagerFactory
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 
 
 interface AppContainer {
 interface AppContainer {
     val telephonyManager: TelephonyManager
     val telephonyManager: TelephonyManager
     val euiccChannelManager: EuiccChannelManager
     val euiccChannelManager: EuiccChannelManager
+    val euiccChannelManagerFactory: EuiccChannelManagerFactory
     val subscriptionManager: SubscriptionManager
     val subscriptionManager: SubscriptionManager
     val preferenceRepository: PreferenceRepository
     val preferenceRepository: PreferenceRepository
     val uiComponentFactory: UiComponentFactory
     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 android.telephony.TelephonyManager
 import im.angry.openeuicc.core.DefaultEuiccChannelFactory
 import im.angry.openeuicc.core.DefaultEuiccChannelFactory
 import im.angry.openeuicc.core.DefaultEuiccChannelManager
 import im.angry.openeuicc.core.DefaultEuiccChannelManager
+import im.angry.openeuicc.core.DefaultEuiccChannelManagerFactory
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.core.EuiccChannelManagerFactory
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 
 
 open class DefaultAppContainer(context: Context) : AppContainer {
 open class DefaultAppContainer(context: Context) : AppContainer {
@@ -17,6 +19,10 @@ open class DefaultAppContainer(context: Context) : AppContainer {
         DefaultEuiccChannelManager(this, context)
         DefaultEuiccChannelManager(this, context)
     }
     }
 
 
+    override val euiccChannelManagerFactory: EuiccChannelManagerFactory by lazy {
+        DefaultEuiccChannelManagerFactory(this)
+    }
+
     override val subscriptionManager by lazy {
     override val subscriptionManager by lazy {
         context.getSystemService(SubscriptionManager::class.java)!!
         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
 package im.angry.openeuicc.ui
 
 
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 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 {
         lifecycleScope.launch {
             withContext(Dispatchers.IO) {
             withContext(Dispatchers.IO) {
                 euiccChannelManager.enumerateEuiccChannels()
                 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.AdapterView
 import android.widget.ArrayAdapter
 import android.widget.ArrayAdapter
 import android.widget.Spinner
 import android.widget.Spinner
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 
 
-open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
+open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     companion object {
     companion object {
         const val TAG = "MainActivity"
         const val TAG = "MainActivity"
     }
     }
@@ -45,10 +43,6 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
         tm = telephonyManager
         tm = telephonyManager
 
 
         spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
         spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
-
-        lifecycleScope.launch {
-            init()
-        }
     }
     }
 
 
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -94,6 +88,12 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
             else -> super.onOptionsItemSelected(item)
             else -> super.onOptionsItemSelected(item)
         }
         }
 
 
+    override fun onInit() {
+        lifecycleScope.launch {
+            init()
+        }
+    }
+
     private suspend fun init() {
     private suspend fun init() {
         withContext(Dispatchers.IO) {
         withContext(Dispatchers.IO) {
             euiccChannelManager.enumerateEuiccChannels()
             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.view.ViewGroup
 import android.widget.TextView
 import android.widget.TextView
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.forEach
 import androidx.core.view.forEach
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.DividerItemDecoration
@@ -27,7 +26,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 import net.typeblog.lpac_jni.LocalProfileNotification
 import net.typeblog.lpac_jni.LocalProfileNotification
 
 
-class NotificationsActivity: AppCompatActivity(), OpenEuiccContextMarker {
+class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     private lateinit var swipeRefresh: SwipeRefreshLayout
     private lateinit var swipeRefresh: SwipeRefreshLayout
     private lateinit var notificationList: RecyclerView
     private lateinit var notificationList: RecyclerView
     private val notificationAdapter = NotificationAdapter()
     private val notificationAdapter = NotificationAdapter()
@@ -39,7 +38,9 @@ class NotificationsActivity: AppCompatActivity(), OpenEuiccContextMarker {
         setContentView(R.layout.activity_notifications)
         setContentView(R.layout.activity_notifications)
         setSupportActionBar(requireViewById(R.id.toolbar))
         setSupportActionBar(requireViewById(R.id.toolbar))
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+    }
 
 
+    override fun onInit() {
         euiccChannel = euiccChannelManager
         euiccChannel = euiccChannelManager
             .findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!!
             .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 toolbar: Toolbar
     private lateinit var spinner: Spinner
     private lateinit var spinner: Spinner
     private val channels: List<EuiccChannel> by lazy {
     private val channels: List<EuiccChannel> by lazy {
-        euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }
+        (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
+            .knownChannels.sortedBy { it.logicalSlotId }
     }
     }
 
 
     override fun onCreateView(
     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 android.util.Log
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.Fragment
 import im.angry.openeuicc.core.EuiccChannel
 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.Dispatchers
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
-import net.typeblog.lpac_jni.LocalProfileAssistant
 
 
 private const val TAG = "EuiccChannelFragmentUtils"
 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
 val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
     get() = requireArguments().getInt("portId")
     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
 val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
     get() =
     get() =
         euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
         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 androidx.fragment.app.Fragment
 import im.angry.openeuicc.OpenEuiccApplication
 import im.angry.openeuicc.OpenEuiccApplication
 import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannel
-import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.di.AppContainer
 import im.angry.openeuicc.di.AppContainer
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.runBlocking
@@ -15,7 +14,7 @@ import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 import net.typeblog.lpac_jni.LocalProfileInfo
 import net.typeblog.lpac_jni.LocalProfileInfo
-import java.lang.RuntimeException
+import kotlin.RuntimeException
 import kotlin.coroutines.resume
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
 import kotlin.coroutines.resumeWithException
 import kotlin.coroutines.suspendCoroutine
 import kotlin.coroutines.suspendCoroutine
@@ -52,9 +51,6 @@ interface OpenEuiccContextMarker {
     val appContainer: AppContainer
     val appContainer: AppContainer
         get() = openEuiccApplication.appContainer
         get() = openEuiccApplication.appContainer
 
 
-    val euiccChannelManager: EuiccChannelManager
-        get() = appContainer.euiccChannelManager
-
     val telephonyManager: TelephonyManager
     val telephonyManager: TelephonyManager
         get() = appContainer.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 im.angry.openeuicc.util.*
 import java.lang.Exception
 import java.lang.Exception
 
 
-class PrivilegedEuiccChannelManager(appContainer: AppContainer, context: Context) :
+class PrivilegedEuiccChannelManager(
+    appContainer: AppContainer,
+    context: Context
+) :
     DefaultEuiccChannelManager(appContainer, context) {
     DefaultEuiccChannelManager(appContainer, context) {
     override val uiccCards: Collection<UiccCardInfoCompat>
     override val uiccCards: Collection<UiccCardInfoCompat>
         get() = tm.uiccCardsInfoCompat
         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 android.content.Context
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.core.EuiccChannelManager
+import im.angry.openeuicc.core.EuiccChannelManagerFactory
 import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
 import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
 import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
 import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
+import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory
 
 
 class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
 class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
     override val euiccChannelManager: EuiccChannelManager by lazy {
     override val euiccChannelManager: EuiccChannelManager by lazy {
         PrivilegedEuiccChannelManager(this, context)
         PrivilegedEuiccChannelManager(this, context)
     }
     }
 
 
+    override val euiccChannelManagerFactory: EuiccChannelManagerFactory by lazy {
+        PrivilegedEuiccChannelManagerFactory(this)
+    }
+
     override val uiComponentFactory by lazy {
     override val uiComponentFactory by lazy {
         PrivilegedUiComponentFactory()
         PrivilegedUiComponentFactory()
     }
     }

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

@@ -1,5 +1,7 @@
 package im.angry.openeuicc.service
 package im.angry.openeuicc.service
 
 
+import android.content.Context
+import android.content.Intent
 import android.os.Build
 import android.os.Build
 import android.service.euicc.*
 import android.service.euicc.*
 import android.telephony.UiccSlotMapping
 import android.telephony.UiccSlotMapping
@@ -8,7 +10,9 @@ import android.telephony.euicc.EuiccInfo
 import android.util.Log
 import android.util.Log
 import net.typeblog.lpac_jni.LocalProfileInfo
 import net.typeblog.lpac_jni.LocalProfileInfo
 import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannel
+import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.util.*
 import im.angry.openeuicc.util.*
+import kotlinx.coroutines.runBlocking
 import java.lang.IllegalStateException
 import java.lang.IllegalStateException
 
 
 class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
 class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
@@ -31,17 +35,51 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
             telephonyManager.uiccCardsInfoCompat.firstOrNull { it.isEuicc }?.physicalSlotIndex == physicalSlotId
             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
         findChannel(slotId)?.lpa?.eID
+    }
 
 
     // When two eSIM cards are present on one device, the Android settings UI
     // 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
     // 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())
         return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf())
     }
     }
 
 
-    override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult {
+    override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = withEuiccChannelManager {
         Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
         Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
         if (shouldIgnoreSlot(slotId)) {
         if (shouldIgnoreSlot(slotId)) {
             Log.i(TAG, "ignoring slot $slotId")
             Log.i(TAG, "ignoring slot $slotId")
@@ -165,7 +203,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
         return EuiccInfo("Unknown") // TODO: Can we actually implement this?
         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")
         Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid")
         if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
         if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
 
 
@@ -212,7 +250,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
         portIndex: Int,
         portIndex: Int,
         iccid: String?,
         iccid: String?,
         forceDeactivateSim: Boolean
         forceDeactivateSim: Boolean
-    ): Int {
+    ): Int = withEuiccChannelManager {
         Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim")
         Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim")
         if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
         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")
     @Deprecated("Deprecated in Java")
     override fun onEraseSubscriptions(slotId: Int): Int {
     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) {
                 withContext(Dispatchers.IO) {
                     // Use the utility method from PrivilegedTelephonyUtils to ensure
                     // Use the utility method from PrivilegedTelephonyUtils to ensure
                     // unmapped ports have all profiles disabled
                     // unmapped ports have all profiles disabled
-                    telephonyManager.updateSimSlotMapping(euiccChannelManager, adapter.mappings)
+                    telephonyManager.updateSimSlotMapping(
+                        (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager,
+                        adapter.mappings
+                    )
                 }
                 }
             } catch (e: Exception) {
             } catch (e: Exception) {
                 Toast.makeText(requireContext(), R.string.slot_mapping_failure, Toast.LENGTH_LONG).show()
                 Toast.makeText(requireContext(), R.string.slot_mapping_failure, Toast.LENGTH_LONG).show()
                 return@launch
                 return@launch
             }
             }
             Toast.makeText(requireContext(), R.string.slot_mapping_completed, Toast.LENGTH_LONG).show()
             Toast.makeText(requireContext(), R.string.slot_mapping_completed, Toast.LENGTH_LONG).show()
-            euiccChannelManager.invalidate()
+            (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager.invalidate()
             requireActivity().finish()
             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)
+    }