ソースを参照

initial implementation of the OMAPI channel

Peter Cai 3 年 前
コミット
f13f4a944e

+ 1 - 0
app/build.gradle

@@ -37,6 +37,7 @@ dependencies {
     implementation 'androidx.appcompat:appcompat:1.4.1'
     implementation 'androidx.appcompat:appcompat:1.4.1'
     implementation 'com.google.android.material:material:1.5.0'
     implementation 'com.google.android.material:material:1.5.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
     testImplementation 'junit:junit:4.13.2'
     testImplementation 'junit:junit:4.13.2'
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -18,7 +18,7 @@
         android:supportsRtl="true"
         android:supportsRtl="true"
         android:theme="@style/Theme.OpenEUICC">
         android:theme="@style/Theme.OpenEUICC">
         <activity
         <activity
-            android:name=".MainActivity"
+            android:name=".ui.MainActivity"
             android:exported="true">
             android:exported="true">
             <intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <action android:name="android.intent.action.MAIN" />

+ 0 - 11
app/src/main/java/im/angry/openeuicc/MainActivity.kt

@@ -1,11 +0,0 @@
-package im.angry.openeuicc
-
-import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-
-class MainActivity : AppCompatActivity() {
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
-    }
-}

+ 13 - 0
app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepository.kt

@@ -0,0 +1,13 @@
+package im.angry.openeuicc.core
+
+import com.truphone.lpa.LocalProfileAssistant
+
+data class EuiccChannel(
+    val name: String,
+    val lpa: LocalProfileAssistant
+)
+
+interface EuiccChannelRepository {
+    suspend fun load()
+    val availableChannels: List<EuiccChannel>?
+}

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

@@ -0,0 +1,14 @@
+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)
+
+    override suspend fun load() = inner.load()
+
+    override val availableChannels: List<EuiccChannel>?
+        get() = inner.availableChannels
+}

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

@@ -0,0 +1,29 @@
+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?) {
+    }
+}

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

@@ -0,0 +1,61 @@
+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(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() {
+        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
+}

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

@@ -0,0 +1,38 @@
+package im.angry.openeuicc.ui
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.util.Log
+import androidx.lifecycle.lifecycleScope
+import im.angry.openeuicc.R
+import im.angry.openeuicc.core.EuiccChannelRepositoryProxy
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class MainActivity : AppCompatActivity() {
+    companion object {
+        const val TAG = "MainActivity"
+    }
+
+    private val repo = EuiccChannelRepositoryProxy(this)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+
+        lifecycleScope.launch {
+            init()
+        }
+    }
+
+    private suspend fun init() {
+        withContext(Dispatchers.IO) {
+            repo.load()
+            repo.availableChannels?.forEach {
+                Log.d(TAG, it.name)
+                Log.d(TAG, it.lpa.eid)
+            }
+        }
+    }
+}

+ 19 - 0
app/src/main/java/im/angry/openeuicc/util/StringUtils.kt

@@ -0,0 +1,19 @@
+package im.angry.openeuicc.util
+
+fun hexStringToByteArray(str: String): ByteArray {
+    val length = str.length / 2
+    val out = ByteArray(length)
+    for (i in 0 until length) {
+        val i2 = i * 2
+        out[i] = str.substring(i2, i2 + 2).toInt(16).toByte()
+    }
+    return out
+}
+fun byteArrayToHex(arr: ByteArray): String {
+    val sb = StringBuilder()
+    val length = arr.size
+    for (i in 0 until length) {
+        sb.append(String.format("%02X", arr[i]))
+    }
+    return sb.toString()
+}

+ 1 - 1
app/src/main/res/layout/activity_main.xml

@@ -4,7 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_height="match_parent"
-    tools:context=".MainActivity">
+    tools:context=".ui.MainActivity">
 
 
     <TextView
     <TextView
         android:layout_width="wrap_content"
         android:layout_width="wrap_content"