Compare commits

...

4 commits

11 changed files with 157 additions and 26 deletions

View file

@ -10,8 +10,8 @@ abstract class EuiccChannel(
val logicalSlotId = port.logicalSlotIndex val logicalSlotId = port.logicalSlotIndex
val portId = port.portIndex val portId = port.portIndex
val cardId = port.card.cardId val cardId = port.card.cardId
val name = "SLOT $logicalSlotId"
val removable = port.card.isRemovable val removable = port.card.isRemovable
val isMEP = port.card.isMultipleEnabledProfilesSupported
abstract val lpa: LocalProfileAssistant abstract val lpa: LocalProfileAssistant
val valid: Boolean val valid: Boolean

View file

@ -161,7 +161,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
} }
} }

View file

@ -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,7 +27,7 @@ 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"
@ -38,7 +39,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
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,
@ -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)
@ -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
} }
} }

View file

@ -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, channel.portId)) fragments.add(createEuiccManagementFragment(channel))
} }
if (fragments.isNotEmpty()) { if (fragments.isNotEmpty()) {

View file

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

View file

@ -45,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)
} }
} }

View file

@ -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()
}
}

View file

@ -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,6 +21,9 @@ 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.dsdsEnabled = !item.isChecked
@ -28,9 +32,12 @@ class PrivilegedMainActivity : MainActivity() {
true true
} }
R.id.slot_mapping -> { R.id.slot_mapping -> {
SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG) showSlotMappingFragment()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
} }

View file

@ -111,11 +111,11 @@ class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener {
} }
private suspend fun buildHelpText() = withContext(Dispatchers.IO) { private suspend fun buildHelpText() = withContext(Dispatchers.IO) {
var nLogicalSlots = adapter.mappings.size val nLogicalSlots = adapter.mappings.size
val cards = openEuiccApplication.telephonyManager.uiccCardsInfoCompat val cards = openEuiccApplication.telephonyManager.uiccCardsInfoCompat
var nPhysicalSlots = cards.size val nPhysicalSlots = cards.size
var idxMepCard = -1 var idxMepCard = -1
var nMepPorts = 0 var nMepPorts = 0

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

View file

@ -6,12 +6,14 @@
<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">Slot Mapping</string>
<string name="slot_mapping_logical_slot">Logical slot %d:</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_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">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, associate its %d virtual "ports" to different logical slots shown above.</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.</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_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> <string name="slot_mapping_failure">The specified mapping might be invalid or unsupported by hardware.</string>
</resources> </resources>