瀏覽代碼

ui: Decouple USB-specific logic from MainActivity into a dedicated fragment

Peter Cai 1 年之前
父節點
當前提交
b67791412a

+ 11 - 76
app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt

@@ -1,14 +1,7 @@
 package im.angry.openeuicc.ui
 
 import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
-import android.hardware.usb.UsbDevice
-import android.hardware.usb.UsbManager
-import android.os.Build
 import android.os.Bundle
 import android.telephony.TelephonyManager
 import android.util.Log
@@ -19,9 +12,9 @@ import android.widget.AdapterView
 import android.widget.ArrayAdapter
 import android.widget.ProgressBar
 import android.widget.Spinner
+import androidx.fragment.app.Fragment
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.common.R
-import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -30,7 +23,6 @@ import kotlinx.coroutines.withContext
 open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     companion object {
         const val TAG = "MainActivity"
-        const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
     }
 
     private lateinit var spinnerAdapter: ArrayAdapter<String>
@@ -48,31 +40,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             }
         }
 
-    private val fragments = arrayListOf<EuiccManagementFragment>()
+    private val fragments = arrayListOf<Fragment>()
 
     protected lateinit var tm: TelephonyManager
 
-    private val usbManager: UsbManager by lazy {
-        getSystemService(USB_SERVICE) as UsbManager
-    }
-
-    private var usbDevice: UsbDevice? = null
-    private var usbChannel: EuiccChannel? = null
-
-    private lateinit var usbPendingIntent: PendingIntent
-
-    private val usbPermissionReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context?, intent: Intent?) {
-            if (intent?.action == ACTION_USB_PERMISSION) {
-                if (usbDevice != null && usbManager.hasPermission(usbDevice)) {
-                    lifecycleScope.launch(Dispatchers.Main) {
-                        switchToUsbFragmentIfPossible()
-                    }
-                }
-            }
-        }
-    }
-
     @SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -83,15 +54,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         tm = telephonyManager
 
         spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
-
-        usbPendingIntent = PendingIntent.getBroadcast(this, 0,
-            Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE)
-        val filter = IntentFilter(ACTION_USB_PERMISSION)
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-            registerReceiver(usbPermissionReceiver, filter, Context.RECEIVER_EXPORTED)
-        } else {
-            registerReceiver(usbPermissionReceiver, filter)
-        }
     }
 
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -114,11 +76,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
                     if (position < fragments.size) {
                         supportFragmentManager.beginTransaction()
                             .replace(R.id.fragment_root, fragments[position]).commit()
-                    } else if (position == fragments.size) {
-                        // If we are at the last position, this is the USB device
-                        lifecycleScope.launch(Dispatchers.Main) {
-                            switchToUsbFragmentIfPossible()
-                        }
                     }
                 }
 
@@ -164,10 +121,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             }
         }
 
-        withContext(Dispatchers.IO) {
-            val res = euiccChannelManager.enumerateUsbEuiccChannel()
-            usbDevice = res.first
-            usbChannel = res.second
+        val (usbDevice, _) = withContext(Dispatchers.IO) {
+            euiccChannelManager.enumerateUsbEuiccChannel()
         }
 
         withContext(Dispatchers.Main) {
@@ -179,16 +134,19 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             }
 
             // If USB readers exist, add them at the very last
-            // The adapter logic depends on this assumption
-            usbDevice?.let { spinnerAdapter.add(it.productName) }
+            // We use a wrapper fragment to handle logic specific to USB readers
+            usbDevice?.let {
+                spinnerAdapter.add(it.productName)
+                fragments.add(UsbCcidReaderFragment())
+            }
 
             if (fragments.isNotEmpty()) {
                 if (this@MainActivity::spinner.isInitialized) {
                     spinnerItem.isVisible = true
                 }
-                supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments.first()).commit()
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.fragment_root, fragments.first()).commit()
             } else {
-                // TODO: Handle cases where there is _only_ a USB reader
                 supportFragmentManager.beginTransaction().replace(
                     R.id.fragment_root,
                     appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
@@ -196,27 +154,4 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             }
         }
     }
-
-    private suspend fun switchToUsbFragmentIfPossible() {
-        if (usbDevice != null && usbChannel == null) {
-            if (!usbManager.hasPermission(usbDevice)) {
-                usbManager.requestPermission(usbDevice, usbPendingIntent)
-                return
-            }  else {
-               val (device, channel) = withContext(Dispatchers.IO) {
-                    euiccChannelManager.enumerateUsbEuiccChannel()
-                }
-
-                if (device != null && channel != null) {
-                    usbDevice = device
-                    usbChannel = channel
-                }
-            }
-        }
-
-        if (usbChannel != null) {
-            supportFragmentManager.beginTransaction().replace(R.id.fragment_root,
-                appContainer.uiComponentFactory.createEuiccManagementFragment(usbChannel!!)).commit()
-        }
-    }
 }

+ 159 - 0
app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt

