From 53fa75419776c9cabeb8025392f5642bb574dacb Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 16 Dec 2023 17:14:04 -0500 Subject: [PATCH] [3/n] Initial implementation of MEP-based slot mapping --- app/build.gradle | 2 + .../openeuicc/ui/PrivilegedMainActivity.kt | 4 + .../angry/openeuicc/ui/SlotMappingFragment.kt | 159 ++++++++++++++++++ .../util/PrivilegedTelephonyCompat.kt | 2 +- .../main/res/layout/fragment_slot_mapping.xml | 29 ++++ .../res/layout/fragment_slot_mapping_item.xml | 25 +++ .../res/menu/activity_main_privileged.xml | 4 + .../main/res/menu/fragment_slot_mapping.xml | 9 + app/src/main/res/values/strings.xml | 4 + libs/hidden-apis-shim/build.gradle | 2 +- .../util/TelephonyManagerHiddenApi.kt | 16 ++ .../android/telephony/UiccSlotMapping.java | 73 ++++++++ 12 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt create mode 100644 app/src/main/res/layout/fragment_slot_mapping.xml create mode 100644 app/src/main/res/layout/fragment_slot_mapping_item.xml create mode 100644 app/src/main/res/menu/fragment_slot_mapping.xml create mode 100644 libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java diff --git a/app/build.gradle b/app/build.gradle index accead3..d6bad2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,6 +57,8 @@ android { } dependencies { + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.3.2' compileOnly project(':libs:hidden-apis-stub') implementation project(':libs:hidden-apis-shim') implementation project(':libs:lpac-jni') diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt index 880d43b..8bcadc8 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedMainActivity.kt @@ -27,6 +27,10 @@ class PrivilegedMainActivity : MainActivity() { finish() true } + R.id.slot_mapping -> { + SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG) + true + } else -> super.onOptionsItemSelected(item) } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt new file mode 100644 index 0000000..8e3fe0e --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/SlotMappingFragment.kt @@ -0,0 +1,159 @@ +package im.angry.openeuicc.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.telephony.TelephonyManager +import android.telephony.UiccSlotMapping +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.ArrayAdapter +import android.widget.Spinner +import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import im.angry.openeuicc.OpenEuiccApplication +import im.angry.openeuicc.R +import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener { + companion object { + const val TAG = "SlotMappingFragment" + } + + private val tm: TelephonyManager by lazy { + (requireContext().applicationContext as OpenEuiccApplication).telephonyManager + } + + private val ports: List by lazy { + tm.uiccCardsInfoCompat.flatMap { it.ports } + } + + private val portsDesc: List by lazy { + ports.map { getString(R.string.slot_mapping_port, it.card.physicalSlotIndex, it.portIndex) } + } + + private lateinit var toolbar: Toolbar + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: SlotMappingAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_slot_mapping, container, false) + toolbar = view.findViewById(R.id.toolbar) + toolbar.inflateMenu(R.menu.fragment_slot_mapping) + recyclerView = view.findViewById(R.id.mapping_list) + recyclerView.layoutManager = + LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + toolbar.title = getString(R.string.slot_mapping) + toolbar.setNavigationOnClickListener { dismiss() } + toolbar.setOnMenuItemClickListener(this) + } + + override fun onResume() { + super.onResume() + setWidthPercent(85) + init() + } + + @SuppressLint("NotifyDataSetChanged") + private fun init() { + lifecycleScope.launch(Dispatchers.Main) { + val mapping = withContext(Dispatchers.IO) { + tm.simSlotMapping + } + + adapter = SlotMappingAdapter(mapping.toMutableList().apply { + sortBy { it.logicalSlotIndex } + }) + recyclerView.adapter = adapter + adapter.notifyDataSetChanged() + + } + } + + private fun commit() { + lifecycleScope.launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { + tm.simSlotMapping = adapter.mappings + } + openEuiccApplication.euiccChannelManager.invalidate() + requireActivity().finish() + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean = + when (item!!.itemId) { + R.id.ok -> { + commit() + true + } + else -> false + } + + inner class ViewHolder(root: View): RecyclerView.ViewHolder(root), OnItemSelectedListener { + private val textViewLogicalSlot: TextView = root.findViewById(R.id.slot_mapping_logical_slot) + private val spinnerPorts: Spinner = root.findViewById(R.id.slot_mapping_ports) + + init { + spinnerPorts.adapter = ArrayAdapter(requireContext(), im.angry.openeuicc.common.R.layout.spinner_item, portsDesc) + spinnerPorts.onItemSelectedListener = this + } + + private lateinit var mappings: MutableList + private var mappingId: Int = -1 + + fun attachView(mappings: MutableList, mappingId: Int) { + this.mappings = mappings + this.mappingId = mappingId + + textViewLogicalSlot.text = getString(R.string.slot_mapping_logical_slot, mappings[mappingId].logicalSlotIndex) + spinnerPorts.setSelection(ports.indexOfFirst { + it.card.physicalSlotIndex == mappings[mappingId].physicalSlotIndex + && it.portIndex == mappings[mappingId].portIndex + }) + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + check(this::mappings.isInitialized) { "mapping not assigned" } + mappings[mappingId] = + UiccSlotMapping( + ports[position].portIndex, ports[position].card.physicalSlotIndex, mappings[mappingId].logicalSlotIndex) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + + } + } + + inner class SlotMappingAdapter(val mappings: MutableList): RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.fragment_slot_mapping_item, parent, false) + return ViewHolder(view) + } + + override fun getItemCount(): Int = mappings.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.attachView(mappings, position) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt index a1a503d..ac4c968 100644 --- a/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt @@ -4,7 +4,7 @@ import android.os.Build import android.telephony.IccOpenLogicalChannelResponse import android.telephony.TelephonyManager -// TODO: Usage of *byPort APIs will still break build in-tree on lower AOSP versions +// TODO: Usage of new APIs from T or later will still break build in-tree on lower AOSP versions // Maybe older versions should simply include hidden-apis-shim when building? fun TelephonyManager.iccOpenLogicalChannelByPortCompat( slotIndex: Int, portIndex: Int, aid: String?, p2: Int diff --git a/app/src/main/res/layout/fragment_slot_mapping.xml b/app/src/main/res/layout/fragment_slot_mapping.xml new file mode 100644 index 0000000..469263b --- /dev/null +++ b/app/src/main/res/layout/fragment_slot_mapping.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_slot_mapping_item.xml b/app/src/main/res/layout/fragment_slot_mapping_item.xml new file mode 100644 index 0000000..44c154e --- /dev/null +++ b/app/src/main/res/layout/fragment_slot_mapping_item.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_privileged.xml b/app/src/main/res/menu/activity_main_privileged.xml index 2bbe20f..cbfaf31 100644 --- a/app/src/main/res/menu/activity_main_privileged.xml +++ b/app/src/main/res/menu/activity_main_privileged.xml @@ -7,4 +7,8 @@ android:checkable="true" android:visible="false" app:showAsAction="never" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_slot_mapping.xml b/app/src/main/res/menu/fragment_slot_mapping.xml new file mode 100644 index 0000000..7f03e18 --- /dev/null +++ b/app/src/main/res/menu/fragment_slot_mapping.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 280289a..8e1c4ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,8 @@ Dual SIM DSDS state switched. Please wait until the modem restarts. + + Slot Mapping + Logical slot %d: + Slot %d Port %d \ No newline at end of file diff --git a/libs/hidden-apis-shim/build.gradle b/libs/hidden-apis-shim/build.gradle index 1c7a212..a3a0472 100644 --- a/libs/hidden-apis-shim/build.gradle +++ b/libs/hidden-apis-shim/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - + compileOnly project(':libs:hidden-apis-stub') implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'com.google.android.material:material:1.6.1' diff --git a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt index 4a9ea64..4203fea 100644 --- a/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt +++ b/libs/hidden-apis-shim/src/main/java/im/angry/openeuicc/util/TelephonyManagerHiddenApi.kt @@ -3,6 +3,7 @@ package im.angry.openeuicc.util import android.telephony.IccOpenLogicalChannelResponse import android.telephony.SubscriptionManager import android.telephony.TelephonyManager +import android.telephony.UiccSlotMapping import java.lang.reflect.Method // Hidden APIs via reflection to enable building without AOSP source tree @@ -46,6 +47,17 @@ private val iccTransmitApduLogicalChannelByPort: Method by lazy { Int::class.java, Int::class.java, Int::class.java, String::class.java ) } +private val getSimSlotMapping: Method by lazy { + TelephonyManager::class.java.getMethod( + "getSimSlotMapping" + ) +} +private val setSimSlotMapping: Method by lazy { + TelephonyManager::class.java.getMethod( + "setSimSlotMapping", + Collection::class.java + ) +} fun TelephonyManager.iccOpenLogicalChannelBySlot( slotId: Int, appletId: String?, p2: Int @@ -79,6 +91,10 @@ fun TelephonyManager.iccTransmitApduLogicalChannelByPort( this, slotId, portId, channel, cla, instruction, p1, p2, p3, data ) as String? +var TelephonyManager.simSlotMapping: Collection + get() = getSimSlotMapping.invoke(this) as Collection + set(new) { setSimSlotMapping.invoke(this, new) } + private val requestEmbeddedSubscriptionInfoListRefresh: Method by lazy { SubscriptionManager::class.java.getMethod("requestEmbeddedSubscriptionInfoListRefresh", Int::class.java) } diff --git a/libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java b/libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java new file mode 100644 index 0000000..e3ea60e --- /dev/null +++ b/libs/hidden-apis-stub/src/main/java/android/telephony/UiccSlotMapping.java @@ -0,0 +1,73 @@ +package android.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +public final class UiccSlotMapping implements Parcelable { + public static final Creator CREATOR = null; + + @Override + public void writeToParcel(Parcel dest, int flags) { + throw new RuntimeException("stub"); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * + * @param portIndex The port index is an enumeration of the ports available on the UICC. + * @param physicalSlotIndex is unique index referring to a physical SIM slot. + * @param logicalSlotIndex is unique index referring to a logical SIM slot. + * + */ + public UiccSlotMapping(int portIndex, int physicalSlotIndex, int logicalSlotIndex) { + throw new RuntimeException("stub"); + } + + /** + * Port index is the unique index referring to a port belonging to the physical SIM slot. + * If the SIM does not support multiple enabled profiles, the port index is default index 0. + * + * @return port index. + */ + public int getPortIndex() { + throw new RuntimeException("stub"); + } + + /** + * Gets the physical slot index for the slot that the UICC is currently inserted in. + * + * @return physical slot index which is the index of actual physical UICC slot. + */ + public int getPhysicalSlotIndex() { + throw new RuntimeException("stub"); + } + + /** + * Gets logical slot index for the slot that the UICC is currently attached. + * Logical slot index is the unique index referring to a logical slot(logical modem stack). + * + * @return logical slot index; + */ + public int getLogicalSlotIndex() { + throw new RuntimeException("stub"); + } + + @Override + public boolean equals(Object obj) { + throw new RuntimeException("stub"); + } + + @Override + public int hashCode() { + throw new RuntimeException("stub"); + } + + @Override + public String toString() { + throw new RuntimeException("stub"); + } +}