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
|
package im.angry.openeuicc
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import im.angry.openeuicc.core.EuiccChannelRepositoryProxy
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
|
|
||||||
class OpenEUICCApplication : Application() {
|
class OpenEUICCApplication : Application() {
|
||||||
val euiccChannelRepo = EuiccChannelRepositoryProxy(this)
|
val euiccChannelManager = EuiccChannelManager(this)
|
||||||
}
|
}
|
|
@ -7,8 +7,3 @@ data class EuiccChannel(
|
||||||
val name: String,
|
val name: String,
|
||||||
val lpa: LocalProfileAssistant
|
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 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
|
||||||
|
|
||||||
interface EuiccFragmentMarker
|
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
|
val <T> T.slotId: Int where T: Fragment, T: EuiccFragmentMarker
|
||||||
get() = requireArguments().getInt("slotId")
|
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
|
val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker
|
||||||
get() =
|
get() =
|
||||||
(requireActivity().application as OpenEUICCApplication).euiccChannelRepo.availableChannels[slotId]
|
euiccChannelManager.findEuiccChannelBySlotBlocking(slotId)!!
|
||||||
|
|
||||||
interface EuiccProfilesChangedListener {
|
interface EuiccProfilesChangedListener {
|
||||||
fun onEuiccProfilesChanged()
|
fun onEuiccProfilesChanged()
|
||||||
|
|
|
@ -99,6 +99,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
||||||
}
|
}
|
||||||
Toast.makeText(context, R.string.toast_profile_enabled, Toast.LENGTH_LONG).show()
|
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
|
// The APDU channel will be invalid when the SIM reboots. For now, just exit the app
|
||||||
|
euiccChannelManager.invalidate()
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, "Failed to enable / disable profile $iccid")
|
Log.d(TAG, "Failed to enable / disable profile $iccid")
|
||||||
|
|
|
@ -14,7 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import im.angry.openeuicc.OpenEUICCApplication
|
import im.angry.openeuicc.OpenEUICCApplication
|
||||||
import im.angry.openeuicc.R
|
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.databinding.ActivityMainBinding
|
||||||
import im.angry.openeuicc.util.dsdsEnabled
|
import im.angry.openeuicc.util.dsdsEnabled
|
||||||
import im.angry.openeuicc.util.supportsDSDS
|
import im.angry.openeuicc.util.supportsDSDS
|
||||||
|
@ -27,7 +27,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
const val TAG = "MainActivity"
|
const val TAG = "MainActivity"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var repo: EuiccChannelRepository
|
private lateinit var manager: EuiccChannelManager
|
||||||
|
|
||||||
private lateinit var spinnerAdapter: ArrayAdapter<String>
|
private lateinit var spinnerAdapter: ArrayAdapter<String>
|
||||||
private lateinit var spinner: Spinner
|
private lateinit var spinner: Spinner
|
||||||
|
@ -45,7 +45,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
tm = getSystemService(TelephonyManager::class.java)
|
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)
|
spinnerAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item)
|
||||||
|
|
||||||
|
@ -95,17 +95,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
repo.load()
|
manager.enumerateEuiccChannels()
|
||||||
repo.availableChannels.forEach {
|
manager.knownChannels.forEach {
|
||||||
Log.d(TAG, it.name)
|
Log.d(TAG, it.name)
|
||||||
Log.d(TAG, it.lpa.eid)
|
Log.d(TAG, it.lpa.eid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
repo.availableChannels.forEachIndexed { idx, channel ->
|
manager.knownChannels.forEach { channel ->
|
||||||
spinnerAdapter.add(channel.name)
|
spinnerAdapter.add(channel.name)
|
||||||
fragments.add(EuiccManagementFragment.newInstance(idx))
|
fragments.add(EuiccManagementFragment.newInstance(channel.slotId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fragments.isNotEmpty()) {
|
if (fragments.isNotEmpty()) {
|
||||||
|
|
Loading…
Reference in a new issue