refactor: Simplify EuiccChannelRepository logic
Using a centralized EuiccChannelManager is way easier to deal with than EuiccChannelRepository and proxying magic
This commit is contained in:
parent
a4b1ebdc08
commit
c78743f03f
|
@ -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)
|
||||
}
|
|
@ -7,8 +7,3 @@ data class EuiccChannel(
|
|||
val name: String,
|
||||
val lpa: LocalProfileAssistant
|
||||
)
|
||||
|
||||
interface EuiccChannelRepository {
|
||||
suspend fun load()
|
||||
val availableChannels: List<EuiccChannel>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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?) {
|
||||
}
|
||||
}
|
|
@ -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?) {
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()) {
|
||||
|
|
Loading…
Reference in New Issue