ソースを参照

refactor: Simplify EuiccChannelRepository logic

Using a centralized EuiccChannelManager is way easier to deal with than
EuiccChannelRepository and proxying magic
Peter Cai 3 年 前
コミット
c78743f03f

+ 2 - 2
app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt

@@ -1,8 +1,8 @@
 package im.angry.openeuicc
 
 import android.app.Application
-import im.angry.openeuicc.core.EuiccChannelRepositoryProxy
+import im.angry.openeuicc.core.EuiccChannelManager
 
 class OpenEUICCApplication : Application() {
-    val euiccChannelRepo = EuiccChannelRepositoryProxy(this)
+    val euiccChannelManager = EuiccChannelManager(this)
 }

+ 1 - 6
app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepository.kt → app/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt

@@ -6,9 +6,4 @@ data class EuiccChannel(
     val slotId: Int,
     val name: String,
     val lpa: LocalProfileAssistant
-)
-
-interface EuiccChannelRepository {
-    suspend fun load()
-    val availableChannels: List<EuiccChannel>
-}
+)

+ 84 - 0
app/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt

@@ -0,0 +1,84 @@
+package im.angry.openeuicc.core
+
+import android.content.Context
+import android.os.Handler
+import android.os.HandlerThread
+import android.se.omapi.SEService
+import android.util.Log
+import com.truphone.lpa.ApduChannel
+import com.truphone.lpa.impl.LocalProfileAssistantImpl
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class EuiccChannelManager(private val context: Context) {
+    companion object {
+        const val TAG = "EuiccChannelManager"
+        const val MAX_SIMS = 3
+    }
+
+    private val channels = mutableListOf<EuiccChannel>()
+
+    private var seService: SEService? = null
+
+    private val handler = Handler(HandlerThread("EuiccChannelManager").also { it.start() }.looper)
+
+    private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
+        var service: SEService? = null
+        service = SEService(context, { handler.post(it) }) {
+            cont.resume(service!!)
+        }
+    }
+
+    private suspend fun ensureSEService() {
+         if (seService == null) {
+             seService = connectSEService()
+         }
+    }
+
+    private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? {
+        ensureSEService()
+        val existing = channels.find { it.slotId == slotId }
+        if (existing != null) return existing
+
+        var apduChannel: ApduChannel?
+        apduChannel = OmapiApduChannel.tryConnectUiccSlot(seService!!, slotId)
+
+        if (apduChannel == null) {
+            return null
+        }
+
+        val channel = EuiccChannel(slotId, "SIM $slotId", LocalProfileAssistantImpl(apduChannel))
+        channels.add(channel)
+        return channel
+    }
+
+    fun findEuiccChannelBySlotBlocking(slotId: Int): EuiccChannel? = runBlocking {
+        withContext(Dispatchers.IO) {
+            findEuiccChannelBySlot(slotId)
+        }
+    }
+
+    suspend fun enumerateEuiccChannels() {
+        withContext(Dispatchers.IO) {
+            ensureSEService()
+
+            for (slotId in 0 until MAX_SIMS) {
+                if (findEuiccChannelBySlot(slotId) != null) {
+                    Log.d(TAG, "Found eUICC on slot $slotId")
+                }
+            }
+        }
+    }
+
+    val knownChannels: List<EuiccChannel>
+        get() = channels.toList()
+
+    fun invalidate() {
+        channels.clear()
+        seService?.shutdown()
+        seService = null
+    }
+}

+ 0 - 23
app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepositoryProxy.kt

@@ -1,23 +0,0 @@
-package im.angry.openeuicc.core
-
-import android.content.Context
-import im.angry.openeuicc.core.omapi.OmapiEuiccChannelRepository
-
-class EuiccChannelRepositoryProxy(context: Context) : EuiccChannelRepository {
-    // TODO: Make this pluggable
-    private val inner: EuiccChannelRepository = OmapiEuiccChannelRepository(context)
-
-    private var loaded = false
-
-    override suspend fun load() {
-        inner.load()
-        loaded = true
-    }
-
-    override val availableChannels: List<EuiccChannel>
-        get() = if (loaded) {
-            inner.availableChannels
-        } else {
-            throw IllegalStateException("Not loaded yet")
-        }
-}

+ 50 - 0
app/src/main/java/im/angry/openeuicc/core/OmapiApduChannel.kt

