Compare commits
4 commits
e708deea7c
...
ed585edf49
Author | SHA1 | Date | |
---|---|---|---|
ed585edf49 | |||
33b70bc315 | |||
fc980cda43 | |||
81b61c76c4 |
11 changed files with 157 additions and 26 deletions
|
@ -10,8 +10,8 @@ abstract class EuiccChannel(
|
|||
val logicalSlotId = port.logicalSlotIndex
|
||||
val portId = port.portIndex
|
||||
val cardId = port.card.cardId
|
||||
val name = "SLOT $logicalSlotId"
|
||||
val removable = port.card.isRemovable
|
||||
val isMEP = port.card.isMultipleEnabledProfilesSupported
|
||||
|
||||
abstract val lpa: LocalProfileAssistant
|
||||
val valid: Boolean
|
||||
|
|
|
@ -161,7 +161,7 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
seService = null
|
||||
}
|
||||
|
||||
open fun notifyEuiccProfilesChanged(slotId: Int) {
|
||||
open fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||
// No-op for unprivileged
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
|
@ -26,7 +27,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Exception
|
||||
|
||||
class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
|
||||
open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
|
||||
companion object {
|
||||
const val TAG = "EuiccManagementFragment"
|
||||
|
||||
|
@ -38,7 +39,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
|||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var profileList: RecyclerView
|
||||
|
||||
private val adapter = EuiccProfileAdapter(listOf())
|
||||
private val adapter = EuiccProfileAdapter()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -76,18 +77,21 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
|||
refresh()
|
||||
}
|
||||
|
||||
protected open suspend fun onCreateFooterViews(parent: ViewGroup): List<View> = listOf()
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun refresh() {
|
||||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
val profiles = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(slotId)
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
channel.lpa.profiles
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.profiles = profiles.operational
|
||||
adapter.footerViews = onCreateFooterViews(profileList)
|
||||
adapter.notifyDataSetChanged()
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
|
@ -130,7 +134,30 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
|
|||
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 name: TextView = root.findViewById(R.id.name)
|
||||
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>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
inner class EuiccProfileAdapter : RecyclerView.Adapter<ViewHolder>() {
|
||||
var profiles: List<LocalProfileInfo> = listOf()
|
||||
var footerViews: List<View> = listOf()
|
||||
|
||||
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) {
|
||||
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.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -72,23 +73,26 @@ open class MainActivity : AppCompatActivity() {
|
|||
return true
|
||||
}
|
||||
|
||||
protected open fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||
|
||||
private suspend fun init() {
|
||||
withContext(Dispatchers.IO) {
|
||||
manager.enumerateEuiccChannels()
|
||||
manager.knownChannels.forEach {
|
||||
Log.d(TAG, it.name)
|
||||
Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
|
||||
Log.d(TAG, it.lpa.eID)
|
||||
// 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,
|
||||
// but it could change in the future
|
||||
manager.notifyEuiccProfilesChanged(it.slotId)
|
||||
manager.notifyEuiccProfilesChanged(it.logicalSlotId)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
manager.knownChannels.forEach { channel ->
|
||||
spinnerAdapter.add(channel.name)
|
||||
fragments.add(EuiccManagementFragment.newInstance(channel.slotId, channel.portId))
|
||||
manager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
|
||||
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
|
||||
fragments.add(createEuiccManagementFragment(channel))
|
||||
}
|
||||
|
||||
if (fragments.isNotEmpty()) {
|
||||
|
|
|
@ -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="unknown">Unknown</string>
|
||||
|
||||
<string name="channel_name_format">Logical Slot %d</string>
|
||||
|
||||
<string name="enabled">Enabled</string>
|
||||
<string name="disabled">Disabled</string>
|
||||
<string name="provider">Provider:</string>
|
||||
|
|
|
@ -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 {
|
||||
findEuiccChannelBySlotBlocking(slotId)?.let {
|
||||
findEuiccChannelBySlotBlocking(logicalSlotId)?.let {
|
||||
tryRefreshCachedEuiccInfo(it.cardId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.widget.Toast
|
||||
import im.angry.openeuicc.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class PrivilegedMainActivity : MainActivity() {
|
||||
|
@ -20,6 +21,9 @@ class PrivilegedMainActivity : MainActivity() {
|
|||
return true
|
||||
}
|
||||
|
||||
internal fun showSlotMappingFragment() =
|
||||
SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG)
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.dsds -> {
|
||||
tm.dsdsEnabled = !item.isChecked
|
||||
|
@ -28,9 +32,12 @@ class PrivilegedMainActivity : MainActivity() {
|
|||
true
|
||||
}
|
||||
R.id.slot_mapping -> {
|
||||
SlotMappingFragment().show(supportFragmentManager, SlotMappingFragment.TAG)
|
||||
showSlotMappingFragment()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||
}
|
|
@ -111,11 +111,11 @@ class SlotMappingFragment: DialogFragment(), OnMenuItemClickListener {
|
|||
}
|
||||
|
||||
private suspend fun buildHelpText() = withContext(Dispatchers.IO) {
|
||||
var nLogicalSlots = adapter.mappings.size
|
||||
val nLogicalSlots = adapter.mappings.size
|
||||
|
||||
val cards = openEuiccApplication.telephonyManager.uiccCardsInfoCompat
|
||||
|
||||
var nPhysicalSlots = cards.size
|
||||
val nPhysicalSlots = cards.size
|
||||
var idxMepCard = -1
|
||||
var nMepPorts = 0
|
||||
|
||||
|
|
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>
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
<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, associate its %d virtual "ports" to different logical slots shown above.</string>
|
||||
<string name="slot_mapping_help_dsds">\nDual SIM mode is supported but disabled.</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>
|
Loading…
Add table
Reference in a new issue