@@ -0,0 +1,159 @@
+package im.angry.openeuicc.ui
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.hardware.usb.UsbDevice
+import android.hardware.usb.UsbManager
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.commit
+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
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * A wrapper fragment over EuiccManagementFragment where we handle
+ * logic specific to USB devices. This is mainly USB permission
+ * requests, and the fact that USB devices may or may not be
+ * available by the time the user selects it from MainActivity.
+ *
+ * Having this fragment allows MainActivity to be (mostly) agnostic
+ * of the underlying implementation of different types of channels.
+ * When permission is granted, this fragment will simply load
+ * EuiccManagementFragment using its own childFragmentManager.
+ *
+ * Note that for now we assume there will only be one USB card reader
+ * device. This is also an implicit assumption in EuiccChannelManager.
+ */
+class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
+    companion object {
+        const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
+    }
+
+    private val euiccChannelManager: EuiccChannelManager by lazy {
+        (requireActivity() as MainActivity).euiccChannelManager
+    }
+
+    private val usbManager: UsbManager by lazy {
+        requireContext().getSystemService(Context.USB_SERVICE) as UsbManager
+    }
+
+    private val usbPermissionReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            if (intent?.action == ACTION_USB_PERMISSION) {
+                if (usbDevice != null && usbManager.hasPermission(usbDevice)) {
+                    lifecycleScope.launch(Dispatchers.Main) {
+                        tryLoadUsbChannel()
+                    }
+                }
+            }
+        }
+    }
+
+    private lateinit var usbPendingIntent: PendingIntent
+
+    private lateinit var text: TextView
+    private lateinit var permissionButton: Button
+
+    private var usbDevice: UsbDevice? = null
+    private var usbChannel: EuiccChannel? = null
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val view = inflater.inflate(R.layout.fragment_usb_ccid_reader, container, false)
+
+        text = view.requireViewById(R.id.usb_reader_text)
+        permissionButton = view.requireViewById(R.id.usb_grant_permission)
+
+        permissionButton.setOnClickListener {
+            usbManager.requestPermission(usbDevice, usbPendingIntent)
+        }
+
+        return view
+    }
+
+    @SuppressLint("UnspecifiedRegisterReceiverFlag", "WrongConstant")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        usbPendingIntent = PendingIntent.getBroadcast(
+            requireContext(), 0,
+            Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE
+        )
+        val filter = IntentFilter(ACTION_USB_PERMISSION)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireContext().registerReceiver(
+                usbPermissionReceiver,
+                filter,
+                Context.RECEIVER_EXPORTED
+            )
+        } else {
+            requireContext().registerReceiver(usbPermissionReceiver, filter)
+        }
+
+        lifecycleScope.launch(Dispatchers.Main) {
+            tryLoadUsbChannel()
+        }
+    }
+
+    override fun onDetach() {
+        super.onDetach()
+        requireContext().unregisterReceiver(usbPermissionReceiver)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        requireContext().unregisterReceiver(usbPermissionReceiver)
+    }
+
+    private suspend fun tryLoadUsbChannel() {
+        text.visibility = View.GONE
+        permissionButton.visibility = View.GONE
+
+        (requireActivity() as MainActivity).loading = true
+
+        val (device, channel) = withContext(Dispatchers.IO) {
+            euiccChannelManager.enumerateUsbEuiccChannel()
+        }
+
+        (requireActivity() as MainActivity).loading = false
+
+        usbDevice = device
+        usbChannel = channel
+
+        if (device != null && channel == null && !usbManager.hasPermission(device)) {
+            text.text = getString(R.string.usb_permission_needed)
+            text.visibility = View.VISIBLE
+            permissionButton.visibility = View.VISIBLE
+        } else if (device != null && channel != null) {
+            childFragmentManager.commit {
+                replace(
+                    R.id.child_container,
+                    appContainer.uiComponentFactory.createEuiccManagementFragment(channel)
+                )
+            }
+        } else {
+            text.text = getString(R.string.usb_failed)
+            text.visibility = View.VISIBLE
+            permissionButton.visibility = View.GONE
+        }
+    }
+}

+ 37 - 0
app-common/src/main/res/layout/fragment_usb_ccid_reader.xml

@@ -0,0 +1,37 @@
+<?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/usb_reader_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="40dp"
+        android:gravity="center"
+        android:visibility="gone"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <Button
+        android:id="@+id/usb_grant_permission"
+        android:text="@string/usb_permission"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"
+        android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/usb_reader_text"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <FrameLayout
+        android:id="@+id/child_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -25,6 +25,10 @@
     <string name="slot_select">Select Slot</string>
     <string name="slot_select_select">Select</string>
 
+    <string name="usb_permission">Grant USB permission</string>
+    <string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string>
+    <string name="usb_failed">Cannot connect to eSIM via a USB smart card reader.</string>
+
     <string name="profile_download">New eSIM</string>
     <string name="profile_download_server">Server (RSP / SM-DP+)</string>
     <string name="profile_download_code">Activation Code</string>