Compare commits

...

18 commits

Author SHA1 Message Date
125f1da6af i18n: Add Simplified Chinese (zh-CN) translation
Also fix up some untranslatable strings and translation mistakes in
ja while we are at it.

Co-authored-by: sekaiacg <sekaiacg@gmail.com>
2024-12-08 19:57:16 -05:00
4a482b9c73 Implement a generic preference overlay mechanism
We'll need this as we'll probably soon have more priv/unpriv-specific
preferences. For example, exposing removable eSIMs to the system.
2024-12-08 17:22:46 -05:00
ca46b578f7 ui: Display ARA-M SHA-1 under info 2024-12-08 17:15:36 -05:00
23022b14be feat: copy ara-m sha-1 in settings 2024-12-08 17:12:51 -05:00
2d66c1f334 ui: Switch completely to the new download flow
...and delete the old ProfileDownloadFragment
2024-12-08 16:26:36 -05:00
09b98b37ab Export DownloadWizardActivity 2024-12-08 16:19:14 -05:00
fdbf9b3252 ui: wizard: Reimplement low nvram warning 2024-12-08 16:17:50 -05:00
84f47cb0f0 ui: wizard: Use dp instead of sp in margins 2024-12-08 16:09:39 -05:00
0229ef41df ui: wizard: Allow saving diagnostics text 2024-12-08 16:07:23 -05:00
15d3b701a5 Switch LuiActivity to the new wizard
...and rename / alias the old DirectProfileDownloadActivity to the new
DownloadWizardActivity
2024-12-08 15:42:24 -05:00
700578a369 ui: wizard: Handle download success
Turns out when you test only errors you forget things can actually
succeed...
2024-12-08 15:34:38 -05:00
eab60bf3d3 ui: priv: Set isMEP and isRemovable when creating footer views
Else, footer views may be created before we actually intialize that
info.
2024-12-08 13:44:02 -05:00
5b80afd5fe ui: Expose download error reason in diagnostics 2024-12-08 13:39:15 -05:00
400c2ff9f9 ProfileDeleteFragment: Stop using non-local returns for no reason 2024-12-08 12:59:27 -05:00
b4f562f90b Make MainActivity permission checks clearer 2024-12-08 12:58:24 -05:00
5a000278d3 Revert meaningless PermissionUtils 2024-12-08 12:55:42 -05:00
38d38523f9 Revert MainActivity changes 2024-12-08 12:54:33 -05:00
022ca1da9d chore: improve readability (#95)
Reviewed-on: PeterCxy/OpenEUICC#95
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-12-08 18:51:48 +01:00
38 changed files with 438 additions and 618 deletions

View file

@ -19,11 +19,6 @@
android:name="im.angry.openeuicc.ui.NotificationsActivity"
android:label="@string/profile_notifications" />
<activity
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
android:label="@string/profile_download"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity
android:name="im.angry.openeuicc.ui.EuiccInfoActivity"
android:label="@string/euicc_info" />
@ -33,9 +28,15 @@
android:label="@string/pref_advanced_logs" />
<activity
android:exported="true"
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
android:label="@string/download_wizard" />
<activity-alias
android:exported="true"
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
android:targetActivity="im.angry.openeuicc.ui.wizard.DownloadWizardActivity" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"

View file

@ -1,12 +1,16 @@
package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
import im.angry.openeuicc.ui.SettingsFragment
open class DefaultUiComponentFactory : UiComponentFactory {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
EuiccManagementFragment.newInstance(slotId, portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
override fun createSettingsFragment(): Fragment = SettingsFragment()
}

View file

@ -1,9 +1,11 @@
package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat
import im.angry.openeuicc.ui.EuiccManagementFragment
interface UiComponentFactory {
fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
fun createNoEuiccPlaceholderFragment(): Fragment
fun createSettingsFragment(): Fragment
}

View file

@ -1,52 +0,0 @@
package im.angry.openeuicc.ui
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragment.SlotSelectedListener, OpenEuiccContextMarker {
override fun onInit() {
lifecycleScope.launch {
val knownChannels = withContext(Dispatchers.IO) {
euiccChannelManager.flowEuiccPorts().map { (slotId, portId) ->
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()
}

View file

@ -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)
}
}
}

View file

@ -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 }

View file

@ -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()

View file

@ -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()
}
}
}

View file

@ -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()
}

View file

