Compare commits

...

3 commits

6 changed files with 244 additions and 81 deletions

View file

@ -1,14 +1,7 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent 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.os.Bundle
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.util.Log import android.util.Log
@ -17,10 +10,11 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ProgressBar
import android.widget.Spinner import android.widget.Spinner
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -29,61 +23,37 @@ import kotlinx.coroutines.withContext
open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
companion object { companion object {
const val TAG = "MainActivity" const val TAG = "MainActivity"
const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
} }
private lateinit var spinnerAdapter: ArrayAdapter<String> private lateinit var spinnerAdapter: ArrayAdapter<String>
private lateinit var spinnerItem: MenuItem private lateinit var spinnerItem: MenuItem
private lateinit var spinner: Spinner private lateinit var spinner: Spinner
private lateinit var loadingProgress: ProgressBar
private val fragments = arrayListOf<EuiccManagementFragment>() var loading: Boolean
get() = loadingProgress.visibility == View.VISIBLE
protected lateinit var tm: TelephonyManager set(value) {
loadingProgress.visibility = if (value) {
private val usbManager: UsbManager by lazy { View.VISIBLE
getSystemService(USB_SERVICE) as UsbManager } else {
} View.GONE
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()
}
}
} }
} }
}
private val fragments = arrayListOf<Fragment>()
protected lateinit var tm: TelephonyManager
@SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag") @SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
setSupportActionBar(requireViewById(R.id.toolbar)) setSupportActionBar(requireViewById(R.id.toolbar))
loadingProgress = requireViewById(R.id.loading)
supportFragmentManager.beginTransaction().replace(
R.id.fragment_root,
appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
).commit()
tm = telephonyManager tm = telephonyManager
spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item) 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -106,11 +76,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
if (position < fragments.size) { if (position < fragments.size) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fragment_root, fragments[position]).commit() .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() { private suspend fun init() {
loading = true
val knownChannels = withContext(Dispatchers.IO) { val knownChannels = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateEuiccChannels().onEach { euiccChannelManager.enumerateEuiccChannels().onEach {
Log.d(TAG, "slot ${it.slotId} port ${it.portId}") Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
@ -154,51 +121,37 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
} }
withContext(Dispatchers.IO) { val (usbDevice, _) = withContext(Dispatchers.IO) {
val res = euiccChannelManager.enumerateUsbEuiccChannel() euiccChannelManager.enumerateUsbEuiccChannel()
usbDevice = res.first
usbChannel = res.second
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loading = false
knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId)) spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel)) fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel))
} }
// If USB readers exist, add them at the very last // If USB readers exist, add them at the very last
// The adapter logic depends on this assumption // We use a wrapper fragment to handle logic specific to USB readers
usbDevice?.let { spinnerAdapter.add(it.productName) } usbDevice?.let {
spinnerAdapter.add(it.productName)
fragments.add(UsbCcidReaderFragment())
}
if (fragments.isNotEmpty()) { if (fragments.isNotEmpty()) {
if (this@MainActivity::spinner.isInitialized) { if (this@MainActivity::spinner.isInitialized) {
spinnerItem.isVisible = true 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()
}
}
} }

View file

@ -151,7 +151,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
super.onStart() super.onStart()
profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable( profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable(
try { try {
telephonyManager.getImei(channel.logicalSlotId) telephonyManager.getImei(channel.logicalSlotId) ?: ""
} catch (e: Exception) { } 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_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" /> 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 <FrameLayout
android:id="@+id/fragment_root" android:id="@+id/fragment_root"
android:layout_width="0dp" 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 Slot</string>
<string name="slot_select_select">Select</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">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string> <string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string> <string name="profile_download_code">Activation Code</string>