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
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_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"
|
||||||
|
|
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 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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue