forked from PeterCxy/OpenEUICC
Compare commits
11 commits
aed2479044
...
9d18253e44
Author | SHA1 | Date | |
---|---|---|---|
9d18253e44 | |||
6039679693 | |||
5a8d92c3df | |||
55c99831f3 | |||
343dfb43f8 | |||
815d4d4324 | |||
ec334d104a | |||
70f1e00eb4 | |||
bc238c45cd | |||
14ea84c36e | |||
aefa79b18b |
32 changed files with 264 additions and 184 deletions
|
@ -37,6 +37,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
return EuiccChannelImpl(
|
||||
context.getString(R.string.omapi),
|
||||
port,
|
||||
intrinsicChannelName = null,
|
||||
OmapiApduInterface(
|
||||
seService!!,
|
||||
port,
|
||||
|
@ -67,6 +68,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
return EuiccChannelImpl(
|
||||
context.getString(R.string.usb),
|
||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||
intrinsicChannelName = usbDevice.productName,
|
||||
UsbApduInterface(
|
||||
conn,
|
||||
bulkIn,
|
||||
|
|
|
@ -16,5 +16,12 @@ interface EuiccChannel {
|
|||
|
||||
val valid: Boolean
|
||||
|
||||
/**
|
||||
* Intrinsic name of this channel. For device-internal SIM slots,
|
||||
* this should be null; for USB readers, this should be the name of
|
||||
* the reader device.
|
||||
*/
|
||||
val intrinsicChannelName: String?
|
||||
|
||||
fun close()
|
||||
}
|
|
@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
|
|||
class EuiccChannelImpl(
|
||||
override val type: String,
|
||||
override val port: UiccPortInfoCompat,
|
||||
override val intrinsicChannelName: String?,
|
||||
apduInterface: ApduInterface,
|
||||
verboseLoggingFlow: Flow<Boolean>,
|
||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||
|
|
|
@ -31,6 +31,8 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
override val lpa: LocalProfileAssistant by lpaDelegate
|
||||
override val valid: Boolean
|
||||
get() = channel.valid
|
||||
override val intrinsicChannelName: String?
|
||||
get() = channel.intrinsicChannelName
|
||||
|
||||
override fun close() = channel.close()
|
||||
|
||||
|
|
|
@ -15,4 +15,5 @@ interface AppContainer {
|
|||
val preferenceRepository: PreferenceRepository
|
||||
val uiComponentFactory: UiComponentFactory
|
||||
val euiccChannelFactory: EuiccChannelFactory
|
||||
val customizableTextProvider: CustomizableTextProvider
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
interface CustomizableTextProvider {
|
||||
/**
|
||||
* Explanation string for when no eUICC is found on the device.
|
||||
* This could be different depending on whether the app is privileged or not.
|
||||
*/
|
||||
val noEuiccExplanation: String
|
||||
|
||||
/**
|
||||
* Shown when we timed out switching between profiles.
|
||||
*/
|
||||
val profileSwitchingTimeoutMessage: String
|
||||
|
||||
/**
|
||||
* Format the name of a logical slot; internal only -- not intended for
|
||||
* other channels such as USB.
|
||||
*/
|
||||
fun formatInternalChannelName(logicalSlotId: Int): String
|
||||
}
|
|
@ -38,4 +38,8 @@ open class DefaultAppContainer(context: Context) : AppContainer {
|
|||
override val euiccChannelFactory by lazy {
|
||||
DefaultEuiccChannelFactory(context)
|
||||
}
|
||||
|
||||
override val customizableTextProvider by lazy {
|
||||
DefaultCustomizableTextProvider(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.content.Context
|
||||
import im.angry.openeuicc.common.R
|
||||
|
||||
open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider {
|
||||
override val noEuiccExplanation: String
|
||||
get() = context.getString(R.string.no_euicc)
|
||||
|
||||
override val profileSwitchingTimeoutMessage: String
|
||||
get() = context.getString(R.string.enable_disable_timeout)
|
||||
|
||||
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||
context.getString(R.string.channel_name_format, logicalSlotId)
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -32,6 +37,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
|||
|
||||
private var logicalSlotId: Int = -1
|
||||
|
||||
data class Item(
|
||||
@StringRes
|
||||
val titleResId: Int,
|
||||
val content: String?,
|
||||
val copiedToastResId: Int? = null
|
||||
)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -41,12 +53,11 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
|||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||
infoList = requireViewById(R.id.recycler_view)
|
||||
|
||||
infoList.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
infoList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
infoList.adapter = EuiccInfoAdapter()
|
||||
infoList = requireViewById<RecyclerView>(R.id.recycler_view).also {
|
||||
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
it.adapter = EuiccInfoAdapter()
|
||||
}
|
||||
|
||||
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||
|
||||
|
@ -81,29 +92,33 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
|||
|
||||
lifecycleScope.launch {
|
||||
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildPairs).map {
|
||||
Pair(getString(it.first), it.second ?: getString(R.string.unknown))
|
||||
}
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
|
||||
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPairs(channel: EuiccChannel) = buildList {
|
||||
add(Pair(R.string.euicc_info_access_mode, channel.type))
|
||||
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
|
||||
add(Item(R.string.euicc_info_access_mode, channel.type))
|
||||
add(
|
||||
Pair(
|
||||
Item(
|
||||
R.string.euicc_info_removable,
|
||||
formatByBoolean(channel.port.card.isRemovable, YES_NO)
|
||||
)
|
||||
)
|
||||
add(Pair(R.string.euicc_info_eid, channel.lpa.eID))
|
||||
add(
|
||||
Item(
|
||||
R.string.euicc_info_eid,
|
||||
channel.lpa.eID,
|
||||
copiedToastResId = R.string.toast_eid_copied
|
||||
)
|
||||
)
|
||||
channel.lpa.euiccInfo2.let { info ->
|
||||
add(Pair(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
||||
add(Pair(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
||||
add(Pair(R.string.euicc_info_pp_version, info?.ppVersion))
|
||||
add(Pair(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
|
||||
add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
|
||||
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
||||
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
||||
add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
|
||||
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
|
||||
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
|
||||
}
|
||||
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
|
||||
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
|
||||
|
@ -116,7 +131,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
|||
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
|
||||
else -> R.string.euicc_info_ci_unknown
|
||||
}
|
||||
add(Pair(R.string.euicc_info_ci_type, getString(resId)))
|
||||
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,15 +147,34 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
|
|||
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
|
||||
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
|
||||
private val content: TextView = root.requireViewById(R.id.euicc_info_content)
|
||||
private var copiedToastResId: Int? = null
|
||||
|
||||
fun bind(item: Pair<String, String>) {
|
||||
title.text = item.first
|
||||
content.text = item.second
|
||||
init {
|
||||
root.setOnClickListener {
|
||||
if (copiedToastResId != null) {
|
||||
val label = title.text.toString()
|
||||
getSystemService(ClipboardManager::class.java)!!
|
||||
.setPrimaryClip(ClipData.newPlainText(label, content.text))
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
this@EuiccInfoActivity,
|
||||
copiedToastResId!!,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: Item) {
|
||||
copiedToastResId = item.copiedToastResId
|
||||
title.setText(item.titleResId)
|
||||
content.text = item.content ?: getString(R.string.unknown)
|
||||
}
|
||||
}
|
||||
|
||||
inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
|
||||
var euiccInfoItems: List<Pair<String, String>> = listOf()
|
||||
var euiccInfoItems: List<Item> = listOf()
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(newVal) {
|
||||
field = newVal
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.LayoutInflater
|
||||
|
@ -261,7 +262,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
invalid = true
|
||||
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setMessage(R.string.enable_disable_timeout)
|
||||
setMessage(appContainer.customizableTextProvider.profileSwitchingTimeoutMessage)
|
||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
|
@ -348,7 +349,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
iccid.setOnLongClickListener {
|
||||
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text))
|
||||
Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast
|
||||
.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.view.View
|
|||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
@ -17,7 +16,6 @@ import im.angry.openeuicc.util.*
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Date
|
||||
|
||||
class LogsActivity : AppCompatActivity() {
|
||||
|
@ -27,15 +25,15 @@ class LogsActivity : AppCompatActivity() {
|
|||
private lateinit var logStr: String
|
||||
|
||||
private val saveLogs =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
if (uri == null) return@registerForActivityResult
|
||||
if (!this::logStr.isInitialized) return@registerForActivityResult
|
||||
contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { os ->
|
||||
os.write(logStr.encodeToByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
setupLogSaving(
|
||||
getLogFileName = {
|
||||
getString(
|
||||
R.string.logs_filename_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
)
|
||||
},
|
||||
getLogText = { logStr }
|
||||
)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
@ -76,9 +74,7 @@ class LogsActivity : AppCompatActivity() {
|
|||
true
|
||||
}
|
||||
R.id.save -> {
|
||||
saveLogs.launch(getString(R.string.logs_filename_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
))
|
||||
saveLogs()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
|
|
|
@ -163,7 +163,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
// but it could change in the future
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
|
||||
val channelName = getString(R.string.channel_name_format, channel.logicalSlotId)
|
||||
val channelName =
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
|
||||
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
|
||||
})
|
||||
|
|
|
@ -4,15 +4,20 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class NoEuiccPlaceholderFragment : Fragment() {
|
||||
class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false)
|
||||
val view = inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false)
|
||||
val textView = view.requireViewById<TextView>(R.id.no_euicc_placeholder)
|
||||
textView.text = appContainer.customizableTextProvider.noEuiccExplanation
|
||||
return view
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
|
@ -14,7 +13,6 @@ import androidx.preference.PreferenceCategory
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -60,25 +58,25 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_download")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_delete")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_switch")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH)
|
||||
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
||||
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM)
|
||||
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
||||
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
|
||||
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
||||
?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow, PreferenceKeys.UNFILTERED_PROFILE_LIST)
|
||||
?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_ignore_tls_certificate")
|
||||
?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow, PreferenceKeys.IGNORE_TLS_CERTIFICATE)
|
||||
?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -99,10 +97,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
|
||||
if (numClicks == 7) {
|
||||
lifecycleScope.launch {
|
||||
preferenceRepository.updatePreference(
|
||||
PreferenceKeys.DEVELOPER_OPTIONS_ENABLED,
|
||||
true
|
||||
)
|
||||
preferenceRepository.developerOptionsEnabledFlow.updatePreference(true)
|
||||
|
||||
lastToast?.cancel()
|
||||
Toast.makeText(
|
||||
|
@ -124,14 +119,14 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
return true
|
||||
}
|
||||
|
||||
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {
|
||||
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||
lifecycleScope.launch {
|
||||
flow.collect { isChecked = it }
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
runBlocking {
|
||||
preferenceRepository.updatePreference(key, newValue as Boolean)
|
||||
flow.updatePreference(newValue as Boolean)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker {
|
||||
companion object {
|
||||
const val TAG = "SlotSelectFragment"
|
||||
|
||||
fun newInstance(slotIds: List<Int>, logicalSlotIds: List<Int>, portIds: List<Int>): SlotSelectFragment {
|
||||
return SlotSelectFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putIntArray("slotIds", slotIds.toIntArray())
|
||||
putIntArray("logicalSlotIds", logicalSlotIds.toIntArray())
|
||||
putIntArray("portIds", portIds.toIntArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SlotSelectedListener {
|
||||
fun onSlotSelected(slotId: Int, portId: Int)
|
||||
fun onSlotSelectCancelled()
|
||||
}
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var spinner: Spinner
|
||||
private lateinit var adapter: ArrayAdapter<String>
|
||||
private lateinit var slotIds: IntArray
|
||||
private lateinit var logicalSlotIds: IntArray
|
||||
private lateinit var portIds: IntArray
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_slot_select, container, false)
|
||||
|
||||
toolbar = view.requireViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.slot_select)
|
||||
toolbar.inflateMenu(R.menu.fragment_slot_select)
|
||||
|
||||
adapter = ArrayAdapter<String>(inflater.context, R.layout.spinner_item)
|
||||
|
||||
spinner = view.requireViewById(R.id.spinner)
|
||||
spinner.adapter = adapter
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
slotIds = requireArguments().getIntArray("slotIds")!!
|
||||
logicalSlotIds = requireArguments().getIntArray("logicalSlotIds")!!
|
||||
portIds = requireArguments().getIntArray("portIds")!!
|
||||
|
||||
logicalSlotIds.forEach { id ->
|
||||
adapter.add(getString(R.string.channel_name_format, id))
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
||||
}
|
||||
toolbar.setOnMenuItemClickListener {
|
||||
val slotId = slotIds[spinner.selectedItemPosition]
|
||||
val portId = portIds[spinner.selectedItemPosition]
|
||||
(requireActivity() as SlotSelectedListener).onSlotSelected(slotId, portId)
|
||||
dismiss()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setWidthPercent(75)
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
||||
}
|
||||
}
|
|
@ -6,10 +6,8 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Date
|
||||
|
||||
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
|
@ -21,14 +19,15 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
|
|||
private lateinit var diagnosticTextView: TextView
|
||||
|
||||
private val saveDiagnostics =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
if (uri == null) return@registerForActivityResult
|
||||
requireActivity().contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { os ->
|
||||
os.write(diagnosticTextView.text.toString().encodeToByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
setupLogSaving(
|
||||
getLogFileName = {
|
||||
getString(
|
||||
R.string.download_wizard_diagnostics_file_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
)
|
||||
},
|
||||
getLogText = { diagnosticTextView.text.toString() }
|
||||
)
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
|
@ -41,12 +40,7 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
|
|||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false)
|
||||
view.requireViewById<View>(R.id.download_wizard_diagnostics_save).setOnClickListener {
|
||||
saveDiagnostics.launch(
|
||||
getString(
|
||||
R.string.download_wizard_diagnostics_file_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
)
|
||||
)
|
||||
saveDiagnostics()
|
||||
}
|
||||
diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text)
|
||||
return view
|
||||
|
|
|
@ -35,7 +35,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
val eID: String,
|
||||
val freeSpace: Int,
|
||||
val imei: String,
|
||||
val enabledProfileName: String?
|
||||
val enabledProfileName: String?,
|
||||
val intrinsicChannelName: String?,
|
||||
)
|
||||
|
||||
private var loaded = false
|
||||
|
@ -61,7 +62,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
setMessage(R.string.profile_download_low_nvram_message)
|
||||
setCancelable(true)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
requireActivity().finish()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +109,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
} catch (e: Exception) {
|
||||
""
|
||||
},
|
||||
channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName
|
||||
channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName,
|
||||
channel.intrinsicChannelName,
|
||||
)
|
||||
}
|
||||
}.toList().sortedBy { it.logicalSlotId }
|
||||
|
@ -177,9 +181,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
}
|
||||
|
||||
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
root.context.getString(R.string.usb)
|
||||
item.intrinsicChannelName ?: root.context.getString(R.string.usb)
|
||||
} else {
|
||||
root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId)
|
||||
}
|
||||
eID.text = item.eID
|
||||
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
|
||||
|
|
|
@ -19,7 +19,7 @@ val Context.preferenceRepository: PreferenceRepository
|
|||
val Fragment.preferenceRepository: PreferenceRepository
|
||||
get() = requireContext().preferenceRepository
|
||||
|
||||
object PreferenceKeys {
|
||||
internal object PreferenceKeys {
|
||||
// ---- Profile Notifications ----
|
||||
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
||||
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
||||
|
@ -51,9 +51,22 @@ class PreferenceRepository(private val context: Context) {
|
|||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||
|
||||
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): Flow<T> =
|
||||
context.dataStore.data.map { it[key] ?: defaultValue }
|
||||
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
|
||||
PreferenceFlowWrapper(context, key, defaultValue)
|
||||
}
|
||||
|
||||
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) =
|
||||
class PreferenceFlowWrapper<T> private constructor(
|
||||
private val context: Context,
|
||||
private val key: Preferences.Key<T>,
|
||||
inner: Flow<T>
|
||||
) : Flow<T> by inner {
|
||||
internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) : this(
|
||||
context,
|
||||
key,
|
||||
context.dataStore.data.map { it[key] ?: defaultValue }
|
||||
)
|
||||
|
||||
suspend fun updatePreference(value: T) {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
}
|
||||
}
|
|
@ -1,17 +1,23 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.common.R
|
||||
import java.io.FileOutputStream
|
||||
|
||||
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
||||
/**
|
||||
|
@ -69,4 +75,44 @@ fun setupRootViewInsets(view: ViewGroup) {
|
|||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : ActivityResultCaller> T.setupLogSaving(
|
||||
getLogFileName: () -> String,
|
||||
getLogText: () -> String
|
||||
): () -> Unit {
|
||||
val launchSaveIntent =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
if (uri == null) return@registerForActivityResult
|
||||
|
||||
val context = when (this@setupLogSaving) {
|
||||
is Context -> this@setupLogSaving
|
||||
is Fragment -> requireContext()
|
||||
else -> throw IllegalArgumentException("Must be either Context or Fragment!")
|
||||
}
|
||||
|
||||
context.contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { os ->
|
||||
os.write(getLogText().encodeToByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setMessage(R.string.logs_saved_message)
|
||||
setNegativeButton(R.string.no) { _, _ -> }
|
||||
setPositiveButton(R.string.yes) { _, _ ->
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
}
|
||||
|
||||
context.startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
return {
|
||||
launchSaveIntent.launch(getLogFileName())
|
||||
}
|
||||
}
|
|
@ -43,7 +43,6 @@
|
|||
<string name="download_wizard_back">戻る</string>
|
||||
<string name="download_wizard_next">次へ</string>
|
||||
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</string>
|
||||
<string name="download_wizard_slot_title">論理スロット %d</string>
|
||||
<string name="download_wizard_slot_type">タイプ:</string>
|
||||
<string name="download_wizard_slot_type_removable">リムーバブル</string>
|
||||
<string name="download_wizard_slot_type_internal">内部</string>
|
||||
|
|
|
@ -81,7 +81,6 @@
|
|||
<string name="download_wizard_back">返回</string>
|
||||
<string name="download_wizard_next">下一步</string>
|
||||
<string name="download_wizard_slot_select">请选择或确认下载目标 eSIM 卡槽:</string>
|
||||
<string name="download_wizard_slot_title">逻辑卡槽 %d</string>
|
||||
<string name="download_wizard_slot_type">类型:</string>
|
||||
<string name="download_wizard_slot_type_removable">可插拔</string>
|
||||
<string name="download_wizard_slot_type_internal">内置</string>
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
|
||||
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
||||
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||
|
||||
<string name="slot_select">Select Slot</string>
|
||||
<string name="slot_select_select">Select</string>
|
||||
|
@ -64,7 +65,6 @@
|
|||
<string name="download_wizard_back">Back</string>
|
||||
<string name="download_wizard_next">Next</string>
|
||||
<string name="download_wizard_slot_select">Select or confirm the eSIM you would like to download to:</string>
|
||||
<string name="download_wizard_slot_title">Logical slot %d</string>
|
||||
<string name="download_wizard_slot_type">Type:</string>
|
||||
<string name="download_wizard_slot_type_removable">Removable</string>
|
||||
<string name="download_wizard_slot_type_internal">Internal</string>
|
||||
|
@ -95,6 +95,8 @@
|
|||
<string name="download_wizard_diagnostics_save">Save</string>
|
||||
<string name="download_wizard_diagnostics_file_template">Diagnostics at %s</string>
|
||||
|
||||
<string name="logs_saved_message">Logs have been saved to the selected path. Would you like to share the log through another app?</string>
|
||||
|
||||
<string name="profile_rename_new_name">New nickname</string>
|
||||
|
||||
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
|
||||
|
|
|
@ -6,4 +6,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context)
|
|||
override val uiComponentFactory by lazy {
|
||||
UnprivilegedUiComponentFactory()
|
||||
}
|
||||
|
||||
override val customizableTextProvider by lazy {
|
||||
UnprivilegedCustomizableTextProvider(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.content.Context
|
||||
import im.angry.easyeuicc.R
|
||||
|
||||
class UnprivilegedCustomizableTextProvider(private val context: Context) :
|
||||
DefaultCustomizableTextProvider(context) {
|
||||
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||
context.getString(R.string.channel_name_format_unpriv, logicalSlotId)
|
||||
}
|
|
@ -3,6 +3,7 @@ package im.angry.openeuicc.ui
|
|||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.preference.Preference
|
||||
|
@ -35,7 +36,8 @@ class UnprivilegedSettingsFragment : SettingsFragment() {
|
|||
setOnPreferenceClickListener {
|
||||
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||
.setPrimaryClip(ClipData.newPlainText("ara-m", summary))
|
||||
Toast.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast
|
||||
.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">EasyEUICC</string>
|
||||
<string name="channel_name_format" translatable="false">SIM %d</string>
|
||||
<string name="channel_name_format_unpriv" translatable="false">SIM %d</string>
|
||||
<string name="compatibility_check">Compatibility Check</string>
|
||||
<string name="open_sim_toolkit">Open SIM Toolkit</string>
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
return EuiccChannelImpl(
|
||||
context.getString(R.string.telephony_manager),
|
||||
port,
|
||||
intrinsicChannelName = null,
|
||||
TelephonyManagerApduInterface(
|
||||
port,
|
||||
tm,
|
||||
|
|
|
@ -23,4 +23,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
|||
override val euiccChannelFactory by lazy {
|
||||
PrivilegedEuiccChannelFactory(context)
|
||||
}
|
||||
|
||||
override val customizableTextProvider by lazy {
|
||||
PrivilegedCustomizableTextProvider(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.content.Context
|
||||
import im.angry.openeuicc.R
|
||||
|
||||
class PrivilegedCustomizableTextProvider(private val context: Context) :
|
||||
DefaultCustomizableTextProvider(context) {
|
||||
override val noEuiccExplanation: String
|
||||
get() = context.getString(R.string.no_euicc_priv)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="no_euicc">このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。</string>
|
||||
<string name="no_euicc_priv">このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。</string>
|
||||
<string name="telephony_manager">TelephonyManager (特権)</string>
|
||||
<string name="dsds">デュアル SIM</string>
|
||||
<string name="toast_dsds_switched">DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。</string>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="no_euicc">在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。</string>
|
||||
<string name="no_euicc_priv">在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。</string>
|
||||
<string name="dsds">双卡</string>
|
||||
<string name="toast_dsds_switched">双卡支持状态已切换。请等待基带重新启动。</string>
|
||||
<string name="footer_mep">此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。</string>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">OpenEUICC</string>
|
||||
<string name="no_euicc">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string>
|
||||
<string name="no_euicc_priv">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string>
|
||||
<string name="telephony_manager">TelephonyManager (Privileged)</string>
|
||||
|
||||
<string name="dsds">Dual SIM</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue