diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index a8fa1d5..5e87564 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -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, diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 4ef6808..541f867 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -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() } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt index 79dec34..a281948 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -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, ignoreTLSCertificateFlow: Flow diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index ab01f22..6011f53 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -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() diff --git a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt index 4b3c3cd..cae7e2e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/AppContainer.kt @@ -15,4 +15,5 @@ interface AppContainer { val preferenceRepository: PreferenceRepository val uiComponentFactory: UiComponentFactory val euiccChannelFactory: EuiccChannelFactory + val customizableTextProvider: CustomizableTextProvider } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt new file mode 100644 index 0000000..2c86273 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/di/CustomizableTextProvider.kt @@ -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 +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt index 93fd8b8..9b70099 100644 --- a/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultAppContainer.kt @@ -38,4 +38,8 @@ open class DefaultAppContainer(context: Context) : AppContainer { override val euiccChannelFactory by lazy { DefaultEuiccChannelFactory(context) } + + override val customizableTextProvider by lazy { + DefaultCustomizableTextProvider(context) + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt new file mode 100644 index 0000000..b493611 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/di/DefaultCustomizableTextProvider.kt @@ -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) +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt index 84b300e..36031dd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt @@ -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(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) { - 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() { - var euiccInfoItems: List> = listOf() + var euiccInfoItems: List = listOf() @SuppressLint("NotifyDataSetChanged") set(newVal) { field = newVal diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 8e7b158..f806ae0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -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 } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt index 49bfa0f..c6ba256 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt @@ -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) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 767e7e0..01d0ab2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -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) }) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt index e9e44b1..7e96af3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NoEuiccPlaceholderFragment.kt @@ -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(R.id.no_euicc_placeholder) + textView.text = appContainer.customizableTextProvider.noEuiccExplanation + return view } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index c2cbee3..cb801af 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -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("pref_notifications_download") - ?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD) + ?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow) findPreference("pref_notifications_delete") - ?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE) + ?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow) findPreference("pref_notifications_switch") - ?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH) + ?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow) findPreference("pref_advanced_disable_safeguard_removable_esim") - ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM) + ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow) findPreference("pref_advanced_verbose_logging") - ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING) + ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow) findPreference("pref_developer_unfiltered_profile_list") - ?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow, PreferenceKeys.UNFILTERED_PROFILE_LIST) + ?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow) findPreference("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, key: Preferences.Key) { + private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper) { lifecycleScope.launch { flow.collect { isChecked = it } } setOnPreferenceChangeListener { _, newValue -> runBlocking { - preferenceRepository.updatePreference(key, newValue as Boolean) + flow.updatePreference(newValue as Boolean) } true } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt deleted file mode 100644 index 2c4fe3c..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SlotSelectFragment.kt +++ /dev/null @@ -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, logicalSlotIds: List, portIds: List): 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 - 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(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() - } -} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index fbbd1a0..e282196 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -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(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 diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 54dbc08..f16a086 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -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) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index e768fa9..f5e3ca2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -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 bindFlow(key: Preferences.Key, defaultValue: T): Flow = - context.dataStore.data.map { it[key] ?: defaultValue } + private fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper = + PreferenceFlowWrapper(context, key, defaultValue) +} - suspend fun updatePreference(key: Preferences.Key, value: T) = +class PreferenceFlowWrapper private constructor( + private val context: Context, + private val key: Preferences.Key, + inner: Flow +) : Flow by inner { + internal constructor(context: Context, key: Preferences.Key, defaultValue: T) : this( + context, + key, + context.dataStore.data.map { it[key] ?: defaultValue } + ) + + suspend fun updatePreference(value: T) { context.dataStore.edit { it[key] = value } + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index fbede87..c8e481c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -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: /** @@ -69,4 +75,44 @@ fun setupRootViewInsets(view: ViewGroup) { WindowInsetsCompat.CONSUMED } +} + +fun 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()) + } } \ No newline at end of file diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index d25af49..7d99cb7 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -43,7 +43,6 @@ 戻る 次へ ダウンロードする eSIM を選択または確認: - 論理スロット %d タイプ: リムーバブル 内部 diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml index 3bb0d04..551435b 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -81,7 +81,6 @@ 返回 下一步 请选择或确认下载目标 eSIM 卡槽: - 逻辑卡槽 %d 类型: 可插拔 内置 diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 61823f7..e349c68 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Nickname cannot be longer than 64 characters Confirmation string mismatch ICCID copied to clipboard + EID copied to clipboard Select Slot Select @@ -64,7 +65,6 @@ Back Next Select or confirm the eSIM you would like to download to: - Logical slot %d Type: Removable Internal @@ -95,6 +95,8 @@ Save Diagnostics at %s + Logs have been saved to the selected path. Would you like to share the log through another app? + New nickname Are you sure you want to delete the profile %s? This operation is irreversible. diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt index 22d5a62..4dbfe41 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedAppContainer.kt @@ -6,4 +6,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context) override val uiComponentFactory by lazy { UnprivilegedUiComponentFactory() } + + override val customizableTextProvider by lazy { + UnprivilegedCustomizableTextProvider(context) + } } \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt new file mode 100644 index 0000000..929ce84 --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/di/UnprivilegedCustomizableTextProvider.kt @@ -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) +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt index 1ca9d0f..1ef3e89 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedSettingsFragment.kt @@ -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 } diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 9d80b0e..afed295 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ EasyEUICC - SIM %d + SIM %d Compatibility Check Open SIM Toolkit diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index 1537fc9..b690c79 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -30,6 +30,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto return EuiccChannelImpl( context.getString(R.string.telephony_manager), port, + intrinsicChannelName = null, TelephonyManagerApduInterface( port, tm, diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt index c5896f2..d821e68 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt @@ -23,4 +23,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { override val euiccChannelFactory by lazy { PrivilegedEuiccChannelFactory(context) } + + override val customizableTextProvider by lazy { + PrivilegedCustomizableTextProvider(context) + } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt b/app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt new file mode 100644 index 0000000..e53832f --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedCustomizableTextProvider.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5ff6740..29f3ef6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,6 +1,6 @@ - このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。 + このデバイスで eUICC が見つかりません。\nデバイスによってはアプリのメニューからデュアル SIM を有効化する必要があります。 TelephonyManager (特権) デュアル SIM DSDS の状態が切り替わりました。モデムが再起動するまでお待ちください。 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9b34bb8..18497b2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,6 +1,6 @@ - 在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。 + 在此设备上找不到 eUICC 芯片。\n在某些设备上,您可能需要先在此应用的菜单中启用双卡支持。 双卡 双卡支持状态已切换。请等待基带重新启动。 此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb2233b..47c88bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ OpenEUICC - No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app. + No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app. TelephonyManager (Privileged) Dual SIM