diff --git a/app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt b/app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt index bdd6520..0ae3505 100644 --- a/app/src/main/java/im/angry/openeuicc/OpenEUICCApplication.kt +++ b/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) } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepository.kt b/app/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt similarity index 62% rename from app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepository.kt rename to app/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index b576dde..8e3b91d 100644 --- a/app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepository.kt +++ b/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 -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt new file mode 100644 index 0000000..9af86a8 --- /dev/null +++ b/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() + + 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 + get() = channels.toList() + + fun invalidate() { + channels.clear() + seService?.shutdown() + seService = null + } +} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepositoryProxy.kt b/app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepositoryProxy.kt deleted file mode 100644 index 1891d19..0000000 --- a/app/src/main/java/im/angry/openeuicc/core/EuiccChannelRepositoryProxy.kt +++ /dev/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 - get() = if (loaded) { - inner.availableChannels - } else { - throw IllegalStateException("Not loaded yet") - } -} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/OmapiApduChannel.kt b/app/src/main/java/im/angry/openeuicc/core/OmapiApduChannel.kt new file mode 100644 index 0000000..ec0ff7d --- /dev/null +++ b/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 { + var res = "" + for (pdu in apdus) { + res = transmitAPDU(pdu) + } + return res + } + + override fun sendStatus() { + } + + override fun setApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) { + } + + override fun removeApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) { + } +} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/omapi/OmapiApduChannel.kt b/app/src/main/java/im/angry/openeuicc/core/omapi/OmapiApduChannel.kt deleted file mode 100644 index 5ba469f..0000000 --- a/app/src/main/java/im/angry/openeuicc/core/omapi/OmapiApduChannel.kt +++ /dev/null @@ -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 { - var res = "" - for (pdu in apdus) { - res = transmitAPDU(pdu) - } - return res - } - - override fun sendStatus() { - } - - override fun setApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) { - } - - override fun removeApduTransmittedListener(apduTransmittedListener: ApduTransmittedListener?) { - } -} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/omapi/OmapiEuiccChannelRepository.kt b/app/src/main/java/im/angry/openeuicc/core/omapi/OmapiEuiccChannelRepository.kt deleted file mode 100644 index ad4b8fd..0000000 --- a/app/src/main/java/im/angry/openeuicc/core/omapi/OmapiEuiccChannelRepository.kt +++ /dev/null @@ -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() - - 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 - get() = channels -} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt b/app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt index 8357b68..74d98ad 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt +++ b/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 newInstanceEuicc(clazz: Class, slotId: Int): T where T: Fragment, T: val T.slotId: Int where T: Fragment, T: EuiccFragmentMarker get() = requireArguments().getInt("slotId") +val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccFragmentMarker + get() = (requireActivity().application as OpenEUICCApplication).euiccChannelManager + val T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker get() = - (requireActivity().application as OpenEUICCApplication).euiccChannelRepo.availableChannels[slotId] + euiccChannelManager.findEuiccChannelBySlotBlocking(slotId)!! interface EuiccProfilesChangedListener { fun onEuiccProfilesChanged() diff --git a/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 97a8681..f6dba33 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/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") diff --git a/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 7b90f54..503fac4 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/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 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(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()) {