[3/n] Initial implementation of MEP-based slot mapping

This commit is contained in:
Peter Cai 2023-12-16 17:14:04 -05:00
parent 4090418146
commit 53fa754197
12 changed files with 327 additions and 2 deletions

View file

@ -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')

View file

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

View file

@ -0,0 +1,159 @@
package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.telephony.TelephonyManager
import android.telephony.UiccSlotMapping
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener {
companion object {
const val TAG = "SlotMappingFragment"
}
private val tm: TelephonyManager by lazy {
(requireContext().applicationContext as OpenEuiccApplication).telephonyManager
}
private val ports: List<UiccPortInfoCompat> by lazy {
tm.uiccCardsInfoCompat.flatMap { it.ports }
}
private val portsDesc: List<String> by lazy {
ports.map { getString(R.string.slot_mapping_port, it.card.physicalSlotIndex, it.portIndex) }
}
private lateinit var toolbar: Toolbar
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: SlotMappingAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_slot_mapping, container, false)
toolbar = view.findViewById(R.id.toolbar)
toolbar.inflateMenu(R.menu.fragment_slot_mapping)
recyclerView = view.findViewById(R.id.mapping_list)
recyclerView.layoutManager =
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.title = getString(R.string.slot_mapping)
toolbar.setNavigationOnClickListener { dismiss() }
toolbar.setOnMenuItemClickListener(this)
}
override fun onResume() {
super.onResume()
setWidthPercent(85)
init()
}
@SuppressLint("NotifyDataSetChanged")
private fun init() {
lifecycleScope.launch(Dispatchers.Main) {
val mapping = withContext(Dispatchers.IO) {
tm.simSlotMapping
}
adapter = SlotMappingAdapter(mapping.toMutableList().apply {
sortBy { it.logicalSlotIndex }
})
recyclerView.adapter = adapter
adapter.notifyDataSetChanged()
}
}
private fun commit() {
lifecycleScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
tm.simSlotMapping = adapter.mappings
}
openEuiccApplication.euiccChannelManager.invalidate()
requireActivity().finish()
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean =
when (item!!.itemId) {
R.id.ok -> {
commit()
true
}
else -> false
}
inner class ViewHolder(root: View): RecyclerView.ViewHolder(root), OnItemSelectedListener {
private val textViewLogicalSlot: TextView = root.findViewById(R.id.slot_mapping_logical_slot)
private val spinnerPorts: Spinner = root.findViewById(R.id.slot_mapping_ports)
init {
spinnerPorts.adapter = ArrayAdapter(requireContext(), im.angry.openeuicc.common.R.layout.spinner_item, portsDesc)
spinnerPorts.onItemSelectedListener = this
}
private lateinit var mappings: MutableList<UiccSlotMapping>
private var mappingId: Int = -1
fun attachView(mappings: MutableList<UiccSlotMapping>, mappingId: Int) {
this.mappings = mappings
this.mappingId = mappingId
textViewLogicalSlot.text = getString(R.string.slot_mapping_logical_slot, mappings[mappingId].logicalSlotIndex)
spinnerPorts.setSelection(ports.indexOfFirst {
it.card.physicalSlotIndex == mappings[mappingId].physicalSlotIndex
&& it.portIndex == mappings[mappingId].portIndex
})
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
check(this::mappings.isInitialized) { "mapping not assigned" }
mappings[mappingId] =
UiccSlotMapping(
ports[position].portIndex, ports[position].card.physicalSlotIndex, mappings[mappingId].logicalSlotIndex)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
inner class SlotMappingAdapter(val mappings: MutableList<UiccSlotMapping>): RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fragment_slot_mapping_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = mappings.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.attachView(mappings, position)
}
}
}

View file

@ -4,7 +4,7 @@ import android.os.Build
import android.telephony.IccOpenLogicalChannelResponse
import android.telephony.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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'

View file

@ -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<UiccSlotMapping>
get() = getSimSlotMapping.invoke(this) as Collection<UiccSlotMapping>
set(new) { setSimSlotMapping.invoke(this, new) }
private val requestEmbeddedSubscriptionInfoListRefresh: Method by lazy {
SubscriptionManager::class.java.getMethod("requestEmbeddedSubscriptionInfoListRefresh", Int::class.java)
}

View file

@ -0,0 +1,73 @@
package android.telephony;
import android.os.Parcel;
import android.os.Parcelable;
public final class UiccSlotMapping implements Parcelable {
public static final Creator<UiccSlotMapping> CREATOR = null;
@Override
public void writeToParcel(Parcel dest, int flags) {
throw new RuntimeException("stub");
}
@Override
public int describeContents() {
return 0;
}
/**
*
* @param portIndex The port index is an enumeration of the ports available on the UICC.
* @param physicalSlotIndex is unique index referring to a physical SIM slot.
* @param logicalSlotIndex is unique index referring to a logical SIM slot.
*
*/
public UiccSlotMapping(int portIndex, int physicalSlotIndex, int logicalSlotIndex) {
throw new RuntimeException("stub");
}
/**
* Port index is the unique index referring to a port belonging to the physical SIM slot.
* If the SIM does not support multiple enabled profiles, the port index is default index 0.
*
* @return port index.
*/
public int getPortIndex() {
throw new RuntimeException("stub");
}
/**
* Gets the physical slot index for the slot that the UICC is currently inserted in.
*
* @return physical slot index which is the index of actual physical UICC slot.
*/
public int getPhysicalSlotIndex() {
throw new RuntimeException("stub");
}
/**
* Gets logical slot index for the slot that the UICC is currently attached.
* Logical slot index is the unique index referring to a logical slot(logical modem stack).
*
* @return logical slot index;
*/
public int getLogicalSlotIndex() {
throw new RuntimeException("stub");
}
@Override
public boolean equals(Object obj) {
throw new RuntimeException("stub");
}
@Override
public int hashCode() {
throw new RuntimeException("stub");
}
@Override
public String toString() {
throw new RuntimeException("stub");
}
}