Compare commits

..

No commits in common. "9d18253e44574a4cf518523a339e4cada2f3f57c" and "aed247904487ba9b2f01ac2030406a9bd0c1c37c" have entirely different histories.

32 changed files with 183 additions and 263 deletions

View file

@ -37,7 +37,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
return EuiccChannelImpl( return EuiccChannelImpl(
context.getString(R.string.omapi), context.getString(R.string.omapi),
port, port,
intrinsicChannelName = null,
OmapiApduInterface( OmapiApduInterface(
seService!!, seService!!,
port, port,
@ -68,7 +67,6 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
return EuiccChannelImpl( return EuiccChannelImpl(
context.getString(R.string.usb), context.getString(R.string.usb),
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
intrinsicChannelName = usbDevice.productName,
UsbApduInterface( UsbApduInterface(
conn, conn,
bulkIn, bulkIn,

View file

@ -16,12 +16,5 @@ interface EuiccChannel {
val valid: Boolean 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() fun close()
} }

View file

@ -10,7 +10,6 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class EuiccChannelImpl( class EuiccChannelImpl(
override val type: String, override val type: String,
override val port: UiccPortInfoCompat, override val port: UiccPortInfoCompat,
override val intrinsicChannelName: String?,
apduInterface: ApduInterface, apduInterface: ApduInterface,
verboseLoggingFlow: Flow<Boolean>, verboseLoggingFlow: Flow<Boolean>,
ignoreTLSCertificateFlow: Flow<Boolean> ignoreTLSCertificateFlow: Flow<Boolean>

View file

@ -31,8 +31,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
override val lpa: LocalProfileAssistant by lpaDelegate override val lpa: LocalProfileAssistant by lpaDelegate
override val valid: Boolean override val valid: Boolean
get() = channel.valid get() = channel.valid
override val intrinsicChannelName: String?
get() = channel.intrinsicChannelName
override fun close() = channel.close() override fun close() = channel.close()

View file

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

View file

@ -1,20 +0,0 @@
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,8 +38,4 @@ open class DefaultAppContainer(context: Context) : AppContainer {
override val euiccChannelFactory by lazy { override val euiccChannelFactory by lazy {
DefaultEuiccChannelFactory(context) DefaultEuiccChannelFactory(context)
} }
override val customizableTextProvider by lazy {
DefaultCustomizableTextProvider(context)
}
} }

View file

@ -1,15 +0,0 @@
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,18 +1,13 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -37,13 +32,6 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
private var logicalSlotId: Int = -1 private var logicalSlotId: Int = -1
data class Item(
@StringRes
val titleResId: Int,
val content: String?,
val copiedToastResId: Int? = null
)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -53,11 +41,12 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
swipeRefresh = requireViewById(R.id.swipe_refresh) swipeRefresh = requireViewById(R.id.swipe_refresh)
infoList = requireViewById<RecyclerView>(R.id.recycler_view).also { infoList = requireViewById(R.id.recycler_view)
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) infoList.layoutManager =
it.adapter = EuiccInfoAdapter() LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
} infoList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
infoList.adapter = EuiccInfoAdapter()
logicalSlotId = intent.getIntExtra("logicalSlotId", 0) logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
@ -92,33 +81,29 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
lifecycleScope.launch { lifecycleScope.launch {
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems) euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildPairs).map {
Pair(getString(it.first), it.second ?: getString(R.string.unknown))
}
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
} }
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList { private fun buildPairs(channel: EuiccChannel) = buildList {
add(Item(R.string.euicc_info_access_mode, channel.type)) add(Pair(R.string.euicc_info_access_mode, channel.type))
add( add(
Item( Pair(
R.string.euicc_info_removable, R.string.euicc_info_removable,
formatByBoolean(channel.port.card.isRemovable, YES_NO) formatByBoolean(channel.port.card.isRemovable, YES_NO)
) )
) )
add( add(Pair(R.string.euicc_info_eid, channel.lpa.eID))
Item(
R.string.euicc_info_eid,
channel.lpa.eID,
copiedToastResId = R.string.toast_eid_copied
)
)
channel.lpa.euiccInfo2.let { info -> channel.lpa.euiccInfo2.let { info ->
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion)) add(Pair(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion)) add(Pair(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
add(Item(R.string.euicc_info_pp_version, info?.ppVersion)) add(Pair(R.string.euicc_info_pp_version, info?.ppVersion))
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) add(Pair(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
} }
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers -> channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24) // SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
@ -131,7 +116,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
else -> R.string.euicc_info_ci_unknown else -> R.string.euicc_info_ci_unknown
} }
add(Item(R.string.euicc_info_ci_type, getString(resId))) add(Pair(R.string.euicc_info_ci_type, getString(resId)))
} }
} }
@ -147,34 +132,15 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) { inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val title: TextView = root.requireViewById(R.id.euicc_info_title) private val title: TextView = root.requireViewById(R.id.euicc_info_title)
private val content: TextView = root.requireViewById(R.id.euicc_info_content) private val content: TextView = root.requireViewById(R.id.euicc_info_content)
private var copiedToastResId: Int? = null
init { fun bind(item: Pair<String, String>) {
root.setOnClickListener { title.text = item.first
if (copiedToastResId != null) { content.text = item.second
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>() { inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
var euiccInfoItems: List<Item> = listOf() var euiccInfoItems: List<Pair<String, String>> = listOf()
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
set(newVal) { set(newVal) {
field = newVal field = newVal

View file

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
import android.view.LayoutInflater import android.view.LayoutInflater
@ -262,7 +261,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
invalid = true invalid = true
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
AlertDialog.Builder(requireContext()).apply { AlertDialog.Builder(requireContext()).apply {
setMessage(appContainer.customizableTextProvider.profileSwitchingTimeoutMessage) setMessage(R.string.enable_disable_timeout)
setPositiveButton(android.R.string.ok) { dialog, _ -> setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
requireActivity().finish() requireActivity().finish()
@ -349,8 +348,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
iccid.setOnLongClickListener { iccid.setOnLongClickListener {
requireContext().getSystemService(ClipboardManager::class.java)!! requireContext().getSystemService(ClipboardManager::class.java)!!
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text)) .setPrimaryClip(ClipData.newPlainText("iccid", iccid.text))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
.show() .show()
true true
} }

View file

@ -8,6 +8,7 @@ import android.view.View
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -16,6 +17,7 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.FileOutputStream
import java.util.Date import java.util.Date
class LogsActivity : AppCompatActivity() { class LogsActivity : AppCompatActivity() {
@ -25,15 +27,15 @@ class LogsActivity : AppCompatActivity() {
private lateinit var logStr: String private lateinit var logStr: String
private val saveLogs = private val saveLogs =
setupLogSaving( registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
getLogFileName = { if (uri == null) return@registerForActivityResult
getString( if (!this::logStr.isInitialized) return@registerForActivityResult
R.string.logs_filename_template, contentResolver.openFileDescriptor(uri, "w")?.use {
SimpleDateFormat.getDateTimeInstance().format(Date()) FileOutputStream(it.fileDescriptor).use { os ->
) os.write(logStr.encodeToByteArray())
}, }
getLogText = { logStr } }
) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@ -74,7 +76,9 @@ class LogsActivity : AppCompatActivity() {
true true
} }
R.id.save -> { R.id.save -> {
saveLogs() saveLogs.launch(getString(R.string.logs_filename_template,
SimpleDateFormat.getDateTimeInstance().format(Date())
))
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)

View file

@ -163,8 +163,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
// but it could change in the future // but it could change in the future
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
val channelName = val channelName = getString(R.string.channel_name_format, channel.logicalSlotId)
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
newPages.add(Page(channel.logicalSlotId, channelName) { newPages.add(Page(channel.logicalSlotId, channelName) {
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId) appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
}) })

View file

@ -4,20 +4,15 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker { class NoEuiccPlaceholderFragment : Fragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val view = inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false) return 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,6 +6,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.datastore.preferences.core.Preferences
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -13,6 +14,7 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -58,25 +60,25 @@ open class SettingsFragment: PreferenceFragmentCompat() {
} }
findPreference<CheckBoxPreference>("pref_notifications_download") findPreference<CheckBoxPreference>("pref_notifications_download")
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow) ?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
findPreference<CheckBoxPreference>("pref_notifications_delete") findPreference<CheckBoxPreference>("pref_notifications_delete")
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow) ?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
findPreference<CheckBoxPreference>("pref_notifications_switch") findPreference<CheckBoxPreference>("pref_notifications_switch")
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow) ?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH)
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim") findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow) ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM)
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging") findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow) ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
findPreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list") findPreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) ?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow, PreferenceKeys.UNFILTERED_PROFILE_LIST)
findPreference<CheckBoxPreference>("pref_ignore_tls_certificate") findPreference<CheckBoxPreference>("pref_ignore_tls_certificate")
?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow) ?.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow, PreferenceKeys.IGNORE_TLS_CERTIFICATE)
} }
override fun onStart() { override fun onStart() {
@ -97,7 +99,10 @@ open class SettingsFragment: PreferenceFragmentCompat() {
if (numClicks == 7) { if (numClicks == 7) {
lifecycleScope.launch { lifecycleScope.launch {
preferenceRepository.developerOptionsEnabledFlow.updatePreference(true) preferenceRepository.updatePreference(
PreferenceKeys.DEVELOPER_OPTIONS_ENABLED,
true
)
lastToast?.cancel() lastToast?.cancel()
Toast.makeText( Toast.makeText(
@ -119,14 +124,14 @@ open class SettingsFragment: PreferenceFragmentCompat() {
return true return true
} }
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) { private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {
lifecycleScope.launch { lifecycleScope.launch {
flow.collect { isChecked = it } flow.collect { isChecked = it }
} }
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
runBlocking { runBlocking {
flow.updatePreference(newValue as Boolean) preferenceRepository.updatePreference(key, newValue as Boolean)
} }
true true
} }

View file

@ -0,0 +1,93 @@
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,8 +6,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import java.io.FileOutputStream
import java.util.Date import java.util.Date
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
@ -19,15 +21,14 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
private lateinit var diagnosticTextView: TextView private lateinit var diagnosticTextView: TextView
private val saveDiagnostics = private val saveDiagnostics =
setupLogSaving( registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
getLogFileName = { if (uri == null) return@registerForActivityResult
getString( requireActivity().contentResolver.openFileDescriptor(uri, "w")?.use {
R.string.download_wizard_diagnostics_file_template, FileOutputStream(it.fileDescriptor).use { os ->
SimpleDateFormat.getDateTimeInstance().format(Date()) os.write(diagnosticTextView.text.toString().encodeToByteArray())
) }
}, }
getLogText = { diagnosticTextView.text.toString() } }
)
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
@ -40,7 +41,12 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
): View? { ): View? {
val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false) val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false)
view.requireViewById<View>(R.id.download_wizard_diagnostics_save).setOnClickListener { view.requireViewById<View>(R.id.download_wizard_diagnostics_save).setOnClickListener {
saveDiagnostics() saveDiagnostics.launch(
getString(
R.string.download_wizard_diagnostics_file_template,
SimpleDateFormat.getDateTimeInstance().format(Date())
)
)
} }
diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text) diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text)
return view return view

View file

@ -35,8 +35,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
val eID: String, val eID: String,
val freeSpace: Int, val freeSpace: Int,
val imei: String, val imei: String,
val enabledProfileName: String?, val enabledProfileName: String?
val intrinsicChannelName: String?,
) )
private var loaded = false private var loaded = false
@ -62,9 +61,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
setMessage(R.string.profile_download_low_nvram_message) setMessage(R.string.profile_download_low_nvram_message)
setCancelable(true) setCancelable(true)
setPositiveButton(android.R.string.ok, null) setPositiveButton(android.R.string.ok, null)
setNegativeButton(android.R.string.cancel) { _, _ -> setNegativeButton(android.R.string.cancel, null)
requireActivity().finish()
}
show() show()
} }
} }
@ -109,8 +106,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
} catch (e: Exception) { } 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 } }.toList().sortedBy { it.logicalSlotId }
@ -181,9 +177,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
} }
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
item.intrinsicChannelName ?: root.context.getString(R.string.usb) root.context.getString(R.string.usb)
} else { } else {
appContainer.customizableTextProvider.formatInternalChannelName(item.logicalSlotId) root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId)
} }
eID.text = item.eID eID.text = item.eID
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown) activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)

