From 2ccfe022040827505e6435e1dece01377071990b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 11:42:17 -0500 Subject: [PATCH 01/16] refactor: [1/n] Add compat classes for UiccCardInfo and UiccPortInfo --- .../openeuicc/core/EuiccChannelManager.kt | 26 +++---- .../angry/openeuicc/util/TelephonyCompat.kt | 75 +++++++++++++++++++ .../core/PrivilegedEuiccChannelManager.kt | 7 +- .../util/PrivilegedTelephonyCompat.kt | 39 ++++++++++ .../util/TelephonyManagerHiddenApi.kt | 37 ++++++++- 5 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt create mode 100644 app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 6484b21..f579c82 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -5,9 +5,9 @@ import android.content.Context import android.os.Handler import android.os.HandlerThread import android.se.omapi.SEService -import android.telephony.UiccCardInfo import android.util.Log import im.angry.openeuicc.OpenEuiccApplication +import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex @@ -52,27 +52,27 @@ open class EuiccChannelManager(protected val context: Context) { } } - protected open fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? { + protected open fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? { // No-op when unprivileged return null } - protected fun tryOpenEuiccChannelUnprivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? { - Log.i(TAG, "Trying OMAPI for slot ${uiccInfo.slotIndex}") + protected fun tryOpenEuiccChannelUnprivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? { + Log.i(TAG, "Trying OMAPI for slot ${uiccInfo.physicalSlotIndex}") try { return OmapiChannel(seService!!, channelInfo) } catch (e: IllegalArgumentException) { // Failed - Log.w(TAG, "OMAPI APDU interface unavailable for slot ${uiccInfo.slotIndex}.") + Log.w(TAG, "OMAPI APDU interface unavailable for slot ${uiccInfo.physicalSlotIndex}.") } return null } - private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfo): EuiccChannel? { + private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfoCompat): EuiccChannel? { lock.withLock { ensureSEService() - val existing = channels.find { it.slotId == uiccInfo.slotIndex } + val existing = channels.find { it.slotId == uiccInfo.physicalSlotIndex } if (existing != null) { if (existing.valid) { return existing @@ -83,10 +83,10 @@ open class EuiccChannelManager(protected val context: Context) { } val channelInfo = EuiccChannelInfo( - uiccInfo.slotIndex, + uiccInfo.physicalSlotIndex, uiccInfo.cardId, - "SIM ${uiccInfo.slotIndex}", - tm.getImei(uiccInfo.slotIndex) ?: return null, + "SIM ${uiccInfo.physicalSlotIndex}", + tm.getImei(uiccInfo.physicalSlotIndex) ?: return null, uiccInfo.isRemovable ) @@ -105,7 +105,7 @@ open class EuiccChannelManager(protected val context: Context) { } private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? { - return tm.uiccCardsInfo.find { it.slotIndex == slotId }?.let { + return tm.uiccCardsInfoCompat.find { it.physicalSlotIndex == slotId }?.let { tryOpenEuiccChannel(it) } } @@ -123,9 +123,9 @@ open class EuiccChannelManager(protected val context: Context) { withContext(Dispatchers.IO) { ensureSEService() - for (uiccInfo in tm.uiccCardsInfo) { + for (uiccInfo in tm.uiccCardsInfoCompat) { if (tryOpenEuiccChannel(uiccInfo) != null) { - Log.d(TAG, "Found eUICC on slot ${uiccInfo.slotIndex}") + Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex}") } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt b/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt new file mode 100644 index 0000000..7d54469 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt @@ -0,0 +1,75 @@ +package im.angry.openeuicc.util + +import android.annotation.SuppressLint +import android.os.Build +import android.telephony.TelephonyManager +import android.telephony.UiccCardInfo +import android.telephony.UiccPortInfo +import im.angry.openeuicc.util.* +import java.lang.RuntimeException + +@Suppress("DEPRECATION") +class UiccCardInfoCompat(val inner: UiccCardInfo) { + val physicalSlotIndex: Int + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + inner.physicalSlotIndex + } else { + inner.slotIndex + } + + val ports: Collection + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + inner.ports.map { UiccPortInfoCompat(it, this) } + } else { + listOf(UiccPortInfoCompat(null, this)) + } + + val isEuicc: Boolean + get() = inner.isEuicc + + val isRemovable: Boolean + get() = inner.isRemovable + + val isMultipleEnabledProfilesSupported: Boolean + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + inner.isMultipleEnabledProfilesSupported + } else { + false + } + + val cardId: Int + get() = inner.cardId +} + +class UiccPortInfoCompat(private val _inner: Any?, val card: UiccCardInfoCompat) { + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + check(_inner != null && _inner is UiccPortInfo) { + "_inner is not UiccPortInfo on TIRAMISU" + } + } + } + + val inner: UiccPortInfo + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + _inner as UiccPortInfo + } else { + throw RuntimeException("UiccPortInfo does not exist before T") + } + + val portIndex: Int + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + inner.portIndex + } else { + 0 + } +} + +val TelephonyManager.uiccCardsInfoCompat: List + @SuppressLint("MissingPermission") + get() = uiccCardsInfo.map { UiccCardInfoCompat(it) } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt index aa1d8c5..f808a41 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt @@ -1,7 +1,6 @@ package im.angry.openeuicc.core import android.content.Context -import android.telephony.UiccCardInfo import android.util.Log import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.util.* @@ -11,7 +10,7 @@ import java.lang.IllegalArgumentException class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) { override fun checkPrivileges() = true // TODO: Implement proper system app check - override fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? { + override fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? { if (uiccInfo.isRemovable) { // Attempt unprivileged (OMAPI) before TelephonyManager // but still try TelephonyManager in case OMAPI is broken @@ -19,13 +18,13 @@ class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(conte } if (uiccInfo.isEuicc) { - Log.i(TAG, "Trying TelephonyManager for slot ${uiccInfo.slotIndex}") + Log.i(TAG, "Trying TelephonyManager for slot ${uiccInfo.physicalSlotIndex}") // TODO: On Tiramisu, we should also connect all available "ports" for MEP support try { return TelephonyManagerChannel(channelInfo, tm) } catch (e: IllegalArgumentException) { // Failed - Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${uiccInfo.slotIndex}, falling back") + Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${uiccInfo.physicalSlotIndex}, falling back") } } return null diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt new file mode 100644 index 0000000..a1a503d --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt @@ -0,0 +1,39 @@ +package im.angry.openeuicc.util + +import android.os.Build +import android.telephony.IccOpenLogicalChannelResponse +import android.telephony.TelephonyManager + +// TODO: Usage of *byPort APIs will still break build in-tree on lower AOSP versions +// Maybe older versions should simply include hidden-apis-shim when building? +fun TelephonyManager.iccOpenLogicalChannelByPortCompat( + slotIndex: Int, portIndex: Int, aid: String?, p2: Int +): IccOpenLogicalChannelResponse = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + iccOpenLogicalChannelByPort(slotIndex, portIndex, aid, p2) + } else { + iccOpenLogicalChannelBySlot(slotIndex, aid, p2) + } + +fun TelephonyManager.iccCloseLogicalChannelByPortCompat( + slotIndex: Int, portIndex: Int, channel: Int +) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + iccCloseLogicalChannelByPort(slotIndex, portIndex, channel) + } else { + iccCloseLogicalChannelBySlot(slotIndex, channel) + } + +fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat( + slotIndex: Int, portIndex: Int, channel: Int, + cla: Int, inst: Int, p1: Int, p2: Int, p3: Int, data: String? +): String? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + iccTransmitApduLogicalChannelByPort( + slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data + ) + } else { + iccTransmitApduLogicalChannelBySlot( + slotIndex, channel, cla, inst, p1, p2, p3, data + ) + } \ No newline at end of file diff --git a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt index 1bc0121..4a9ea64 100644 --- a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt +++ b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt @@ -14,12 +14,24 @@ private val iccOpenLogicalChannelBySlot: Method by lazy { Int::class.java, String::class.java, Int::class.java ) } +private val iccOpenLogicalChannelByPort: Method by lazy { + TelephonyManager::class.java.getMethod( + "iccOpenLogicalChannelByPort", + Int::class.java, Int::class.java, String::class.java, Int::class.java + ) +} private val iccCloseLogicalChannelBySlot: Method by lazy { TelephonyManager::class.java.getMethod( "iccCloseLogicalChannelBySlot", Int::class.java, Int::class.java ) } +private val iccCloseLogicalChannelByPort: Method by lazy { + TelephonyManager::class.java.getMethod( + "iccCloseLogicalChannelByPort", + Int::class.java, Int::class.java, Int::class.java + ) +} private val iccTransmitApduLogicalChannelBySlot: Method by lazy { TelephonyManager::class.java.getMethod( "iccTransmitApduLogicalChannelBySlot", @@ -27,15 +39,30 @@ private val iccTransmitApduLogicalChannelBySlot: Method by lazy { Int::class.java, Int::class.java, Int::class.java, String::class.java ) } +private val iccTransmitApduLogicalChannelByPort: Method by lazy { + TelephonyManager::class.java.getMethod( + "iccTransmitApduLogicalChannelByPort", + Int::class.java, Int::class.java, Int::class.java, Int::class.java, Int::class.java, + Int::class.java, Int::class.java, Int::class.java, String::class.java + ) +} fun TelephonyManager.iccOpenLogicalChannelBySlot( - slotId: Int, appletId: String, p2: Int + slotId: Int, appletId: String?, p2: Int ): IccOpenLogicalChannelResponse = iccOpenLogicalChannelBySlot.invoke(this, slotId, appletId, p2) as IccOpenLogicalChannelResponse +fun TelephonyManager.iccOpenLogicalChannelByPort( + slotId: Int, portId: Int, appletId: String?, p2: Int +): IccOpenLogicalChannelResponse = + iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse + fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean = iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as Boolean +fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int): Boolean = + iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) as Boolean + fun TelephonyManager.iccTransmitApduLogicalChannelBySlot( slotId: Int, channel: Int, cla: Int, instruction: Int, p1: Int, p2: Int, p3: Int, data: String? @@ -44,6 +71,14 @@ fun TelephonyManager.iccTransmitApduLogicalChannelBySlot( this, slotId, channel, cla, instruction, p1, p2, p3, data ) as String? +fun TelephonyManager.iccTransmitApduLogicalChannelByPort( + slotId: Int, portId: Int, channel: Int, cla: Int, instruction: Int, + p1: Int, p2: Int, p3: Int, data: String? +): String? = + iccTransmitApduLogicalChannelByPort.invoke( + this, slotId, portId, channel, cla, instruction, p1, p2, p3, data + ) as String? + private val requestEmbeddedSubscriptionInfoListRefresh: Method by lazy { SubscriptionManager::class.java.getMethod("requestEmbeddedSubscriptionInfoListRefresh", Int::class.java) } From 4090418146e5cad78c28dab264cc3901b121f138 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 15:19:43 -0500 Subject: [PATCH 02/16] refactor: [2/n] Center EuiccChannel's around ports, not cards --- .../im/angry/openeuicc/core/EuiccChannel.kt | 23 +++---- .../openeuicc/core/EuiccChannelManager.kt | 65 +++++++++++-------- .../openeuicc/core/OmapiApduInterface.kt | 11 ++-- .../openeuicc/ui/EuiccChannelFragmentUtils.kt | 7 +- .../openeuicc/ui/EuiccManagementFragment.kt | 10 +-- .../im/angry/openeuicc/ui/MainActivity.kt | 2 +- .../openeuicc/ui/ProfileDeleteFragment.kt | 4 +- .../openeuicc/ui/ProfileDownloadFragment.kt | 17 +++-- .../openeuicc/ui/ProfileRenameFragment.kt | 4 +- .../angry/openeuicc/util/TelephonyCompat.kt | 8 +++ .../core/PrivilegedEuiccChannelManager.kt | 14 ++-- .../core/TelephonyManagerApduInterface.kt | 16 ++--- 12 files changed, 102 insertions(+), 79 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 80746c4..756ef3c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -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 diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index f579c82..4d3c54a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -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}") + } } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index 1c8dddc..fb5a264 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -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()) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt index 5aa3d73..34f4240 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt @@ -8,23 +8,26 @@ import im.angry.openeuicc.util.openEuiccApplication interface EuiccFragmentMarker -fun newInstanceEuicc(clazz: Class, slotId: Int): T where T: Fragment, T: EuiccFragmentMarker { +fun newInstanceEuicc(clazz: Class, 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.slotId: Int where T: Fragment, T: EuiccFragmentMarker get() = requireArguments().getInt("slotId") +val T.portId: Int where T: Fragment, T: EuiccFragmentMarker + get() = requireArguments().getInt("portId") val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccFragmentMarker get() = openEuiccApplication.euiccChannelManager val T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker get() = - euiccChannelManager.findEuiccChannelBySlotBlocking(slotId)!! + euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! interface EuiccProfilesChangedListener { fun onEuiccProfilesChanged() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 8966dbb..e44d495 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -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 } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 159552c..4bd45e4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -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()) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 92571ad..e7b70b6 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -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) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index b4937db..f77c2ee 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -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 diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index d940036..35c5edb 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -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) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt b/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt index 7d54469..b89253c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt @@ -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 diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt index f808a41..450fc7f 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt @@ -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 diff --git a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt index e4924ea..f4dc483 100644 --- a/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt +++ b/app/src/main/java/im/angry/openeuicc/core/TelephonyManagerApduInterface.kt @@ -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() ) } \ No newline at end of file From 53fa75419776c9cabeb8025392f5642bb574dacb Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 17:14:04 -0500 Subject: [PATCH 03/16] [3/n] Initial implementation of MEP-based slot mapping --- app/build.gradle | 2 + .../openeuicc/ui/PrivilegedMainActivity.kt | 4 + .../angry/openeuicc/ui/SlotMappingFragment.kt | 159 ++++++++++++++++++ .../util/PrivilegedTelephonyCompat.kt | 2 +- .../main/res/layout/fragment_slot_mapping.xml | 29 ++++ .../res/layout/fragment_slot_mapping_item.xml | 25 +++ .../res/menu/activity_main_privileged.xml | 4 + .../main/res/menu/fragment_slot_mapping.xml | 9 + app/src/main/res/values/strings.xml | 4 + libs/hidden-apis-shim/build.gradle | 2 +- .../util/TelephonyManagerHiddenApi.kt | 16 ++ .../android/telephony/UiccSlotMapping.java | 73 ++++++++ 12 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt create mode 100644 app/src/main/res/layout/fragment_slot_mapping.xml create mode 100644 app/src/main/res/layout/fragment_slot_mapping_item.xml create mode 100644 app/src/main/res/menu/fragment_slot_mapping.xml create mode 100644 libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java diff --git a/app/build.gradle b/app/build.gradle index accead3..d6bad2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,6 +57,8 @@ android { } dependencies { + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.3.2' compileOnly project(':libs:hidden-apis-stub') implementation project(':libs:hidden-apis-shim') implementation project(':libs:lpac-jni') diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt index 880d43b..8bcadc8 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt @@ -27,6 +27,10 @@ class PrivilegedMainActivity : MainActivity() { finish() true } + R.id.slot_mapping -> { + SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG) + true + } else -> super.onOptionsItemSelected(item) } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt new file mode 100644 index 0000000..8e3fe0e --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt @@ -0,0 +1,159 @@ +package im.angry.openeuicc.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.telephony.TelephonyManager +import android.telephony.UiccSlotMapping +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.ArrayAdapter +import android.widget.Spinner +import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import im.angry.openeuicc.OpenEuiccApplication +import im.angry.openeuicc.R +import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener { + companion object { + const val TAG = "SlotMappingFragment" + } + + private val tm: TelephonyManager by lazy { + (requireContext().applicationContext as OpenEuiccApplication).telephonyManager + } + + private val ports: List by lazy { + tm.uiccCardsInfoCompat.flatMap { it.ports } + } + + private val portsDesc: List by lazy { + ports.map { getString(R.string.slot_mapping_port, it.card.physicalSlotIndex, it.portIndex) } + } + + private lateinit var toolbar: Toolbar + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: SlotMappingAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_slot_mapping, container, false) + toolbar = view.findViewById(R.id.toolbar) + toolbar.inflateMenu(R.menu.fragment_slot_mapping) + recyclerView = view.findViewById(R.id.mapping_list) + recyclerView.layoutManager = + LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + toolbar.title = getString(R.string.slot_mapping) + toolbar.setNavigationOnClickListener { dismiss() } + toolbar.setOnMenuItemClickListener(this) + } + + override fun onResume() { + super.onResume() + setWidthPercent(85) + init() + } + + @SuppressLint("NotifyDataSetChanged") + private fun init() { + lifecycleScope.launch(Dispatchers.Main) { + val mapping = withContext(Dispatchers.IO) { + tm.simSlotMapping + } + + adapter = SlotMappingAdapter(mapping.toMutableList().apply { + sortBy { it.logicalSlotIndex } + }) + recyclerView.adapter = adapter + adapter.notifyDataSetChanged() + + } + } + + private fun commit() { + lifecycleScope.launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { + tm.simSlotMapping = adapter.mappings + } + openEuiccApplication.euiccChannelManager.invalidate() + requireActivity().finish() + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean = + when (item!!.itemId) { + R.id.ok -> { + commit() + true + } + else -> false + } + + inner class ViewHolder(root: View): RecyclerView.ViewHolder(root), OnItemSelectedListener { + private val textViewLogicalSlot: TextView = root.findViewById(R.id.slot_mapping_logical_slot) + private val spinnerPorts: Spinner = root.findViewById(R.id.slot_mapping_ports) + + init { + spinnerPorts.adapter = ArrayAdapter(requireContext(), im.angry.openeuicc.common.R.layout.spinner_item, portsDesc) + spinnerPorts.onItemSelectedListener = this + } + + private lateinit var mappings: MutableList + private var mappingId: Int = -1 + + fun attachView(mappings: MutableList, mappingId: Int) { + this.mappings = mappings + this.mappingId = mappingId + + textViewLogicalSlot.text = getString(R.string.slot_mapping_logical_slot, mappings[mappingId].logicalSlotIndex) + spinnerPorts.setSelection(ports.indexOfFirst { + it.card.physicalSlotIndex == mappings[mappingId].physicalSlotIndex + && it.portIndex == mappings[mappingId].portIndex + }) + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + check(this::mappings.isInitialized) { "mapping not assigned" } + mappings[mappingId] = + UiccSlotMapping( + ports[position].portIndex, ports[position].card.physicalSlotIndex, mappings[mappingId].logicalSlotIndex) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + + } + } + + inner class SlotMappingAdapter(val mappings: MutableList): RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.fragment_slot_mapping_item, parent, false) + return ViewHolder(view) + } + + override fun getItemCount(): Int = mappings.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.attachView(mappings, position) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt index a1a503d..ac4c968 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt @@ -4,7 +4,7 @@ import android.os.Build import android.telephony.IccOpenLogicalChannelResponse import android.telephony.TelephonyManager -// TODO: Usage of *byPort APIs will still break build in-tree on lower AOSP versions +// TODO: Usage of new APIs from T or later will still break build in-tree on lower AOSP versions // Maybe older versions should simply include hidden-apis-shim when building? fun TelephonyManager.iccOpenLogicalChannelByPortCompat( slotIndex: Int, portIndex: Int, aid: String?, p2: Int diff --git a/app/src/main/res/layout/fragment_slot_mapping.xml b/app/src/main/res/layout/fragment_slot_mapping.xml new file mode 100644 index 0000000..469263b --- /dev/null +++ b/app/src/main/res/layout/fragment_slot_mapping.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_slot_mapping_item.xml b/app/src/main/res/layout/fragment_slot_mapping_item.xml new file mode 100644 index 0000000..44c154e --- /dev/null +++ b/app/src/main/res/layout/fragment_slot_mapping_item.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_privileged.xml b/app/src/main/res/menu/activity_main_privileged.xml index 2bbe20f..cbfaf31 100644 --- a/app/src/main/res/menu/activity_main_privileged.xml +++ b/app/src/main/res/menu/activity_main_privileged.xml @@ -7,4 +7,8 @@ android:checkable="true" android:visible="false" app:showAsAction="never" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_slot_mapping.xml b/app/src/main/res/menu/fragment_slot_mapping.xml new file mode 100644 index 0000000..7f03e18 --- /dev/null +++ b/app/src/main/res/menu/fragment_slot_mapping.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 280289a..8e1c4ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,8 @@ Dual SIM DSDS state switched. Please wait until the modem restarts. + + Slot Mapping + Logical slot %d: + Slot %d Port %d \ No newline at end of file diff --git a/libs/hidden-apis-shim/build.gradle b/libs/hidden-apis-shim/build.gradle index 1c7a212..a3a0472 100644 --- a/libs/hidden-apis-shim/build.gradle +++ b/libs/hidden-apis-shim/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - + compileOnly project(':libs:hidden-apis-stub') implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'com.google.android.material:material:1.6.1' diff --git a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt index 4a9ea64..4203fea 100644 --- a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt +++ b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt @@ -3,6 +3,7 @@ package im.angry.openeuicc.util import android.telephony.IccOpenLogicalChannelResponse import android.telephony.SubscriptionManager import android.telephony.TelephonyManager +import android.telephony.UiccSlotMapping import java.lang.reflect.Method // Hidden APIs via reflection to enable building without AOSP source tree @@ -46,6 +47,17 @@ private val iccTransmitApduLogicalChannelByPort: Method by lazy { Int::class.java, Int::class.java, Int::class.java, String::class.java ) } +private val getSimSlotMapping: Method by lazy { + TelephonyManager::class.java.getMethod( + "getSimSlotMapping" + ) +} +private val setSimSlotMapping: Method by lazy { + TelephonyManager::class.java.getMethod( + "setSimSlotMapping", + Collection::class.java + ) +} fun TelephonyManager.iccOpenLogicalChannelBySlot( slotId: Int, appletId: String?, p2: Int @@ -79,6 +91,10 @@ fun TelephonyManager.iccTransmitApduLogicalChannelByPort( this, slotId, portId, channel, cla, instruction, p1, p2, p3, data ) as String? +var TelephonyManager.simSlotMapping: Collection + get() = getSimSlotMapping.invoke(this) as Collection + set(new) { setSimSlotMapping.invoke(this, new) } + private val requestEmbeddedSubscriptionInfoListRefresh: Method by lazy { SubscriptionManager::class.java.getMethod("requestEmbeddedSubscriptionInfoListRefresh", Int::class.java) } diff --git a/libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java b/libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java new file mode 100644 index 0000000..e3ea60e --- /dev/null +++ b/libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java @@ -0,0 +1,73 @@ +package android.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +public final class UiccSlotMapping implements Parcelable { + public static final Creator CREATOR = null; + + @Override + public void writeToParcel(Parcel dest, int flags) { + throw new RuntimeException("stub"); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * + * @param portIndex The port index is an enumeration of the ports available on the UICC. + * @param physicalSlotIndex is unique index referring to a physical SIM slot. + * @param logicalSlotIndex is unique index referring to a logical SIM slot. + * + */ + public UiccSlotMapping(int portIndex, int physicalSlotIndex, int logicalSlotIndex) { + throw new RuntimeException("stub"); + } + + /** + * Port index is the unique index referring to a port belonging to the physical SIM slot. + * If the SIM does not support multiple enabled profiles, the port index is default index 0. + * + * @return port index. + */ + public int getPortIndex() { + throw new RuntimeException("stub"); + } + + /** + * Gets the physical slot index for the slot that the UICC is currently inserted in. + * + * @return physical slot index which is the index of actual physical UICC slot. + */ + public int getPhysicalSlotIndex() { + throw new RuntimeException("stub"); + } + + /** + * Gets logical slot index for the slot that the UICC is currently attached. + * Logical slot index is the unique index referring to a logical slot(logical modem stack). + * + * @return logical slot index; + */ + public int getLogicalSlotIndex() { + throw new RuntimeException("stub"); + } + + @Override + public boolean equals(Object obj) { + throw new RuntimeException("stub"); + } + + @Override + public int hashCode() { + throw new RuntimeException("stub"); + } + + @Override + public String toString() { + throw new RuntimeException("stub"); + } +} From 53be772591c24207124b231a20537bf5b4d4647d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 17:31:00 -0500 Subject: [PATCH 04/16] [4/n] Only try to open ports with a mapped slot index --- .../src/main/java/im/angry/openeuicc/core/EuiccChannel.kt | 2 +- .../java/im/angry/openeuicc/core/EuiccChannelManager.kt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 756ef3c..c3036a1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -10,7 +10,7 @@ abstract class EuiccChannel( val logicalSlotId = port.logicalSlotIndex val portId = port.portIndex val cardId = port.card.cardId - val name = "SLOT ${port.card.physicalSlotIndex}:${port.portIndex}" + val name = "SLOT $logicalSlotId" val removable = port.card.isRemovable abstract val lpa: LocalProfileAssistant diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 4d3c54a..906ac6f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Handler import android.os.HandlerThread import android.se.omapi.SEService +import android.telephony.SubscriptionManager import android.util.Log import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.util.* @@ -77,7 +78,7 @@ open class EuiccChannelManager(protected val context: Context) { ensureSEService() val existing = channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } if (existing != null) { - if (existing.valid) { + if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) { return existing } else { existing.close() @@ -85,6 +86,11 @@ open class EuiccChannelManager(protected val context: Context) { } } + if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + // We can only open channels on ports that are actually enabled + return null + } + var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port) if (euiccChannel == null) { From e708deea7caa621d0d5390e435a82bca196e4305 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 18:18:12 -0500 Subject: [PATCH 05/16] [5/n] Add more help text for slot remapping --- .../angry/openeuicc/ui/SlotMappingFragment.kt | 44 ++++++++++++++++++- .../main/res/layout/fragment_slot_mapping.xml | 18 +++++++- app/src/main/res/values/strings.xml | 5 +++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt index 8e3fe0e..c0bdc63 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt @@ -13,6 +13,7 @@ import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter import android.widget.Spinner import android.widget.TextView +import android.widget.Toast import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener import androidx.fragment.app.DialogFragment @@ -46,6 +47,7 @@ class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener { private lateinit var toolbar: Toolbar private lateinit var recyclerView: RecyclerView private lateinit var adapter: SlotMappingAdapter + private lateinit var helpTextView: TextView override fun onCreateView( inflater: LayoutInflater, @@ -58,6 +60,7 @@ class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener { recyclerView = view.findViewById(R.id.mapping_list) recyclerView.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) + helpTextView = view.findViewById(R.id.mapping_help) return view } @@ -87,19 +90,56 @@ class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener { recyclerView.adapter = adapter adapter.notifyDataSetChanged() + helpTextView.text = buildHelpText() } } private fun commit() { lifecycleScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { - tm.simSlotMapping = adapter.mappings + try { + withContext(Dispatchers.IO) { + tm.simSlotMapping = adapter.mappings + } + } catch (e: Exception) { + Toast.makeText(requireContext(), R.string.slot_mapping_failure, Toast.LENGTH_LONG).show() + return@launch } + Toast.makeText(requireContext(), R.string.slot_mapping_completed, Toast.LENGTH_LONG).show() openEuiccApplication.euiccChannelManager.invalidate() requireActivity().finish() } } + private suspend fun buildHelpText() = withContext(Dispatchers.IO) { + var nLogicalSlots = adapter.mappings.size + + val cards = openEuiccApplication.telephonyManager.uiccCardsInfoCompat + + var nPhysicalSlots = cards.size + var idxMepCard = -1 + var nMepPorts = 0 + + for (card in cards) { + if (card.isMultipleEnabledProfilesSupported) { + idxMepCard = card.physicalSlotIndex + nMepPorts = card.ports.size + } + } + + val mayEnableDSDS = + openEuiccApplication.telephonyManager.supportsDSDS && !openEuiccApplication.telephonyManager.dsdsEnabled + val extraText = + if (nLogicalSlots == 1 && mayEnableDSDS) { + getString(R.string.slot_mapping_help_dsds) + } else if (idxMepCard != -1) { + getString(R.string.slot_mapping_help_mep, idxMepCard, nMepPorts) + } else { + "" + } + + getString(R.string.slot_mapping_help, nLogicalSlots, nPhysicalSlots, extraText) + } + override fun onMenuItemClick(item: MenuItem?): Boolean = when (item!!.itemId) { R.id.ok -> { diff --git a/app/src/main/res/layout/fragment_slot_mapping.xml b/app/src/main/res/layout/fragment_slot_mapping.xml index 469263b..656dd3d 100644 --- a/app/src/main/res/layout/fragment_slot_mapping.xml +++ b/app/src/main/res/layout/fragment_slot_mapping.xml @@ -22,8 +22,24 @@ android:layout_height="wrap_content" android:paddingTop="6dp" app:layout_constraintTop_toBottomOf="@id/toolbar" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/mapping_help" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e1c4ba..dc9cfd4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,9 @@ Slot Mapping Logical slot %d: Slot %d Port %d + Your phone has %d logical and %d physical SIM slots.%s\n\nSelect which physical slot and/or "port" you want each logical slot to correspond to. Note that not all mapping modes may be supported by hardware. + \n\nPhysical slot %d supports Multiple Enabled Profiles (MEP). To use this feature, associate its %d virtual "ports" to different logical slots shown above. + \nDual SIM mode is supported but disabled. + Your new slot mapping has been set. Please wait until modem refreshes the slots. + The specified mapping might be invalid or unsupported by hardware. \ No newline at end of file From 81b61c76c452b7791449f84a30ede2a935c7235b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 21:40:08 -0500 Subject: [PATCH 06/16] [6/n] Show special footer for SIM cards with MEP support --- .../im/angry/openeuicc/core/EuiccChannel.kt | 1 + .../openeuicc/ui/EuiccManagementFragment.kt | 80 ++++++++++++++++--- .../im/angry/openeuicc/ui/MainActivity.kt | 6 +- .../ui/PrivilegedEuiccManagementFragment.kt | 24 ++++++ .../openeuicc/ui/PrivilegedMainActivity.kt | 9 ++- app/src/main/res/layout/footer_mep.xml | 32 ++++++++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt create mode 100644 app/src/main/res/layout/footer_mep.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index c3036a1..a16073f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -12,6 +12,7 @@ abstract class EuiccChannel( val cardId = port.card.cardId val name = "SLOT $logicalSlotId" val removable = port.card.isRemovable + val isMEP = port.card.isMultipleEnabledProfilesSupported abstract val lpa: LocalProfileAssistant val valid: Boolean diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index e44d495..01e82ce 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageButton import android.widget.PopupMenu import android.widget.TextView @@ -26,7 +27,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.lang.Exception -class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener { +open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener { companion object { const val TAG = "EuiccManagementFragment" @@ -38,7 +39,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh private lateinit var fab: FloatingActionButton private lateinit var profileList: RecyclerView - private val adapter = EuiccProfileAdapter(listOf()) + private val adapter = EuiccProfileAdapter() override fun onCreateView( inflater: LayoutInflater, @@ -76,6 +77,8 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh refresh() } + protected open suspend fun onCreateFooterViews(parent: ViewGroup): List = listOf() + @SuppressLint("NotifyDataSetChanged") private fun refresh() { swipeRefresh.isRefreshing = true @@ -88,6 +91,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh withContext(Dispatchers.Main) { adapter.profiles = profiles.operational + adapter.footerViews = onCreateFooterViews(profileList) adapter.notifyDataSetChanged() swipeRefresh.isRefreshing = false } @@ -130,7 +134,30 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh channel.lpa.disableProfile(iccid) } - inner class ViewHolder(private val root: View) : RecyclerView.ViewHolder(root) { + sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { + enum class Type(val value: Int) { + PROFILE(0), + FOOTER(1); + + companion object { + fun fromInt(value: Int) = + Type.values().first { it.value == value } + } + } + } + + inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) { + fun attach(view: View) { + view.parent?.let { (it as ViewGroup).removeView(view) } + (itemView as FrameLayout).addView(view) + } + + fun detach() { + (itemView as FrameLayout).removeAllViews() + } + } + + inner class ProfileViewHolder(private val root: View) : ViewHolder(root) { private val iccid: TextView = root.findViewById(R.id.iccid) private val name: TextView = root.findViewById(R.id.name) private val state: TextView = root.findViewById(R.id.state) @@ -208,16 +235,49 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh } } - inner class EuiccProfileAdapter(var profiles: List) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false) - return ViewHolder(view) - } + inner class EuiccProfileAdapter : RecyclerView.Adapter() { + var profiles: List = listOf() + var footerViews: List = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + when (ViewHolder.Type.fromInt(viewType)) { + ViewHolder.Type.PROFILE -> { + val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false) + ProfileViewHolder(view) + } + ViewHolder.Type.FOOTER -> { + FooterViewHolder() + } + } + + override fun getItemViewType(position: Int): Int = + when { + position < profiles.size -> { + ViewHolder.Type.PROFILE.value + } + position >= profiles.size && position < profiles.size + footerViews.size -> { + ViewHolder.Type.FOOTER.value + } + else -> -1 + } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.setProfile(profiles[position]) + when (holder) { + is ProfileViewHolder -> { + holder.setProfile(profiles[position]) + } + is FooterViewHolder -> { + holder.attach(footerViews[position - profiles.size]) + } + } } - override fun getItemCount(): Int = profiles.size + override fun onViewRecycled(holder: ViewHolder) { + if (holder is FooterViewHolder) { + holder.detach() + } + } + + override fun getItemCount(): Int = profiles.size + footerViews.size } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 4bd45e4..961676d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -11,6 +11,7 @@ import android.widget.Spinner import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R +import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers @@ -72,6 +73,9 @@ open class MainActivity : AppCompatActivity() { return true } + protected open fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment = + EuiccManagementFragment.newInstance(channel.slotId, channel.portId) + private suspend fun init() { withContext(Dispatchers.IO) { manager.enumerateEuiccChannels() @@ -88,7 +92,7 @@ open class MainActivity : AppCompatActivity() { withContext(Dispatchers.Main) { manager.knownChannels.forEach { channel -> spinnerAdapter.add(channel.name) - fragments.add(EuiccManagementFragment.newInstance(channel.slotId, channel.portId)) + fragments.add(createEuiccManagementFragment(channel)) } if (fragments.isNotEmpty()) { diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt new file mode 100644 index 0000000..730fc49 --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt @@ -0,0 +1,24 @@ +package im.angry.openeuicc.ui + +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import im.angry.openeuicc.R + +class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { + companion object { + fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = + newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId) + } + + override suspend fun onCreateFooterViews(parent: ViewGroup): List = + if (channel.isMEP) { + val view = layoutInflater.inflate(R.layout.footer_mep, parent, false) + view.findViewById