@ -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<Preference>("pref_info_app_version")?.apply {
@ -74,9 +74,6 @@ class SettingsFragment: PreferenceFragmentCompat() {
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
findPreference<CheckBoxPreference>("pref_developer_experimental_download_wizard")
?.bindBooleanFlow(preferenceRepository.experimentalDownloadWizardFlow, PreferenceKeys.EXPERIMENTAL_DOWNLOAD_WIZARD)
findPreference<CheckBoxPreference>("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<PreferenceCategory>(overlayKey)!!
val targetCat = findPreference<PreferenceCategory>(targetKey)!!
val prefs = buildList {
for (i in 0..<overlayCat.preferenceCount) {
add(overlayCat.getPreference(i))
}
}
prefs.forEach {
overlayCat.removePreference(it)
targetCat.addPreference(it)
}
overlayCat.parent?.removePreference(overlayCat)
}
}

View file

@ -120,7 +120,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
try {
requireContext().unregisterReceiver(usbPermissionReceiver)
} catch (_: Exception) {
// ignore
}
}
@ -129,7 +129,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
try {
requireContext().unregisterReceiver(usbPermissionReceiver)
} catch (_: Exception) {
// ignore
}
}
@ -155,8 +155,8 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
replace(
R.id.child_container,
appContainer.uiComponentFactory.createEuiccManagementFragment(
EuiccChannelManager.USB_CHANNEL_ID,
0
slotId = EuiccChannelManager.USB_CHANNEL_ID,
portId = 0
)
)
}

View file

@ -1,12 +1,16 @@
package im.angry.openeuicc.ui.wizard
import android.icu.text.SimpleDateFormat
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
import java.io.FileOutputStream
import java.util.Date
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
override val hasNext: Boolean
@ -16,6 +20,16 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS
private lateinit var diagnosticTextView: TextView
private val saveDiagnostics =
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
if (uri == null) return@registerForActivityResult
requireActivity().contentResolver.openFileDescriptor(uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use { os ->
os.write(diagnosticTextView.text.toString().encodeToByteArray())
}
}
}
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<TextView>(R.id.download_wizard_diagnostics_text)
view.requireViewById<View>(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

View file

@ -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()
}

View file

@ -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<SlotInfo> = 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)

View file

@ -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)

View file

@ -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"

View file

@ -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" />
<ImageButton
android:id="@+id/download_wizard_diagnostics_save"
android:src="@drawable/ic_save_as_black"
android:layout_margin="20dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/download_wizard_diagnostics_save"
app:tint="?attr/colorAccent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/download_wizard_diagnostics_text"
android:layout_width="match_parent"

View file

@ -11,10 +11,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"

View file

@ -11,10 +11,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"

View file

@ -11,10 +11,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"

View file

@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1"
app:navigationIcon="?homeAsUpIndicator" />
<View
android:id="@+id/guideline"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/toolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/guideline"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/profile_download_server"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:hint="@string/profile_download_server"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintWidth_percent=".8">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/profile_download_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="15dp"
android:hint="@string/profile_download_code"
app:layout_constraintTop_toBottomOf="@id/profile_download_server"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintWidth_percent=".8"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/profile_download_confirmation_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="15dp"
android:hint="@string/profile_download_confirmation_code"
app:layout_constraintTop_toBottomOf="@id/profile_download_code"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintWidth_percent=".8"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/profile_download_imei"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginBottom="6dp"
android:hint="@string/profile_download_imei"
app:layout_constraintTop_toBottomOf="@id/profile_download_confirmation_code"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/profile_download_free_space"
app:layout_constraintWidth_percent=".8"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/profile_download_free_space"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="11sp"
android:layout_marginBottom="4dp"
app:layout_constraintTop_toBottomOf="@id/profile_download_imei"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/scan"
android:icon="@drawable/ic_scan_black"
android:title="@string/profile_download_scan"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/scan_from_gallery"
android:icon="@drawable/ic_gallery_black"
android:title="@string/profile_download_scan_from_gallery"
app:showAsAction="ifRoom" />
<item
android:id="@+id/ok"
android:icon="@drawable/ic_check_black"
android:title="@string/profile_download_ok"
app:showAsAction="always"/>
</menu>

View file

