Browse Source

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

Peter Cai 2 years ago
parent
commit
53fa754197

+ 2 - 0
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')

+ 4 - 0
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)
     }
 }

+ 159 - 0
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<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)
+        }
+    }
+}

+ 1 - 1
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

+ 29 - 0
app/src/main/res/layout/fragment_slot_mapping.xml

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

+ 25 - 0
app/src/main/res/layout/fragment_slot_mapping_item.xml

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

+ 4 - 0
app/src/main/res/menu/activity_main_privileged.xml

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

+ 9 - 0
app/src/main/res/menu/fragment_slot_mapping.xml

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

+ 4 - 0
app/src/main/res/values/strings.xml

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

+ 1 - 1
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'

+ 16 - 0
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<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)
 }

+ 73 - 0
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<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");
+    }
+}