Compare commits

...

2 commits

24 changed files with 429 additions and 81 deletions

View file

@ -1,24 +1,17 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileAssistant 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( abstract class EuiccChannel(
info: EuiccChannelInfo port: UiccPortInfoCompat
) { ) {
val slotId = info.slotId val slotId = port.card.physicalSlotIndex // PHYSICAL slot
val cardId = info.cardId val logicalSlotId = port.logicalSlotIndex
val name = info.name val portId = port.portIndex
val imei = info.imei val cardId = port.card.cardId
val removable = info.removable val name = "SLOT ${port.card.physicalSlotIndex}:${port.portIndex}"
val removable = port.card.isRemovable
abstract val lpa: LocalProfileAssistant abstract val lpa: LocalProfileAssistant
val valid: Boolean val valid: Boolean

View file

@ -1,6 +1,5 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
@ -17,7 +16,6 @@ import java.lang.IllegalArgumentException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@SuppressLint("MissingPermission") // We rely on ARA-based privileges, not READ_PRIVILEGED_PHONE_STATE
open class EuiccChannelManager(protected val context: Context) { open class EuiccChannelManager(protected val context: Context) {
companion object { companion object {
const val TAG = "EuiccChannelManager" 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 // No-op when unprivileged
return null return null
} }
protected fun tryOpenEuiccChannelUnprivileged(uiccInfo: UiccCardInfoCompat, channelInfo: EuiccChannelInfo): EuiccChannel? { protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? {
Log.i(TAG, "Trying OMAPI for slot ${uiccInfo.physicalSlotIndex}") 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 { try {
return OmapiChannel(seService!!, channelInfo) return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
// Failed // 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 return null
} }
private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfoCompat): EuiccChannel? { private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock { lock.withLock {
ensureSEService() 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 != null) {
if (existing.valid) { if (existing.valid) {
return existing return existing
@ -82,18 +85,10 @@ open class EuiccChannelManager(protected val context: Context) {
} }
} }
val channelInfo = EuiccChannelInfo( var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port)
uiccInfo.physicalSlotIndex,
uiccInfo.cardId,
"SIM ${uiccInfo.physicalSlotIndex}",
tm.getImei(uiccInfo.physicalSlotIndex) ?: return null,
uiccInfo.isRemovable
)
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(uiccInfo, channelInfo)
if (euiccChannel == null) { if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(uiccInfo, channelInfo) euiccChannel = tryOpenEuiccChannelUnprivileged(port)
} }
if (euiccChannel != null) { if (euiccChannel != null) {
@ -104,16 +99,28 @@ open class EuiccChannelManager(protected val context: Context) {
} }
} }
private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? { fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
return tm.uiccCardsInfoCompat.find { it.physicalSlotIndex == slotId }?.let { runBlocking {
tryOpenEuiccChannel(it) 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 if (!checkPrivileges()) return@runBlocking null
withContext(Dispatchers.IO) { 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() ensureSEService()
for (uiccInfo in tm.uiccCardsInfoCompat) { for (uiccInfo in tm.uiccCardsInfoCompat) {
if (tryOpenEuiccChannel(uiccInfo) != null) { for (port in uiccInfo.ports) {
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex}") 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.Channel
import android.se.omapi.SEService import android.se.omapi.SEService
import android.se.omapi.Session import android.se.omapi.Session
import im.angry.openeuicc.util.UiccPortInfoCompat
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
@ -10,13 +11,13 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class OmapiApduInterface( class OmapiApduInterface(
private val service: SEService, private val service: SEService,
private val info: EuiccChannelInfo private val port: UiccPortInfoCompat
): ApduInterface { ): ApduInterface {
private lateinit var session: Session private lateinit var session: Session
private lateinit var lastChannel: Channel private lateinit var lastChannel: Channel
override fun connect() { override fun connect() {
session = service.getUiccReader(info.slotId + 1).openSession() session = service.getUiccReader(port.logicalSlotIndex + 1).openSession()
} }
override fun disconnect() { override fun disconnect() {
@ -50,9 +51,9 @@ class OmapiApduInterface(
class OmapiChannel( class OmapiChannel(
service: SEService, service: SEService,
info: EuiccChannelInfo, port: UiccPortInfoCompat,
) : EuiccChannel(info) { ) : EuiccChannel(port) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl( override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
OmapiApduInterface(service, info), OmapiApduInterface(service, port),
HttpInterfaceImpl()) HttpInterfaceImpl())
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,6 +68,14 @@ class UiccPortInfoCompat(private val _inner: Any?, val card: UiccCardInfoCompat)
} else { } else {
0 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> val TelephonyManager.uiccCardsInfoCompat: List<UiccCardInfoCompat>

View file

@ -57,6 +57,8 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
compileOnly project(':libs:hidden-apis-stub') compileOnly project(':libs:hidden-apis-stub')
implementation project(':libs:hidden-apis-shim') implementation project(':libs:hidden-apis-shim')
implementation project(':libs:lpac-jni') implementation project(':libs:lpac-jni')

View file

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

View file

@ -9,7 +9,7 @@ import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class TelephonyManagerApduInterface( class TelephonyManagerApduInterface(
private val info: EuiccChannelInfo, private val port: UiccPortInfoCompat,
private val tm: TelephonyManager private val tm: TelephonyManager
): ApduInterface { ): ApduInterface {
private var lastChannel: Int = -1 private var lastChannel: Int = -1
@ -25,9 +25,9 @@ class TelephonyManagerApduInterface(
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {
check(lastChannel == -1) { "Already initialized" } check(lastChannel == -1) { "Already initialized" }
val hex = aid.encodeHex() 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) { 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 lastChannel = channel.channel
return lastChannel return lastChannel
@ -35,7 +35,7 @@ class TelephonyManagerApduInterface(
override fun logicalChannelClose(handle: Int) { override fun logicalChannelClose(handle: Int) {
check(handle == lastChannel) { "Invalid channel handle " } check(handle == lastChannel) { "Invalid channel handle " }
tm.iccCloseLogicalChannelBySlot(info.slotId, handle) tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
lastChannel = -1 lastChannel = -1
} }
@ -49,18 +49,18 @@ class TelephonyManagerApduInterface(
val p3 = tx[4].toUByte().toInt() val p3 = tx[4].toUByte().toInt()
val p4 = tx.drop(5).toByteArray().encodeHex() 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() cla, instruction, p1, p2, p3, p4)?.decodeHex() ?: byteArrayOf()
} }
} }
class TelephonyManagerChannel( class TelephonyManagerChannel(
info: EuiccChannelInfo, port: UiccPortInfoCompat,
private val tm: TelephonyManager private val tm: TelephonyManager
) : EuiccChannel(info) { ) : EuiccChannel(port) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl( override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
TelephonyManagerApduInterface(info, tm), TelephonyManagerApduInterface(port, tm),
HttpInterfaceImpl() HttpInterfaceImpl()
) )
} }

View file

@ -27,6 +27,10 @@ class PrivilegedMainActivity : MainActivity() {
finish() finish()
true true
} }
R.id.slot_mapping -> {
SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG)
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View file

@ -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<UiccPortInfoCompat> by lazy {
tm.uiccCardsInfoCompat.flatMap { it.ports }
}
private val portsDesc: List<String> 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<UiccSlotMapping>
private var mappingId: Int = -1
fun attachView(mappings: MutableList<UiccSlotMapping>, 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<UiccSlotMapping>): RecyclerView.Adapter<ViewHolder>() {
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)
}
}
}

View file

@ -4,7 +4,7 @@ import android.os.Build
import android.telephony.IccOpenLogicalChannelResponse import android.telephony.IccOpenLogicalChannelResponse
import android.telephony.TelephonyManager 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? // Maybe older versions should simply include hidden-apis-shim when building?
fun TelephonyManager.iccOpenLogicalChannelByPortCompat( fun TelephonyManager.iccOpenLogicalChannelByPortCompat(
slotIndex: Int, portIndex: Int, aid: String?, p2: Int slotIndex: Int, portIndex: Int, aid: String?, p2: Int

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/Theme.OpenEUICC"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1"
app:navigationIcon="?homeAsUpIndicator" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mapping_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="6dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48sp"
android:gravity="center">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/slot_mapping_logical_slot"
android:textSize="14sp"
android:layout_width="wrap_content"
android:layout_height="32sp"
android:layout_marginEnd="10sp" />
<Spinner
android:id="@+id/slot_mapping_ports"
android:layout_width="wrap_content"
android:layout_height="32sp" />
</LinearLayout>
</LinearLayout>

View file

@ -7,4 +7,8 @@
android:checkable="true" android:checkable="true"
android:visible="false" android:visible="false"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/slot_mapping"
android:title="@string/slot_mapping"
app:showAsAction="never" />
</menu> </menu>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/ok"
android:icon="@drawable/ic_check_black"
android:title="@string/slot_mapping"
app:showAsAction="ifRoom"/>
</menu>

View file

@ -5,4 +5,8 @@
<string name="dsds">Dual SIM</string> <string name="dsds">Dual SIM</string>
<string name="toast_dsds_switched">DSDS state switched. Please wait until the modem restarts.</string> <string name="toast_dsds_switched">DSDS state switched. Please wait until the modem restarts.</string>
<string name="slot_mapping">Slot Mapping</string>
<string name="slot_mapping_logical_slot">Logical slot %d:</string>
<string name="slot_mapping_port">Slot %d Port %d</string>
</resources> </resources>

View file

@ -31,7 +31,7 @@ android {
} }
dependencies { dependencies {
compileOnly project(':libs:hidden-apis-stub')
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1' implementation 'com.google.android.material:material:1.6.1'

View file

@ -3,6 +3,7 @@ package im.angry.openeuicc.util
import android.telephony.IccOpenLogicalChannelResponse import android.telephony.IccOpenLogicalChannelResponse
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.telephony.UiccSlotMapping
import java.lang.reflect.Method import java.lang.reflect.Method
// Hidden APIs via reflection to enable building without AOSP source tree // 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 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( fun TelephonyManager.iccOpenLogicalChannelBySlot(
slotId: Int, appletId: String?, p2: Int slotId: Int, appletId: String?, p2: Int
@ -79,6 +91,10 @@ fun TelephonyManager.iccTransmitApduLogicalChannelByPort(
this, slotId, portId, channel, cla, instruction, p1, p2, p3, data this, slotId, portId, channel, cla, instruction, p1, p2, p3, data
) as String? ) as String?
var TelephonyManager.simSlotMapping: Collection<UiccSlotMapping>
get() = getSimSlotMapping.invoke(this) as Collection<UiccSlotMapping>
set(new) { setSimSlotMapping.invoke(this, new) }
private val requestEmbeddedSubscriptionInfoListRefresh: Method by lazy { private val requestEmbeddedSubscriptionInfoListRefresh: Method by lazy {
SubscriptionManager::class.java.getMethod("requestEmbeddedSubscriptionInfoListRefresh", Int::class.java) SubscriptionManager::class.java.getMethod("requestEmbeddedSubscriptionInfoListRefresh", Int::class.java)
} }

View file

@ -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<UiccSlotMapping> 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");
}
}