Compare commits

...

3 commits

6 changed files with 244 additions and 81 deletions

View file

@ -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>()
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
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))
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()
}
}
}
private suspend fun switchToUsbFragmentIfPossible() {
if (usbDevice != null && usbChannel == null) {
if (!usbManager.hasPermission(usbDevice)) {
usbManager.requestPermission(usbDevice, usbPendingIntent)
return
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_root, fragments.first()).commit()
} else {
val (device, channel) = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateUsbEuiccChannel()
}
if (device != null && channel != null) {
usbDevice = device
usbChannel = channel
supportFragmentManager.beginTransaction().replace(
R.id.fragment_root,
appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
).commit()
}
}
}
if (usbChannel != null) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_root,
appContainer.uiComponentFactory.createEuiccManagementFragment(usbChannel!!)).commit()
}
}
}

View file

@ -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) {
""
}

View file

@ -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
}
}
}

View file

@ -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"

View 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>

View file

@ -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>