Browse Source

feat: Direct profile download interface with slot selection

This can be used by both the system LuiActivity and potentially in the
unprivileged version as a carrier interface as well.
Peter Cai 2 years ago
parent
commit
c807588521

+ 5 - 0
app-common/src/main/AndroidManifest.xml

@@ -16,6 +16,11 @@
             android:name="im.angry.openeuicc.ui.NotificationsActivity"
             android:name="im.angry.openeuicc.ui.NotificationsActivity"
             android:label="@string/profile_notifications" />
             android:label="@string/profile_notifications" />
 
 
+        <activity
+            android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
+            android:label="@string/profile_download"
+            android:theme="@style/Theme.AppCompat.Translucent" />
+
         <activity
         <activity
             android:name="com.journeyapps.barcodescanner.CaptureActivity"
             android:name="com.journeyapps.barcodescanner.CaptureActivity"
             android:screenOrientation="fullSensor"
             android:screenOrientation="fullSensor"

+ 7 - 0
app-common/src/main/java/im/angry/openeuicc/ui/BaseMaterialDialogFragment.kt

@@ -2,11 +2,18 @@ package im.angry.openeuicc.ui
 
 
 import android.app.Dialog
 import android.app.Dialog
 import android.os.Bundle
 import android.os.Bundle
+import android.view.LayoutInflater
 import android.view.Window
 import android.view.Window
