Compare commits

...

11 commits

Author SHA1 Message Date
9d18253e44 ui: Cancel download from the low nvram warning dialog 2024-12-14 22:47:00 -05:00
6039679693 refactor: Remove the need for specifying preference keys when binding
Co-authored-by: septs <github@septs.pw>
2024-12-14 20:59:15 -05:00
5a8d92c3df feat: hide copied toast in android 13 or higher (#114)
see https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications

Co-authored-by: Peter Cai <peter@typeblog.net>
Reviewed-on: PeterCxy/OpenEUICC#114
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-12-15 02:44:44 +01:00
55c99831f3 ui: Copyable eid string
peter: redid parts of EuiccInfoViewHolder to remove the need for root
context (we can get it from activity directly).

Co-Authored-By: septs <github@septs.pw>

commit 2a6eb746bff034ed6032b8eac7462963bb39bcf1
Author: septs <github@septs.pw>
Date:   Sat Dec 14 11:26:42 2024 +0800

    feat: hide copied toast in android 13 or higher

commit d5bfd090b7b2e2c74c2cef698a43fc3607db260c
Merge: 0b2a498 aed2479
Author: septs <github@septs.pw>
Date:   Sat Dec 14 03:58:59 2024 +0800

    Merge branch 'master' into copyable-eid-string

commit 0b2a4988f2a2c2c7131a06867fc6bf622b281828
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:35:27 2024 +0800

    refactor: eid copied toast

commit ce854575aae304de68bddc0490998af5729632ab
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:27:18 2024 +0800

    revert: EuiccManagementFragment

commit 41229cac5727001ecb21992f5005b5b96b1d9557
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:27:10 2024 +0800

    revert: EuiccManagementFragment

commit 1f2b2d73ddfdc690442c0167e7f2f1748a78c921
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:25:01 2024 +0800

    feat: add euiccinfo activity specific strings

commit ca76ac841eab489ff1f825fef2f2aecd1ae88bb1
Merge: 110d490 55d96c6
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:23:34 2024 +0800

    Merge branch 'master' into copyable-eid-string

commit 110d49005a0cba26ee72eedfda0fc9e38a501d2a
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:23:21 2024 +0800

    revert: common strings

commit dc12e8d4107b9c80a1ca2bee0c5897f3e7d8dfa4
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:22:47 2024 +0800

    revert: unpriv strings

commit ccba9be141c533145464da3dbbc88afa16fac1fd
Merge: 34c5e41 0687354
Author: septs <github@septs.pw>
Date:   Fri Dec 13 12:15:42 2024 +0800

    Merge branch 'master' of ssh://gitea-ssh.angry.im:2222/PeterCxy/OpenEUICC into copyable-eid-string

commit 34c5e41dde663026b72b2c80301bfde2d14c0964
Author: septs <github@septs.pw>
Date:   Thu Dec 12 11:45:59 2024 +0800

    chore: simplify clickable

commit 4f1ed329f12b590691d273ac2150cb81ded11521
Author: septs <github@septs.pw>
Date:   Thu Dec 12 11:37:15 2024 +0800

    feat: copyable eid string
2024-12-14 20:38:24 -05:00
343dfb43f8 Remove unused SlotSelectFragment 2024-12-14 16:13:47 -05:00
815d4d4324 Expose USB device name as intrinsic name for use with download wizard 2024-12-14 16:11:49 -05:00
ec334d104a ui: Implement log content sharing 2024-12-14 15:18:47 -05:00
70f1e00eb4 refactor: Extract shared log-saving behavior
...so that we implement log-sharing once and apply it to both spots.
2024-12-14 14:55:27 -05:00
bc238c45cd ui: Add switching timeout message to the new DI text provider 2024-12-14 14:38:09 -05:00
14ea84c36e ui: Add placeholder text for when no eUICC is found to TextProvider 2024-12-14 14:21:05 -05:00
aefa79b18b refactor: Channel format should use DI instead of resource overriding
i18n makes resource overriding unreliable.
2024-12-14 13:47:23 -05:00
32 changed files with 264 additions and 184 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -15,4 +15,5 @@ interface AppContainer {
val preferenceRepository: PreferenceRepository
val uiComponentFactory: UiComponentFactory
val euiccChannelFactory: EuiccChannelFactory
val customizableTextProvider: CustomizableTextProvider
}

View file

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

View file

@ -38,4 +38,8 @@ open class DefaultAppContainer(context: Context) : AppContainer {
override val euiccChannelFactory by lazy {
DefaultEuiccChannelFactory(context)
}
override val customizableTextProvider by lazy {
DefaultCustomizableTextProvider(context)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) =
context.dataStore.edit { it[key] = value }
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
PreferenceFlowWrapper(context, key, defaultValue)
}
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 }
}
}

View file

@ -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>
/**
@ -70,3 +76,43 @@ 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())
}
}

View file

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

View file

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

View file

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

View file

@ -6,4 +6,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context)
override val uiComponentFactory by lazy {
UnprivilegedUiComponentFactory()
}
override val customizableTextProvider by lazy {
UnprivilegedCustomizableTextProvider(context)
}
}

View file

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

View file

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

View file

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

View file

@ -30,6 +30,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
return EuiccChannelImpl(
context.getString(R.string.telephony_manager),
port,
intrinsicChannelName = null,
TelephonyManagerApduInterface(
port,
tm,

View file

@ -23,4 +23,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
override val euiccChannelFactory by lazy {
PrivilegedEuiccChannelFactory(context)
}
override val customizableTextProvider by lazy {
PrivilegedCustomizableTextProvider(context)
}
}

View file

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

View file

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

View file

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

View file

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