ソースを参照

ui: Start designing UI for selectiing slot in the new download flow

incomplete
Peter Cai 1 年間 前
コミット
375d13b7c4

+ 69 - 0
app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt

@@ -2,17 +2,24 @@ package im.angry.openeuicc.ui.wizard
 
 
 import android.os.Bundle
 import android.os.Bundle
 import android.view.View
 import android.view.View
+import android.widget.Button
 import android.widget.ProgressBar
 import android.widget.ProgressBar
 import androidx.activity.OnBackPressedCallback
 import androidx.activity.OnBackPressedCallback
 import androidx.activity.enableEdgeToEdge
 import androidx.activity.enableEdgeToEdge
 import androidx.core.view.ViewCompat
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updatePadding
 import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.ui.BaseEuiccAccessActivity
 import im.angry.openeuicc.ui.BaseEuiccAccessActivity
+import im.angry.openeuicc.util.*
 
 
 class DownloadWizardActivity: BaseEuiccAccessActivity() {
 class DownloadWizardActivity: BaseEuiccAccessActivity() {
     private lateinit var progressBar: ProgressBar
     private lateinit var progressBar: ProgressBar
+    private lateinit var nextButton: Button
+    private lateinit var prevButton: Button
+
+    private var currentFragment: DownloadWizardStepFragment? = null
 
 
     override fun onCreate(savedInstanceState: Bundle?) {
     override fun onCreate(savedInstanceState: Bundle?) {
         enableEdgeToEdge()
         enableEdgeToEdge()
@@ -25,6 +32,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
         })
         })
 
 
         progressBar = requireViewById(R.id.progress)
         progressBar = requireViewById(R.id.progress)
+        nextButton = requireViewById(R.id.download_wizard_next)
+        prevButton = requireViewById(R.id.download_wizard_back)
 
 
         val navigation = requireViewById<View>(R.id.download_wizard_navigation)
         val navigation = requireViewById<View>(R.id.download_wizard_navigation)
         val origHeight = navigation.layoutParams.height
         val origHeight = navigation.layoutParams.height
@@ -40,9 +49,69 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
             navigation.layoutParams = newParams
             navigation.layoutParams = newParams
             WindowInsetsCompat.CONSUMED
             WindowInsetsCompat.CONSUMED
         }
         }
+
+        val fragmentRoot = requireViewById<View>(R.id.step_fragment_container)
+        ViewCompat.setOnApplyWindowInsetsListener(fragmentRoot) { v, insets ->
+            val bars = insets.getInsets(
+                WindowInsetsCompat.Type.systemBars()
+                        or WindowInsetsCompat.Type.displayCutout()
+            )
+            v.updatePadding(bars.left, bars.top, bars.right, 0)
+            WindowInsetsCompat.CONSUMED
+        }
     }
     }
 
 
     override fun onInit() {
     override fun onInit() {
         progressBar.visibility = View.GONE
         progressBar.visibility = View.GONE
+        showFragment(DownloadWizardSlotSelectFragment())
+    }
+
+    private fun showFragment(nextFrag: DownloadWizardStepFragment) {
+        currentFragment = nextFrag
+        supportFragmentManager.beginTransaction().replace(R.id.step_fragment_container, nextFrag)
+            .commit()
+        refreshButtons()
+    }
+
+    private fun refreshButtons() {
+        currentFragment?.let {
+            nextButton.visibility = if (it.hasNext) {
+                View.VISIBLE
+            } else {
+                View.GONE
+            }
+            prevButton.visibility = if (it.hasNext) {
+                View.VISIBLE
+            } else {
+                View.GONE
+            }
+        }
+    }
+
+    abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
+        abstract val hasNext: Boolean
+        abstract val hasPrev: Boolean
+        abstract fun createNextFragment(): DownloadWizardStepFragment
+        abstract fun createPrevFragment(): DownloadWizardStepFragment
+
+        protected fun hideProgressBar() {
+            (requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
+        }
+
+        protected fun showProgressBar(progressValue: Int) {
+            (requireActivity() as DownloadWizardActivity).progressBar.apply {
+                visibility = View.VISIBLE
+                if (progressValue >= 0) {
+                    isIndeterminate = false
+                    progress = progressValue
+                } else {
+                    isIndeterminate = true
+                }
+            }
+        }
+
+        protected fun refreshButtons() {
+            (requireActivity() as DownloadWizardActivity).refreshButtons()
+        }
     }
     }
 }
 }

+ 108 - 0
app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt

@@ -0,0 +1,108 @@
+package im.angry.openeuicc.ui.wizard
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import im.angry.openeuicc.common.R
+import im.angry.openeuicc.util.*
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import net.typeblog.lpac_jni.LocalProfileInfo
+
+class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
+    private data class SlotInfo(
+        val logicalSlotId: Int,
+        val eID: String,
+        val enabledProfileName: String?
+    )
+
+    private var loaded = false
+
+    private val adapter = SlotInfoAdapter()
+
+    override val hasNext: Boolean
+        get() = loaded
+    override val hasPrev: Boolean
+        get() = false
+
+    override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment {
+        TODO("Not yet implemented")
+    }
+
+    override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment {
+        TODO("Not yet implemented")
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val view = inflater.inflate(R.layout.fragment_download_slot_select, container, false)
+        val recyclerView = view.requireViewById<RecyclerView>(R.id.download_slot_list)
+        recyclerView.adapter = adapter
+        recyclerView.layoutManager =
+            LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
+        return view
+    }
+
+    override fun onStart() {
+        super.onStart()
+        if (!loaded) {
+            lifecycleScope.launch { init() }
+        }
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    private suspend fun init() {
+        showProgressBar(-1)
+        val slots = euiccChannelManager.flowEuiccPorts().map { (slotId, portId) ->
+            euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
+                SlotInfo(
+                    channel.logicalSlotId,
+                    channel.lpa.eID,
+                    channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName
+                )
+            }
+        }.toList()
+        adapter.slots = slots
+        adapter.notifyDataSetChanged()
+        hideProgressBar()
+        refreshButtons()
+    }
+
+    private class SlotItemHolder(val root: View) : ViewHolder(root) {
+        private val title = root.requireViewById<TextView>(R.id.slot_item_title)
+        private val eID = root.requireViewById<TextView>(R.id.slot_item_eid)
+        private val activeProfile = root.requireViewById<TextView>(R.id.slot_item_active_profile)
+
+        fun bind(item: SlotInfo) {
+            title.text = root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId)
+            eID.text = item.eID
+            activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
+        }
+    }
+
+    private class SlotInfoAdapter : RecyclerView.Adapter<SlotItemHolder>() {
+        var slots: List<SlotInfo> = listOf()
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder {
+            val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false)
+            return SlotItemHolder(root)
+        }
+
+        override fun getItemCount(): Int = slots.size
+
+        override fun onBindViewHolder(holder: SlotItemHolder, position: Int) {
+            holder.bind(slots[position])
+        }
+    }
+}

+ 3 - 3
app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt

@@ -32,9 +32,9 @@ val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
 val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
 val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
     get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
     get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
 
 
-val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
+val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
     get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
     get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
-val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker
+val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
     get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
     get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
 
 
 suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
 suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
@@ -42,7 +42,7 @@ suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where
     return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
     return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
 }
 }
 
 
-suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
+suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
     (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
     (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
 
 
 interface EuiccProfilesChangedListener {
 interface EuiccProfilesChangedListener {

+ 9 - 0
app-common/src/main/res/layout/activity_download_wizard.xml

@@ -4,6 +4,15 @@
     android:layout_height="match_parent"
     android:layout_height="match_parent"
     xmlns:app="http://schemas.android.com/apk/res-auto">
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
 
+    <FrameLayout
+        android:id="@+id/step_fragment_container"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
     <View
     <View
         android:id="@+id/guideline"
         android:id="@+id/guideline"
         android:layout_width="0dp"
         android:layout_width="0dp"

+ 77 - 0
app-common/src/main/res/layout/download_slot_item.xml

@@ -0,0 +1,77 @@
+<?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="wrap_content"
+    android:layout_marginBottom="10sp"
+    android:layout_marginTop="10sp"
+    android:paddingStart="20sp"
+    android:paddingEnd="20sp">
+
+    <TextView
+        android:id="@+id/slot_item_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_margin="10sp"
+        android:textSize="18sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/slot_item_eid_label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/download_wizard_slot_eid"
+        android:textSize="14sp" />
+
+    <TextView
+        android:id="@+id/slot_item_eid"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textSize="14sp" />
+
+    <TextView
+        android:id="@+id/slot_item_active_profile_label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/download_wizard_slot_active_profile"
+        android:textSize="14sp" />
+
+    <TextView
+        android:id="@+id/slot_item_active_profile"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textSize="14sp" />
+
+    <androidx.constraintlayout.helper.widget.Flow
+        android:id="@+id/flow1"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="10sp"
+        android:layout_marginTop="20sp"
+        android:layout_marginEnd="10sp"
+        app:constraint_referenced_ids="slot_item_eid_label,slot_item_eid,slot_item_active_profile_label,slot_item_active_profile"
+        app:flow_wrapMode="aligned"
+        app:flow_horizontalAlign="start"
+        app:flow_horizontalBias="1"
+        app:flow_horizontalGap="10sp"
+        app:flow_horizontalStyle="packed"
+        app:flow_maxElementsWrap="2"
+        app:flow_verticalBias="0"
+        app:flow_verticalGap="16sp"
+        app:flow_verticalStyle="packed"
+        app:layout_constraintEnd_toStartOf="@id/slot_checkbox"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/slot_item_title" />
+
+    <CheckBox
+        android:id="@+id/slot_checkbox"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/flow1"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 29 - 0
app-common/src/main/res/layout/fragment_download_slot_select.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"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <TextView
+        android:id="@+id/download_slot_select_title"
+        android:text="@string/download_wizard_slot_select"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="20sp"
+        android:layout_margin="20sp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/download_slot_list"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/download_slot_select_title"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constrainedHeight="true"
+        app:layout_constraintHeight_max="300dp" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -61,6 +61,10 @@
     <string name="download_wizard">Download Wizard</string>
     <string name="download_wizard">Download Wizard</string>
     <string name="download_wizard_back">Back</string>
     <string name="download_wizard_back">Back</string>
     <string name="download_wizard_next">Next</string>
     <string name="download_wizard_next">Next</string>
+    <string name="download_wizard_slot_select">Select an eSIM slot:</string>
+    <string name="download_wizard_slot_title">Logical slot %d</string>
+    <string name="download_wizard_slot_eid">eID:</string>
+    <string name="download_wizard_slot_active_profile">Active Profile:</string>
 
 
     <string name="profile_rename_new_name">New nickname</string>
     <string name="profile_rename_new_name">New nickname</string>