@@ -0,0 +1,50 @@
+package im.angry.openeuicc.core
+
+import android.se.omapi.Channel
+import android.se.omapi.SEService
+import android.util.Log
+import com.truphone.lpa.ApduChannel
+import com.truphone.lpa.ApduTransmittedListener
+import im.angry.openeuicc.util.byteArrayToHex
+import im.angry.openeuicc.util.hexStringToByteArray
+import java.lang.Exception
+
+class OmapiApduChannel(private val channel: Channel) : ApduChannel {
+    companion object {
+        private const val TAG = "OmapiApduChannel"
+        private val APPLET_ID = byteArrayOf(-96, 0, 0, 5, 89, 16, 16, -1, -1, -1, -1, -119, 0, 0, 1, 0)
+
+        fun tryConnectUiccSlot(service: SEService, slotId: Int): OmapiApduChannel? {
+            try {
+                val reader = service.getUiccReader(slotId + 1) // slotId from telephony starts from 0
+                val session = reader.openSession()
+                val channel = session.openLogicalChannel(APPLET_ID) ?: return null
+                return OmapiApduChannel(channel)
+            } catch (e: Exception) {
+                Log.e(TAG, "Unable to open eUICC channel for slot ${slotId}, skipping")
+                Log.e(TAG, Log.getStackTraceString(e))
+                return null
+            }
+        }
+    }
+
+    override fun transmitAPDU(apdu: String): String =
+        byteArrayToHex(channel.transmit(hexStringToByteArray(apdu)))
+
+    override fun transmitAPDUS(apdus: MutableList<String>): String {
+        var res = ""
+        for (pdu in apdus) {
+            res = transmitAPDU(pdu)
+        }
+        return res
+    }
+
+    override fun sendStatus() {
+    }
+
+    override fun setApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) {
+    }
+
+    override fun removeApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) {
+    }
+}

+ 0 - 29
app/src/main/java/im/angry/openeuicc/core/omapi/OmapiApduChannel.kt

@@ -1,29 +0,0 @@
-package im.angry.openeuicc.core.omapi
-
-import android.se.omapi.Channel
-import com.truphone.lpa.ApduChannel
-import com.truphone.lpa.ApduTransmittedListener
-import im.angry.openeuicc.util.byteArrayToHex
-import im.angry.openeuicc.util.hexStringToByteArray
-
-class OmapiApduChannel(private val channel: Channel) : ApduChannel {
-    override fun transmitAPDU(apdu: String): String =
-        byteArrayToHex(channel.transmit(hexStringToByteArray(apdu)))
-
-    override fun transmitAPDUS(apdus: MutableList<String>): String {
-        var res = ""
-        for (pdu in apdus) {
-            res = transmitAPDU(pdu)
-        }
-        return res
-    }
-
-    override fun sendStatus() {
-    }
-
-    override fun setApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) {
-    }
-
-    override fun removeApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) {
-    }
-}

+ 0 - 62
app/src/main/java/im/angry/openeuicc/core/omapi/OmapiEuiccChannelRepository.kt

@@ -1,62 +0,0 @@
-package im.angry.openeuicc.core.omapi
-
-import android.content.Context
-import android.os.Handler
-import android.os.HandlerThread
-import android.se.omapi.SEService
-import android.util.Log
-import com.truphone.lpa.impl.LocalProfileAssistantImpl
-import im.angry.openeuicc.core.EuiccChannel
-import im.angry.openeuicc.core.EuiccChannelRepository
-import java.lang.Exception
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-class OmapiEuiccChannelRepository(private val context: Context) : EuiccChannelRepository {
-    companion object {
-        const val TAG = "OmapiEuiccChannelRepository"
-        val APPLET_ID = byteArrayOf(-96, 0, 0, 5, 89, 16, 16, -1, -1, -1, -1, -119, 0, 0, 1, 0)
-    }
-
-    private val handler = Handler(HandlerThread("OMAPI").also { it.start() }.looper)
-
-    private val channels = mutableListOf<EuiccChannel>()
-
-    private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
-        var service: SEService? = null
-        service = SEService(context, { handler.post(it) }) {
-            cont.resume(service!!)
-        }
-    }
-
-    private fun tryConnectSlot(service: SEService, slotId: Int): EuiccChannel? {
-        try {
-            val reader = service.getUiccReader(slotId)
-            val session = reader.openSession()
-            val channel = session.openLogicalChannel(APPLET_ID) ?: return null
-            val apduChannel = OmapiApduChannel(channel)
-            val lpa = LocalProfileAssistantImpl(apduChannel)
-
-            return EuiccChannel(slotId, reader.name, lpa)
-        } catch (e: Exception) {
-            Log.e(TAG, "Unable to open eUICC channel for slot ${slotId}, skipping")
-            Log.e(TAG, Log.getStackTraceString(e))
-            return null
-        }
-    }
-
-    override suspend fun load() {
-        channels.clear()
-        val service = connectSEService()
-
-        for (slotId in 1..3) {
-            tryConnectSlot(service, slotId)?.let {
-                Log.d(TAG, "New eUICC eSE channel: ${it.name}")
-                channels.add(it)
-            }
-        }
-    }
-
-    override val availableChannels: List<EuiccChannel>
-        get() = channels
-}

+ 5 - 1
app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt

@@ -4,6 +4,7 @@ import android.os.Bundle
 import androidx.fragment.app.Fragment
 import im.angry.openeuicc.OpenEUICCApplication
 import im.angry.openeuicc.core.EuiccChannel
+import im.angry.openeuicc.core.EuiccChannelManager
 
 interface EuiccFragmentMarker
 
@@ -18,9 +19,12 @@ fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int): T where T: Fragment, T:
 val <T> T.slotId: Int where T: Fragment, T: EuiccFragmentMarker
     get() = requireArguments().getInt("slotId")
 
+val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccFragmentMarker
+    get() = (requireActivity().application as OpenEUICCApplication).euiccChannelManager
+
 val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker
     get() =
-        (requireActivity().application as OpenEUICCApplication).euiccChannelRepo.availableChannels[slotId]
+        euiccChannelManager.findEuiccChannelBySlotBlocking(slotId)!!
 
 interface EuiccProfilesChangedListener {
     fun onEuiccProfilesChanged()

+ 1 - 0
app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -99,6 +99,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
                 }
                 Toast.makeText(context, R.string.toast_profile_enabled, Toast.LENGTH_LONG).show()
                 // The APDU channel will be invalid when the SIM reboots. For now, just exit the app
+                euiccChannelManager.invalidate()
                 requireActivity().finish()
             } catch (e: Exception) {
                 Log.d(TAG, "Failed to enable / disable profile $iccid")

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

@@ -14,7 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.OpenEUICCApplication
 import im.angry.openeuicc.R
-import im.angry.openeuicc.core.EuiccChannelRepository
+import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.databinding.ActivityMainBinding
 import im.angry.openeuicc.util.dsdsEnabled
 import im.angry.openeuicc.util.supportsDSDS
@@ -27,7 +27,7 @@ class MainActivity : AppCompatActivity() {
         const val TAG = "MainActivity"
     }
 
-    private lateinit var repo: EuiccChannelRepository
+    private lateinit var manager: EuiccChannelManager
 
     private lateinit var spinnerAdapter: ArrayAdapter<String>
     private lateinit var spinner: Spinner
@@ -45,7 +45,7 @@ class MainActivity : AppCompatActivity() {
 
         tm = getSystemService(TelephonyManager::class.java)
 
-        repo = (application as OpenEUICCApplication).euiccChannelRepo
+        manager = (application as OpenEUICCApplication).euiccChannelManager
 
         spinnerAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item)
 
@@ -95,17 +95,17 @@ class MainActivity : AppCompatActivity() {
 
     private suspend fun init() {
         withContext(Dispatchers.IO) {
-            repo.load()
-            repo.availableChannels.forEach {
+            manager.enumerateEuiccChannels()
+            manager.knownChannels.forEach {
                 Log.d(TAG, it.name)
                 Log.d(TAG, it.lpa.eid)
             }
         }
 
         withContext(Dispatchers.Main) {
-            repo.availableChannels.forEachIndexed { idx, channel ->
+            manager.knownChannels.forEach { channel ->
                 spinnerAdapter.add(channel.name)
-                fragments.add(EuiccManagementFragment.newInstance(idx))
+                fragments.add(EuiccManagementFragment.newInstance(channel.slotId))
             }
 
             if (fragments.isNotEmpty()) {