refactor: Simplify EuiccChannelRepository logic

Using a centralized EuiccChannelManager is way easier to deal with than
EuiccChannelRepository and proxying magic
This commit is contained in:
Peter Cai 2022-05-02 15:11:08 -04:00
parent a4b1ebdc08
commit c78743f03f
10 changed files with 150 additions and 130 deletions

View File

@ -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)
}

View File

@ -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>
}
)

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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?) {
}
}

View File

@ -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?) {
}
}

View File

@ -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
}

View File

@ -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()

View File

@ -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")

View File

@ -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()) {