forked from PeterCxy/OpenEUICC
Compare commits
17 commits
e69fcc8d17
...
29d72e8db0
Author | SHA1 | Date | |
---|---|---|---|
29d72e8db0 | |||
edfd20e624 | |||
f6c50490b8 | |||
c2659ddb69 | |||
5dd9eed4fe | |||
17102be7cb | |||
ece231f17b | |||
db8063cd5f | |||
d3df70501a | |||
53f9459aed | |||
6557ce45a7 | |||
2b86d719dd | |||
7edde1ffa4 | |||
e5753ec2d9 | |||
c528962f29 | |||
dbad335366 | |||
71368b6fc5 |
30 changed files with 479 additions and 108 deletions
|
@ -30,7 +30,20 @@
|
|||
<activity
|
||||
android:exported="true"
|
||||
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||
android:label="@string/download_wizard" />
|
||||
android:label="@string/download_wizard">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- Accepts URIs that begin with "lpa:" -->
|
||||
<!-- for example: "LPA:1$..." -->
|
||||
<!-- refs: https://www.iana.org/assignments/uri-schemes/prov/lpa -->
|
||||
<data android:scheme="lpa"/>
|
||||
<data android:sspPrefix="1$"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:exported="true"
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -446,30 +447,30 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
iccid: String,
|
||||
enable: Boolean, // Enable or disable the profile indicated in iccid
|
||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
) =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_switch),
|
||||
getString(R.string.task_profile_switch_failure),
|
||||
R.drawable.ic_task_switch
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
val (res, refreshed) = euiccChannelManager.withEuiccChannel(
|
||||
slotId,
|
||||
portId
|
||||
) { channel ->
|
||||
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
|
||||
// Sometimes, we *can* enable or disable the profile, but we cannot
|
||||
// send the refresh command to the modem because the profile somehow
|
||||
// makes the modem "busy". In this case, we can still switch by setting
|
||||
// refresh to false, but then the switch cannot take effect until the
|
||||
// user resets the modem manually by toggling airplane mode or rebooting.
|
||||
Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
|
||||
} else {
|
||||
Pair(true, true)
|
||||
}
|
||||
}
|
||||
suspend fun onSwitch(channel: EuiccChannel): Pair<Boolean, Boolean> {
|
||||
val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
|
||||
val response = channel.lpa.switchProfile(iccid, enable, refresh)
|
||||
if (response || !refresh) return Pair(response, refresh)
|
||||
// refresh failed, but refresh was requested
|
||||
// Sometimes, we *can* enable or disable the profile, but we cannot
|
||||
// send the refresh command to the modem because the profile somehow
|
||||
// makes the modem "busy". In this case, we can still switch by setting
|
||||
// refresh to false, but then the switch cannot take effect until the
|
||||
// user resets the modem manually by toggling airplane mode or rebooting.
|
||||
return Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
val (response, refreshed) =
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId, ::onSwitch)
|
||||
|
||||
if (!response) {
|
||||
throw RuntimeException("Could not switch profile")
|
||||
}
|
||||
|
||||
|
@ -495,4 +496,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
preferenceRepository.notificationSwitchFlow.first()
|
||||
}
|
||||
}
|
||||
|
||||
fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_euicc_memory_reset),
|
||||
getString(R.string.task_euicc_memory_reset_failure),
|
||||
R.drawable.ic_euicc_memory_reset
|
||||
) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.euiccMemoryReset()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,8 +38,10 @@ import im.angry.openeuicc.util.*
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||
|
@ -55,6 +57,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var profileList: RecyclerView
|
||||
private var logicalSlotId: Int = -1
|
||||
private lateinit var eid: String
|
||||
|
||||
private val adapter = EuiccProfileAdapter()
|
||||
|
||||
|
@ -131,31 +134,42 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
inflater.inflate(R.menu.fragment_euicc, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.show_notifications -> {
|
||||
if (logicalSlotId != -1) {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.show_notifications).isVisible =
|
||||
logicalSlotId != -1
|
||||
menu.findItem(R.id.euicc_info).isVisible =
|
||||
logicalSlotId != -1
|
||||
menu.findItem(R.id.euicc_memory_reset).isVisible =
|
||||
runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
|
||||
}
|
||||
|
||||
R.id.euicc_info -> {
|
||||
if (logicalSlotId != -1) {
|
||||
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
true
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.show_notifications -> {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.euicc_info -> {
|
||||
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.euicc_memory_reset -> {
|
||||
EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
|
||||
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
protected open suspend fun onCreateFooterViews(
|
||||
parent: ViewGroup,
|
||||
profiles: List<LocalProfileInfo>
|
||||
|
@ -192,6 +206,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
val profiles = withEuiccChannel { channel ->
|
||||
logicalSlotId = channel.logicalSlotId
|
||||
eid = channel.lpa.eID
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
if (unfilteredProfileListFlow.value)
|
||||
channel.lpa.profiles
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
|
||||
import im.angry.openeuicc.util.EuiccProfilesChangedListener
|
||||
import im.angry.openeuicc.util.ensureEuiccChannelManager
|
||||
import im.angry.openeuicc.util.euiccChannelManagerService
|
||||
import im.angry.openeuicc.util.newInstanceEuicc
|
||||
import im.angry.openeuicc.util.notifyEuiccProfilesChanged
|
||||
import im.angry.openeuicc.util.portId
|
||||
import im.angry.openeuicc.util.slotId
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||
companion object {
|
||||
const val TAG = "EuiccMemoryResetFragment"
|
||||
|
||||
private const val FIELD_EID = "eid"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, eid: String) =
|
||||
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
|
||||
putString(FIELD_EID, eid)
|
||||
}
|
||||
}
|
||||
|
||||
private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! }
|
||||
|
||||
private val confirmText: String by lazy {
|
||||
getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8))
|
||||
}
|
||||
|
||||
private inline val isMatched: Boolean
|
||||
get() = editText.text.toString() == confirmText
|
||||
|
||||
private var confirmed = false
|
||||
|
||||
private var toast: Toast? = null
|
||||
set(value) {
|
||||
toast?.cancel()
|
||||
field = value
|
||||
value?.show()
|
||||
}
|
||||
|
||||
private val editText by lazy {
|
||||
EditText(requireContext()).apply {
|
||||
isLongClickable = false
|
||||
typeface = Typeface.MONOSPACE
|
||||
hint = Editable.Factory.getInstance()
|
||||
.newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText))
|
||||
}
|
||||
}
|
||||
|
||||
private inline val alertDialog: AlertDialog
|
||||
get() = requireDialog() as AlertDialog
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?) =
|
||||
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
|
||||
.setTitle(R.string.euicc_memory_reset_title)
|
||||
.setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText))
|
||||
.setView(editText)
|
||||
// Set listener to null to prevent auto closing
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.euicc_memory_reset_invoke_button, null)
|
||||
.create()
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
alertDialog.setCanceledOnTouchOutside(false)
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
.setOnClickListener { if (!confirmed) confirmation() }
|
||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||
.setOnClickListener { if (!confirmed) dismiss() }
|
||||
}
|
||||
|
||||
private fun confirmation() {
|
||||
toast?.cancel()
|
||||
if (!isMatched) {
|
||||
Log.d(TAG, buildString {
|
||||
appendLine("User input is mismatch:")
|
||||
appendLine(editText.text)
|
||||
appendLine(confirmText)
|
||||
})
|
||||
val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched
|
||||
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
|
||||
return
|
||||
}
|
||||
confirmed = true
|
||||
preventUserAction()
|
||||
|
||||
requireParentFragment().lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
euiccChannelManagerService.launchMemoryReset(slotId, portId)
|
||||
.onStart {
|
||||
parentFragment?.notifyEuiccProfilesChanged()
|
||||
|
||||
val resId = R.string.toast_euicc_memory_reset_finitshed
|
||||
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
|
||||
|
||||
runCatching(::dismiss)
|
||||
}
|
||||
.waitDone()
|
||||
}
|
||||
}
|
||||
|
||||
private fun preventUserAction() {
|
||||
editText.isEnabled = false
|
||||
alertDialog.setCancelable(false)
|
||||
alertDialog.setCanceledOnTouchOutside(false)
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
|
||||
}
|
||||
}
|
|
@ -77,6 +77,12 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
|
||||
requirePreference<CheckBoxPreference>("pref_developer_ignore_tls_certificate")
|
||||
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
||||
|
||||
requirePreference<CheckBoxPreference>("pref_developer_refresh_after_switch")
|
||||
.bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow)
|
||||
|
||||
requirePreference<CheckBoxPreference>("pref_developer_euicc_memory_reset")
|
||||
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
|
||||
}
|
||||
|
||||
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
||||
|
@ -122,7 +128,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
|||
return true
|
||||
}
|
||||
|
||||
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||
protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||
lifecycleScope.launch {
|
||||
flow.collect { isChecked = it }
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
|||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||
|
@ -35,6 +34,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
var downloadStarted: Boolean,
|
||||
var downloadTaskID: Long,
|
||||
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
|
||||
var skipMethodSelect: Boolean,
|
||||
var confirmationCodeRequired: Boolean,
|
||||
)
|
||||
|
||||
private lateinit var state: DownloadWizardState
|
||||
|
@ -63,17 +64,21 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
})
|
||||
|
||||
state = DownloadWizardState(
|
||||
null,
|
||||
intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
"",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
null
|
||||
currentStepFragmentClassName = null,
|
||||
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
smdp = "",
|
||||
matchingId = null,
|
||||
confirmationCode = null,
|
||||
imei = null,
|
||||
downloadStarted = false,
|
||||
downloadTaskID = -1,
|
||||
downloadError = null,
|
||||
skipMethodSelect = false,
|
||||
confirmationCodeRequired = false,
|
||||
)
|
||||
|
||||
handleDeepLink()
|
||||
|
||||
progressBar = requireViewById(R.id.progress)
|
||||
nextButton = requireViewById(R.id.download_wizard_next)
|
||||
prevButton = requireViewById(R.id.download_wizard_back)
|
||||
|
@ -113,10 +118,24 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleDeepLink() {
|
||||
// If we get an LPA string from deep-link intents, extract from there.
|
||||
// Note that `onRestoreInstanceState` could override this with user input,
|
||||
// but that _is_ the desired behavior.
|
||||
val uri = intent.data
|
||||
if (uri?.scheme == "lpa") {
|
||||
val parsed = LPAString.parse(uri.schemeSpecificPart)
|
||||
state.smdp = parsed.address
|
||||
state.matchingId = parsed.matchingId
|
||||
state.confirmationCodeRequired = parsed.confirmationCodeRequired
|
||||
state.skipMethodSelect = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
outContent?.webUri = try {
|
||||
val activationCode = ActivationCode(
|
||||
val activationCode = LPAString(
|
||||
state.smdp,
|
||||
state.matchingId,
|
||||
null,
|
||||
|
@ -138,6 +157,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
outState.putString("imei", state.imei)
|
||||
outState.putBoolean("downloadStarted", state.downloadStarted)
|
||||
outState.putLong("downloadTaskID", state.downloadTaskID)
|
||||
outState.putBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
|
@ -154,6 +174,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
state.downloadStarted =
|
||||
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
||||
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
||||
state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
|
||||
state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
|
||||
}
|
||||
|
||||
private fun onPrevPressed() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.util.Patterns
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
|
@ -36,7 +37,11 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
DownloadWizardProgressFragment()
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardMethodSelectFragment()
|
||||
if (state.skipMethodSelect) {
|
||||
DownloadWizardSlotSelectFragment()
|
||||
} else {
|
||||
DownloadWizardMethodSelectFragment()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -51,6 +56,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
smdp.editText!!.addTextChangedListener {
|
||||
updateInputCompleteness()
|
||||
}
|
||||
confirmationCode.editText!!.addTextChangedListener {
|
||||
updateInputCompleteness()
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
|
@ -61,6 +69,15 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
confirmationCode.editText!!.setText(state.confirmationCode)
|
||||
imei.editText!!.setText(state.imei)
|
||||
updateInputCompleteness()
|
||||
|
||||
if (state.confirmationCodeRequired) {
|
||||
confirmationCode.editText!!.requestFocus()
|
||||
confirmationCode.editText!!.hint =
|
||||
getString(R.string.profile_download_confirmation_code_required)
|
||||
} else {
|
||||
confirmationCode.editText!!.hint =
|
||||
getString(R.string.profile_download_confirmation_code)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
@ -70,6 +87,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
|||
|
||||
private fun updateInputCompleteness() {
|
||||
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
|
||||
if (state.confirmationCodeRequired) {
|
||||
inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
|
||||
}
|
||||
refreshButtons()
|
||||
}
|
||||
}
|
|
@ -42,21 +42,16 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
|
||||
if (result == null) return@registerForActivityResult
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
requireContext().contentResolver.openInputStream(result)?.let { input ->
|
||||
val bmp = BitmapFactory.decodeStream(input)
|
||||
input.close()
|
||||
|
||||
decodeQrFromBitmap(bmp)?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
processLpaString(it)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
val decoded = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
requireContext().contentResolver.openInputStream(result)?.use { input ->
|
||||
BitmapFactory.decodeStream(input).use(::decodeQrFromBitmap)
|
||||
}
|
||||
|
||||
bmp.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
decoded.getOrNull()?.let { processLpaString(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,18 +121,10 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
|
||||
private fun processLpaString(input: String) {
|
||||
try {
|
||||
val parsed = ActivationCode.fromString(input)
|
||||
val parsed = LPAString.parse(input)
|
||||
state.smdp = parsed.address
|
||||
state.matchingId = parsed.matchingId
|
||||
if (parsed.confirmationCodeRequired) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.profile_download_required_confirmation_code)
|
||||
setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||
setCancelable(true)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
show()
|
||||
}
|
||||
}
|
||||
state.confirmationCodeRequired = parsed.confirmationCodeRequired
|
||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
|
@ -150,14 +137,19 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
|||
}
|
||||
}
|
||||
|
||||
private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
|
||||
private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
|
||||
private val icon = root.requireViewById<ImageView>(R.id.download_method_icon)
|
||||
private val title = root.requireViewById<TextView>(R.id.download_method_title)
|
||||
|
||||
fun bind(item: DownloadMethod) {
|
||||
icon.setImageResource(item.iconRes)
|
||||
title.setText(item.titleRes)
|
||||
root.setOnClickListener { item.onClick() }
|
||||
root.setOnClickListener {
|
||||
// If the user elected to use another download method, reset the confirmation code flag
|
||||
// too
|
||||
state.confirmationCodeRequired = false
|
||||
item.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
get() = true
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardMethodSelectFragment()
|
||||
if (state.skipMethodSelect) {
|
||||
DownloadWizardDetailsFragment()
|
||||
} else {
|
||||
DownloadWizardMethodSelectFragment()
|
||||
}
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
data class ActivationCode(
|
||||
data class LPAString(
|
||||
val address: String,
|
||||
val matchingId: String? = null,
|
||||
val oid: String? = null,
|
||||
val confirmationCodeRequired: Boolean = false,
|
||||
val matchingId: String?,
|
||||
val oid: String?,
|
||||
val confirmationCodeRequired: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun fromString(input: String): ActivationCode {
|
||||
fun parse(input: String): LPAString {
|
||||
val components = input.removePrefix("LPA:").split('$')
|
||||
if (components.size < 2 || components[0] != "1") {
|
||||
throw IllegalArgumentException("Invalid activation code format")
|
||||
}
|
||||
return ActivationCode(
|
||||
return LPAString(
|
||||
address = components[1].trim(),
|
||||
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
|
||||
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
|
|
@ -26,6 +26,7 @@ internal object PreferenceKeys {
|
|||
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
|
||||
|
||||
// ---- Advanced ----
|
||||
val REFRESH_AFTER_SWITCH = booleanPreferencesKey("refresh_after_switch")
|
||||
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
|
||||
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
|
||||
|
||||
|
@ -33,9 +34,10 @@ internal object PreferenceKeys {
|
|||
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
||||
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
||||
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
|
||||
}
|
||||
|
||||
class PreferenceRepository(private val context: Context) {
|
||||
open class PreferenceRepository(private val context: Context) {
|
||||
// Expose flows so that we can also handle default values
|
||||
// ---- Profile Notifications ----
|
||||
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
|
||||
|
@ -47,24 +49,23 @@ class PreferenceRepository(private val context: Context) {
|
|||
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
|
||||
|
||||
// ---- Developer Options ----
|
||||
val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)
|
||||
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
||||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||
val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
|
||||
|
||||
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
|
||||
protected fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
|
||||
PreferenceFlowWrapper(context, key, defaultValue)
|
||||
}
|
||||
|
||||
class PreferenceFlowWrapper<T> private constructor(
|
||||
private val context: Context,
|
||||
private val key: Preferences.Key<T>,
|
||||
inner: Flow<T>
|
||||
inner: Flow<T>,
|
||||
) : Flow<T> by inner {
|
||||
internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) : this(
|
||||
context,
|
||||
key,
|
||||
context.dataStore.data.map { it[key] ?: defaultValue }
|
||||
)
|
||||
internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) :
|
||||
this(context, key, context.dataStore.data.map { it[key] ?: defaultValue })
|
||||
|
||||
suspend fun updatePreference(value: T) {
|
||||
context.dataStore.edit { it[key] = value }
|
||||
|
|
|
@ -86,6 +86,13 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c
|
|||
}
|
||||
}
|
||||
|
||||
inline fun <T> Bitmap.use(f: (Bitmap) -> T): T =
|
||||
try {
|
||||
f(this)
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
|
||||
fun decodeQrFromBitmap(bmp: Bitmap): String? =
|
||||
runCatching {
|
||||
val pixels = IntArray(bmp.width * bmp.height)
|
||||
|
|
18
app-common/src/main/res/drawable/ic_euicc_memory_reset.xml
Normal file
18
app-common/src/main/res/drawable/ic_euicc_memory_reset.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="21"
|
||||
android:viewportHeight="21">
|
||||
<path
|
||||
android:pathData="m3.578,6.487c1.385,-2.384 3.966,-3.987 6.922,-3.987 4.418,0 8,3.582 8,8s-3.582,8 -8,8 -8,-3.582 -8,-8"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="m7.5,6.5l-4,0l-0,-4"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
|
@ -10,4 +10,9 @@
|
|||
android:id="@+id/euicc_info"
|
||||
android:title="@string/euicc_info"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/euicc_memory_reset"
|
||||
android:title="@string/euicc_memory_reset"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -42,12 +42,11 @@
|
|||
<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_confirmation_code_required">確認コード (必須)</string>
|
||||
<string name="profile_download_imei">IMEI (オプション)</string>
|
||||
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
|
||||
<string name="profile_download_low_nvram_title">残り容量が少ない</string>
|
||||
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
|
||||
<string name="profile_download_no_lpa_string">クリップボードに LPA コードがありません</string>
|
||||
<string name="profile_download_required_confirmation_code">確認コードが必要です</string>
|
||||
<string name="profile_download_required_confirmation_code_message">クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。</string>
|
||||
<string name="profile_download_incorrect_lpa_string">解析できません</string>
|
||||
<string name="profile_download_incorrect_lpa_string_message">QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。</string>
|
||||
<string name="download_wizard">ダウンロードウィザード</string>
|
||||
|
@ -151,4 +150,16 @@
|
|||
<string name="pref_info">情報</string>
|
||||
<string name="pref_info_app_version">アプリバージョン</string>
|
||||
<string name="pref_info_source_code">ソースコード</string>
|
||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">確認文字列が一致しません</string>
|
||||
<string name="toast_euicc_memory_reset_finitshed">このチップは消去されました</string>
|
||||
<string name="task_euicc_memory_reset">eSIM チップを消去しています</string>
|
||||
<string name="task_euicc_memory_reset_failure">eSIM チップの消去は失敗しました</string>
|
||||
<string name="euicc_memory_reset">eSIM を消去する</string>
|
||||
<string name="euicc_memory_reset_title">eSIM を消去する</string>
|
||||
<string name="euicc_memory_reset_message">このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %s\n\n%s</string>
|
||||
<string name="euicc_memory_reset_hint_text">確認のため、ここに「%s」を入力してください</string>
|
||||
<string name="euicc_memory_reset_confirm_text">EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。</string>
|
||||
<string name="euicc_memory_reset_invoke_button">消去する</string>
|
||||
<string name="pref_developer_euicc_memory_reset">eUICC の消去を可能にする</string>
|
||||
<string name="pref_developer_euicc_memory_reset_desc">この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。</string>
|
||||
</resources>
|
||||
|
|
|
@ -37,8 +37,14 @@
|
|||
<string name="profile_download_server">服务器 (RSP / SM-DP+)</string>
|
||||
<string name="profile_download_code">激活码</string>
|
||||
<string name="profile_download_confirmation_code">确认码 (可选)</string>
|
||||
<string name="toast_sn_copied">已复制序列号到剪贴板</string>
|
||||
<string name="euicc_info_sku">产品名称</string>
|
||||
<string name="euicc_info_sn">产品序列号</string>
|
||||
<string name="euicc_info_bl_ver">产品 Bootloader 版本</string>
|
||||
<string name="euicc_info_fw_ver">产品固件版本</string>
|
||||
<string name="profile_download_confirmation_code_required">确认码 (必需)</string>
|
||||
<string name="profile_download_imei">IMEI (可选)</string>
|
||||
<string name="profile_download_low_nvram_title">本次下载可能会失败</string>
|
||||
<string name="profile_download_low_nvram_title">剩余空间不足</string>
|
||||
<string name="profile_download_low_nvram_message">当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?</string>
|
||||
<string name="logs_saved_message">日志已保存到指定路径。需要通过其他 App 分享吗?</string>
|
||||
<string name="profile_rename_new_name">新昵称</string>
|
||||
|
@ -144,6 +150,16 @@
|
|||
<string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
|
||||
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
|
||||
<string name="information_unavailable">无信息</string>
|
||||
<string name="profile_download_required_confirmation_code">需要确认码</string>
|
||||
<string name="profile_download_required_confirmation_code_message">您扫描的二维码或粘贴的 LPA 码需要一个额外的确认码</string>
|
||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">输入的确认文本不匹配</string>
|
||||
<string name="toast_euicc_memory_reset_finitshed">此芯片已被擦除</string>
|
||||
<string name="task_euicc_memory_reset">正在擦除 eSIM 芯片</string>
|
||||
<string name="task_euicc_memory_reset_failure">eSIM 芯片擦除失败</string>
|
||||
<string name="euicc_memory_reset">擦除 eSIM 芯片</string>
|
||||
<string name="euicc_memory_reset_title">擦除 eSIM 芯片</string>
|
||||
<string name="euicc_memory_reset_message">请确认删除此芯片上的所有配置文件,并了解此操作不可逆。\n\nEID: %s\n\n%s</string>
|
||||
<string name="euicc_memory_reset_hint_text">请在此处输入「%s」以确认</string>
|
||||
<string name="euicc_memory_reset_confirm_text">我确认擦除 EID 以 %s 结尾的芯片,并了解此操作不可逆</string>
|
||||
<string name="euicc_memory_reset_invoke_button">擦除</string>
|
||||
<string name="pref_developer_euicc_memory_reset">允许擦除 eUICC</string>
|
||||
<string name="pref_developer_euicc_memory_reset_desc">此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。</string>
|
||||
</resources>
|
|
@ -37,8 +37,14 @@
|
|||
<string name="profile_download_server">伺服器 (RSP / SM-DP+)</string>
|
||||
<string name="profile_download_code">啟用碼</string>
|
||||
<string name="profile_download_confirmation_code">確認碼 (可選)</string>
|
||||
<string name="toast_sn_copied">已複製序號到剪貼簿</string>
|
||||
<string name="euicc_info_sku">產品名稱</string>
|
||||
<string name="euicc_info_sn">產品序號</string>
|
||||
<string name="euicc_info_bl_ver">產品引導程式版本</string>
|
||||
<string name="euicc_info_fw_ver">產品韌體版本</string>
|
||||
<string name="profile_download_confirmation_code_required">確認碼 (必需)</string>
|
||||
<string name="profile_download_imei">IMEI (可選)</string>
|
||||
<string name="profile_download_low_nvram_title">本次下載可能會失敗</string>
|
||||
<string name="profile_download_low_nvram_title">剩餘空間不足</string>
|
||||
<string name="profile_download_low_nvram_message">目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?</string>
|
||||
<string name="logs_saved_message">日誌已儲存到指定路徑。需要透過其他 App 分享嗎?</string>
|
||||
<string name="profile_rename_new_name">新名稱</string>
|
||||
|
@ -144,4 +150,16 @@
|
|||
<string name="pref_developer_ignore_tls_certificate">忽略 SM-DP+ 的 TLS 證書</string>
|
||||
<string name="pref_developer_ignore_tls_certificate_desc">允許 RSP 伺服器使用任意證書</string>
|
||||
<string name="information_unavailable">無資訊</string>
|
||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">輸入的確認文字不匹配</string>
|
||||
<string name="toast_euicc_memory_reset_finitshed">此晶片已被擦除</string>
|
||||
<string name="task_euicc_memory_reset">正在擦除 eSIM 晶片</string>
|
||||
<string name="task_euicc_memory_reset_failure">eSIM 晶片擦除失敗</string>
|
||||
<string name="euicc_memory_reset">擦除 eSIM 晶片</string>
|
||||
<string name="euicc_memory_reset_title">擦除 eSIM 晶片</string>
|
||||
<string name="euicc_memory_reset_message">請確認刪除此晶片上的所有配置文件,並了解此操作不可逆。\n\nEID: %s\n\n%s</string>
|
||||
<string name="euicc_memory_reset_hint_text">請在此輸入「%s」以確認</string>
|
||||
<string name="euicc_memory_reset_confirm_text">我確認擦除 EID 以 %s 結尾的晶片,並了解此操作不可逆</string>
|
||||
<string name="euicc_memory_reset_invoke_button">擦除</string>
|
||||
<string name="pref_developer_euicc_memory_reset">允許擦除 eUICC</string>
|
||||
<string name="pref_developer_euicc_memory_reset_desc">此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。</string>
|
||||
</resources>
|
|
@ -30,8 +30,10 @@
|
|||
|
||||
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
||||
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||
<string name="toast_euicc_memory_reset_finitshed">This chip has been erased</string>
|
||||
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
||||
<string name="toast_sn_copied">Serial Number copied to clipboard</string>
|
||||
<string name="toast_sn_copied">Serial number copied to clipboard</string>
|
||||
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||
<string name="toast_atr_copied">ATR copied to clipboard</string>
|
||||
|
||||
|
@ -48,18 +50,19 @@
|
|||
<string name="task_profile_delete_failure">Failed to delete eSIM profile</string>
|
||||
<string name="task_profile_switch">Switching eSIM profile</string>
|
||||
<string name="task_profile_switch_failure">Failed to switch eSIM profile</string>
|
||||
<string name="task_euicc_memory_reset">Erasing eSIM chip</string>
|
||||
<string name="task_euicc_memory_reset_failure">Failed to erase eSIM chip</string>
|
||||
|
||||
<string name="profile_download">New eSIM</string>
|
||||
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
|
||||
<string name="profile_download_code">Activation Code</string>
|
||||
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
|
||||
<string name="profile_download_confirmation_code_required">Confirmation Code (Required)</string>
|
||||
<string name="profile_download_imei">IMEI (Optional)</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>
|
||||
<string name="profile_download_low_nvram_title">Low remaining capacity</string>
|
||||
<string name="profile_download_low_nvram_message">This profile may fail to download due to low remaining capacity.</string>
|
||||
<string name="profile_download_no_lpa_string">No LPA code found in clipboard</string>
|
||||
<string name="profile_download_required_confirmation_code">Confirmation Code Required</string>
|
||||
<string name="profile_download_required_confirmation_code_message">Please provide a confirmation code as required by the scanned QR code or LPA code from clipboard.</string>
|
||||
<string name="profile_download_incorrect_lpa_string">Unable to parse</string>
|
||||
<string name="profile_download_incorrect_lpa_string_message">Could not parse QR code or clipboard content as a LPA code.</string>
|
||||
|
||||
|
@ -143,6 +146,13 @@
|
|||
<string name="euicc_info_ci_unknown">Unknown eSIM CI</string>
|
||||
<string name="euicc_info_atr" translatable="false">Answer To Reset (ATR)</string>
|
||||
|
||||
<string name="euicc_memory_reset">Erase eUICC</string>
|
||||
<string name="euicc_memory_reset_title">Erase eUICC</string>
|
||||
<string name="euicc_memory_reset_message">Please confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %s\n\n%s</string>
|
||||
<string name="euicc_memory_reset_hint_text">Type \'%s\' here to confirm</string>
|
||||
<string name="euicc_memory_reset_confirm_text">I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE</string>
|
||||
<string name="euicc_memory_reset_invoke_button">Erase</string>
|
||||
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
|
||||
|
@ -162,6 +172,8 @@
|
|||
<string name="pref_notifications_switch">Switching</string>
|
||||
<string name="pref_notifications_switch_desc">Send notifications for <i>switching</i> profiles\nNote that this type of notification is unreliable.</string>
|
||||
<string name="pref_advanced">Advanced</string>
|
||||
<string name="pref_advanced_refresh_after_switch">Refresh status after switching</string>
|
||||
<string name="pref_advanced_refresh_after_switch_desc">If crash after switch, Please trying disable the function</string>
|
||||
<string name="pref_advanced_disable_safeguard_removable_esim">Allow Disabling / Deleting Active Profile</string>
|
||||
<string name="pref_advanced_disable_safeguard_removable_esim_desc">By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may <i>sometimes</i> render it inaccessible.\nCheck this box to <i>remove</i> this safeguard.</string>
|
||||
<string name="pref_advanced_verbose_logging">Verbose Logging</string>
|
||||
|
@ -175,6 +187,8 @@
|
|||
<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">Accept any TLS certificate used by the RSP server</string>
|
||||
<string name="pref_developer_euicc_memory_reset">Allow erasing eUICC</string>
|
||||
<string name="pref_developer_euicc_memory_reset_desc">This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.</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>
|
||||
|
|
|
@ -52,11 +52,17 @@
|
|||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
|
||||
app:key="pref_developer"
|
||||
app:title="@string/pref_developer"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="pref_developer_refresh_after_switch"
|
||||
app:summary="@string/pref_advanced_refresh_after_switch_desc"
|
||||
app:title="@string/pref_advanced_refresh_after_switch" />
|
||||
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="pref_developer_unfiltered_profile_list"
|
||||
|
@ -69,7 +75,14 @@
|
|||
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
|
||||
app:title="@string/pref_developer_ignore_tls_certificate" />
|
||||
|
||||
</PreferenceCategory>
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:isPreferenceVisible="false"
|
||||
app:key="pref_developer_euicc_memory_reset"
|
||||
app:summary="@string/pref_developer_euicc_memory_reset_desc"
|
||||
app:title="@string/pref_developer_euicc_memory_reset" />
|
||||
|
||||
</im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="pref_info"
|
||||
|
|
|
@ -6,9 +6,11 @@ import android.content.pm.PackageManager
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
import im.angry.easyeuicc.R
|
||||
import im.angry.openeuicc.util.encodeHex
|
||||
import im.angry.openeuicc.util.preferenceRepository
|
||||
import java.security.MessageDigest
|
||||
|
||||
class UnprivilegedSettingsFragment : SettingsFragment() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.util.Log
|
|||
import im.angry.openeuicc.OpenEuiccApplication
|
||||
import im.angry.openeuicc.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
|
||||
|
@ -21,7 +22,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
super.tryOpenEuiccChannel(port)?.let { return it }
|
||||
}
|
||||
|
||||
if (port.card.isEuicc) {
|
||||
if (port.card.isEuicc || (context.preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow.first()) {
|
||||
Log.i(
|
||||
DefaultEuiccChannelManager.TAG,
|
||||
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
|
||||
|
|
|
@ -6,6 +6,7 @@ import im.angry.openeuicc.core.EuiccChannelManagerFactory
|
|||
import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
|
||||
import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
|
||||
import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
||||
override val euiccChannelManager: EuiccChannelManager by lazy {
|
||||
|
@ -27,4 +28,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
|||
override val customizableTextProvider by lazy {
|
||||
PrivilegedCustomizableTextProvider(context)
|
||||
}
|
||||
|
||||
override val preferenceRepository by lazy {
|
||||
PrivilegedPreferenceRepository(context)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
import im.angry.openeuicc.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class PrivilegedSettingsFragment : SettingsFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey)
|
||||
addPreferencesFromResource(R.xml.pref_privileged_settings)
|
||||
mergePreferenceOverlay("pref_developer_overlay", "pref_developer")
|
||||
|
||||
// It's stupid to _disable_ things for privileged, but for now, the per-app locale picker
|
||||
// is not usable for apps signed with the platform key.
|
||||
// ref: <https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-15.0.0_r6/src/com/android/settings/applications/AppLocaleUtil.java#60>
|
||||
|
@ -13,5 +19,9 @@ class PrivilegedSettingsFragment : SettingsFragment() {
|
|||
// eventually work for platform-signed apps. Or, at some point we might introduce our own
|
||||
// locale picker, which hopefully works whether privileged or not.
|
||||
requirePreference<Preference>("pref_advanced_language").isVisible = false
|
||||
|
||||
// Force use TelephonyManager API
|
||||
requirePreference<CheckBoxPreference>("pref_developer_tmapi_removable")
|
||||
.bindBooleanFlow((preferenceRepository as PrivilegedPreferenceRepository).removableTelephonyManagerFlow)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
|
||||
internal object PrivilegedPreferenceKeys {
|
||||
// ---- Developer Options ----
|
||||
val REMOVABLE_TELEPHONY_MANAGER = booleanPreferencesKey("removable_telephony_manager")
|
||||
}
|
||||
|
||||
class PrivilegedPreferenceRepository(context: Context) : PreferenceRepository(context) {
|
||||
// ---- Developer Options ----
|
||||
val removableTelephonyManagerFlow =
|
||||
bindFlow(PrivilegedPreferenceKeys.REMOVABLE_TELEPHONY_MANAGER, false)
|
||||
}
|
|
@ -17,4 +17,6 @@
|
|||
<string name="lui_desc">使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。</string>
|
||||
<string name="lui_skip">スキップ</string>
|
||||
<string name="lui_download">eSIM をダウンロード</string>
|
||||
<string name="pref_developer_telephony_manager_removable">TelephonyManagerをどこでも使用</string>
|
||||
<string name="pref_developer_telephony_manager_removable_desc">デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。</string>
|
||||
</resources>
|
||||
|
|
|
@ -17,4 +17,6 @@
|
|||
<string name="lui_skip">跳过</string>
|
||||
<string name="lui_download">下载 eSIM</string>
|
||||
<string name="telephony_manager">TelephonyManager (特权)</string>
|
||||
<string name="pref_developer_telephony_manager_removable">全局使用 TelephonyManager</string>
|
||||
<string name="pref_developer_telephony_manager_removable_desc">在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。</string>
|
||||
</resources>
|
|
@ -17,4 +17,6 @@
|
|||
<string name="lui_skip">跳過</string>
|
||||
<string name="lui_download">下載 eSIM</string>
|
||||
<string name="telephony_manager">TelephonyManager (特權)</string>
|
||||
<string name="pref_developer_telephony_manager_removable">全域使用 TelephonyManager</string>
|
||||
<string name="pref_developer_telephony_manager_removable_desc">在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。</string>
|
||||
</resources>
|
|
@ -22,4 +22,8 @@
|
|||
<string name="lui_desc">Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.</string>
|
||||
<string name="lui_skip">Skip</string>
|
||||
<string name="lui_download">Download eSIM</string>
|
||||
|
||||
<!-- Preference -->
|
||||
<string name="pref_developer_telephony_manager_removable">Use TelephonyManager everywhere</string>
|
||||
<string name="pref_developer_telephony_manager_removable_desc">By default, only OMAPI is attempted for removable eUICCs to match what is done in unprivileged mode (i.e. EasyEUICC). This may not work well on some devices. Select this option to force the use of TelephonyManager even for removable eUICCs.</string>
|
||||
</resources>
|
12
app/src/main/res/xml/pref_privileged_settings.xml
Normal file
12
app/src/main/res/xml/pref_privileged_settings.xml
Normal 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_developer_overlay">
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="pref_developer_tmapi_removable"
|
||||
app:summary="@string/pref_developer_telephony_manager_removable_desc"
|
||||
app:title="@string/pref_developer_telephony_manager_removable" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -126,8 +126,11 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
|||
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
|
||||
if (ret < 0) {
|
||||
ret = - (int) es10b_load_bound_profile_package_result.errorReason;
|
||||
goto out;
|
||||
}
|
||||
|
||||
euicc_http_cleanup(ctx);
|
||||
|
||||
out:
|
||||
// We expect Java side to call cancelSessions after any error -- thus, `euicc_http_cleanup` is done there
|
||||
// This is so that Java side can access the last HTTP and/or APDU errors when we return.
|
||||
|
|
Loading…
Add table
Reference in a new issue