@ -37,11 +37,6 @@
<string name="profile_download_code">アクティベーションコード</string>
<string name="profile_download_confirmation_code">確認コード (オプション)</string>
<string name="profile_download_imei">IMEI (オプション)</string>
<string name="profile_download_free_space">残りの容量: %s</string>
<string name="profile_download_scan">QR コードをスキャン</string>
<string name="profile_download_scan_from_gallery">ギャラリーから QR コードをスキャン</string>
<string name="profile_download_ok">ダウンロード</string>
<string name="profile_download_failed">eSIM のダウンロードに失敗しました。アクティベーションまたは QR コードを確認してください。</string>
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
<string name="download_wizard">ダウンロードウィザード</string>
@ -53,7 +48,6 @@
<string name="download_wizard_slot_type_removable">リムーバブル</string>
<string name="download_wizard_slot_type_internal">内部</string>
<string name="download_wizard_slot_type_internal_port">内部 - ポート: %d</string>
<string name="download_wizard_slot_eid">eID:</string>
<string name="download_wizard_slot_active_profile">有効なプロファイル:</string>
<string name="download_wizard_slot_free_space">空き容量:</string>
<string name="download_wizard_method_select">eSIM プロファイルをどの方法でダウンロードしますか?</string>
@ -68,6 +62,7 @@
<string name="download_wizard_progress_step_downloading">eSIM プロファイルをダウンロード中です</string>
<string name="download_wizard_progress_step_finalizing">eSIM プロファイルをストレージに読み込み中です</string>
<string name="download_wizard_diagnostics">エラー診断</string>
<string name="download_wizard_diagnostics_error_code">エラーコード: %s</string>
<string name="download_wizard_diagnostics_last_http_status">最終の HTTP ステータス (サーバー): %d</string>
<string name="download_wizard_diagnostics_last_http_response">最終の HTTP レスポンス (サーバー):</string>
<string name="download_wizard_diagnostics_last_http_exception">最終の HTTP 例外:</string>
@ -75,6 +70,8 @@
<string name="download_wizard_diagnostics_last_apdu_response_success">最終の APDU レスポンス (SIM) は成功しました</string>
<string name="download_wizard_diagnostics_last_apdu_response_fail">最終の APDU レスポンス (SIM) は失敗しました</string>
<string name="download_wizard_diagnostics_last_apdu_exception">最終の APDU 例外:</string>
<string name="download_wizard_diagnostics_save">保存</string>
<string name="download_wizard_diagnostics_file_template">%s のエラー診断</string>
<string name="profile_rename_new_name">新しいニックネーム</string>
<string name="profile_delete_confirm">%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。</string>
<string name="profile_delete_confirm_input">削除を確認するには「%s」を入力してください</string>
@ -92,11 +89,10 @@
<string name="euicc_info_activity_title">eUICC 情報 (%s)</string>
<string name="euicc_info_access_mode">アクセスモード</string>
<string name="euicc_info_removable">リムーバブル</string>
<string name="euicc_info_eid">EID</string>
<string name="euicc_info_firmware_version">eUICC OS のバージョン</string>
<string name="euicc_info_globalplatform_version">グローバルプラットフォームのバージョン</string>
<string name="euicc_info_sas_accreditation_number">SAS 認定番号</string>
<string name="euicc_info_pp_version">保護されたプロファイルのバージョン</string>
<string name="euicc_info_pp_version">Protected Profileのバージョン</string>
<string name="euicc_info_free_nvram">NVRAM の空き容量 (eSIM プロファイルストレージ)</string>
<string name="euicc_info_gsma_prod">GSMA プロダクション証明書</string>
<string name="euicc_info_gsma_test">GSMA テスト証明書</string>
@ -125,8 +121,6 @@
<string name="pref_advanced_logs">ログ</string>
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string>
<string name="pref_developer">開発者オプション</string>
<string name="pref_developer_experimental_download_wizard">実験的なダウンロードウィザード</string>
<string name="pref_developer_experimental_download_wizard_desc">実験的な新しいダウンロードウィザードを有効化します。まだ完全に機能していないことにご注意ください。</string>
<string name="pref_developer_ignore_tls_certificate">SM-DP+ TLS 証明書を無視する</string>
<string name="pref_developer_ignore_tls_certificate_desc">SM-DP+ TLS 証明書を無視して任意の RSP を許可します</string>
<string name="pref_info">情報</string>

