diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index f53e6ff..6edaaf1 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -30,7 +30,20 @@ + android:label="@string/download_wizard"> + + + + + + + + + + + + + + channel.lpa.euiccMemoryReset() + } + } } \ No newline at end of file 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 842f4ec..12995ff 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 @@ -38,8 +38,10 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, @@ -55,6 +57,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, private lateinit var fab: FloatingActionButton private lateinit var profileList: RecyclerView private var logicalSlotId: Int = -1 + private lateinit var eid: String private val adapter = EuiccProfileAdapter() @@ -131,31 +134,42 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, inflater.inflate(R.menu.fragment_euicc, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean = - when (item.itemId) { - R.id.show_notifications -> { - if (logicalSlotId != -1) { - Intent(requireContext(), NotificationsActivity::class.java).apply { - putExtra("logicalSlotId", logicalSlotId) - startActivity(this) - } - } - true - } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + menu.findItem(R.id.show_notifications).isVisible = + logicalSlotId != -1 + menu.findItem(R.id.euicc_info).isVisible = + logicalSlotId != -1 + menu.findItem(R.id.euicc_memory_reset).isVisible = + runBlocking { preferenceRepository.euiccMemoryResetFlow.first() } + } - R.id.euicc_info -> { - if (logicalSlotId != -1) { - Intent(requireContext(), EuiccInfoActivity::class.java).apply { - putExtra("logicalSlotId", logicalSlotId) - startActivity(this) - } - } - true + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.show_notifications -> { + Intent(requireContext(), NotificationsActivity::class.java).apply { + putExtra("logicalSlotId", logicalSlotId) + startActivity(this) } - - else -> super.onOptionsItemSelected(item) + true } + R.id.euicc_info -> { + Intent(requireContext(), EuiccInfoActivity::class.java).apply { + putExtra("logicalSlotId", logicalSlotId) + startActivity(this) + } + true + } + + R.id.euicc_memory_reset -> { + EuiccMemoryResetFragment.newInstance(slotId, portId, eid) + .show(childFragmentManager, EuiccMemoryResetFragment.TAG) + true + } + + else -> super.onOptionsItemSelected(item) + } + protected open suspend fun onCreateFooterViews( parent: ViewGroup, profiles: List @@ -192,6 +206,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, val profiles = withEuiccChannel { channel -> logicalSlotId = channel.logicalSlotId + eid = channel.lpa.eID euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) if (unfilteredProfileListFlow.value) channel.lpa.profiles diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt new file mode 100644 index 0000000..086a849 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccMemoryResetFragment.kt @@ -0,0 +1,126 @@ +package im.angry.openeuicc.ui + +import android.graphics.Typeface +import android.os.Bundle +import android.text.Editable +import android.util.Log +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import im.angry.openeuicc.common.R +import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone +import im.angry.openeuicc.util.EuiccChannelFragmentMarker +import im.angry.openeuicc.util.EuiccProfilesChangedListener +import im.angry.openeuicc.util.ensureEuiccChannelManager +import im.angry.openeuicc.util.euiccChannelManagerService +import im.angry.openeuicc.util.newInstanceEuicc +import im.angry.openeuicc.util.notifyEuiccProfilesChanged +import im.angry.openeuicc.util.portId +import im.angry.openeuicc.util.slotId +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker { + companion object { + const val TAG = "EuiccMemoryResetFragment" + + private const val FIELD_EID = "eid" + + fun newInstance(slotId: Int, portId: Int, eid: String) = + newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) { + putString(FIELD_EID, eid) + } + } + + private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! } + + private val confirmText: String by lazy { + getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8)) + } + + private inline val isMatched: Boolean + get() = editText.text.toString() == confirmText + + private var confirmed = false + + private var toast: Toast? = null + set(value) { + toast?.cancel() + field = value + value?.show() + } + + private val editText by lazy { + EditText(requireContext()).apply { + isLongClickable = false + typeface = Typeface.MONOSPACE + hint = Editable.Factory.getInstance() + .newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText)) + } + } + + private inline val alertDialog: AlertDialog + get() = requireDialog() as AlertDialog + + override fun onCreateDialog(savedInstanceState: Bundle?) = + AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme) + .setTitle(R.string.euicc_memory_reset_title) + .setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText)) + .setView(editText) + // Set listener to null to prevent auto closing + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.euicc_memory_reset_invoke_button, null) + .create() + + override fun onResume() { + super.onResume() + alertDialog.setCanceledOnTouchOutside(false) + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setOnClickListener { if (!confirmed) confirmation() } + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) + .setOnClickListener { if (!confirmed) dismiss() } + } + + private fun confirmation() { + toast?.cancel() + if (!isMatched) { + Log.d(TAG, buildString { + appendLine("User input is mismatch:") + appendLine(editText.text) + appendLine(confirmText) + }) + val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched + toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG) + return + } + confirmed = true + preventUserAction() + + requireParentFragment().lifecycleScope.launch { + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() + + euiccChannelManagerService.launchMemoryReset(slotId, portId) + .onStart { + parentFragment?.notifyEuiccProfilesChanged() + + val resId = R.string.toast_euicc_memory_reset_finitshed + toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG) + + runCatching(::dismiss) + } + .waitDone() + } + } + + private fun preventUserAction() { + editText.isEnabled = false + alertDialog.setCancelable(false) + alertDialog.setCanceledOnTouchOutside(false) + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false + } +} 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 c54d6a1..d137e90 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 @@ -77,6 +77,12 @@ open class SettingsFragment: PreferenceFragmentCompat() { requirePreference("pref_developer_ignore_tls_certificate") .bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow) + + requirePreference("pref_developer_refresh_after_switch") + .bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow) + + requirePreference("pref_developer_euicc_memory_reset") + .bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow) } protected fun requirePreference(key: CharSequence) = diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 8cab18b..a9f868f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -21,7 +21,6 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import net.typeblog.lpac_jni.LocalProfileAssistant class DownloadWizardActivity: BaseEuiccAccessActivity() { @@ -35,6 +34,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { var downloadStarted: Boolean, var downloadTaskID: Long, var downloadError: LocalProfileAssistant.ProfileDownloadException?, + var skipMethodSelect: Boolean, + var confirmationCodeRequired: Boolean, ) private lateinit var state: DownloadWizardState @@ -63,17 +64,21 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { }) state = DownloadWizardState( - null, - intent.getIntExtra("selectedLogicalSlot", 0), - "", - null, - null, - null, - false, - -1, - null + currentStepFragmentClassName = null, + selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0), + smdp = "", + matchingId = null, + confirmationCode = null, + imei = null, + downloadStarted = false, + downloadTaskID = -1, + downloadError = null, + skipMethodSelect = false, + confirmationCodeRequired = false, ) + handleDeepLink() + progressBar = requireViewById(R.id.progress) nextButton = requireViewById(R.id.download_wizard_next) prevButton = requireViewById(R.id.download_wizard_back) @@ -113,10 +118,24 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { } } + private fun handleDeepLink() { + // If we get an LPA string from deep-link intents, extract from there. + // Note that `onRestoreInstanceState` could override this with user input, + // but that _is_ the desired behavior. + val uri = intent.data + if (uri?.scheme == "lpa") { + val parsed = LPAString.parse(uri.schemeSpecificPart) + state.smdp = parsed.address + state.matchingId = parsed.matchingId + state.confirmationCodeRequired = parsed.confirmationCodeRequired + state.skipMethodSelect = true + } + } + override fun onProvideAssistContent(outContent: AssistContent?) { super.onProvideAssistContent(outContent) outContent?.webUri = try { - val activationCode = ActivationCode( + val activationCode = LPAString( state.smdp, state.matchingId, null, @@ -138,6 +157,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { outState.putString("imei", state.imei) outState.putBoolean("downloadStarted", state.downloadStarted) outState.putLong("downloadTaskID", state.downloadTaskID) + outState.putBoolean("confirmationCodeRequired", state.confirmationCodeRequired) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -154,6 +174,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { state.downloadStarted = savedInstanceState.getBoolean("downloadStarted", state.downloadStarted) state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID) + state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode) + state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired) } private fun onPrevPressed() { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index 5fa8002..402e7a5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -5,6 +5,7 @@ import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.widget.addTextChangedListener import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R @@ -36,7 +37,11 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF DownloadWizardProgressFragment() override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = - DownloadWizardMethodSelectFragment() + if (state.skipMethodSelect) { + DownloadWizardSlotSelectFragment() + } else { + DownloadWizardMethodSelectFragment() + } override fun onCreateView( inflater: LayoutInflater, @@ -51,6 +56,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF smdp.editText!!.addTextChangedListener { updateInputCompleteness() } + confirmationCode.editText!!.addTextChangedListener { + updateInputCompleteness() + } return view } @@ -61,6 +69,15 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF confirmationCode.editText!!.setText(state.confirmationCode) imei.editText!!.setText(state.imei) updateInputCompleteness() + + if (state.confirmationCodeRequired) { + confirmationCode.editText!!.requestFocus() + confirmationCode.editText!!.hint = + getString(R.string.profile_download_confirmation_code_required) + } else { + confirmationCode.editText!!.hint = + getString(R.string.profile_download_confirmation_code) + } } override fun onPause() { @@ -70,6 +87,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF private fun updateInputCompleteness() { inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches() + if (state.confirmationCodeRequired) { + inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty() + } refreshButtons() } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index 2846fd7..4b02b7a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -42,21 +42,16 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard registerForActivityResult(ActivityResultContracts.GetContent()) { result -> if (result == null) return@registerForActivityResult - lifecycleScope.launch(Dispatchers.IO) { - runCatching { - requireContext().contentResolver.openInputStream(result)?.let { input -> - val bmp = BitmapFactory.decodeStream(input) - input.close() - - decodeQrFromBitmap(bmp)?.let { - withContext(Dispatchers.Main) { - processLpaString(it) - } + lifecycleScope.launch { + val decoded = withContext(Dispatchers.IO) { + runCatching { + requireContext().contentResolver.openInputStream(result)?.use { input -> + BitmapFactory.decodeStream(input).use(::decodeQrFromBitmap) } - - bmp.recycle() } } + + decoded.getOrNull()?.let { processLpaString(it) } } } @@ -126,18 +121,10 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard private fun processLpaString(input: String) { try { - val parsed = ActivationCode.fromString(input) + val parsed = LPAString.parse(input) state.smdp = parsed.address state.matchingId = parsed.matchingId - if (parsed.confirmationCodeRequired) { - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.profile_download_required_confirmation_code) - setMessage(R.string.profile_download_required_confirmation_code_message) - setCancelable(true) - setPositiveButton(android.R.string.ok, null) - show() - } - } + state.confirmationCodeRequired = parsed.confirmationCodeRequired gotoNextFragment(DownloadWizardDetailsFragment()) } catch (e: IllegalArgumentException) { AlertDialog.Builder(requireContext()).apply { @@ -150,14 +137,19 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard } } - private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) { + private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) { private val icon = root.requireViewById(R.id.download_method_icon) private val title = root.requireViewById(R.id.download_method_title) fun bind(item: DownloadMethod) { icon.setImageResource(item.iconRes) title.setText(item.titleRes) - root.setOnClickListener { item.onClick() } + root.setOnClickListener { + // If the user elected to use another download method, reset the confirmation code flag + // too + state.confirmationCodeRequired = false + item.onClick() + } } } 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 5510fb0..28bc9f0 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 @@ -49,7 +49,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt get() = true override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment = - DownloadWizardMethodSelectFragment() + if (state.skipMethodSelect) { + DownloadWizardDetailsFragment() + } else { + DownloadWizardMethodSelectFragment() + } override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null diff --git a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt similarity index 79% rename from app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt rename to app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt index c21e837..20956fb 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/ActivationCode.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/LPAString.kt @@ -1,18 +1,18 @@ package im.angry.openeuicc.util -data class ActivationCode( +data class LPAString( val address: String, - val matchingId: String? = null, - val oid: String? = null, - val confirmationCodeRequired: Boolean = false, + val matchingId: String?, + val oid: String?, + val confirmationCodeRequired: Boolean, ) { companion object { - fun fromString(input: String): ActivationCode { + fun parse(input: String): LPAString { val components = input.removePrefix("LPA:").split('$') if (components.size < 2 || components[0] != "1") { throw IllegalArgumentException("Invalid activation code format") } - return ActivationCode( + return LPAString( address = components[1].trim(), matchingId = components.getOrNull(2)?.trim()?.ifBlank { null }, oid = components.getOrNull(3)?.trim()?.ifBlank { null }, 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 b38eb5d..6ea342c 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 @@ -34,9 +34,10 @@ internal object PreferenceKeys { val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled") val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list") val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") + val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset") } -class PreferenceRepository(private val context: Context) { +open class PreferenceRepository(private val context: Context) { // Expose flows so that we can also handle default values // ---- Profile Notifications ---- val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true) @@ -52,8 +53,9 @@ class PreferenceRepository(private val context: Context) { val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) + val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false) - private fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper = + protected fun bindFlow(key: Preferences.Key, defaultValue: T): PreferenceFlowWrapper = PreferenceFlowWrapper(context, key, defaultValue) } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt index 444c176..5a559f9 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/Utils.kt @@ -86,6 +86,13 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c } } +inline fun Bitmap.use(f: (Bitmap) -> T): T = + try { + f(this) + } finally { + recycle() + } + fun decodeQrFromBitmap(bmp: Bitmap): String? = runCatching { val pixels = IntArray(bmp.width * bmp.height) diff --git a/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml new file mode 100644 index 0000000..f1ca8c1 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_euicc_memory_reset.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app-common/src/main/res/menu/fragment_euicc.xml b/app-common/src/main/res/menu/fragment_euicc.xml index b54eaf1..6e2dfbe 100644 --- a/app-common/src/main/res/menu/fragment_euicc.xml +++ b/app-common/src/main/res/menu/fragment_euicc.xml @@ -10,4 +10,9 @@ android:id="@+id/euicc_info" android:title="@string/euicc_info" app:showAsAction="never" /> + + \ 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 e4969c1..1fd361c 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -42,12 +42,11 @@ サーバー (RSP / SM-DP+) アクティベーションコード 確認コード (オプション) + 確認コード (必須) IMEI (オプション) - ダウンロードに失敗する可能性があります + 残り容量が少ない 残り容量が少ないため、ダウンロードに失敗する可能性があります。 クリップボードに LPA コードがありません - 確認コードが必要です - クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。 解析できません QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。 ダウンロードウィザード @@ -151,4 +150,16 @@ 情報 アプリバージョン ソースコード + 確認文字列が一致しません + このチップは消去されました + eSIM チップを消去しています + eSIM チップの消去は失敗しました + eSIM を消去する + eSIM を消去する + このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %s\n\n%s + 確認のため、ここに「%s」を入力してください + EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。 + 消去する + eUICC の消去を可能にする + この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。 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 2cadc03..8c36091 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -37,8 +37,14 @@ 服务器 (RSP / SM-DP+) 激活码 确认码 (可选) + 已复制序列号到剪贴板 + 产品名称 + 产品序列号 + 产品 Bootloader 版本 + 产品固件版本 + 确认码 (必需) IMEI (可选) - 本次下载可能会失败 + 剩余空间不足 当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载? 日志已保存到指定路径。需要通过其他 App 分享吗? 新昵称 @@ -144,6 +150,16 @@ 无视 SM-DP+ 的 TLS 证书 允许 RSP 服务器使用任意证书 无信息 - 需要确认码 - 您扫描的二维码或粘贴的 LPA 码需要一个额外的确认码 + 输入的确认文本不匹配 + 此芯片已被擦除 + 正在擦除 eSIM 芯片 + eSIM 芯片擦除失败 + 擦除 eSIM 芯片 + 擦除 eSIM 芯片 + 请确认删除此芯片上的所有配置文件,并了解此操作不可逆。\n\nEID: %s\n\n%s + 请在此处输入「%s」以确认 + 我确认擦除 EID 以 %s 结尾的芯片,并了解此操作不可逆 + 擦除 + 允许擦除 eUICC + 此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。 \ No newline at end of file diff --git a/app-common/src/main/res/values-zh-rTW/strings.xml b/app-common/src/main/res/values-zh-rTW/strings.xml index 28691a0..3d8270d 100644 --- a/app-common/src/main/res/values-zh-rTW/strings.xml +++ b/app-common/src/main/res/values-zh-rTW/strings.xml @@ -37,8 +37,14 @@ 伺服器 (RSP / SM-DP+) 啟用碼 確認碼 (可選) + 已複製序號到剪貼簿 + 產品名稱 + 產品序號 + 產品引導程式版本 + 產品韌體版本 + 確認碼 (必需) IMEI (可選) - 本次下載可能會失敗 + 剩餘空間不足 目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載? 日誌已儲存到指定路徑。需要透過其他 App 分享嗎? 新名稱 @@ -144,4 +150,16 @@ 忽略 SM-DP+ 的 TLS 證書 允許 RSP 伺服器使用任意證書 無資訊 + 輸入的確認文字不匹配 + 此晶片已被擦除 + 正在擦除 eSIM 晶片 + eSIM 晶片擦除失敗 + 擦除 eSIM 晶片 + 擦除 eSIM 晶片 + 請確認刪除此晶片上的所有配置文件,並了解此操作不可逆。\n\nEID: %s\n\n%s + 請在此輸入「%s」以確認 + 我確認擦除 EID 以 %s 結尾的晶片,並了解此操作不可逆 + 擦除 + 允許擦除 eUICC + 此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。 \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index a0c2d98..5964c32 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -30,8 +30,10 @@ Cannot switch to new eSIM profile. Confirmation string mismatch + Confirmation string mismatch + This chip has been erased ICCID copied to clipboard - Serial Number copied to clipboard + Serial number copied to clipboard EID copied to clipboard ATR copied to clipboard @@ -48,18 +50,19 @@ Failed to delete eSIM profile Switching eSIM profile Failed to switch eSIM profile + Erasing eSIM chip + Failed to erase eSIM chip New eSIM Server (RSP / SM-DP+) Activation Code Confirmation Code (Optional) + Confirmation Code (Required) IMEI (Optional) - This download may fail - This download may fail due to low remaining capacity. + Low remaining capacity + This profile may fail to download due to low remaining capacity. No LPA code found in clipboard - Confirmation Code Required - Please provide a confirmation code as required by the scanned QR code or LPA code from clipboard. Unable to parse Could not parse QR code or clipboard content as a LPA code. @@ -143,6 +146,13 @@ Unknown eSIM CI Answer To Reset (ATR) + Erase eUICC + Erase eUICC + Please confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %s\n\n%s + Type \'%s\' here to confirm + I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE + Erase + Yes No @@ -177,6 +187,8 @@ Include non-production profiles in the list Ignore SM-DP+ TLS certificate Accept any TLS certificate used by the RSP server + Allow erasing eUICC + This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually. Info App Version Source Code diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 9d2f189..f719ef5 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -52,26 +52,37 @@ - + + - + + + ("pref_developer_refreshed_after_switch") - .bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow) - requirePreference("pref_info_ara_m").apply { summary = firstSigner.encodeHex() setOnPreferenceClickListener { diff --git a/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml b/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml index df5bfee..3281caf 100644 --- a/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml +++ b/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml @@ -1,15 +1,5 @@ - - - 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 b690c79..2044c02 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -5,6 +5,7 @@ import android.util.Log import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.R import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.first import java.lang.IllegalArgumentException class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) { @@ -21,7 +22,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto super.tryOpenEuiccChannel(port)?.let { return it } } - if (port.card.isEuicc) { + if (port.card.isEuicc || (context.preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow.first()) { Log.i( DefaultEuiccChannelManager.TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}" 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 d821e68..08b3cf4 100644 --- a/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt +++ b/app/src/main/java/im/angry/openeuicc/di/PrivilegedAppContainer.kt @@ -6,6 +6,7 @@ import im.angry.openeuicc.core.EuiccChannelManagerFactory import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory import im.angry.openeuicc.core.PrivilegedEuiccChannelManager import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory +import im.angry.openeuicc.util.* class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { override val euiccChannelManager: EuiccChannelManager by lazy { @@ -27,4 +28,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { override val customizableTextProvider by lazy { PrivilegedCustomizableTextProvider(context) } + + override val preferenceRepository by lazy { + PrivilegedPreferenceRepository(context) + } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedSettingsFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedSettingsFragment.kt index 3306c73..7229688 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedSettingsFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedSettingsFragment.kt @@ -1,11 +1,17 @@ package im.angry.openeuicc.ui import android.os.Bundle +import androidx.preference.CheckBoxPreference import androidx.preference.Preference +import im.angry.openeuicc.R +import im.angry.openeuicc.util.* class PrivilegedSettingsFragment : SettingsFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { super.onCreatePreferences(savedInstanceState, rootKey) + addPreferencesFromResource(R.xml.pref_privileged_settings) + mergePreferenceOverlay("pref_developer_overlay", "pref_developer") + // It's stupid to _disable_ things for privileged, but for now, the per-app locale picker // is not usable for apps signed with the platform key. // ref: @@ -13,5 +19,9 @@ class PrivilegedSettingsFragment : SettingsFragment() { // eventually work for platform-signed apps. Or, at some point we might introduce our own // locale picker, which hopefully works whether privileged or not. requirePreference("pref_advanced_language").isVisible = false + + // Force use TelephonyManager API + requirePreference("pref_developer_tmapi_removable") + .bindBooleanFlow((preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow) } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/util/PrivilegedPreferenceRepository.kt b/app/src/main/java/im/angry/openeuicc/util/PrivilegedPreferenceRepository.kt new file mode 100644 index 0000000..9e1ffcc --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/util/PrivilegedPreferenceRepository.kt @@ -0,0 +1,15 @@ +package im.angry.openeuicc.util + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey + +internal object PrivilegedPreferenceKeys { + // ---- Developer Options ---- + val REMOVABLE_TELEPHONY_MANAGER = booleanPreferencesKey("removable_telephony_manager") +} + +class PrivilegedPreferenceRepository(context: Context) : PreferenceRepository(context) { + // ---- Developer Options ---- + val removableTelephonyManagerFlow = + bindFlow(PrivilegedPreferenceKeys.REMOVABLE_TELEPHONY_MANAGER, false) +} \ 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 29f3ef6..acc1728 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -17,4 +17,6 @@ 使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。 スキップ eSIM をダウンロード + TelephonyManagerをどこでも使用 + デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 18497b2..d6befc2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -17,4 +17,6 @@ 跳过 下载 eSIM TelephonyManager (特权) + 全局使用 TelephonyManager + 在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 368efbc..10285d3 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -17,4 +17,6 @@ 跳過 下載 eSIM TelephonyManager (特權) + 全域使用 TelephonyManager + 在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47c88bd..ddf17e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,4 +22,8 @@ Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM. Skip Download eSIM + + + Use TelephonyManager everywhere + By default, only OMAPI is attempted for removable eUICCs to match what is done in unprivileged mode (i.e. EasyEUICC). This may not work well on some devices. Select this option to force the use of TelephonyManager even for removable eUICCs. \ No newline at end of file diff --git a/app/src/main/res/xml/pref_privileged_settings.xml b/app/src/main/res/xml/pref_privileged_settings.xml new file mode 100644 index 0000000..339233b --- /dev/null +++ b/app/src/main/res/xml/pref_privileged_settings.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c index 028e30d..bae2ee8 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c @@ -126,8 +126,11 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason); if (ret < 0) { ret = - (int) es10b_load_bound_profile_package_result.errorReason; + goto out; } + euicc_http_cleanup(ctx); + out: // We expect Java side to call cancelSessions after any error -- thus, `euicc_http_cleanup` is done there // This is so that Java side can access the last HTTP and/or APDU errors when we return.