View file

@ -19,7 +19,7 @@ val Context.preferenceRepository: PreferenceRepository
val Fragment.preferenceRepository: PreferenceRepository val Fragment.preferenceRepository: PreferenceRepository
get() = requireContext().preferenceRepository get() = requireContext().preferenceRepository
internal object PreferenceKeys { object PreferenceKeys {
// ---- Profile Notifications ---- // ---- Profile Notifications ----
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download") val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete") val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
@ -51,22 +51,9 @@ class PreferenceRepository(private val context: Context) {
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> = private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): Flow<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 } context.dataStore.data.map { it[key] ?: defaultValue }
)
suspend fun updatePreference(value: T) { suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) =
context.dataStore.edit { it[key] = value } context.dataStore.edit { it[key] = value }
}
} }

View file

@ -1,23 +1,17 @@
package im.angry.openeuicc.util package im.angry.openeuicc.util
import android.content.Context
import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import android.view.ViewGroup 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.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import java.io.FileOutputStream
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height> // Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
/** /**
@ -76,43 +70,3 @@ fun setupRootViewInsets(view: ViewGroup) {
WindowInsetsCompat.CONSUMED 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,6 +43,7 @@
<string name="download_wizard_back">戻る</string> <string name="download_wizard_back">戻る</string>
<string name="download_wizard_next">次へ</string> <string name="download_wizard_next">次へ</string>
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</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">タイプ:</string>
<string name="download_wizard_slot_type_removable">リムーバブル</string> <string name="download_wizard_slot_type_removable">リムーバブル</string>
<string name="download_wizard_slot_type_internal">内部</string> <string name="download_wizard_slot_type_internal">内部</string>

View file

@ -81,6 +81,7 @@
<string name="download_wizard_back">返回</string> <string name="download_wizard_back">返回</string>
<string name="download_wizard_next">下一步</string> <string name="download_wizard_next">下一步</string>
<string name="download_wizard_slot_select">请选择或确认下载目标 eSIM 卡槽:</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">类型:</string>
<string name="download_wizard_slot_type_removable">可插拔</string> <string name="download_wizard_slot_type_removable">可插拔</string>
<string name="download_wizard_slot_type_internal">内置</string> <string name="download_wizard_slot_type_internal">内置</string>

View file

@ -31,7 +31,6 @@
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string> <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_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_iccid_copied">ICCID copied to clipboard</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 Slot</string>
<string name="slot_select_select">Select</string> <string name="slot_select_select">Select</string>
@ -65,6 +64,7 @@
<string name="download_wizard_back">Back</string> <string name="download_wizard_back">Back</string>
<string name="download_wizard_next">Next</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_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">Type:</string>
<string name="download_wizard_slot_type_removable">Removable</string> <string name="download_wizard_slot_type_removable">Removable</string>
<string name="download_wizard_slot_type_internal">Internal</string> <string name="download_wizard_slot_type_internal">Internal</string>
@ -95,8 +95,6 @@
<string name="download_wizard_diagnostics_save">Save</string> <string name="download_wizard_diagnostics_save">Save</string>
<string name="download_wizard_diagnostics_file_template">Diagnostics at %s</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_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> <string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>

View file

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

View file

@ -1,10 +0,0 @@
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,7 +3,6 @@ package im.angry.openeuicc.ui
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.preference.Preference import androidx.preference.Preference
@ -36,8 +35,7 @@ class UnprivilegedSettingsFragment : SettingsFragment() {
setOnPreferenceClickListener { setOnPreferenceClickListener {
requireContext().getSystemService(ClipboardManager::class.java)!! requireContext().getSystemService(ClipboardManager::class.java)!!
.setPrimaryClip(ClipData.newPlainText("ara-m", summary)) .setPrimaryClip(ClipData.newPlainText("ara-m", summary))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast Toast.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
.show() .show()
true true
} }

View file

@ -1,6 +1,6 @@
<resources> <resources>
<string name="app_name" translatable="false">EasyEUICC</string> <string name="app_name" translatable="false">EasyEUICC</string>
<string name="channel_name_format_unpriv" translatable="false">SIM %d</string> <string name="channel_name_format" translatable="false">SIM %d</string>
<string name="compatibility_check">Compatibility Check</string> <string name="compatibility_check">Compatibility Check</string>
<string name="open_sim_toolkit">Open SIM Toolkit</string> <string name="open_sim_toolkit">Open SIM Toolkit</string>

View file

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

View file

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

View file

@ -1,10 +0,0 @@
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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="no_euicc_priv">このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。</string> <string name="no_euicc">このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。</string>
<string name="telephony_manager">TelephonyManager (特権)</string> <string name="telephony_manager">TelephonyManager (特権)</string>
<string name="dsds">デュアル SIM</string> <string name="dsds">デュアル SIM</string>
<string name="toast_dsds_switched">DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。</string> <string name="toast_dsds_switched">DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="no_euicc_priv">在此设备上找不到 eUICC 芯片。\n在某些设备上您可能需要先在此应用的菜单中启用双卡支持。</string> <string name="no_euicc">在此设备上找不到 eUICC 芯片。\n在某些设备上您可能需要先在此应用的菜单中启用双卡支持。</string>
<string name="dsds">双卡</string> <string name="dsds">双卡</string>
<string name="toast_dsds_switched">双卡支持状态已切换。请等待基带重新启动。</string> <string name="toast_dsds_switched">双卡支持状态已切换。请等待基带重新启动。</string>
<string name="footer_mep">此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。</string> <string name="footer_mep">此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。</string>

View file

@ -1,6 +1,6 @@
<resources> <resources>
<string name="app_name" translatable="false">OpenEUICC</string> <string name="app_name" translatable="false">OpenEUICC</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="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="telephony_manager">TelephonyManager (Privileged)</string> <string name="telephony_manager">TelephonyManager (Privileged)</string>
<string name="dsds">Dual SIM</string> <string name="dsds">Dual SIM</string>