View file

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc">在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。</string>
<string name="no_profile">此 eSIM 上还没有配置文件</string>
<string name="unknown">未知</string>
<string name="help">帮助</string>
<string name="reload">重新加载卡槽</string>
<string name="channel_name_format">逻辑卡槽 %d</string>
<string name="enabled">已启用</string>
<string name="disabled">已禁用</string>
<string name="provider">提供商:</string>
<string name="profile_class">类型:</string>
<string name="enable">启用</string>
<string name="disable">禁用</string>
<string name="delete">删除</string>
<string name="rename">重命名</string>
<string name="enable_disable_timeout">等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机</string>
<string name="switch_did_not_refresh">操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。</string>
<string name="toast_profile_enable_failed">无法切换到新的 eSIM 配置文件。</string>
<string name="toast_profile_name_too_long">昵称不能超过 64 个字符</string>
<string name="toast_iccid_copied">已复制 ICCID 到剪贴板</string>
<string name="slot_select">选择卡槽</string>
<string name="slot_select_select">选择</string>
<string name="usb_permission">授予 USB 权限</string>
<string name="usb_permission_needed">需要获得访问 USB 智能卡读卡器的权限。</string>
<string name="usb_failed">无法通过 USB 智能卡读卡器连接到 eSIM。</string>
<string name="task_notification">长时间运行的后台任务</string>
<string name="task_profile_download">正在下载 eSIM 配置文件</string>
<string name="task_profile_download_failure">无法下载 eSIM 配置文件</string>
<string name="task_profile_rename">正在重命名 eSIM 配置文件</string>
<string name="task_profile_rename_failure">无法重命名 eSIM 配置文件</string>
<string name="task_profile_delete">正在删除 eSIM 配置文件</string>
<string name="task_profile_delete_failure">无法删除 eSIM 配置文件</string>
<string name="task_profile_switch">正在切换 eSIM 配置文件</string>
<string name="task_profile_switch_failure">无法切换 eSIM 配置文件</string>
<string name="profile_download">添加新 eSIM</string>
<string name="profile_download_server">服务器 (RSP / SM-DP+)</string>
<string name="profile_download_code">激活码</string>
<string name="profile_download_confirmation_code">确认码 (可选)</string>
<string name="profile_download_imei">IMEI (可选)</string>
<string name="profile_download_low_nvram_title">本次下载可能会失败</string>
<string name="profile_download_low_nvram_message">当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载</string>
<string name="profile_rename_new_name">新昵称</string>
<string name="profile_delete_confirm">您确定要删除 %s 吗?此操作是不可逆的。</string>
<string name="profile_delete_confirm_input">请输入\'%s\'以确认删除</string>
<string name="profile_notifications">通知列表</string>
<string name="profile_notifications_detailed_format">通知列表 (%s)</string>
<string name="profile_notifications_show">管理通知</string>
<string name="profile_notifications_help">eSIM 配置文件可以在下载、删除、启用或禁用时向运营商发送通知。此处列出了要发送的这些通知的队列。\n\n在\"设置\"中,您可以指定是否自动发送每种类型的通知。请注意,即使通知已发送,也不会自动从记录中删除,除非队列空间不足。\n\n在这里您可以手动发送或删除每个待处理的通知。</string>
<string name="profile_notification_operation_download">已下载</string>
<string name="profile_notification_operation_delete">已删除</string>
<string name="profile_notification_operation_enable">已启用</string>
<string name="profile_notification_operation_disable">已禁用</string>
<string name="profile_notification_process">处理</string>
<string name="profile_notification_delete">删除</string>
<string name="logs_save">保存日志</string>
<string name="logs_filename_template">%s 的日志</string>
<string name="pref_settings">设置</string>
<string name="pref_notifications">通知</string>
<string name="pref_notifications_desc">操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。</string>
<string name="pref_notifications_download">下载</string>
<string name="pref_notifications_download_desc">发送 <i>下载</i> 配置文件的通知</string>
<string name="pref_notifications_delete">删除</string>
<string name="pref_notifications_delete_desc">发送 <i>删除</i> 配置文件的通知</string>
<string name="pref_notifications_switch">切换</string>
<string name="pref_notifications_switch_desc">发送 <i>切换</i> 配置文件的通知\n注意这种类型的通知是不可靠的。</string>
<string name="pref_advanced">高级</string>
<string name="pref_advanced_disable_safeguard_removable_esim">允许 禁用/删除 已启用的配置文件</string>
<string name="pref_advanced_disable_safeguard_removable_esim_desc">默认情况下,此应用程序会阻止您禁用可插拔 eSIM 中已启用的配置文件。\n因为这样做 <i>有时</i> 会使其无法访问。\n勾选此框以 <i>移除</i> 此保护措施。</string>
<string name="pref_advanced_verbose_logging">记录详细日志</string>
<string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
<string name="pref_advanced_logs">日志</string>
<string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
<string name="pref_info">信息</string>
<string name="pref_info_app_version">App 版本</string>
<string name="pref_info_source_code">源码</string>
<string name="profile_class_testing">测试</string>
<string name="profile_class_provisioning">准备中</string>
<string name="profile_class_operational">可用</string>
<string name="download_wizard">下载向导</string>
<string name="download_wizard_back">返回</string>
<string name="download_wizard_next">下一步</string>
<string name="download_wizard_slot_select">请选择或确认下载目标 eSIM 卡槽:</string>
<string name="download_wizard_slot_title">逻辑卡槽 %d</string>
<string name="download_wizard_slot_type">类型:</string>
<string name="download_wizard_slot_type_removable">可插拔</string>
<string name="download_wizard_slot_type_internal">内置</string>
<string name="download_wizard_slot_type_internal_port">内置, 端口 %d</string>
<string name="download_wizard_slot_active_profile">当前配置文件:</string>
<string name="download_wizard_slot_free_space">剩余空间:</string>
<string name="download_wizard_method_select">您想要如何下载 eSIM 配置文件?</string>
<string name="download_wizard_method_qr_code">用相机扫描二维码</string>
<string name="download_wizard_method_gallery">从图库选择二维码</string>
<string name="download_wizard_method_manual">手动输入</string>
<string name="download_wizard_details">请输入或确认下载 eSIM 的详细信息:</string>
<string name="download_wizard_progress">正在下载您的 eSIM...</string>
<string name="download_wizard_progress_step_preparing">准备中</string>
<string name="download_wizard_progress_step_connecting">正在连接服务器</string>
<string name="download_wizard_progress_step_authenticating">正在向服务器认证您的设备</string>
<string name="download_wizard_progress_step_downloading">正在下载 eSIM 配置文件</string>
<string name="download_wizard_progress_step_finalizing">正在写入 eSIM 配置文件</string>
<string name="download_wizard_diagnostics">错误诊断</string>
<string name="download_wizard_diagnostics_error_code">错误代码: %s</string>
<string name="download_wizard_diagnostics_last_http_status">上次 HTTP 状态码 (来自服务器): %d</string>
<string name="download_wizard_diagnostics_last_http_response">上次 HTTP 应答 (来自服务器):</string>
<string name="download_wizard_diagnostics_last_http_exception">上次 HTTP 错误:</string>
<string name="download_wizard_diagnostics_last_apdu_response">上次 APDU 应答 (来自 SIM): %s</string>
<string name="download_wizard_diagnostics_last_apdu_response_success">上次 APDU 应答 (来自 SIM) 是成功的</string>
<string name="download_wizard_diagnostics_last_apdu_response_fail">上次 APDU 应答 (来自 SIM) 是失败的</string>
<string name="download_wizard_diagnostics_last_apdu_exception">上次 APDU 错误:</string>
<string name="download_wizard_diagnostics_save">保存</string>
<string name="download_wizard_diagnostics_file_template">%s 的错误诊断</string>
<string name="euicc_info">eUICC 详情</string>
<string name="euicc_info_activity_title">eUICC 详情 (%s)</string>
<string name="euicc_info_access_mode">访问方式</string>
<string name="euicc_info_removable">可插拔</string>
<string name="euicc_info_firmware_version">eUICC OS 版本</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform 版本</string>
<string name="euicc_info_sas_accreditation_number">SAS 认证号码</string>
<string name="euicc_info_pp_version">Protected Profile 版本</string>
<string name="euicc_info_free_nvram">NVRAM 剩余空间 (eSIM 存储容量)</string>
<string name="euicc_info_gsma_prod">GSMA 生产环境证书</string>
<string name="euicc_info_gsma_test">GSMA 测试环境证书</string>
<string name="supported">兼容</string>
<string name="unsupported">不兼容</string>
<string name="yes"></string>
<string name="no"></string>
<string name="developer_options_steps">还有 %d 步成为开发者</string>
<string name="developer_options_enabled">你现在是开发者了!</string>
<string name="pref_language">语言</string>
<string name="pref_language_desc">选择 App 语言</string>
<string name="pref_developer">开发者选项</string>
<string name="pref_developer_unfiltered_profile_list">显示未经过滤的配置文件列表</string>
<string name="pref_developer_unfiltered_profile_list_desc">在配置文件列表中包括非生产环境的配置文件</string>
<string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
</resources>

