Compare commits
18 commits
5f88bf089c
...
bbfffe3abf
Author | SHA1 | Date | |
---|---|---|---|
bbfffe3abf | |||
f583e35697 | |||
d4b875c808 | |||
aec8232521 | |||
100d2e2866 | |||
470fe5c545 | |||
d95341b764 | |||
5948499896 | |||
ed585edf49 | |||
33b70bc315 | |||
fc980cda43 | |||
81b61c76c4 | |||
e708deea7c | |||
53be772591 | |||
53fa754197 | |||
4090418146 | |||
2ccfe02204 | |||
19aeefaba9 |
31 changed files with 994 additions and 142 deletions
|
@ -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 removable = port.card.isRemovable
|
||||||
|
val isMEP = port.card.isMultipleEnabledProfilesSupported
|
||||||
|
|
||||||
abstract val lpa: LocalProfileAssistant
|
abstract val lpa: LocalProfileAssistant
|
||||||
val valid: Boolean
|
val valid: Boolean
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
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
|
||||||
import android.se.omapi.SEService
|
import android.se.omapi.SEService
|
||||||
import android.telephony.UiccCardInfo
|
import android.telephony.SubscriptionManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
@ -17,7 +17,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,29 +51,34 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, 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: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? {
|
protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? {
|
||||||
Log.i(TAG, "Trying OMAPI for slot ${uiccInfo.slotIndex}")
|
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.slotIndex}.")
|
Log.w(TAG, "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfo): EuiccChannel? {
|
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
ensureSEService()
|
ensureSEService()
|
||||||
val existing = channels.find { it.slotId == uiccInfo.slotIndex }
|
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 && port.logicalSlotIndex == existing.logicalSlotId) {
|
||||||
return existing
|
return existing
|
||||||
} else {
|
} else {
|
||||||
existing.close()
|
existing.close()
|
||||||
|
@ -82,18 +86,15 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val channelInfo = EuiccChannelInfo(
|
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
|
||||||
uiccInfo.slotIndex,
|
// We can only open channels on ports that are actually enabled
|
||||||
uiccInfo.cardId,
|
return null
|
||||||
"SIM ${uiccInfo.slotIndex}",
|
}
|
||||||
tm.getImei(uiccInfo.slotIndex) ?: return null,
|
|
||||||
uiccInfo.isRemovable
|
|
||||||
)
|
|
||||||
|
|
||||||
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(uiccInfo, channelInfo)
|
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port)
|
||||||
|
|
||||||
if (euiccChannel == null) {
|
if (euiccChannel == null) {
|
||||||
euiccChannel = tryOpenEuiccChannelUnprivileged(uiccInfo, channelInfo)
|
euiccChannel = tryOpenEuiccChannelUnprivileged(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (euiccChannel != null) {
|
if (euiccChannel != null) {
|
||||||
|
@ -104,16 +105,52 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? {
|
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
|
||||||
return tm.uiccCardsInfo.find { it.slotIndex == 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
|
||||||
|
if (!checkPrivileges()) return@runBlocking null
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
for (card in tm.uiccCardsInfoCompat) {
|
||||||
|
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||||
|
for (port in card.ports) {
|
||||||
|
tryOpenEuiccChannel(port)?.let { return@withContext it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findEuiccChannelBySlotBlocking(slotId: Int): EuiccChannel? = runBlocking {
|
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
|
||||||
|
if (!checkPrivileges()) return@runBlocking null
|
||||||
|
for (card in tm.uiccCardsInfoCompat) {
|
||||||
|
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||||
|
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
||||||
|
.ifEmpty { null }
|
||||||
|
}
|
||||||
|
return@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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,9 +160,11 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ensureSEService()
|
ensureSEService()
|
||||||
|
|
||||||
for (uiccInfo in tm.uiccCardsInfo) {
|
for (uiccInfo in tm.uiccCardsInfoCompat) {
|
||||||
if (tryOpenEuiccChannel(uiccInfo) != null) {
|
for (port in uiccInfo.ports) {
|
||||||
Log.d(TAG, "Found eUICC on slot ${uiccInfo.slotIndex}")
|
if (tryOpenEuiccChannel(port) != null) {
|
||||||
|
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +185,7 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
seService = null
|
seService = null
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun notifyEuiccProfilesChanged(slotId: Int) {
|
open fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||||
// No-op for unprivileged
|
// No-op for unprivileged
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -26,19 +27,19 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
|
||||||
class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
|
open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
|
||||||
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
|
||||||
private lateinit var fab: FloatingActionButton
|
private lateinit var fab: FloatingActionButton
|
||||||
private lateinit var profileList: RecyclerView
|
private lateinit var profileList: RecyclerView
|
||||||
|
|
||||||
private val adapter = EuiccProfileAdapter(listOf())
|
private val adapter = EuiccProfileAdapter()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -62,7 +63,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,18 +77,21 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open suspend fun onCreateFooterViews(parent: ViewGroup): List<View> = listOf()
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun refresh() {
|
private fun refresh() {
|
||||||
swipeRefresh.isRefreshing = true
|
swipeRefresh.isRefreshing = true
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val profiles = withContext(Dispatchers.IO) {
|
val profiles = withContext(Dispatchers.IO) {
|
||||||
euiccChannelManager.notifyEuiccProfilesChanged(slotId)
|
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||||
channel.lpa.profiles
|
channel.lpa.profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
adapter.profiles = profiles.operational
|
adapter.profiles = profiles.operational
|
||||||
|
adapter.footerViews = onCreateFooterViews(profileList)
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
swipeRefresh.isRefreshing = false
|
swipeRefresh.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
@ -130,7 +134,30 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
||||||
channel.lpa.disableProfile(iccid)
|
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 iccid: TextView = root.findViewById(R.id.iccid)
|
||||||
private val name: TextView = root.findViewById(R.id.name)
|
private val name: TextView = root.findViewById(R.id.name)
|
||||||
private val state: TextView = root.findViewById(R.id.state)
|
private val state: TextView = root.findViewById(R.id.state)
|
||||||
|
@ -195,12 +222,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
|
||||||
}
|
}
|
||||||
|
@ -208,16 +235,49 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class EuiccProfileAdapter(var profiles: List<LocalProfileInfo>) : RecyclerView.Adapter<ViewHolder>() {
|
inner class EuiccProfileAdapter : RecyclerView.Adapter<ViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
var profiles: List<LocalProfileInfo> = listOf()
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
|
var footerViews: List<View> = listOf()
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
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) {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,6 +11,7 @@ import android.widget.Spinner
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.core.EuiccChannel
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -72,23 +73,26 @@ open class MainActivity : AppCompatActivity() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||||
|
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||||
|
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
manager.enumerateEuiccChannels()
|
manager.enumerateEuiccChannels()
|
||||||
manager.knownChannels.forEach {
|
manager.knownChannels.forEach {
|
||||||
Log.d(TAG, it.name)
|
Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
|
||||||
Log.d(TAG, it.lpa.eID)
|
Log.d(TAG, it.lpa.eID)
|
||||||
// Request the system to refresh the list of profiles every time we start
|
// Request the system to refresh the list of profiles every time we start
|
||||||
// Note that this is currently supposed to be no-op when unprivileged,
|
// Note that this is currently supposed to be no-op when unprivileged,
|
||||||
// but it could change in the future
|
// but it could change in the future
|
||||||
manager.notifyEuiccProfilesChanged(it.slotId)
|
manager.notifyEuiccProfilesChanged(it.logicalSlotId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
manager.knownChannels.forEach { channel ->
|
manager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
|
||||||
spinnerAdapter.add(channel.name)
|
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
|
||||||
fragments.add(EuiccManagementFragment.newInstance(channel.slotId))
|
fragments.add(createEuiccManagementFragment(channel))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fragments.isNotEmpty()) {
|
if (fragments.isNotEmpty()) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||||
|
|
||||||
val LocalProfileInfo.displayName: String
|
val LocalProfileInfo.displayName: String
|
||||||
|
@ -8,4 +9,10 @@ val LocalProfileInfo.displayName: String
|
||||||
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
|
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
|
||||||
get() = filter {
|
get() = filter {
|
||||||
it.profileClass == LocalProfileInfo.Clazz.Operational
|
it.profileClass == LocalProfileInfo.Clazz.Operational
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LocalProfileAssistant.disableActiveProfileWithUndo(): () -> Unit =
|
||||||
|
profiles.find { it.state == LocalProfileInfo.State.Enabled }?.let {
|
||||||
|
disableProfile(it.iccid)
|
||||||
|
return { enableProfile(it.iccid) }
|
||||||
|
} ?: { }
|
|
@ -0,0 +1,83 @@
|
||||||
|
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<UiccPortInfoCompat>
|
||||||
|
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 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>
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
get() = uiccCardsInfo.map { UiccCardInfoCompat(it) }
|
|
@ -3,6 +3,8 @@
|
||||||
<string name="no_euicc">No eUICC card on this device is accessible by this app.\nInsert a supported eUICC card, or try out the privileged OpenEUICC app instead.</string>
|
<string name="no_euicc">No eUICC card on this device is accessible by this app.\nInsert a supported eUICC card, or try out the privileged OpenEUICC app instead.</string>
|
||||||
<string name="unknown">Unknown</string>
|
<string name="unknown">Unknown</string>
|
||||||
|
|
||||||
|
<string name="channel_name_format">Logical Slot %d</string>
|
||||||
|
|
||||||
<string name="enabled">Enabled</string>
|
<string name="enabled">Enabled</string>
|
||||||
<string name="disabled">Disabled</string>
|
<string name="disabled">Disabled</string>
|
||||||
<string name="provider">Provider:</string>
|
<string name="provider">Provider:</string>
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.telephony.UiccCardInfo
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
@ -11,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: UiccCardInfo, 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.slotIndex}")
|
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.slotIndex}, falling back")
|
Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -46,9 +45,9 @@ class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(conte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun notifyEuiccProfilesChanged(slotId: Int) {
|
override fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||||
(context.applicationContext as OpenEuiccApplication).subscriptionManager.apply {
|
(context.applicationContext as OpenEuiccApplication).subscriptionManager.apply {
|
||||||
findEuiccChannelBySlotBlocking(slotId)?.let {
|
findEuiccChannelBySlotBlocking(logicalSlotId)?.let {
|
||||||
tryRefreshCachedEuiccInfo(it.cardId)
|
tryRefreshCachedEuiccInfo(it.cardId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,20 +1,35 @@
|
||||||
package im.angry.openeuicc.service
|
package im.angry.openeuicc.service
|
||||||
|
|
||||||
import android.service.euicc.*
|
import android.service.euicc.*
|
||||||
|
import android.telephony.UiccSlotMapping
|
||||||
import android.telephony.euicc.DownloadableSubscription
|
import android.telephony.euicc.DownloadableSubscription
|
||||||
import android.telephony.euicc.EuiccInfo
|
import android.telephony.euicc.EuiccInfo
|
||||||
|
import android.util.Log
|
||||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
import im.angry.openeuicc.core.EuiccChannel
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
class OpenEuiccService : EuiccService() {
|
class OpenEuiccService : EuiccService() {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "OpenEuiccService"
|
||||||
|
}
|
||||||
|
|
||||||
private val openEuiccApplication
|
private val openEuiccApplication
|
||||||
get() = application as OpenEuiccApplication
|
get() = application as OpenEuiccApplication
|
||||||
|
|
||||||
private fun findChannel(slotId: Int): EuiccChannel? =
|
private fun findChannel(physicalSlotId: Int): EuiccChannel? =
|
||||||
openEuiccApplication.euiccChannelManager
|
openEuiccApplication.euiccChannelManager
|
||||||
.findEuiccChannelBySlotBlocking(slotId)
|
.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
|
||||||
|
|
||||||
|
private fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
|
||||||
|
openEuiccApplication.euiccChannelManager
|
||||||
|
.findEuiccChannelByPortBlocking(slotId, portId)
|
||||||
|
|
||||||
|
private fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
|
||||||
|
openEuiccApplication.euiccChannelManager
|
||||||
|
.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
|
||||||
|
|
||||||
override fun onGetEid(slotId: Int): String? =
|
override fun onGetEid(slotId: Int): String? =
|
||||||
findChannel(slotId)?.lpa?.eID
|
findChannel(slotId)?.lpa?.eID
|
||||||
|
@ -25,6 +40,47 @@ class OpenEuiccService : EuiccService() {
|
||||||
private fun EuiccChannel.profileExists(iccid: String?) =
|
private fun EuiccChannel.profileExists(iccid: String?) =
|
||||||
lpa.profiles.any { it.iccid == iccid }
|
lpa.profiles.any { it.iccid == iccid }
|
||||||
|
|
||||||
|
private fun ensurePortIsMapped(slotId: Int, portId: Int) {
|
||||||
|
val mappings = openEuiccApplication.telephonyManager.simSlotMapping.toMutableList()
|
||||||
|
|
||||||
|
mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId }?.let {
|
||||||
|
throw IllegalStateException("Slot $slotId port $portId has already been mapped")
|
||||||
|
}
|
||||||
|
|
||||||
|
val idx = mappings.indexOfFirst { it.physicalSlotIndex != slotId || it.portIndex != portId }
|
||||||
|
if (idx >= 0) {
|
||||||
|
mappings[idx] = UiccSlotMapping(portId, slotId, mappings[idx].logicalSlotIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId } ?: run {
|
||||||
|
throw IllegalStateException("Cannot map slot $slotId port $portId")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
openEuiccApplication.telephonyManager.simSlotMapping = mappings
|
||||||
|
return
|
||||||
|
} catch (_: Exception) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes hardware supports one ordering but not the reverse
|
||||||
|
openEuiccApplication.telephonyManager.simSlotMapping = mappings.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> retryWithTimeout(timeoutMillis: Int, backoff: Int = 1000, f: () -> T?): T? {
|
||||||
|
val startTimeMillis = System.currentTimeMillis()
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
f()?.let { return@retryWithTimeout it }
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Ignore
|
||||||
|
} finally {
|
||||||
|
Thread.sleep(backoff.toLong())
|
||||||
|
}
|
||||||
|
} while (System.currentTimeMillis() - startTimeMillis < timeoutMillis)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGetOtaStatus(slotId: Int): Int {
|
override fun onGetOtaStatus(slotId: Int): Int {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
return 5 // EUICC_OTA_STATUS_UNAVAILABLE
|
return 5 // EUICC_OTA_STATUS_UNAVAILABLE
|
||||||
|
@ -56,6 +112,7 @@ class OpenEuiccService : EuiccService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult? {
|
override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult? {
|
||||||
|
Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
|
||||||
val channel = findChannel(slotId) ?: return null
|
val channel = findChannel(slotId) ?: return null
|
||||||
val profiles = channel.lpa.profiles.operational.map {
|
val profiles = channel.lpa.profiles.operational.map {
|
||||||
EuiccProfileInfo.Builder(it.iccid).apply {
|
EuiccProfileInfo.Builder(it.iccid).apply {
|
||||||
|
@ -86,23 +143,27 @@ class OpenEuiccService : EuiccService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteSubscription(slotId: Int, iccid: String): Int {
|
override fun onDeleteSubscription(slotId: Int, iccid: String): Int {
|
||||||
|
Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid")
|
||||||
try {
|
try {
|
||||||
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
|
val channels = findAllChannels(slotId) ?: return RESULT_FIRST_USER
|
||||||
|
|
||||||
if (!channel.profileExists(iccid)) {
|
if (!channels[0].profileExists(iccid)) {
|
||||||
return RESULT_FIRST_USER
|
return RESULT_FIRST_USER
|
||||||
}
|
}
|
||||||
|
|
||||||
val profile = channel.lpa.profiles.find {
|
// If the profile is enabled by ANY channel (port), we cannot delete it
|
||||||
it.iccid == iccid
|
channels.forEach { channel ->
|
||||||
} ?: return RESULT_FIRST_USER
|
val profile = channel.lpa.profiles.find {
|
||||||
|
it.iccid == iccid
|
||||||
|
} ?: return RESULT_FIRST_USER
|
||||||
|
|
||||||
if (profile.state == LocalProfileInfo.State.Enabled) {
|
if (profile.state == LocalProfileInfo.State.Enabled) {
|
||||||
// Must disable the profile first
|
// Must disable the profile first
|
||||||
return RESULT_FIRST_USER
|
return RESULT_FIRST_USER
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (channel.lpa.deleteProfile(iccid)) {
|
return if (channels[0].lpa.deleteProfile(iccid)) {
|
||||||
RESULT_OK
|
RESULT_OK
|
||||||
} else {
|
} else {
|
||||||
RESULT_FIRST_USER
|
RESULT_FIRST_USER
|
||||||
|
@ -112,40 +173,63 @@ class OpenEuiccService : EuiccService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: on some devices we need to update the mapping (and potentially disable a pSIM)
|
|
||||||
// for eSIM to be usable, in which case we will have to respect forceDeactivateSim.
|
|
||||||
// This is the same for our custom LUI. Both have to take this into consideration.
|
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onSwitchToSubscription(
|
override fun onSwitchToSubscription(
|
||||||
slotId: Int,
|
slotId: Int,
|
||||||
iccid: String?,
|
iccid: String?,
|
||||||
forceDeactivateSim: Boolean
|
forceDeactivateSim: Boolean
|
||||||
): Int {
|
): Int =
|
||||||
try {
|
// -1 = any port
|
||||||
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
|
onSwitchToSubscriptionWithPort(slotId, -1, iccid, forceDeactivateSim)
|
||||||
|
|
||||||
if (!channel.profileExists(iccid)) {
|
override fun onSwitchToSubscriptionWithPort(
|
||||||
|
slotId: Int,
|
||||||
|
portIndex: Int,
|
||||||
|
iccid: String?,
|
||||||
|
forceDeactivateSim: Boolean
|
||||||
|
): Int {
|
||||||
|
Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim")
|
||||||
|
try {
|
||||||
|
// retryWithTimeout is needed here because this function may be called just after
|
||||||
|
// AOSP has switched slot mappings, in which case the slots may not be ready yet.
|
||||||
|
val channel = if (portIndex == -1) {
|
||||||
|
retryWithTimeout(5000) { findChannel(slotId) }
|
||||||
|
} else {
|
||||||
|
retryWithTimeout(5000) { findChannel(slotId, portIndex) }
|
||||||
|
} ?: run {
|
||||||
|
if (!forceDeactivateSim) {
|
||||||
|
// The user must select which SIM to deactivate
|
||||||
|
return@onSwitchToSubscriptionWithPort RESULT_MUST_DEACTIVATE_SIM
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// If we are allowed to deactivate any SIM we like, try mapping the indicated port first
|
||||||
|
ensurePortIsMapped(slotId, portIndex)
|
||||||
|
retryWithTimeout(5000) { findChannel(slotId, portIndex) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// We cannot map the port (or it is already mapped)
|
||||||
|
// but we can also use any port available on the card
|
||||||
|
retryWithTimeout(5000) { findChannel(slotId) }
|
||||||
|
} ?: return@onSwitchToSubscriptionWithPort RESULT_FIRST_USER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iccid != null && !channel.profileExists(iccid)) {
|
||||||
|
Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found")
|
||||||
return RESULT_FIRST_USER
|
return RESULT_FIRST_USER
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iccid == null) {
|
// Disable any active profile first if present
|
||||||
// Disable active profile
|
channel.lpa.profiles.find {
|
||||||
val activeProfile = channel.lpa.profiles.find {
|
it.state == LocalProfileInfo.State.Enabled
|
||||||
it.state == LocalProfileInfo.State.Enabled
|
}?.let { if (!channel.lpa.disableProfile(it.iccid)) return RESULT_FIRST_USER }
|
||||||
} ?: return RESULT_OK
|
|
||||||
|
|
||||||
return if (channel.lpa.disableProfile(activeProfile.iccid)) {
|
if (iccid != null) {
|
||||||
RESULT_OK
|
if (!channel.lpa.enableProfile(iccid)) {
|
||||||
} else {
|
return RESULT_FIRST_USER
|
||||||
RESULT_FIRST_USER
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return if (channel.lpa.enableProfile(iccid)) {
|
|
||||||
RESULT_OK
|
|
||||||
} else {
|
|
||||||
RESULT_FIRST_USER
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return RESULT_OK
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return RESULT_FIRST_USER
|
return RESULT_FIRST_USER
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -154,6 +238,7 @@ class OpenEuiccService : EuiccService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int {
|
override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int {
|
||||||
|
Log.i(TAG, "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname")
|
||||||
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
|
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
|
||||||
if (!channel.profileExists(iccid)) {
|
if (!channel.profileExists(iccid)) {
|
||||||
return RESULT_FIRST_USER
|
return RESULT_FIRST_USER
|
||||||
|
|
|
@ -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<View> =
|
||||||
|
if (channel.isMEP) {
|
||||||
|
val view = layoutInflater.inflate(R.layout.footer_mep, parent, false)
|
||||||
|
view.findViewById<Button>(R.id.footer_mep_slot_mapping).setOnClickListener {
|
||||||
|
(requireActivity() as PrivilegedMainActivity).showSlotMappingFragment()
|
||||||
|
}
|
||||||
|
listOf(view)
|
||||||
|
} else {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import im.angry.openeuicc.R
|
import im.angry.openeuicc.R
|
||||||
|
import im.angry.openeuicc.core.EuiccChannel
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
|
||||||
class PrivilegedMainActivity : MainActivity() {
|
class PrivilegedMainActivity : MainActivity() {
|
||||||
|
@ -20,13 +21,23 @@ class PrivilegedMainActivity : MainActivity() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun showSlotMappingFragment() =
|
||||||
|
SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG)
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
R.id.dsds -> {
|
R.id.dsds -> {
|
||||||
tm.dsdsEnabled = !item.isChecked
|
tm.setDsdsEnabled(openEuiccApplication.euiccChannelManager, !item.isChecked)
|
||||||
Toast.makeText(this, R.string.toast_dsds_switched, Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.toast_dsds_switched, Toast.LENGTH_LONG).show()
|
||||||
finish()
|
finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.slot_mapping -> {
|
||||||
|
showSlotMappingFragment()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||||
|
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||||
}
|
}
|
201
app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt
Normal file
201
app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
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 android.widget.Toast
|
||||||
|
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
|
||||||
|
private lateinit var helpTextView: TextView
|
||||||
|
|
||||||
|
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)
|
||||||
|
helpTextView = view.findViewById(R.id.mapping_help)
|
||||||
|
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()
|
||||||
|
|
||||||
|
helpTextView.text = buildHelpText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun commit() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// Use the utility method from PrivilegedTelephonyUtils to ensure
|
||||||
|
// unmapped ports have all profiles disabled
|
||||||
|
tm.updateSimSlotMapping(openEuiccApplication.euiccChannelManager, 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) {
|
||||||
|
val nLogicalSlots = adapter.mappings.size
|
||||||
|
|
||||||
|
val cards = openEuiccApplication.telephonyManager.uiccCardsInfoCompat
|
||||||
|
|
||||||
|
val 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 -> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.telephony.IccOpenLogicalChannelResponse
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
|
|
||||||
|
// 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
|
||||||
|
): 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,17 +2,58 @@ package im.angry.openeuicc.util
|
||||||
|
|
||||||
import android.telephony.SubscriptionManager
|
import android.telephony.SubscriptionManager
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
|
import android.telephony.UiccSlotMapping
|
||||||
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
|
||||||
val TelephonyManager.supportsDSDS: Boolean
|
val TelephonyManager.supportsDSDS: Boolean
|
||||||
get() = supportedModemCount == 2
|
get() = supportedModemCount == 2
|
||||||
|
|
||||||
var TelephonyManager.dsdsEnabled: Boolean
|
val TelephonyManager.dsdsEnabled: Boolean
|
||||||
get() = activeModemCount >= 2
|
get() = activeModemCount >= 2
|
||||||
set(value) {
|
|
||||||
switchMultiSimConfig(if (value) { 2 } else {1})
|
fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: Boolean) {
|
||||||
|
runBlocking {
|
||||||
|
euiccManager.enumerateEuiccChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable all eSIM profiles before performing a DSDS switch
|
||||||
|
euiccManager.knownChannels.forEach {
|
||||||
|
it.lpa.disableActiveProfileWithUndo()
|
||||||
|
}
|
||||||
|
|
||||||
|
switchMultiSimConfig(if (enabled) { 2 } else { 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable eSIM profiles before switching the slot mapping
|
||||||
|
// This ensures that unmapped eSIM ports never have "ghost" profiles enabled
|
||||||
|
fun TelephonyManager.updateSimSlotMapping(
|
||||||
|
euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
|
||||||
|
currentMapping: Collection<UiccSlotMapping> = simSlotMapping
|
||||||
|
) {
|
||||||
|
val unmapped = currentMapping.filterNot { mapping ->
|
||||||
|
// If the same physical slot + port pair is not found in the new mapping, it is unmapped
|
||||||
|
newMapping.any {
|
||||||
|
it.physicalSlotIndex == mapping.physicalSlotIndex && it.portIndex == mapping.portIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val undo = unmapped.mapNotNull { mapping ->
|
||||||
|
euiccManager.findEuiccChannelByPortBlocking(mapping.physicalSlotIndex, mapping.portIndex)?.let { channel ->
|
||||||
|
return@mapNotNull channel.lpa.disableActiveProfileWithUndo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
simSlotMapping = newMapping
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
undo.forEach { it() } // Undo what we just did
|
||||||
|
throw e // Rethrow for caller to handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
|
fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
|
||||||
if (cardId != 0) {
|
if (cardId != 0) {
|
||||||
try {
|
try {
|
||||||
|
|
32
app/src/main/res/layout/footer_mep.xml
Normal file
32
app/src/main/res/layout/footer_mep.xml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?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="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/footer_mep_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="40dp"
|
||||||
|
android:layout_marginEnd="40dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/footer_mep"
|
||||||
|
android:textStyle="italic"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/footer_mep_slot_mapping"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/footer_mep_slot_mapping"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/slot_mapping"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
|
style="?android:attr/borderlessButtonStyle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/footer_mep_text"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
45
app/src/main/res/layout/fragment_slot_mapping.xml
Normal file
45
app/src/main/res/layout/fragment_slot_mapping.xml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?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_toTopOf="@id/mapping_help"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/mapping_help"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="?attr/colorSecondary"
|
||||||
|
android:textStyle="italic"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/mapping_list"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
25
app/src/main/res/layout/fragment_slot_mapping_item.xml
Normal file
25
app/src/main/res/layout/fragment_slot_mapping_item.xml
Normal 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>
|
|
@ -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>
|
9
app/src/main/res/menu/fragment_slot_mapping.xml
Normal file
9
app/src/main/res/menu/fragment_slot_mapping.xml
Normal 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>
|
|
@ -5,4 +5,15 @@
|
||||||
<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="footer_mep">Multiple Enabled Profiles (MEP) is supported by this slot. To enable or disable this feature, use the \"Slot Mapping\" tool.</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>
|
||||||
|
<string name="slot_mapping_help">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.</string>
|
||||||
|
<string name="slot_mapping_help_mep">\n\nPhysical slot %d supports Multiple Enabled Profiles (MEP). To use this feature, assign its %d virtual \"ports\" to different logical slots shown above.\n\nWith MEP enabled, the \"ports\" will behave like separate eSIM slots in OpenEUICC, except with a shared profile list.</string>
|
||||||
|
<string name="slot_mapping_help_dsds">\nDual SIM mode is supported but disabled. If your device comes with an internal eSIM chip, it might not be enabled by default. Change mapping above or enable dual SIM to access your eSIM.</string>
|
||||||
|
<string name="slot_mapping_completed">Your new slot mapping has been set. Please wait until modem refreshes the slots.</string>
|
||||||
|
<string name="slot_mapping_failure">The specified mapping might be invalid or unsupported by hardware.</string>
|
||||||
</resources>
|
</resources>
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
@ -14,12 +15,24 @@ private val iccOpenLogicalChannelBySlot: Method by lazy {
|
||||||
Int::class.java, String::class.java, Int::class.java
|
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 {
|
private val iccCloseLogicalChannelBySlot: Method by lazy {
|
||||||
TelephonyManager::class.java.getMethod(
|
TelephonyManager::class.java.getMethod(
|
||||||
"iccCloseLogicalChannelBySlot",
|
"iccCloseLogicalChannelBySlot",
|
||||||
Int::class.java, Int::class.java
|
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 {
|
private val iccTransmitApduLogicalChannelBySlot: Method by lazy {
|
||||||
TelephonyManager::class.java.getMethod(
|
TelephonyManager::class.java.getMethod(
|
||||||
"iccTransmitApduLogicalChannelBySlot",
|
"iccTransmitApduLogicalChannelBySlot",
|
||||||
|
@ -27,15 +40,41 @@ private val iccTransmitApduLogicalChannelBySlot: 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 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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
|
||||||
): IccOpenLogicalChannelResponse =
|
): IccOpenLogicalChannelResponse =
|
||||||
iccOpenLogicalChannelBySlot.invoke(this, slotId, appletId, p2) as 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 =
|
fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean =
|
||||||
iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as 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(
|
fun TelephonyManager.iccTransmitApduLogicalChannelBySlot(
|
||||||
slotId: Int, channel: Int, cla: Int, instruction: Int,
|
slotId: Int, channel: Int, cla: Int, instruction: Int,
|
||||||
p1: Int, p2: Int, p3: Int, data: String?
|
p1: Int, p2: Int, p3: Int, data: String?
|
||||||
|
@ -44,6 +83,18 @@ fun TelephonyManager.iccTransmitApduLogicalChannelBySlot(
|
||||||
this, slotId, channel, cla, instruction, p1, p2, p3, data
|
this, slotId, channel, cla, instruction, p1, p2, p3, data
|
||||||
) as String?
|
) 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?
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,11 +20,10 @@ class LocalProfileAssistantImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val profiles: List<LocalProfileInfo>
|
override val profiles: List<LocalProfileInfo>
|
||||||
get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList() // TODO: Maybe we need better error handling
|
get() = LpacJni.es10cGetProfilesInfo(contextHandle)!!.asList()
|
||||||
|
|
||||||
override val eID: String by lazy {
|
override val eID: String
|
||||||
LpacJni.es10cGetEid(contextHandle)!!
|
get() = LpacJni.es10cGetEid(contextHandle)!!
|
||||||
}
|
|
||||||
|
|
||||||
override val euiccInfo2: EuiccInfo2?
|
override val euiccInfo2: EuiccInfo2?
|
||||||
get() = LpacJni.es10cexGetEuiccInfo2(contextHandle)
|
get() = LpacJni.es10cexGetEuiccInfo2(contextHandle)
|
||||||
|
|
Loading…
Add table
Reference in a new issue