diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index a33838f..f53e6ff 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -19,11 +19,6 @@ android:name="im.angry.openeuicc.ui.NotificationsActivity" android:label="@string/profile_notifications" /> - - @@ -33,9 +28,15 @@ 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 824b683..8e7b158 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,7 +37,6 @@ 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 @@ -110,16 +109,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) fab.setOnClickListener { - 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) - } + Intent(requireContext(), DownloadWizardActivity::class.java).apply { + putExtra("selectedLogicalSlot", logicalSlotId) + startActivity(this) } } } 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 7711643..b11a3dd 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,7 +126,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } private fun ensureNotificationPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + 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) { requestPermissions( arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), PERMISSION_REQUEST_CODE @@ -160,38 +163,29 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { // but it could change in the future euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - newPages.add( - Page( - channel.logicalSlotId, - getString(R.string.channel_name_format, channel.logicalSlotId) - ) { - appContainer.uiComponentFactory.createEuiccManagementFragment( - slotId, - portId - ) - }) + val channelName = getString(R.string.channel_name_format, channel.logicalSlotId) + newPages.add(Page(channel.logicalSlotId, channelName) { + 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 { - newPages.add( - Page( - EuiccChannelManager.USB_CHANNEL_ID, - it.productName ?: getString(R.string.usb) - ) { UsbCcidReaderFragment() }) + val productName = it.productName ?: getString(R.string.usb) + newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) { + 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 181aeee..a06b587 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,14 +38,13 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { get() = editText.text.toString() == requireArguments().getString("name")!! private var deleting = false - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + 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 deleted file mode 100644 index f590d36..0000000 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ /dev/null @@ -1,298 +0,0 @@ -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 52e3272..bb299a3 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,10 +4,14 @@ 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) @@ -15,8 +19,9 @@ 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 6ad74c4..c2cbee3 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 -class SettingsFragment: PreferenceFragmentCompat() { +open class SettingsFragment: PreferenceFragmentCompat() { private lateinit var developerPref: PreferenceCategory // Hidden developer options switch @@ -35,9 +35,9 @@ 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,9 +74,6 @@ 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) @@ -139,4 +136,22 @@ 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 @@ -26,7 +40,15 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false) - diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text) + 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) return view } @@ -44,6 +66,14 @@ 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 f6f63fd..1b816d4 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,18 +113,19 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep is EuiccChannelManagerService.ForegroundTaskState.Done -> { hideProgressBar() - // Change the state of the last InProgress item to Error + state.downloadError = + it.error as? LocalProfileAssistant.ProfileDownloadException + + // Change the state of the last InProgress item to success (or error) progressItems.forEachIndexed { index, progressItem -> if (progressItem.state == ProgressState.InProgress) { - progressItem.state = ProgressState.Error + progressItem.state = + if (state.downloadError == null) ProgressState.Done else 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 c9a9e0f..b268b62 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,6 +7,7 @@ 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 @@ -20,6 +21,11 @@ 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, @@ -45,6 +51,21 @@ 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?, @@ -165,6 +186,9 @@ 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 807706e..e768fa9 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,7 +31,6 @@ 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") } @@ -49,7 +48,6 @@ 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 ca36d64..3f58844 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="20sp" - android:layout_marginBottom="20sp" - android:layout_marginStart="60sp" - android:layout_marginEnd="60sp" + android:layout_marginTop="20dp" + android:layout_marginBottom="20dp" + android:layout_marginStart="60dp" + android:layout_marginEnd="60dp" 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 b9a0bc2..88b1ffb 100644 --- a/app-common/src/main/res/layout/fragment_download_diagnostics.xml +++ b/app-common/src/main/res/layout/fragment_download_diagnostics.xml @@ -17,15 +17,26 @@ android:layout_height="wrap_content" android:gravity="center_horizontal" android:textSize="20sp" - android:layout_marginTop="20sp" - android:layout_marginBottom="20sp" - android:layout_marginStart="60sp" - android:layout_marginEnd="60sp" + android:layout_marginTop="20dp" + android:layout_marginBottom="20dp" + android:layout_marginStart="60dp" + android:layout_marginEnd="60dp" 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 deleted file mode 100644 index f93ae8d..0000000 --- a/app-common/src/main/res/menu/fragment_profile_download.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 fed185d..d25af49 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -37,11 +37,6 @@ アクティベーションコード 確認コード (オプション) IMEI (オプション) - 残りの容量: %s - QR コードをスキャン - ギャラリーから QR コードをスキャン - ダウンロード - eSIM のダウンロードに失敗しました。アクティベーションまたは QR コードを確認してください。 ダウンロードに失敗する可能性があります 残り容量が少ないため、ダウンロードに失敗する可能性があります。 ダウンロードウィザード @@ -53,7 +48,6 @@ リムーバブル 内部 内部 - ポート: %d - eID: 有効なプロファイル: 空き容量: eSIM プロファイルをどの方法でダウンロードしますか? @@ -68,6 +62,7 @@ eSIM プロファイルをダウンロード中です eSIM プロファイルをストレージに読み込み中です エラー診断 + エラーコード: %s 最終の HTTP ステータス (サーバー): %d 最終の HTTP レスポンス (サーバー): 最終の HTTP 例外: @@ -75,6 +70,8 @@ 最終の APDU レスポンス (SIM) は成功しました 最終の APDU レスポンス (SIM) は失敗しました 最終の APDU 例外: + 保存 + %s のエラー診断 新しいニックネーム %s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。 削除を確認するには「%s」を入力してください @@ -92,11 +89,10 @@ eUICC 情報 (%s) アクセスモード リムーバブル - EID eUICC OS のバージョン グローバルプラットフォームのバージョン SAS 認定番号 - 保護されたプロファイルのバージョン + Protected Profileのバージョン NVRAM の空き容量 (eSIM プロファイルストレージ) GSMA プロダクション証明書 GSMA テスト証明書 @@ -125,8 +121,6 @@ ログ アプリの最新デバッグログを表示します 開発者オプション - 実験的なダウンロードウィザード - 実験的な新しいダウンロードウィザードを有効化します。まだ完全に機能していないことにご注意ください。 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 new file mode 100644 index 0000000..3bb0d04 --- /dev/null +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,137 @@ + + + 在此设备上未检测到此应用程序可访问的可插拔 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 a5a05ab..4a9f529 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -53,11 +53,6 @@ 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. @@ -71,7 +66,7 @@ Removable Internal Internal, port %d - eID: + eID: Active Profile: Free Space: How would you like to download the eSIM profile? @@ -86,6 +81,7 @@ 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: @@ -93,6 +89,8 @@ Last APDU response (from SIM) is successful Last APDU response (from SIM) is a failure Last APDU exception: + Save + Diagnostics at %s New nickname @@ -116,7 +114,7 @@ eUICC Info (%s) Access Mode Removable - EID + EID eUICC OS Version GlobalPlatform Version SAS Accreditation Number @@ -156,12 +154,10 @@ 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 - Ignore SM-DP+ TLS certificate, allow any RSP + Accept any TLS certificate used by the RSP server 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 dd9d189..e1a13f8 100644 --- a/app-common/src/main/res/xml/locale_config.xml +++ b/app-common/src/main/res/xml/locale_config.xml @@ -2,4 +2,5 @@ + \ 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 52815b9..1da1fd4 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -57,12 +57,6 @@ 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 b8fac39..053a8d1 100644 --- a/app-unpriv/src/main/res/values-ja/strings.xml +++ b/app-unpriv/src/main/res/values-ja/strings.xml @@ -30,4 +30,5 @@ 挿入された取り外し可能な 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 new file mode 100644 index 0000000..8d3060d --- /dev/null +++ b/app-unpriv/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,32 @@ + + 兼容性检查 + 打开 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 3cf7347..9d80b0e 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -4,6 +4,12 @@ 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 new file mode 100644 index 0000000..3281caf --- /dev/null +++ b/app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml @@ -0,0 +1,12 @@ + + + + + + \ 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 d626333..3c522c5 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 d7ac213..de2ca24 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/LuiActivity.kt @@ -8,6 +8,7 @@ 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() { @@ -25,10 +26,11 @@ class LuiActivity : AppCompatActivity() { } requireViewById(R.id.lui_skip).setOnClickListener { finish() } - // TODO: Deactivate LuiActivity if there is no eSIM found. + // TODO: Deactivate DownloadWizardActivity 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, DirectProfileDownloadActivity::class.java)) + startActivity(Intent(this, DownloadWizardActivity::class.java)) + finish() } } } \ 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 688ae6c..12b60bd 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt @@ -17,19 +17,16 @@ 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