View file

@ -53,11 +53,6 @@
<string name="profile_download_code">Activation Code</string>
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
<string name="profile_download_imei">IMEI (Optional)</string>
<string name="profile_download_free_space">Space remaining: %s</string>
<string name="profile_download_scan">Scan QR Code</string>
<string name="profile_download_scan_from_gallery">Scan QR Code from Gallery</string>
<string name="profile_download_ok">Download</string>
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
<string name="profile_download_low_nvram_title">This download may fail</string>
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
@ -71,7 +66,7 @@
<string name="download_wizard_slot_type_removable">Removable</string>
<string name="download_wizard_slot_type_internal">Internal</string>
<string name="download_wizard_slot_type_internal_port">Internal, port %d</string>
<string name="download_wizard_slot_eid">eID:</string>
<string name="download_wizard_slot_eid" translatable="false">eID:</string>
<string name="download_wizard_slot_active_profile">Active Profile:</string>
<string name="download_wizard_slot_free_space">Free Space:</string>
<string name="download_wizard_method_select">How would you like to download the eSIM profile?</string>
@ -86,6 +81,7 @@
<string name="download_wizard_progress_step_downloading">Downloading eSIM profile</string>
<string name="download_wizard_progress_step_finalizing">Loading eSIM profile into storage</string>
<string name="download_wizard_diagnostics">Error diagnostics</string>
<string name="download_wizard_diagnostics_error_code">Error code: %s</string>
<string name="download_wizard_diagnostics_last_http_status">Last HTTP status (from server): %d</string>
<string name="download_wizard_diagnostics_last_http_response">Last HTTP response (from server):</string>
<string name="download_wizard_diagnostics_last_http_exception">Last HTTP exception:</string>
@ -93,6 +89,8 @@
<string name="download_wizard_diagnostics_last_apdu_response_success">Last APDU response (from SIM) is successful</string>
<string name="download_wizard_diagnostics_last_apdu_response_fail">Last APDU response (from SIM) is a failure</string>
<string name="download_wizard_diagnostics_last_apdu_exception">Last APDU exception:</string>
<string name="download_wizard_diagnostics_save">Save</string>
<string name="download_wizard_diagnostics_file_template">Diagnostics at %s</string>
<string name="profile_rename_new_name">New nickname</string>
@ -116,7 +114,7 @@
<string name="euicc_info_activity_title">eUICC Info (%s)</string>
<string name="euicc_info_access_mode">Access Mode</string>
<string name="euicc_info_removable">Removable</string>
<string name="euicc_info_eid">EID</string>
<string name="euicc_info_eid" translatable="false">EID</string>
<string name="euicc_info_firmware_version">eUICC OS Version</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>
<string name="euicc_info_sas_accreditation_number">SAS Accreditation Number</string>
@ -156,12 +154,10 @@
<string name="pref_advanced_logs">Logs</string>
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
<string name="pref_developer">Developer Options</string>
<string name="pref_developer_experimental_download_wizard">Experimental Download Wizard</string>
<string name="pref_developer_experimental_download_wizard_desc">Enable the experimental new download wizard. Note that it is not fully working yet.</string>
<string name="pref_developer_unfiltered_profile_list">Show unfiltered profile list</string>
<string name="pref_developer_unfiltered_profile_list_desc">Include non-production profiles in the list</string>
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
<string name="pref_developer_ignore_tls_certificate_desc">Ignore SM-DP+ TLS certificate, allow any RSP</string>
<string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
<string name="pref_info">Info</string>
<string name="pref_info_app_version">App Version</string>
<string name="pref_info_source_code">Source Code</string>

