Compare commits
3 commits
70f20f9de8
...
0a971b68b2
Author | SHA1 | Date | |
---|---|---|---|
0a971b68b2 | |||
b67791412a | |||
3960a2d9d8 |
6 changed files with 244 additions and 81 deletions
|
@ -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
|
||||
|
@ -17,10 +10,11 @@ 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
|
||||
|
@ -29,61 +23,37 @@ 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>
|
||||
private lateinit var spinnerItem: MenuItem
|
||||
private lateinit var spinner: Spinner
|
||||
private lateinit var loadingProgress: ProgressBar
|
||||
|
||||
private val fragments = arrayListOf<EuiccManagementFragment>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
var loading: Boolean
|
||||
get() = loadingProgress.visibility == View.VISIBLE
|
||||
set(value) {
|
||||
loadingProgress.visibility = if (value) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val fragments = arrayListOf<Fragment>()
|
||||
|
||||
protected lateinit var tm: TelephonyManager
|
||||
|
||||
@SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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
|
||||
|
||||
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 {
|
||||
|
@ -106,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,6 +108,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}")
|
||||
|
@ -154,51 +121,37 @@ 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) {
|
||||
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
|
||||
// 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 {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.fragment_root,
|
||||
appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
|
||||
).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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,16 @@
|
|||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_root"
|
||||
android:layout_width="0dp"
|
||||
|
|
37
app-common/src/main/res/layout/fragment_usb_ccid_reader.xml
Normal file
37
app-common/src/main/res/layout/fragment_usb_ccid_reader.xml
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue