diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index f53e6ff..a33838f 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -19,6 +19,11 @@ android:name="im.angry.openeuicc.ui.NotificationsActivity" android:label="@string/profile_notifications" /> + + @@ -28,15 +33,9 @@ android:label="@string/pref_advanced_logs" /> - - + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + Triple(slotId, channel.logicalSlotId, portId) + } + }.toList().sortedBy { it.second } + } + + when { + knownChannels.isEmpty() -> { + finish() + } + // Detect multiple eUICC chips + knownChannels.distinctBy { it.first }.size > 1 -> { + SlotSelectFragment.newInstance( + knownChannels.map { it.first }, + knownChannels.map { it.second }, + knownChannels.map { it.third }) + .show(supportFragmentManager, SlotSelectFragment.TAG) + } + else -> { + // If the device has only one eSIM "chip" (but may be mapped to multiple slots), + // we can skip the slot selection dialog since there is only one chip to save to. + onSlotSelected( + knownChannels[0].first, + knownChannels[0].third + ) + } + } + } + } + + override fun onSlotSelected(slotId: Int, portId: Int) { + ProfileDownloadFragment.newInstance(slotId, portId, finishWhenDone = true) + .show(supportFragmentManager, ProfileDownloadFragment.TAG) + } + + override fun onSlotSelectCancelled() = finish() +} \ 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 8e7b158..824b683 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 @@ -37,6 +37,7 @@ 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.withContext @@ -109,9 +110,16 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) fab.setOnClickListener { - Intent(requireContext(), DownloadWizardActivity::class.java).apply { - putExtra("selectedLogicalSlot", logicalSlotId) - startActivity(this) + lifecycleScope.launch { + if (preferenceRepository.experimentalDownloadWizardFlow.first()) { + Intent(requireContext(), DownloadWizardActivity::class.java).apply { + putExtra("selectedLogicalSlot", logicalSlotId) + startActivity(this) + } + } else { + ProfileDownloadFragment.newInstance(slotId, portId) + .show(childFragmentManager, ProfileDownloadFragment.TAG) + } } } } 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 b11a3dd..7711643 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 @@ -126,10 +126,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } private fun ensureNotificationPermissions() { - val needsNotificationPerms = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; - val notificationPermsGranted = - needsNotificationPerms && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED - if (needsNotificationPerms && !notificationPermsGranted) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { requestPermissions( arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), PERMISSION_REQUEST_CODE @@ -163,29 +160,38 @@ 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) - newPages.add(Page(channel.logicalSlotId, channelName) { - appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId) - }) + newPages.add( + Page( + channel.logicalSlotId, + getString(R.string.channel_name_format, channel.logicalSlotId) + ) { + appContainer.uiComponentFactory.createEuiccManagementFragment( + slotId, + portId + ) + }) } }.collect() // If USB readers exist, add them at the very last // We use a wrapper fragment to handle logic specific to USB readers usbDevice?.let { - val productName = it.productName ?: getString(R.string.usb) - newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) { - UsbCcidReaderFragment() - }) + newPages.add( + Page( + EuiccChannelManager.USB_CHANNEL_ID, + it.productName ?: getString(R.string.usb) + ) { UsbCcidReaderFragment() }) } viewPager.visibility = View.VISIBLE if (newPages.size > 1) { tabs.visibility = View.VISIBLE } else if (newPages.isEmpty()) { - newPages.add(Page(-1, "") { - appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() - }) + newPages.add( + Page( + -1, + "" + ) { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() }) } newPages.sortBy { it.logicalSlotId } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index a06b587..181aeee 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -38,13 +38,14 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { get() = editText.text.toString() == requireArguments().getString("name")!! private var deleting = false - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply { setMessage(getString(R.string.profile_delete_confirm, requireArguments().getString("name"))) setView(editText) setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing setNegativeButton(android.R.string.cancel, null) }.create() + } override fun onResume() { super.onResume() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt new file mode 100644 index 0000000..f590d36 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -0,0 +1,298 @@ +package im.angry.openeuicc.ui + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.DialogInterface +import android.graphics.BitmapFactory +import android.os.Bundle +import android.text.Editable +import android.util.Log +import android.view.* +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.lifecycleScope +import com.google.android.material.textfield.TextInputLayout +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions +import im.angry.openeuicc.common.R +import im.angry.openeuicc.service.EuiccChannelManagerService +import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone +import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ProfileDownloadFragment : BaseMaterialDialogFragment(), + Toolbar.OnMenuItemClickListener, EuiccChannelFragmentMarker { + companion object { + const val TAG = "ProfileDownloadFragment" + + const val LOW_NVRAM_THRESHOLD = 30 * 1024 // < 30 KiB, the alert may fail + + fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment = + newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) { + putBoolean("finishWhenDone", finishWhenDone) + } + } + + private lateinit var toolbar: Toolbar + private lateinit var profileDownloadServer: TextInputLayout + private lateinit var profileDownloadCode: TextInputLayout + private lateinit var profileDownloadConfirmationCode: TextInputLayout + private lateinit var profileDownloadIMEI: TextInputLayout + private lateinit var profileDownloadFreeSpace: TextView + private lateinit var progress: ProgressBar + + private var freeNvram: Int = -1 + + private var downloading = false + + private val finishWhenDone by lazy { + requireArguments().getBoolean("finishWhenDone", false) + } + + private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> + result.contents?.let { content -> + onScanResult(content) + } + } + + private val gallerySelectorLauncher = 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) { + onScanResult(it) + } + } + + bmp.recycle() + } + } + } + } + + private fun onScanResult(result: String) { + val components = result.split("$") + if (components.size < 3 || components[0] != "LPA:1") return + profileDownloadServer.editText?.setText(components[1]) + profileDownloadCode.editText?.setText(components[2]) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.fragment_profile_download, container, false) + + toolbar = view.requireViewById(R.id.toolbar) + profileDownloadServer = view.requireViewById(R.id.profile_download_server) + profileDownloadCode = view.requireViewById(R.id.profile_download_code) + profileDownloadConfirmationCode = view.requireViewById(R.id.profile_download_confirmation_code) + profileDownloadIMEI = view.requireViewById(R.id.profile_download_imei) + profileDownloadFreeSpace = view.requireViewById(R.id.profile_download_free_space) + progress = view.requireViewById(R.id.progress) + + toolbar.inflateMenu(R.menu.fragment_profile_download) + + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + toolbar.apply { + setTitle(R.string.profile_download) + setNavigationOnClickListener { + if (!downloading) { + dismiss() + } + } + setOnMenuItemClickListener(this@ProfileDownloadFragment) + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean = downloading || + when (item.itemId) { + R.id.scan -> { + barcodeScannerLauncher.launch(ScanOptions().apply { + setDesiredBarcodeFormats(ScanOptions.QR_CODE) + setOrientationLocked(false) + }) + true + } + R.id.scan_from_gallery -> { + gallerySelectorLauncher.launch("image/*") + true + } + R.id.ok -> { + if (freeNvram > LOW_NVRAM_THRESHOLD) { + startDownloadProfile() + } else { + AlertDialog.Builder(requireContext()).apply { + setTitle(R.string.profile_download_low_nvram_title) + setMessage(R.string.profile_download_low_nvram_message) + setIcon(android.R.drawable.ic_dialog_alert) + setCancelable(true) + setPositiveButton(android.R.string.ok) { _, _ -> + startDownloadProfile() + } + setNegativeButton(android.R.string.cancel, null) + show() + } + } + true + } + else -> false + } + + override fun onResume() { + super.onResume() + setWidthPercent(95) + } + + @SuppressLint("MissingPermission") + override fun onStart() { + super.onStart() + + lifecycleScope.launch(Dispatchers.IO) { + ensureEuiccChannelManager() + if (euiccChannelManagerService.isForegroundTaskRunning) { + withContext(Dispatchers.Main) { + dismiss() + } + return@launch + } + + withEuiccChannel { channel -> + val imei = try { + telephonyManager.getImei(channel.logicalSlotId) ?: "" + } catch (e: Exception) { + "" + } + + // Fetch remaining NVRAM + val str = channel.lpa.euiccInfo2?.freeNvram?.also { + freeNvram = it + }?.let { formatFreeSpace(it) } + + withContext(Dispatchers.Main) { + profileDownloadFreeSpace.text = getString( + R.string.profile_download_free_space, + str ?: getText(R.string.unknown) + ) + profileDownloadIMEI.editText!!.text = + Editable.Factory.getInstance().newEditable(imei) + } + } + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).also { + it.setCanceledOnTouchOutside(false) + } + } + + private fun startDownloadProfile() { + val server = profileDownloadServer.editText!!.let { + it.text.toString().trim().apply { + if (isEmpty()) { + it.requestFocus() + return@startDownloadProfile + } + } + } + + val code = profileDownloadCode.editText!!.text.toString().trim() + .ifBlank { null } + val confirmationCode = profileDownloadConfirmationCode.editText!!.text.toString().trim() + .ifBlank { null } + val imei = profileDownloadIMEI.editText!!.text.toString().trim() + .ifBlank { null } + + downloading = true + + profileDownloadServer.editText!!.isEnabled = false + profileDownloadCode.editText!!.isEnabled = false + profileDownloadConfirmationCode.editText!!.isEnabled = false + profileDownloadIMEI.editText!!.isEnabled = false + + progress.isIndeterminate = true + progress.visibility = View.VISIBLE + + lifecycleScope.launch { + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() + val err = doDownloadProfile(server, code, confirmationCode, imei) + + if (err != null) { + Log.d(TAG, "Error downloading profile") + Log.d(TAG, Log.getStackTraceString(err)) + + Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show() + } + + if (parentFragment is EuiccProfilesChangedListener) { + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + } + + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } + } + } + + private suspend fun doDownloadProfile( + server: String, + code: String?, + confirmationCode: String?, + imei: String? + ) = withContext(Dispatchers.Main) { + // The service is responsible for launching the actual blocking part on the IO context + // On our side, we need the Main context because of the UI updates + euiccChannelManagerService.launchProfileDownloadTask( + slotId, + portId, + server, + code, + confirmationCode, + imei + ).onEach { + if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) { + progress.progress = it.progress + progress.isIndeterminate = it.progress == 0 + } else { + progress.progress = 100 + progress.isIndeterminate = false + } + }.waitDone() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (finishWhenDone) { + activity?.finish() + } + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + if (finishWhenDone) { + activity?.finish() + } + } +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt index bb299a3..52e3272 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt @@ -4,14 +4,10 @@ import android.os.Bundle import android.view.MenuItem import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* class SettingsActivity: AppCompatActivity() { - private val appContainer - get() = (application as OpenEuiccApplication).appContainer - override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -19,9 +15,8 @@ class SettingsActivity: AppCompatActivity() { setSupportActionBar(requireViewById(R.id.toolbar)) setupToolbarInsets() supportActionBar!!.setDisplayHomeAsUpEnabled(true) - val settingsFragment = appContainer.uiComponentFactory.createSettingsFragment() supportFragmentManager.beginTransaction() - .replace(R.id.settings_container, settingsFragment) + .replace(R.id.settings_container, SettingsFragment()) .commit() } 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..6ad74c4 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 @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -open class SettingsFragment: PreferenceFragmentCompat() { +class SettingsFragment: PreferenceFragmentCompat() { private lateinit var developerPref: PreferenceCategory // Hidden developer options switch @@ -35,9 +35,9 @@ open class SettingsFragment: PreferenceFragmentCompat() { // Show / hide developer preference based on whether it is enabled lifecycleScope.launch { - preferenceRepository.developerOptionsEnabledFlow - .onEach { developerPref.isVisible = it } - .collect() + preferenceRepository.developerOptionsEnabledFlow.onEach { + developerPref.isVisible = it + }.collect() } findPreference("pref_info_app_version")?.apply { @@ -74,6 +74,9 @@ open class SettingsFragment: PreferenceFragmentCompat() { findPreference("pref_advanced_verbose_logging") ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING) + findPreference("pref_developer_experimental_download_wizard") + ?.bindBooleanFlow(preferenceRepository.experimentalDownloadWizardFlow, PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD) + findPreference("pref_developer_unfiltered_profile_list") ?.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow, PreferenceKeys.UNFILTERED_PROFILE_LIST) @@ -136,22 +139,4 @@ open class SettingsFragment: PreferenceFragmentCompat() { true } } - - protected fun mergePreferenceOverlay(overlayKey: String, targetKey: String) { - val overlayCat = findPreference(overlayKey)!! - val targetCat = findPreference(targetKey)!! - - val prefs = buildList { - for (i in 0.. - if (uri == null) return@registerForActivityResult - requireActivity().contentResolver.openFileDescriptor(uri, "w")?.use { - FileOutputStream(it.fileDescriptor).use { os -> - os.write(diagnosticTextView.text.toString().encodeToByteArray()) - } - } - } - override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null @@ -40,15 +26,7 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS savedInstanceState: Bundle? ): 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()) - ) - ) - } - diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text) + diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text) return view } @@ -66,14 +44,6 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS private fun buildDiagnosticsText(): String? = state.downloadError?.let { err -> val ret = StringBuilder() - ret.appendLine( - getString( - R.string.download_wizard_diagnostics_error_code, - err.lpaErrorReason - ) - ) - ret.appendLine() - err.lastHttpResponse?.let { resp -> if (resp.rcode != 200) { // Only show the status if it's not 200 diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index 1b816d4..f6f63fd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -113,19 +113,18 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep is EuiccChannelManagerService.ForegroundTaskState.Done -> { hideProgressBar() - state.downloadError = - it.error as? LocalProfileAssistant.ProfileDownloadException - - // Change the state of the last InProgress item to success (or error) + // Change the state of the last InProgress item to Error progressItems.forEachIndexed { index, progressItem -> if (progressItem.state == ProgressState.InProgress) { - progressItem.state = - if (state.downloadError == null) ProgressState.Done else ProgressState.Error + progressItem.state = ProgressState.Error } adapter.notifyItemChanged(index) } + state.downloadError = + it.error as? LocalProfileAssistant.ProfileDownloadException + isDone = true refreshButtons() } 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 b268b62..c9a9e0f 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 @@ -7,7 +7,6 @@ import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.TextView -import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -21,11 +20,6 @@ import kotlinx.coroutines.launch import net.typeblog.lpac_jni.LocalProfileInfo class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { - companion object { - const val LOW_NVRAM_THRESHOLD = - 30 * 1024 // < 30 KiB, alert about potential download failure - } - private data class SlotInfo( val logicalSlotId: Int, val isRemovable: Boolean, @@ -51,21 +45,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null - override fun beforeNext() { - super.beforeNext() - - if (adapter.selected.freeSpace < LOW_NVRAM_THRESHOLD) { - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.profile_download_low_nvram_title) - setMessage(R.string.profile_download_low_nvram_message) - setCancelable(true) - setPositiveButton(android.R.string.ok, null) - setNegativeButton(android.R.string.cancel, null) - show() - } - } - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -186,9 +165,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt var slots: List = listOf() var currentSelectedIdx = -1 - val selected: SlotInfo - get() = slots[currentSelectedIdx] - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder { val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false) return SlotItemHolder(root) 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..807706e 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 @@ -31,6 +31,7 @@ object PreferenceKeys { // ---- Developer Options ---- val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled") + val EXPERIMENTAL_DOWNLOAD_WIZARD = booleanPreferencesKey("experimental_download_wizard") val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list") val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate") } @@ -48,6 +49,7 @@ class PreferenceRepository(private val context: Context) { // ---- Developer Options ---- val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false) + val experimentalDownloadWizardFlow = bindFlow(PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD, false) val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false) val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false) diff --git a/app-common/src/main/res/layout/fragment_download_details.xml b/app-common/src/main/res/layout/fragment_download_details.xml index 3f58844..ca36d64 100644 --- a/app-common/src/main/res/layout/fragment_download_details.xml +++ b/app-common/src/main/res/layout/fragment_download_details.xml @@ -16,10 +16,10 @@ android:layout_height="wrap_content" android:gravity="center_horizontal" android:textSize="20sp" - android:layout_marginTop="20dp" - android:layout_marginBottom="20dp" - android:layout_marginStart="60dp" - android:layout_marginEnd="60dp" + android:layout_marginTop="20sp" + android:layout_marginBottom="20sp" + android:layout_marginStart="60sp" + android:layout_marginEnd="60sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constrainedWidth="true" diff --git a/app-common/src/main/res/layout/fragment_download_diagnostics.xml b/app-common/src/main/res/layout/fragment_download_diagnostics.xml index 88b1ffb..b9a0bc2 100644 --- a/app-common/src/main/res/layout/fragment_download_diagnostics.xml +++ b/app-common/src/main/res/layout/fragment_download_diagnostics.xml @@ -17,26 +17,15 @@ android:layout_height="wrap_content" android:gravity="center_horizontal" android:textSize="20sp" - android:layout_marginTop="20dp" - android:layout_marginBottom="20dp" - android:layout_marginStart="60dp" - android:layout_marginEnd="60dp" + android:layout_marginTop="20sp" + android:layout_marginBottom="20sp" + android:layout_marginStart="60sp" + android:layout_marginEnd="60sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constrainedWidth="true" app:layout_constraintTop_toTopOf="parent" /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/menu/fragment_profile_download.xml b/app-common/src/main/res/menu/fragment_profile_download.xml new file mode 100644 index 0000000..f93ae8d --- /dev/null +++ b/app-common/src/main/res/menu/fragment_profile_download.xml @@ -0,0 +1,21 @@ + + + + + + + + \ 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..fed185d 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -37,6 +37,11 @@ アクティベーションコード 確認コード (オプション) IMEI (オプション) + 残りの容量: %s + QR コードをスキャン + ギャラリーから QR コードをスキャン + ダウンロード + eSIM のダウンロードに失敗しました。アクティベーションまたは QR コードを確認してください。 ダウンロードに失敗する可能性があります 残り容量が少ないため、ダウンロードに失敗する可能性があります。 ダウンロードウィザード @@ -48,6 +53,7 @@ リムーバブル 内部 内部 - ポート: %d + eID: 有効なプロファイル: 空き容量: eSIM プロファイルをどの方法でダウンロードしますか? @@ -62,7 +68,6 @@ eSIM プロファイルをダウンロード中です eSIM プロファイルをストレージに読み込み中です エラー診断 - エラーコード: %s 最終の HTTP ステータス (サーバー): %d 最終の HTTP レスポンス (サーバー): 最終の HTTP 例外: @@ -70,8 +75,6 @@ 最終の APDU レスポンス (SIM) は成功しました 最終の APDU レスポンス (SIM) は失敗しました 最終の APDU 例外: - 保存 - %s のエラー診断 新しいニックネーム %s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。 削除を確認するには「%s」を入力してください @@ -89,10 +92,11 @@ eUICC 情報 (%s) アクセスモード リムーバブル + EID eUICC OS のバージョン グローバルプラットフォームのバージョン SAS 認定番号 - Protected Profileのバージョン + 保護されたプロファイルのバージョン NVRAM の空き容量 (eSIM プロファイルストレージ) GSMA プロダクション証明書 GSMA テスト証明書 @@ -121,6 +125,8 @@ ログ アプリの最新デバッグログを表示します 開発者オプション + 実験的なダウンロードウィザード + 実験的な新しいダウンロードウィザードを有効化します。まだ完全に機能していないことにご注意ください。 SM-DP+ TLS 証明書を無視する SM-DP+ TLS 証明書を無視して任意の RSP を許可します 情報 diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml deleted file mode 100644 index 3bb0d04..0000000 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - 在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。 - 此 eSIM 上还没有配置文件 - 未知 - 帮助 - 重新加载卡槽 - 逻辑卡槽 %d - 已启用 - 已禁用 - 提供商: - 类型: - 启用 - 禁用 - 删除 - 重命名 - 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机 - 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。 - 无法切换到新的 eSIM 配置文件。 - 昵称不能超过 64 个字符 - 已复制 ICCID 到剪贴板 - 选择卡槽 - 选择 - 授予 USB 权限 - 需要获得访问 USB 智能卡读卡器的权限。 - 无法通过 USB 智能卡读卡器连接到 eSIM。 - 长时间运行的后台任务 - 正在下载 eSIM 配置文件 - 无法下载 eSIM 配置文件 - 正在重命名 eSIM 配置文件 - 无法重命名 eSIM 配置文件 - 正在删除 eSIM 配置文件 - 无法删除 eSIM 配置文件 - 正在切换 eSIM 配置文件 - 无法切换 eSIM 配置文件 - 添加新 eSIM - 服务器 (RSP / SM-DP+) - 激活码 - 确认码 (可选) - IMEI (可选) - 本次下载可能会失败 - 当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载? - 新昵称 - 您确定要删除 %s 吗?此操作是不可逆的。 - 请输入\'%s\'以确认删除 - 通知列表 - 通知列表 (%s) - 管理通知 - eSIM 配置文件可以在下载、删除、启用或禁用时向运营商发送通知。此处列出了要发送的这些通知的队列。\n\n在\"设置\"中,您可以指定是否自动发送每种类型的通知。请注意,即使通知已发送,也不会自动从记录中删除,除非队列空间不足。\n\n在这里,您可以手动发送或删除每个待处理的通知。 - 已下载 - 已删除 - 已启用 - 已禁用 - 处理 - 删除 - 保存日志 - %s 的日志 - 设置 - 通知 - 操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。 - 下载 - 发送 下载 配置文件的通知 - 删除 - 发送 删除 配置文件的通知 - 切换 - 发送 切换 配置文件的通知\n注意,这种类型的通知是不可靠的。 - 高级 - 允许 禁用/删除 已启用的配置文件 - 默认情况下,此应用程序会阻止您禁用可插拔 eSIM 中已启用的配置文件。\n因为这样做 有时 会使其无法访问。\n勾选此框以 移除 此保护措施。 - 记录详细日志 - 详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。 - 日志 - 查看应用程序的最新调试日志 - 信息 - App 版本 - 源码 - 测试 - 准备中 - 可用 - 下载向导 - 返回 - 下一步 - 请选择或确认下载目标 eSIM 卡槽: - 逻辑卡槽 %d - 类型: - 可插拔 - 内置 - 内置, 端口 %d - 当前配置文件: - 剩余空间: - 您想要如何下载 eSIM 配置文件? - 用相机扫描二维码 - 从图库选择二维码 - 手动输入 - 请输入或确认下载 eSIM 的详细信息: - 正在下载您的 eSIM... - 准备中 - 正在连接服务器 - 正在向服务器认证您的设备 - 正在下载 eSIM 配置文件 - 正在写入 eSIM 配置文件 - 错误诊断 - 错误代码: %s - 上次 HTTP 状态码 (来自服务器): %d - 上次 HTTP 应答 (来自服务器): - 上次 HTTP 错误: - 上次 APDU 应答 (来自 SIM): %s - 上次 APDU 应答 (来自 SIM) 是成功的 - 上次 APDU 应答 (来自 SIM) 是失败的 - 上次 APDU 错误: - 保存 - %s 的错误诊断 - eUICC 详情 - eUICC 详情 (%s) - 访问方式 - 可插拔 - eUICC OS 版本 - GlobalPlatform 版本 - SAS 认证号码 - Protected Profile 版本 - NVRAM 剩余空间 (eSIM 存储容量) - GSMA 生产环境证书 - GSMA 测试环境证书 - 兼容 - 不兼容 - - - 还有 %d 步成为开发者 - 你现在是开发者了! - 语言 - 选择 App 语言 - 开发者选项 - 显示未经过滤的配置文件列表 - 在配置文件列表中包括非生产环境的配置文件 - 无视 SM-DP+ 的 TLS 证书 - 允许 RSP 服务器使用任意证书 - \ 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 4a9f529..a5a05ab 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -53,6 +53,11 @@ Activation Code Confirmation Code (Optional) IMEI (Optional) + Space remaining: %s + Scan QR Code + Scan QR Code from Gallery + Download + Failed to download eSIM. Check your activation / QR code. This download may fail This download may fail due to low remaining capacity. @@ -66,7 +71,7 @@ Removable Internal Internal, port %d - eID: + eID: Active Profile: Free Space: How would you like to download the eSIM profile? @@ -81,7 +86,6 @@ Downloading eSIM profile Loading eSIM profile into storage Error diagnostics - Error code: %s Last HTTP status (from server): %d Last HTTP response (from server): Last HTTP exception: @@ -89,8 +93,6 @@ Last APDU response (from SIM) is successful Last APDU response (from SIM) is a failure Last APDU exception: - Save - Diagnostics at %s New nickname @@ -114,7 +116,7 @@ eUICC Info (%s) Access Mode Removable - EID + EID eUICC OS Version GlobalPlatform Version SAS Accreditation Number @@ -154,10 +156,12 @@ Logs View recent debug logs of the application Developer Options + Experimental Download Wizard + Enable the experimental new download wizard. Note that it is not fully working yet. Show unfiltered profile list Include non-production profiles in the list Ignore SM-DP+ TLS certificate - Accept any TLS certificate used by the RSP server + Ignore SM-DP+ TLS certificate, allow any RSP Info App Version Source Code diff --git a/app-common/src/main/res/xml/locale_config.xml b/app-common/src/main/res/xml/locale_config.xml index e1a13f8..dd9d189 100644 --- a/app-common/src/main/res/xml/locale_config.xml +++ b/app-common/src/main/res/xml/locale_config.xml @@ -2,5 +2,4 @@ - \ No newline at end of file diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index 1da1fd4..52815b9 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -57,6 +57,12 @@ app:title="@string/pref_developer" app:iconSpaceReserved="false"> + + ("pref_info_ara_m")?.apply { - summary = firstSigner.encodeHex() - setOnPreferenceClickListener { - requireContext().getSystemService(ClipboardManager::class.java)!! - .setPrimaryClip(ClipData.newPlainText("ara-m", summary)) - Toast.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT) - .show() - true - } - } - } -} \ No newline at end of file diff --git a/app-unpriv/src/main/res/values-ja/strings.xml b/app-unpriv/src/main/res/values-ja/strings.xml index 053a8d1..b8fac39 100644 --- a/app-unpriv/src/main/res/values-ja/strings.xml +++ b/app-unpriv/src/main/res/values-ja/strings.xml @@ -30,5 +30,4 @@ 挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s 挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。 - ARA-M SHA-1 をクリップボードにコピーしました diff --git a/app-unpriv/src/main/res/values-zh-rCN/strings.xml b/app-unpriv/src/main/res/values-zh-rCN/strings.xml deleted file mode 100644 index 8d3060d..0000000 --- a/app-unpriv/src/main/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - 兼容性检查 - 打开 SIM 卡应用程序 - 系统功能 - 您的设备是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的电话功能和 OMAPI 支持。 - 您的设备没有电话功能。 - 您的设备/系统未声明支持 OMAPI。这可能是由于缺少硬件支持,或者可能仅仅是由于缺少标志。请参阅以下两项检查以确定 OMAPI 是否确实受支持。 - OMAPI 连接 - 您的设备是否允许通过 OMAPI 访问 SIM 卡上的安全元件? - 无法通过 OMAPI 检测到 SIM 卡的 Secure Element。如果您尚未在此设备中插入 SIM 卡,请尝试插入一张 SIM 卡并重试此检查。 - 已成功检测到可访问 Secure Element 的卡槽,但仅限于以下 SIM 卡槽:SIM%s - ISD-R 通道访问 - 您的设备是否支持通过 OMAPI 打开 eSIM 的 ISD-R (管理) 通道? - 无法确定是否支持通过 OMAPI 进行 ISD-R 访问。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重试。 - OMAPI 只能在以下 SIM 插槽上访问 ISD-R:SIM%s - 不在已知的 BUG 名单中 - 确保您的设备不存在与可插拔 eSIM 相关的错误。 - 糟糕,您的设备在访问可插拔 eSIM 时存在错误。这并不表示完全无法使用,但我们不保证该应用在您设备上的行为。 - USB 读卡器支持 - 您的设备是否支持通过 USB 读卡器管理 eSIM? - 您可以通过此设备上的标准 USB CCID 读取器管理 eSIM (即使您在这里有任何其他检查项失败)。请插入读卡器,然后打开此应用程序以这种方式管理 eSIM。 - 您的设备不支持 USB 读卡器。 - 结论 (USB 读卡器以外) - 根据之前的所有检查,您的设备与可插拔 eSIM 卡兼容的可能性有多大? - 您可以使用和管理插入此设备的可插拔 eSIM 卡。 - 已知您的设备在访问可插拔 eSIM 卡时存在问题。\n%s - 我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。不过,您的设备确实声明支持 OMAPI,因此它工作的可能性略高。\n%s - 我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。由于您的设备未声明支持OMAPI,因此更有可能不支持在此设备上管理可插拔 eSIM。\n%s - 我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。\n%s - 然而,已经加载了eSIM配置文件的可插拔 eSIM 卡仍然可以工作; 即使无法在装置上直接管理可插拔 eSIM 卡中的配置文件,您仍然可以使用 USB 卡读卡器来管理配置文件。 - ARA-M SHA-1 已拷贝到剪贴板 - \ No newline at end of file diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 9d80b0e..3cf7347 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -4,12 +4,6 @@ Compatibility Check Open SIM Toolkit - - ARA-M SHA-1 - - - ARA-M SHA-1 copied to clipboard - System Features Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support. diff --git a/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml b/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml deleted file mode 100644 index 3281caf..0000000 --- a/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt index 3c522c5..d626333 100644 --- a/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt +++ b/app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt @@ -110,7 +110,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker { telephonyManager.simSlotMapping = mappings return } catch (_: Exception) { - // ignore + } // Sometimes hardware supports one ordering but not the reverse diff --git a/app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt index de2ca24..d7ac213 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt @@ -8,7 +8,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import im.angry.openeuicc.R -import im.angry.openeuicc.ui.wizard.DownloadWizardActivity class LuiActivity : AppCompatActivity() { override fun onStart() { @@ -26,11 +25,10 @@ class LuiActivity : AppCompatActivity() { } requireViewById(R.id.lui_skip).setOnClickListener { finish() } - // TODO: Deactivate DownloadWizardActivity if there is no eSIM found. + // TODO: Deactivate LuiActivity if there is no eSIM found. // TODO: Support pre-filled download info (from carrier apps); UX requireViewById(R.id.lui_download).setOnClickListener { - startActivity(Intent(this, DownloadWizardActivity::class.java)) - finish() + startActivity(Intent(this, DirectProfileDownloadActivity::class.java)) } } } \ No newline at end of file diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt index 12b60bd..688ae6c 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt @@ -17,16 +17,19 @@ class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { private var isMEP = false private var isRemovable = false + override suspend fun doRefresh() { + super.doRefresh() + withEuiccChannel { channel -> + isMEP = channel.isMEP + isRemovable = channel.port.card.isRemovable + } + } + override suspend fun onCreateFooterViews( parent: ViewGroup, profiles: List ): List = super.onCreateFooterViews(parent, profiles).let { footers -> - withEuiccChannel { channel -> - isMEP = channel.isMEP - isRemovable = channel.port.card.isRemovable - } - if (isMEP) { val view = layoutInflater.inflate(R.layout.footer_mep, parent, false) view.requireViewById