View file

@ -2,4 +2,5 @@
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en-US" />
<locale android:name="ja" />
<locale android:name="zh-CN" />
</locale-config>

View file

@ -57,12 +57,6 @@
app:title="@string/pref_developer"
app:iconSpaceReserved="false">
<CheckBoxPreference
app:key="pref_developer_experimental_download_wizard"
app:iconSpaceReserved="false"
app:title="@string/pref_developer_experimental_download_wizard"
app:summary="@string/pref_developer_experimental_download_wizard_desc" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_unfiltered_profile_list"
@ -78,6 +72,7 @@
</PreferenceCategory>
<PreferenceCategory
app:key="pref_info"
app:title="@string/pref_info"
app:iconSpaceReserved="false">
<Preference

View file

@ -2,8 +2,10 @@ package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.SettingsFragment
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
@ -11,4 +13,7 @@ class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createNoEuiccPlaceholderFragment(): Fragment =
UnprivilegedNoEuiccPlaceholderFragment()
override fun createSettingsFragment(): Fragment =
UnprivilegedSettingsFragment()
}

View file

@ -0,0 +1,44 @@
package im.angry.openeuicc.ui
import android.content.ClipData
import android.content.ClipboardManager
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.preference.Preference
import im.angry.easyeuicc.R
import im.angry.openeuicc.util.encodeHex
import java.security.MessageDigest
class UnprivilegedSettingsFragment : SettingsFragment() {
private val firstSigner by lazy {
val packageInfo = requireContext().let {
it.packageManager.getPackageInfo(
it.packageName,
PackageManager.GET_SIGNING_CERTIFICATES,
)
}
packageInfo.signingInfo!!.apkContentsSigners.first().let {
MessageDigest.getInstance("SHA-1")
.apply { update(it.toByteArray()) }
.digest()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
addPreferencesFromResource(R.xml.pref_unprivileged_settings)
mergePreferenceOverlay("pref_info_overlay", "pref_info")
findPreference<Preference>("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
}
}
}
}

