refactor: [2/n] Center EuiccChannel's around ports, not cards

This commit is contained in:
Peter Cai 2023-12-16 15:19:43 -05:00
parent 2ccfe02204
commit 4090418146
12 changed files with 102 additions and 79 deletions

View file

@ -1,24 +1,17 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileAssistant
// A custom type to avoid compatibility issues with UiccCardInfo / UiccPortInfo
data class EuiccChannelInfo(
val slotId: Int,
val cardId: Int,
val name: String,
val imei: String,
val removable: Boolean
)
abstract class EuiccChannel(
info: EuiccChannelInfo
port: UiccPortInfoCompat
) {
val slotId = info.slotId
val cardId = info.cardId
val name = info.name
val imei = info.imei
val removable = info.removable
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
val logicalSlotId = port.logicalSlotIndex
val portId = port.portIndex
val cardId = port.card.cardId
val name = "SLOT ${port.card.physicalSlotIndex}:${port.portIndex}"
val removable = port.card.isRemovable
abstract val lpa: LocalProfileAssistant
val valid: Boolean

View file

@ -1,6 +1,5 @@
package im.angry.openeuicc.core
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
@ -17,7 +16,6 @@ import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@SuppressLint("MissingPermission") // We rely on ARA-based privileges, not READ_PRIVILEGED_PHONE_STATE
open class EuiccChannelManager(protected val context: Context) {
companion object {
const val TAG = "EuiccChannelManager"
@ -52,27 +50,32 @@ open class EuiccChannelManager(protected val context: Context) {
}
}
protected open fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? {
protected open fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
// No-op when unprivileged
return null
}
protected fun tryOpenEuiccChannelUnprivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? {
Log.i(TAG, "Trying OMAPI for slot ${uiccInfo.physicalSlotIndex}")
protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(TAG, "OMAPI channel attempted on non-zero portId, ignoring")
return null
}
Log.i(TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try {
return OmapiChannel(seService!!, channelInfo)
return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "OMAPI APDU interface unavailable for slot ${uiccInfo.physicalSlotIndex}.")
Log.w(TAG, "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}.")
}
return null
}
private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfoCompat): EuiccChannel? {
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
ensureSEService()
val existing = channels.find { it.slotId == uiccInfo.physicalSlotIndex }
val existing = channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid) {
return existing
@ -82,18 +85,10 @@ open class EuiccChannelManager(protected val context: Context) {
}
}
val channelInfo = EuiccChannelInfo(
uiccInfo.physicalSlotIndex,
uiccInfo.cardId,
"SIM ${uiccInfo.physicalSlotIndex}",
tm.getImei(uiccInfo.physicalSlotIndex) ?: return null,
uiccInfo.isRemovable
)
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(uiccInfo, channelInfo)
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port)
if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(uiccInfo, channelInfo)
euiccChannel = tryOpenEuiccChannelUnprivileged(port)
}
if (euiccChannel != null) {
@ -104,16 +99,28 @@ open class EuiccChannelManager(protected val context: Context) {
}
}
private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? {
return tm.uiccCardsInfoCompat.find { it.physicalSlotIndex == slotId }?.let {
tryOpenEuiccChannel(it)
}
}
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking {
if (!checkPrivileges()) return@runBlocking null
withContext(Dispatchers.IO) {
for (card in tm.uiccCardsInfoCompat) {
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
fun findEuiccChannelBySlotBlocking(slotId: Int): EuiccChannel? = runBlocking {
null
}
}
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
if (!checkPrivileges()) return@runBlocking null
withContext(Dispatchers.IO) {
findEuiccChannelBySlot(slotId)
tm.uiccCardsInfoCompat.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
}
@ -124,8 +131,10 @@ open class EuiccChannelManager(protected val context: Context) {
ensureSEService()
for (uiccInfo in tm.uiccCardsInfoCompat) {
if (tryOpenEuiccChannel(uiccInfo) != null) {
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex}")
for (port in uiccInfo.ports) {
if (tryOpenEuiccChannel(port) != null) {
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
}
}
}
}

View file

@ -3,6 +3,7 @@ package im.angry.openeuicc.core
import android.se.omapi.Channel
import android.se.omapi.SEService
import android.se.omapi.Session
import im.angry.openeuicc.util.UiccPortInfoCompat
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
@ -10,13 +11,13 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class OmapiApduInterface(
private val service: SEService,
private val info: EuiccChannelInfo
private val port: UiccPortInfoCompat
): ApduInterface {
private lateinit var session: Session
private lateinit var lastChannel: Channel
override fun connect() {
session = service.getUiccReader(info.slotId + 1).openSession()
session = service.getUiccReader(port.logicalSlotIndex + 1).openSession()
}
override fun disconnect() {
@ -50,9 +51,9 @@ class OmapiApduInterface(
class OmapiChannel(
service: SEService,
info: EuiccChannelInfo,
) : EuiccChannel(info) {
port: UiccPortInfoCompat,
) : EuiccChannel(port) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
OmapiApduInterface(service, info),
OmapiApduInterface(service, port),
HttpInterfaceImpl())
}

View file

@ -8,23 +8,26 @@ import im.angry.openeuicc.util.openEuiccApplication
interface EuiccFragmentMarker
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int): T where T: Fragment, T: EuiccFragmentMarker {
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int): T where T: Fragment, T: EuiccFragmentMarker {
val instance = clazz.newInstance()
instance.arguments = Bundle().apply {
putInt("slotId", slotId)
putInt("portId", portId)
}
return instance
}
val <T> T.slotId: Int where T: Fragment, T: EuiccFragmentMarker
get() = requireArguments().getInt("slotId")
val <T> T.portId: Int where T: Fragment, T: EuiccFragmentMarker
get() = requireArguments().getInt("portId")
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccFragmentMarker
get() = openEuiccApplication.euiccChannelManager
val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker
get() =
euiccChannelManager.findEuiccChannelBySlotBlocking(slotId)!!
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
interface EuiccProfilesChangedListener {
fun onEuiccProfilesChanged()

View file

@ -30,8 +30,8 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
companion object {
const val TAG = "EuiccManagementFragment"
fun newInstance(slotId: Int): EuiccManagementFragment =
newInstanceEuicc(EuiccManagementFragment::class.java, slotId)
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId)
}
private lateinit var swipeRefresh: SwipeRefreshLayout
@ -62,7 +62,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
fab.setOnClickListener {
ProfileDownloadFragment.newInstance(slotId)
ProfileDownloadFragment.newInstance(slotId, portId)
.show(childFragmentManager, ProfileDownloadFragment.TAG)
}
}
@ -195,12 +195,12 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
true
}
R.id.rename -> {
ProfileRenameFragment.newInstance(slotId, profile.iccid, profile.displayName)
ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
.show(childFragmentManager, ProfileRenameFragment.TAG)
true
}
R.id.delete -> {
ProfileDeleteFragment.newInstance(slotId, profile.iccid, profile.displayName)
ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName)
.show(childFragmentManager, ProfileDeleteFragment.TAG)
true
}

