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 f8a1915..4641732 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,7 +1,14 @@ 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 @@ -10,11 +17,10 @@ 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.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 @@ -23,37 +29,61 @@ 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 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() + 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) setContentView(R.layout.activity_main) setSupportActionBar(requireViewById(R.id.toolbar)) - loadingProgress = requireViewById(R.id.loading) + + supportFragmentManager.beginTransaction().replace( + R.id.fragment_root, + appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() + ).commit() 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 { @@ -76,6 +106,11 @@ 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() + } } } @@ -108,8 +143,6 @@ 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}") @@ -121,37 +154,51 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } } - val (usbDevice, _) = withContext(Dispatchers.IO) { - euiccChannelManager.enumerateUsbEuiccChannel() + withContext(Dispatchers.IO) { + val res = euiccChannelManager.enumerateUsbEuiccChannel() + usbDevice = res.first + usbChannel = res.second } 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)) } // If USB readers exist, add them at the very last - // We use a wrapper fragment to handle logic specific to USB readers - usbDevice?.let { - spinnerAdapter.add(it.productName) - fragments.add(UsbCcidReaderFragment()) - } + // The adapter logic depends on this assumption + usbDevice?.let { spinnerAdapter.add(it.productName) } if (fragments.isNotEmpty()) { if (this@MainActivity::spinner.isInitialized) { spinnerItem.isVisible = true } - supportFragmentManager.beginTransaction() - .replace(R.id.fragment_root, fragments.first()).commit() - } else { - supportFragmentManager.beginTransaction().replace( - R.id.fragment_root, - appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() - ).commit() + supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments.first()).commit() } } } + + 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/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index f8399a9..bb406e2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -151,7 +151,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), super.onStart() profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable( try { - telephonyManager.getImei(channel.logicalSlotId) ?: "" + telephonyManager.getImei(channel.logicalSlotId) } catch (e: Exception) { "" } 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 deleted file mode 100644 index 4660d97..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/ui/UsbCcidReaderFragment.kt +++ /dev/null @@ -1,159 +0,0 @@ -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/activity_main.xml b/app-common/src/main/res/layout/activity_main.xml index 89b9d7e..4f1020d 100644 --- a/app-common/src/main/res/layout/activity_main.xml +++ b/app-common/src/main/res/layout/activity_main.xml @@ -14,16 +14,6 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintWidth_percent="1" /> - - - - - - -