View file

@ -30,4 +30,5 @@
<string name="compatibility_check_verdict_unknown_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string>
<string name="compatibility_check_verdict_unknown">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
</resources>

View file

@ -0,0 +1,32 @@
<resources>
<string name="compatibility_check">兼容性检查</string>
<string name="open_sim_toolkit">打开 SIM 卡应用程序</string>
<string name="compatibility_check_system_features">系统功能</string>
<string name="compatibility_check_system_features_desc">您的设备是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的电话功能和 OMAPI 支持。</string>
<string name="compatibility_check_system_features_no_telephony">您的设备没有电话功能。</string>
<string name="compatibility_check_system_features_no_omapi">您的设备/系统未声明支持 OMAPI。这可能是由于缺少硬件支持或者可能仅仅是由于缺少标志。请参阅以下两项检查以确定 OMAPI 是否确实受支持。</string>
<string name="compatibility_check_omapi_connectivity">OMAPI 连接</string>
<string name="compatibility_check_omapi_connectivity_desc">您的设备是否允许通过 OMAPI 访问 SIM 卡上的安全元件?</string>
<string name="compatibility_check_omapi_connectivity_fail">无法通过 OMAPI 检测到 SIM 卡的 Secure Element。如果您尚未在此设备中插入 SIM 卡,请尝试插入一张 SIM 卡并重试此检查。</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">已成功检测到可访问 Secure Element 的卡槽,但仅限于以下 SIM 卡槽:<b>SIM%s</b></string>
<string name="compatibility_check_isdr_channel">ISD-R 通道访问</string>
<string name="compatibility_check_isdr_channel_desc">您的设备是否支持通过 OMAPI 打开 eSIM 的 ISD-R (管理) 通道?</string>
<string name="compatibility_check_isdr_channel_desc_unknown">无法确定是否支持通过 OMAPI 进行 ISD-R 访问。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重试。</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI 只能在以下 SIM 插槽上访问 ISD-R<b>SIM%s</b></string>
<string name="compatibility_check_known_broken">不在已知的 BUG 名单中</string>
<string name="compatibility_check_known_broken_desc">确保您的设备不存在与可插拔 eSIM 相关的错误。</string>
<string name="compatibility_check_known_broken_fail">糟糕,您的设备在访问可插拔 eSIM 时存在错误。这并不表示完全无法使用,但我们不保证该应用在您设备上的行为。</string>
<string name="compatibility_check_usb">USB 读卡器支持</string>
<string name="compatibility_check_usb_desc">您的设备是否支持通过 USB 读卡器管理 eSIM</string>
<string name="compatibility_check_usb_ok">您可以通过此设备上的标准 USB CCID 读取器管理 eSIM (即使您在这里有任何其他检查项失败)。请插入读卡器,然后打开此应用程序以这种方式管理 eSIM。</string>
<string name="compatibility_check_usb_fail">您的设备不支持 USB 读卡器。</string>
<string name="compatibility_check_verdict">结论 (USB 读卡器以外)</string>
<string name="compatibility_check_verdict_desc">根据之前的所有检查,您的设备与可插拔 eSIM 卡兼容的可能性有多大?</string>
<string name="compatibility_check_verdict_ok">您可以使用和管理插入此设备的可插拔 eSIM 卡。</string>
<string name="compatibility_check_verdict_known_broken">已知您的设备在访问可插拔 eSIM 卡时存在问题。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_ok">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。不过,您的设备确实声明支持 OMAPI因此它工作的可能性略高。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_fail">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。由于您的设备未声明支持OMAPI因此更有可能不支持在此设备上管理可插拔 eSIM。\n%s</string>
<string name="compatibility_check_verdict_unknown">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">然而已经加载了eSIM配置文件的可插拔 eSIM 卡仍然可以工作; 即使无法在装置上直接管理可插拔 eSIM 卡中的配置文件,您仍然可以使用 USB 卡读卡器来管理配置文件。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 已拷贝到剪贴板</string>
</resources>