View file

@ -88,7 +88,7 @@ open class MainActivity : AppCompatActivity() {
withContext(Dispatchers.Main) {
manager.knownChannels.forEach { channel ->
spinnerAdapter.add(channel.name)
fragments.add(EuiccManagementFragment.newInstance(channel.slotId))
fragments.add(EuiccManagementFragment.newInstance(channel.slotId, channel.portId))
}
if (fragments.isNotEmpty()) {

View file

@ -16,8 +16,8 @@ class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
companion object {
const val TAG = "ProfileDeleteFragment"
fun newInstance(slotId: Int, iccid: String, name: String): ProfileDeleteFragment {
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId)
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
instance.requireArguments().apply {
putString("iccid", iccid)
putString("name", name)

View file

@ -1,5 +1,6 @@
package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.text.Editable
@ -16,19 +17,20 @@ import com.google.android.material.textfield.TextInputLayout
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.openEuiccApplication
import im.angry.openeuicc.util.setWidthPercent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.ProfileDownloadCallback
import java.lang.Exception
import kotlin.Exception
class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.OnMenuItemClickListener {
companion object {
const val TAG = "ProfileDownloadFragment"
fun newInstance(slotId: Int): ProfileDownloadFragment =
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId)
fun newInstance(slotId: Int, portId: Int): ProfileDownloadFragment =
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId)
}
private lateinit var toolbar: Toolbar
@ -105,9 +107,16 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
setWidthPercent(95)
}
@SuppressLint("MissingPermission")
override fun onStart() {
super.onStart()
profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable(channel.imei)
profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable(
try {
openEuiccApplication.telephonyManager.getImei(channel.logicalSlotId)
} catch (e: Exception) {
""
}
)
lifecycleScope.launch(Dispatchers.IO) {
// Fetch remaining NVRAM

View file

@ -25,8 +25,8 @@ class ProfileRenameFragment : DialogFragment(), EuiccFragmentMarker {
companion object {
const val TAG = "ProfileRenameFragment"
fun newInstance(slotId: Int, iccid: String, currentName: String): ProfileRenameFragment {
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId)
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment {
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId)
instance.requireArguments().apply {
putString("iccid", iccid)
putString("currentName", currentName)

View file

@ -68,6 +68,14 @@ class UiccPortInfoCompat(private val _inner: Any?, val card: UiccCardInfoCompat)
} else {
0
}
val logicalSlotIndex: Int
get() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
inner.logicalSlotIndex
} else {
card.physicalSlotIndex // logical is the same as physical below TIRAMISU
}
}
val TelephonyManager.uiccCardsInfoCompat: List<UiccCardInfoCompat>

View file

@ -10,21 +10,21 @@ import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
override fun checkPrivileges() = true // TODO: Implement proper system app check
override fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? {
if (uiccInfo.isRemovable) {
override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannelUnprivileged(uiccInfo, channelInfo)?.let { return it }
super.tryOpenEuiccChannelUnprivileged(port)?.let { return it }
}
if (uiccInfo.isEuicc) {
Log.i(TAG, "Trying TelephonyManager for slot ${uiccInfo.physicalSlotIndex}")
if (port.card.isEuicc) {
Log.i(TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
// TODO: On Tiramisu, we should also connect all available "ports" for MEP support
try {
return TelephonyManagerChannel(channelInfo, tm)
return TelephonyManagerChannel(port, tm)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${uiccInfo.physicalSlotIndex}, falling back")
Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back")
}
}
return null

View file

@ -9,7 +9,7 @@ import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class TelephonyManagerApduInterface(
private val info: EuiccChannelInfo,
private val port: UiccPortInfoCompat,
private val tm: TelephonyManager
): ApduInterface {
private var lastChannel: Int = -1
@ -25,9 +25,9 @@ class TelephonyManagerApduInterface(
override fun logicalChannelOpen(aid: ByteArray): Int {
check(lastChannel == -1) { "Already initialized" }
val hex = aid.encodeHex()
val channel = tm.iccOpenLogicalChannelBySlot(info.slotId, hex, 0)
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
throw IllegalArgumentException("Cannot open logical channel " + hex + " via TelephonManager on slot " + info.slotId);
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}");
}
lastChannel = channel.channel
return lastChannel
@ -35,7 +35,7 @@ class TelephonyManagerApduInterface(
override fun logicalChannelClose(handle: Int) {
check(handle == lastChannel) { "Invalid channel handle " }
tm.iccCloseLogicalChannelBySlot(info.slotId, handle)
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
lastChannel = -1
}
@ -49,18 +49,18 @@ class TelephonyManagerApduInterface(
val p3 = tx[4].toUByte().toInt()
val p4 = tx.drop(5).toByteArray().encodeHex()
return tm.iccTransmitApduLogicalChannelBySlot(info.slotId, lastChannel,
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
cla, instruction, p1, p2, p3, p4)?.decodeHex() ?: byteArrayOf()
}
}
class TelephonyManagerChannel(
info: EuiccChannelInfo,
port: UiccPortInfoCompat,
private val tm: TelephonyManager
) : EuiccChannel(info) {
) : EuiccChannel(port) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
TelephonyManagerApduInterface(info, tm),
TelephonyManagerApduInterface(port, tm),
HttpInterfaceImpl()
)
}