From 3960a2d9d8578223eab0469ff6b449a6a0039104 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 30 Jun 2024 20:20:32 -0400 Subject: [PATCH 1/3] ui: Add progress spinner when MainActivity is loading --- .../im/angry/openeuicc/ui/MainActivity.kt | 28 +++++++++++++++---- .../src/main/res/layout/activity_main.xml | 10 +++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 4641732..274a8ec 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -17,6 +17,7 @@ import android.view.MenuItem import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.ProgressBar import android.widget.Spinner import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R @@ -35,6 +36,17 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { private lateinit var spinnerAdapter: ArrayAdapter private lateinit var spinnerItem: MenuItem private lateinit var spinner: Spinner + private lateinit var loadingProgress: ProgressBar + + var loading: Boolean + get() = loadingProgress.visibility == View.VISIBLE + set(value) { + loadingProgress.visibility = if (value) { + View.VISIBLE + } else { + View.GONE + } + } private val fragments = arrayListOf() @@ -66,11 +78,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(requireViewById(R.id.toolbar)) - - supportFragmentManager.beginTransaction().replace( - R.id.fragment_root, - appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() - ).commit() + loadingProgress = requireViewById(R.id.loading) tm = telephonyManager @@ -143,6 +151,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } private suspend fun init() { + loading = true + val knownChannels = withContext(Dispatchers.IO) { euiccChannelManager.enumerateEuiccChannels().onEach { Log.d(TAG, "slot ${it.slotId} port ${it.portId}") @@ -161,6 +171,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } withContext(Dispatchers.Main) { + loading = false + knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId)) fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel)) @@ -175,6 +187,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { spinnerItem.isVisible = true } 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() + ).commit() } } } diff --git a/app-common/src/main/res/layout/activity_main.xml b/app-common/src/main/res/layout/activity_main.xml index 4f1020d..89b9d7e 100644 --- a/app-common/src/main/res/layout/activity_main.xml +++ b/app-common/src/main/res/layout/activity_main.xml @@ -14,6 +14,16 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintWidth_percent="1" /> + + Date: Sun, 30 Jun 2024 21:00:30 -0400 Subject: [PATCH 2/3] ui: Decouple USB-specific logic from MainActivity into a dedicated fragment --- .../im/angry/openeuicc/ui/MainActivity.kt | 87 ++-------- .../openeuicc/ui/UsbCcidReaderFragment.kt | 159 ++++++++++++++++++ .../res/layout/fragment_usb_ccid_reader.xml | 37 ++++ app-common/src/main/res/values/strings.xml | 4 + 4 files changed, 211 insertions(+), 76 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt create mode 100644 app-common/src/main/res/layout/fragment_usb_ccid_reader.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 274a8ec..f8a1915 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/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 @@ -48,31 +40,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } } - private val fragments = arrayListOf() + private val fragments = arrayListOf() 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(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() - } - } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt new file mode 100644 index 0000000..4660d97 --- /dev/null +++ b/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 + } + } +} \ No newline at end of file diff --git a/app-common/src/main/res/layout/fragment_usb_ccid_reader.xml b/app-common/src/main/res/layout/fragment_usb_ccid_reader.xml new file mode 100644 index 0000000..1207990 --- /dev/null +++ b/app-common/src/main/res/layout/fragment_usb_ccid_reader.xml @@ -0,0 +1,37 @@ + + + + + +