View file

@ -4,6 +4,12 @@
<string name="compatibility_check">Compatibility Check</string>
<string name="open_sim_toolkit">Open SIM Toolkit</string>
<!-- Settings -->
<string name="pref_developer_ara_m" translatable="false">ARA-M SHA-1</string>
<!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string>
<string name="compatibility_check_system_features_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:isPreferenceVisible="false"
app:key="pref_info_overlay">
<Preference
app:enableCopying="true"
app:iconSpaceReserved="false"
app:key="pref_info_ara_m"
app:title="@string/pref_developer_ara_m" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -110,7 +110,7 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
telephonyManager.simSlotMapping = mappings
return
} catch (_: Exception) {
// ignore
}
// Sometimes hardware supports one ordering but not the reverse

View file

@ -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<View>(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<View>(R.id.lui_download).setOnClickListener {
startActivity(Intent(this, DirectProfileDownloadActivity::class.java))
startActivity(Intent(this, DownloadWizardActivity::class.java))
finish()
}
}
}

View file

@ -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<LocalProfileInfo>
): List<View> =
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<Button>(R.id.footer_mep_slot_mapping).setOnClickListener {

View file

@ -16,10 +16,9 @@ class PrivilegedMainActivity : MainActivity() {
menu.findItem(R.id.slot_mapping).isVisible = false
}
if (tm.supportsDSDS) {
val dsds = menu.findItem(R.id.dsds)
dsds.isVisible = true
dsds.isChecked = tm.dsdsEnabled
menu.findItem(R.id.dsds).apply {
isVisible = tm.supportsDSDS
isChecked = tm.dsdsEnabled
}
return true

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc">在此设备上找不到 eUICC 芯片。\n在某些设备上您可能需要先在此应用的菜单中启用双卡支持。</string>
<string name="dsds">双卡</string>
<string name="toast_dsds_switched">双卡支持状态已切换。请等待基带重新启动。</string>
<string name="footer_mep">此卡槽支持多个启用配置文件 (MEP)。要启用或禁用此功能,请使用\"卡槽映射\"工具。</string>
<string name="slot_mapping">卡槽映射</string>
<string name="slot_mapping_logical_slot">逻辑卡槽 %d:</string>
<string name="slot_mapping_port">卡槽 %1$d 端口 %2$d</string>
<string name="slot_mapping_help">您的手机有 %1$d 个逻辑 SIM 卡槽和 %2$d 个物理 SIM 卡槽。%3$s\n\n选择您希望每个逻辑卡槽对应的物理卡槽 和/或 \"端口\"。请注意,并非所有映射模式都受硬件支持。</string>
<string name="slot_mapping_help_mep">\n\n物理卡槽 %1$d 支持多个启用的配置文件 (MEP)。要使用此功能,请将其 %2$d 个虚拟\"端口\"分配给上面显示的不同逻辑卡槽。\n\n启用 MEP 后,\"端口\"会在 OpenEUICC 中显示为共享 eSIM 配置文件的独立的 eSIM 卡槽。</string>
<string name="slot_mapping_help_dsds">\n支持双卡模式但已禁用。如果您的设备带有内置 eSIM 芯片,则默认情况下可能不会启用。更改上面的映射或启用双卡以访问您的 eSIM。</string>
<string name="slot_mapping_completed">您的新卡槽映射已设置完毕。请等待基带刷新卡槽。</string>
<string name="slot_mapping_failure">指定的映射可能无效或硬件不支持您指定的映射。</string>
<string name="lui_title">通过下载 eSIM 连接到移动网络</string>
<string name="lui_desc">您的设备支持 eSIM。要连接到移动网络请下载运营商发布的 eSIM或插入物理 SIM 卡。</string>
<string name="lui_skip">跳过</string>
<string name="lui_download">下载 eSIM</string>
<string name="telephony_manager">TelephonyManager (特权)</string>
</resources>