+import androidx.appcompat.view.ContextThemeWrapper
 import androidx.fragment.app.DialogFragment
 import androidx.fragment.app.DialogFragment
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.common.R
 
 
 abstract class BaseMaterialDialogFragment: DialogFragment() {
 abstract class BaseMaterialDialogFragment: DialogFragment() {
+    override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
+        val inflater = super.onGetLayoutInflater(savedInstanceState)
+        return inflater.cloneInContext(ContextThemeWrapper(requireContext(), R.style.Theme_OpenEUICC))
+    }
+
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         return super.onCreateDialog(savedInstanceState).also {
         return super.onCreateDialog(savedInstanceState).also {
             it.window?.requestFeature(Window.FEATURE_NO_TITLE)
             it.window?.requestFeature(Window.FEATURE_NO_TITLE)

+ 30 - 0
app-common/src/main/java/im/angry/openeuicc/ui/DirectProfileDownloadActivity.kt

@@ -0,0 +1,30 @@
+package im.angry.openeuicc.ui
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import im.angry.openeuicc.util.openEuiccApplication
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class DirectProfileDownloadActivity : AppCompatActivity(), SlotSelectFragment.SlotSelectedListener {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        lifecycleScope.launch {
+            withContext(Dispatchers.IO) {
+                openEuiccApplication.euiccChannelManager.enumerateEuiccChannels()
+            }
+
+            SlotSelectFragment.newInstance(this@DirectProfileDownloadActivity)
+                .show(supportFragmentManager, SlotSelectFragment.TAG)
+        }
+    }
+
+    override fun onSlotSelected(slotId: Int, portId: Int) {
+        ProfileDownloadFragment.newInstance(slotId, portId)
+            .show(supportFragmentManager, ProfileDownloadFragment.TAG)
+    }
+
+    override fun onCancel() = finish()
+}

+ 76 - 0
app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt

@@ -0,0 +1,76 @@
+package im.angry.openeuicc.ui
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import androidx.appcompat.widget.Toolbar
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.core.EuiccChannel
+import im.angry.openeuicc.util.openEuiccApplication
+import im.angry.openeuicc.util.setWidthPercent
+
+class SlotSelectFragment(private var listener: SlotSelectedListener) : BaseMaterialDialogFragment() {
+    companion object {
+        const val TAG = "SlotSelectFragment"
+
+        fun newInstance(listener: SlotSelectedListener): SlotSelectFragment {
+            return SlotSelectFragment(listener)
+        }
+    }
+
+    interface SlotSelectedListener {
+        fun onSlotSelected(slotId: Int, portId: Int)
+        fun onCancel()
+    }
+
+    private lateinit var toolbar: Toolbar
+    private lateinit var spinner: Spinner
+    private val channels: List<EuiccChannel> by lazy {
+        openEuiccApplication.euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val view = inflater.inflate(R.layout.fragment_slot_select, container, false)
+
+        toolbar = view.findViewById(R.id.toolbar)
+        toolbar.setTitle(R.string.slot_select)
+        toolbar.inflateMenu(R.menu.fragment_slot_select)
+
+        val adapter = ArrayAdapter<String>(inflater.context, R.layout.spinner_item)
+
+        spinner = view.findViewById(R.id.spinner)
+        spinner.adapter = adapter
+
+        channels.forEach { channel ->
+            adapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
+        }
+
+        toolbar.setNavigationOnClickListener { listener.onCancel() }
+        toolbar.setOnMenuItemClickListener {
+            val channel = channels[spinner.selectedItemPosition]
+            listener.onSlotSelected(channel.slotId, channel.portId)
+            dismiss()
+            true
+        }
+
+        return view
+    }
+
+    override fun onResume() {
+        super.onResume()
+        setWidthPercent(75)
+    }
+
+    override fun onCancel(dialog: DialogInterface) {
+        super.onCancel(dialog)
+        listener.onCancel()
+    }
+}

+ 2 - 1
app-common/src/main/res/layout/fragment_profile_download.xml

@@ -2,7 +2,8 @@
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
 
 
     <com.google.android.material.appbar.MaterialToolbar
     <com.google.android.material.appbar.MaterialToolbar
         android:id="@+id/toolbar"
         android:id="@+id/toolbar"

+ 27 - 0
app-common/src/main/res/layout/fragment_slot_select.xml

@@ -0,0 +1,27 @@
+<?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"
+    android:background="?attr/colorSurface">
+
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/toolbar"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintWidth_percent="1"
+        app:navigationIcon="?homeAsUpIndicator" />
+
+    <Spinner
+        android:id="@+id/spinner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginVertical="48dp"
+        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>

+ 9 - 0
app-common/src/main/res/menu/fragment_slot_select.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_select_select"
+        app:showAsAction="ifRoom"/>
+</menu>

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

@@ -19,6 +19,9 @@
     <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
     <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
     <string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
     <string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
 
 
+    <string name="slot_select">Select Slot</string>
+    <string name="slot_select_select">Select</string>
+
     <string name="profile_download">New eSIM</string>
     <string name="profile_download">New eSIM</string>
     <string name="profile_download_server">Server (RSP / SM-DP+)</string>
     <string name="profile_download_server">Server (RSP / SM-DP+)</string>
     <string name="profile_download_code">Activation Code</string>
     <string name="profile_download_code">Activation Code</string>

+ 10 - 0
app-common/src/main/res/values/themes.xml

@@ -28,4 +28,14 @@
     <style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
     <style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
         <item name="android:textColor">?attr/colorSecondary</item>
         <item name="android:textColor">?attr/colorSecondary</item>
     </style>
     </style>
+
+    <style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+    </style>
+
 </resources>
 </resources>

+ 1 - 2
app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt

@@ -11,11 +11,10 @@ class LuiActivity : AppCompatActivity() {
         setContentView(R.layout.activity_lui)
         setContentView(R.layout.activity_lui)
 
 
         findViewById<View>(R.id.lui_skip).setOnClickListener { finish() }
         findViewById<View>(R.id.lui_skip).setOnClickListener { finish() }
-        // TODO: Allow users to select slots, and then hand over directly to ProfileDownloadFragment
         // TODO: Deactivate LuiActivity if there is no eSIM found.
         // TODO: Deactivate LuiActivity if there is no eSIM found.
         // TODO: Support pre-filled download info (from carrier apps); UX
         // TODO: Support pre-filled download info (from carrier apps); UX
         findViewById<View>(R.id.lui_download).setOnClickListener {
         findViewById<View>(R.id.lui_download).setOnClickListener {
-            startActivity(Intent(this, PrivilegedMainActivity::class.java))
+            startActivity(Intent(this, DirectProfileDownloadActivity::class.java))
         }
         }
     }
     }
 }
 }