forked from PeterCxy/OpenEUICC
Compare commits
No commits in common. "90878438f90659a46b5aa0e0f72ff21d9ad40d3c" and "c4b513fc0affa3ef0a3a1d3ad6adafa009590608" have entirely different histories.
90878438f9
...
c4b513fc0a
35 changed files with 143 additions and 1380 deletions
27
.gitignore
vendored
27
.gitignore
vendored
|
@ -1,11 +1,20 @@
|
|||
/.gradle
|
||||
/captures
|
||||
|
||||
# Configuration files
|
||||
|
||||
/keystore.properties
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
|
||||
# macOS
|
||||
|
||||
/keystore.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/libs/**/build
|
||||
/buildSrc/build
|
||||
/app-deps/libs
|
14
.idea/.gitignore
generated
vendored
14
.idea/.gitignore
generated
vendored
|
@ -1,13 +1,3 @@
|
|||
/shelf
|
||||
/caches
|
||||
/libraries
|
||||
/assetWizardSettings.xml
|
||||
/deploymentTargetDropDown.xml
|
||||
/gradle.xml
|
||||
/misc.xml
|
||||
/modules.xml
|
||||
/navEditor.xml
|
||||
/runConfigurations.xml
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
|
||||
**/*.iml
|
39
.idea/gradle.xml
generated
Normal file
39
.idea/gradle.xml
generated
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<compositeConfiguration>
|
||||
<compositeBuild compositeDefinitionSource="SCRIPT">
|
||||
<builds>
|
||||
<build path="$PROJECT_DIR$/buildSrc" name="buildSrc">
|
||||
<projects>
|
||||
<project path="$PROJECT_DIR$/buildSrc" />
|
||||
</projects>
|
||||
</build>
|
||||
</builds>
|
||||
</compositeBuild>
|
||||
</compositeConfiguration>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="/usr/share/java/gradle" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/app-common" />
|
||||
<option value="$PROJECT_DIR$/app-deps" />
|
||||
<option value="$PROJECT_DIR$/app-unpriv" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/libs" />
|
||||
<option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
|
||||
<option value="$PROJECT_DIR$/libs/hidden-apis-stub" />
|
||||
<option value="$PROJECT_DIR$/libs/lpac-jni" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
25
.idea/misc.xml
generated
Normal file
25
.idea/misc.xml
generated
Normal file
|
@ -0,0 +1,25 @@
|
|||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="app/src/main/res/drawable/ic_add.xml" value="0.2015" />
|
||||
<entry key="app/src/main/res/layout/activity_main.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/layout/euicc_profile.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/layout/fragment_euicc.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/layout/fragment_profile_download.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/layout/fragment_profile_rename.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/menu/activity_main.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/menu/activity_main_slot_spinner.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/menu/fragment_profile_download.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/menu/fragment_profile_rename.xml" value="0.19375" />
|
||||
<entry key="app/src/main/res/menu/profile_options.xml" value="0.19375" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
|
@ -46,7 +46,7 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
|
|||
imei: String?,
|
||||
confirmationCode: String?,
|
||||
callback: ProfileDownloadCallback
|
||||
) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
|
||||
): Boolean = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
|
||||
|
||||
override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber)
|
||||
|
||||
|
|
|
@ -15,19 +15,16 @@ import im.angry.openeuicc.core.EuiccChannelManager
|
|||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.isActive
|
||||
|
@ -68,18 +65,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
*/
|
||||
suspend fun Flow<ForegroundTaskState>.waitDone(): Throwable? =
|
||||
(this.last() as ForegroundTaskState.Done).error
|
||||
|
||||
/**
|
||||
* Apply transform to a ForegroundTaskState flow so that it completes when a Done is seen.
|
||||
*
|
||||
* This must be applied each time a flow is returned for subscription purposes. If applied
|
||||
* beforehand, we lose the ability to subscribe multiple times.
|
||||
*/
|
||||
private fun Flow<ForegroundTaskState>.applyCompletionTransform() =
|
||||
transformWhile {
|
||||
emit(it)
|
||||
it !is ForegroundTaskState.Done
|
||||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
|
@ -113,25 +98,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
private val foregroundTaskState: MutableStateFlow<ForegroundTaskState> =
|
||||
MutableStateFlow(ForegroundTaskState.Idle)
|
||||
|
||||
/**
|
||||
* A simple wrapper over a flow with taskId added.
|
||||
*
|
||||
* taskID is the exact millisecond-precision timestamp when the task is launched.
|
||||
*/
|
||||
class ForegroundTaskSubscriberFlow(val taskId: Long, inner: Flow<ForegroundTaskState>) :
|
||||
Flow<ForegroundTaskState> by inner
|
||||
|
||||
/**
|
||||
* A cache of subscribers to 5 recently-launched foreground tasks, identified by ID
|
||||
*
|
||||
* Only one can be run at the same time, but those that are done will be kept in this
|
||||
* map for a little while -- because UI components may be stopped and recreated while
|
||||
* tasks are running. Having this buffer allows the components to re-subscribe even if
|
||||
* the task completes while they are being recreated.
|
||||
*/
|
||||
private val foregroundTaskSubscribers: MutableMap<Long, SharedFlow<ForegroundTaskState>> =
|
||||
mutableMapOf()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return LocalBinder()
|
||||
|
@ -209,26 +175,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the subscriber to a foreground task that is recently launched.
|
||||
*
|
||||
* null if the task doesn't exist, or was launched too long ago.
|
||||
*/
|
||||
fun recoverForegroundTaskSubscriber(taskId: Long): ForegroundTaskSubscriberFlow? =
|
||||
foregroundTaskSubscribers[taskId]?.let {
|
||||
ForegroundTaskSubscriberFlow(taskId, it.applyCompletionTransform())
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a potentially blocking foreground task in this service's lifecycle context.
|
||||
* This function does not block, but returns a Flow that emits ForegroundTaskState
|
||||
* updates associated with this task. The last update the returned flow will emit is
|
||||
* always ForegroundTaskState.Done.
|
||||
*
|
||||
* The returned flow can only be subscribed to once even though the underlying implementation
|
||||
* is a SharedFlow. This is due to the need to apply transformations so that the stream
|
||||
* actually completes. In order to subscribe multiple times, use `recoverForegroundTaskSubscriber`
|
||||
* to acquire another instance.
|
||||
* always ForegroundTaskState.Done. The returned flow MUST be started in order for the
|
||||
* foreground task to run.
|
||||
*
|
||||
* The task closure is expected to update foregroundTaskState whenever appropriate.
|
||||
* If a foreground task is already running, this function returns null.
|
||||
|
@ -242,9 +194,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
failureTitle: String,
|
||||
iconRes: Int,
|
||||
task: suspend EuiccChannelManagerService.() -> Unit
|
||||
): ForegroundTaskSubscriberFlow {
|
||||
val taskID = System.currentTimeMillis()
|
||||
|
||||
): Flow<ForegroundTaskState> {
|
||||
// Atomically set the state to InProgress. If this returns true, we are
|
||||
// the only task currently in progress.
|
||||
if (!foregroundTaskState.compareAndSet(
|
||||
|
@ -252,9 +202,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
ForegroundTaskState.InProgress(0)
|
||||
)
|
||||
) {
|
||||
return ForegroundTaskSubscriberFlow(
|
||||
taskID,
|
||||
flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) })
|
||||
return flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) }
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
|
@ -296,70 +244,34 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
}
|
||||
}
|
||||
|
||||
// This is the flow we are going to return. We allow multiple subscribers by
|
||||
// re-emitting state updates into this flow from another coroutine.
|
||||
// replay = 2 ensures that we at least have 1 previous state whenever subscribed to.
|
||||
// This is helpful when the task completed and is then re-subscribed to due to a
|
||||
// UI recreation event -- this way, the UI will know at least one last progress event
|
||||
// before completion / failure
|
||||
val subscriberFlow = MutableSharedFlow<ForegroundTaskState>(
|
||||
replay = 2,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
// We should be the only task running, so we can subscribe to foregroundTaskState
|
||||
// until we encounter ForegroundTaskState.Done.
|
||||
// Then, we complete the returned flow, but we also set the state back to Idle.
|
||||
// The state update back to Idle won't show up in the returned stream, because
|
||||
// it has been completed by that point.
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
foregroundTaskState
|
||||
.applyCompletionTransform()
|
||||
.onEach {
|
||||
// Also update our notification when we see an update
|
||||
// But ignore the first progress = 0 update -- that is the current value.
|
||||
// we need that to be handled by the main coroutine after it finishes.
|
||||
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
|
||||
updateForegroundNotification(title, iconRes)
|
||||
}
|
||||
|
||||
subscriberFlow.emit(it)
|
||||
return foregroundTaskState.transformWhile {
|
||||
// Also update our notification when we see an update
|
||||
// But ignore the first progress = 0 update -- that is the current value.
|
||||
// we need that to be handled by the main coroutine after it finishes.
|
||||
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateForegroundNotification(title, iconRes)
|
||||
}
|
||||
.onCompletion {
|
||||
// Reset state back to Idle when we are done.
|
||||
// We do it here because otherwise Idle and Done might become conflated
|
||||
// when emitted by the main coroutine in quick succession.
|
||||
// Doing it here ensures we've seen Done. This Idle event won't be
|
||||
// emitted to the consumer because the subscription has completed here.
|
||||
foregroundTaskState.value = ForegroundTaskState.Idle
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
|
||||
foregroundTaskSubscribers[taskID] = subscriberFlow.asSharedFlow()
|
||||
|
||||
if (foregroundTaskSubscribers.size > 5) {
|
||||
// Remove enough elements so that the size is kept at 5
|
||||
for (key in foregroundTaskSubscribers.keys.sorted()
|
||||
.take(foregroundTaskSubscribers.size - 5)) {
|
||||
foregroundTaskSubscribers.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Before we return, and after we have set everything up,
|
||||
// self-start with foreground permission.
|
||||
// This is going to unblock the main coroutine handling the task.
|
||||
startForegroundService(
|
||||
Intent(
|
||||
this@EuiccChannelManagerService,
|
||||
this@EuiccChannelManagerService::class.java
|
||||
)
|
||||
)
|
||||
|
||||
return ForegroundTaskSubscriberFlow(
|
||||
taskID,
|
||||
subscriberFlow.asSharedFlow().applyCompletionTransform()
|
||||
)
|
||||
emit(it)
|
||||
it !is ForegroundTaskState.Done
|
||||
}.onStart {
|
||||
// When this Flow is started, we unblock the coroutine launched above by
|
||||
// self-starting as a foreground service.
|
||||
withContext(Dispatchers.Main) {
|
||||
startForegroundService(
|
||||
Intent(
|
||||
this@EuiccChannelManagerService,
|
||||
this@EuiccChannelManagerService::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
}.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle }
|
||||
}
|
||||
|
||||
val isForegroundTaskRunning: Boolean
|
||||
|
@ -377,14 +289,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
matchingId: String?,
|
||||
confirmationCode: String?,
|
||||
imei: String?
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
): Flow<ForegroundTaskState> =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_download),
|
||||
getString(R.string.task_profile_download_failure),
|
||||
R.drawable.ic_task_sim_card_download
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
|
@ -399,6 +311,11 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
})
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
// TODO: Provide more details on the error
|
||||
throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.")
|
||||
}
|
||||
|
||||
preferenceRepository.notificationDownloadFlow.first()
|
||||
}
|
||||
}
|
||||
|
@ -408,7 +325,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
portId: Int,
|
||||
iccid: String,
|
||||
name: String
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
): Flow<ForegroundTaskState> =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_rename),
|
||||
getString(R.string.task_profile_rename_failure),
|
||||
|
@ -430,7 +347,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
slotId: Int,
|
||||
portId: Int,
|
||||
iccid: String
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
): Flow<ForegroundTaskState> =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_delete),
|
||||
getString(R.string.task_profile_delete_failure),
|
||||
|
@ -453,7 +370,7 @@ 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, useful for USB readers
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
): Flow<ForegroundTaskState> =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_switch),
|
||||
getString(R.string.task_profile_switch_failure),
|
||||
|
|
|
@ -13,19 +13,10 @@ import androidx.fragment.app.Fragment
|
|||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
||||
import im.angry.openeuicc.util.*
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||
data class DownloadWizardState(
|
||||
var currentStepFragmentClassName: String?,
|
||||
var selectedLogicalSlot: Int,
|
||||
var smdp: String,
|
||||
var matchingId: String?,
|
||||
var confirmationCode: String?,
|
||||
var imei: String?,
|
||||
var downloadStarted: Boolean,
|
||||
var downloadTaskID: Long,
|
||||
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
|
||||
var selectedLogicalSlot: Int
|
||||
)
|
||||
|
||||
private lateinit var state: DownloadWizardState
|
||||
|
@ -35,12 +26,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
private lateinit var prevButton: Button
|
||||
|
||||
private var currentFragment: DownloadWizardStepFragment? = null
|
||||
set(value) {
|
||||
if (this::state.isInitialized) {
|
||||
state.currentStepFragmentClassName = value?.javaClass?.name
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
@ -48,35 +33,18 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
setContentView(R.layout.activity_download_wizard)
|
||||
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
// Make back == prev
|
||||
onPrevPressed()
|
||||
// TODO: Actually implement this
|
||||
}
|
||||
})
|
||||
|
||||
state = DownloadWizardState(
|
||||
null,
|
||||
intent.getIntExtra("selectedLogicalSlot", 0),
|
||||
"",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
-1,
|
||||
null
|
||||
intent.getIntExtra("selectedLogicalSlot", 0)
|
||||
)
|
||||
|
||||
progressBar = requireViewById(R.id.progress)
|
||||
nextButton = requireViewById(R.id.download_wizard_next)
|
||||
prevButton = requireViewById(R.id.download_wizard_back)
|
||||
|
||||
nextButton.setOnClickListener {
|
||||
onNextPressed()
|
||||
}
|
||||
|
||||
prevButton.setOnClickListener {
|
||||
onPrevPressed()
|
||||
}
|
||||
|
||||
val navigation = requireViewById<View>(R.id.download_wizard_navigation)
|
||||
val origHeight = navigation.layoutParams.height
|
||||
|
||||
|
@ -103,76 +71,14 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
||||
outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
||||
outState.putString("smdp", state.smdp)
|
||||
outState.putString("matchingId", state.matchingId)
|
||||
outState.putString("confirmationCode", state.confirmationCode)
|
||||
outState.putString("imei", state.imei)
|
||||
outState.putBoolean("downloadStarted", state.downloadStarted)
|
||||
outState.putLong("downloadTaskID", state.downloadTaskID)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
state.currentStepFragmentClassName = savedInstanceState.getString(
|
||||
"currentStepFragmentClassName",
|
||||
state.currentStepFragmentClassName
|
||||
)
|
||||
state.selectedLogicalSlot =
|
||||
savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot)
|
||||
state.smdp = savedInstanceState.getString("smdp", state.smdp)
|
||||
state.matchingId = savedInstanceState.getString("matchingId", state.matchingId)
|
||||
state.imei = savedInstanceState.getString("imei", state.imei)
|
||||
state.downloadStarted =
|
||||
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
||||
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
||||
}
|
||||
|
||||
private fun onPrevPressed() {
|
||||
if (currentFragment?.hasPrev == true) {
|
||||
val prevFrag = currentFragment?.createPrevFragment()
|
||||
if (prevFrag == null) {
|
||||
finish()
|
||||
} else {
|
||||
showFragment(prevFrag, R.anim.slide_in_left, R.anim.slide_out_right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNextPressed() {
|
||||
if (currentFragment?.hasNext == true) {
|
||||
currentFragment?.beforeNext()
|
||||
val nextFrag = currentFragment?.createNextFragment()
|
||||
if (nextFrag == null) {
|
||||
finish()
|
||||
} else {
|
||||
showFragment(nextFrag, R.anim.slide_in_right, R.anim.slide_out_left)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInit() {
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
if (state.currentStepFragmentClassName != null) {
|
||||
val clazz = Class.forName(state.currentStepFragmentClassName!!)
|
||||
showFragment(clazz.getDeclaredConstructor().newInstance() as DownloadWizardStepFragment)
|
||||
} else {
|
||||
showFragment(DownloadWizardSlotSelectFragment())
|
||||
}
|
||||
showFragment(DownloadWizardSlotSelectFragment())
|
||||
}
|
||||
|
||||
private fun showFragment(
|
||||
nextFrag: DownloadWizardStepFragment,
|
||||
enterAnim: Int = 0,
|
||||
exitAnim: Int = 0
|
||||
) {
|
||||
private fun showFragment(nextFrag: DownloadWizardStepFragment) {
|
||||
currentFragment = nextFrag
|
||||
supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim)
|
||||
.replace(R.id.step_fragment_container, nextFrag)
|
||||
supportFragmentManager.beginTransaction().replace(R.id.step_fragment_container, nextFrag)
|
||||
.commit()
|
||||
refreshButtons()
|
||||
}
|
||||
|
@ -201,15 +107,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
abstract fun createNextFragment(): DownloadWizardStepFragment?
|
||||
abstract fun createPrevFragment(): DownloadWizardStepFragment?
|
||||
|
||||
protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) {
|
||||
val realNext = next ?: createNextFragment()
|
||||
(requireActivity() as DownloadWizardActivity).showFragment(
|
||||
realNext!!,
|
||||
R.anim.slide_in_right,
|
||||
R.anim.slide_out_left
|
||||
)
|
||||
}
|
||||
|
||||
protected fun hideProgressBar() {
|
||||
(requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE
|
||||
}
|
||||
|
@ -229,7 +126,5 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
|||
protected fun refreshButtons() {
|
||||
(requireActivity() as DownloadWizardActivity).refreshButtons()
|
||||
}
|
||||
|
||||
open fun beforeNext() {}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
|
||||
class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
private var inputComplete = false
|
||||
|
||||
override val hasNext: Boolean
|
||||
get() = inputComplete
|
||||
override val hasPrev: Boolean
|
||||
get() = true
|
||||
|
||||
private lateinit var smdp: TextInputLayout
|
||||
private lateinit var matchingId: TextInputLayout
|
||||
private lateinit var confirmationCode: TextInputLayout
|
||||
private lateinit var imei: TextInputLayout
|
||||
|
||||
override fun beforeNext() {
|
||||
state.smdp = smdp.editText!!.text.toString().trim()
|
||||
// Treat empty inputs as null -- this is important for the download step
|
||||
state.matchingId = matchingId.editText!!.text.toString().trim().ifBlank { null }
|
||||
state.confirmationCode = confirmationCode.editText!!.text.toString().trim().ifBlank { null }
|
||||
state.imei = imei.editText!!.text.toString().ifBlank { null }
|
||||
}
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardProgressFragment()
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardMethodSelectFragment()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_details, container, false)
|
||||
smdp = view.requireViewById(R.id.profile_download_server)
|
||||
matchingId = view.requireViewById(R.id.profile_download_code)
|
||||
confirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
|
||||
imei = view.requireViewById(R.id.profile_download_imei)
|
||||
smdp.editText!!.addTextChangedListener {
|
||||
updateInputCompleteness()
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
smdp.editText!!.setText(state.smdp)
|
||||
matchingId.editText!!.setText(state.matchingId)
|
||||
confirmationCode.editText!!.setText(state.confirmationCode)
|
||||
imei.editText!!.setText(state.imei)
|
||||
updateInputCompleteness()
|
||||
}
|
||||
|
||||
private fun updateInputCompleteness() {
|
||||
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
|
||||
refreshButtons()
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
override val hasNext: Boolean
|
||||
get() = true
|
||||
override val hasPrev: Boolean
|
||||
get() = false
|
||||
|
||||
private lateinit var diagnosticTextView: TextView
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false)
|
||||
diagnosticTextView = view.requireViewById<TextView>(R.id.download_wizard_diagnostics_text)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val str = buildDiagnosticsText()
|
||||
if (str == null) {
|
||||
requireActivity().finish()
|
||||
return
|
||||
}
|
||||
|
||||
diagnosticTextView.text = str
|
||||
}
|
||||
|
||||
private fun buildDiagnosticsText(): String? = state.downloadError?.let { err ->
|
||||
val ret = StringBuilder()
|
||||
|
||||
err.lastHttpResponse?.let { resp ->
|
||||
if (resp.rcode != 200) {
|
||||
// Only show the status if it's not 200
|
||||
// Because we can have errors even if the rcode is 200 due to SM-DP+ servers being dumb
|
||||
// and showing 200 might mislead users
|
||||
ret.appendLine(
|
||||
getString(
|
||||
R.string.download_wizard_diagnostics_last_http_status,
|
||||
resp.rcode
|
||||
)
|
||||
)
|
||||
ret.appendLine()
|
||||
}
|
||||
|
||||
ret.appendLine(getString(R.string.download_wizard_diagnostics_last_http_response))
|
||||
ret.appendLine()
|
||||
|
||||
val str = resp.data.decodeToString(throwOnInvalidSequence = false)
|
||||
ret.appendLine(
|
||||
if (str.startsWith('{')) {
|
||||
str.prettyPrintJson()
|
||||
} else {
|
||||
str
|
||||
}
|
||||
)
|
||||
|
||||
ret.appendLine()
|
||||
}
|
||||
|
||||
err.lastHttpException?.let { e ->
|
||||
ret.appendLine(getString(R.string.download_wizard_diagnostics_last_http_exception))
|
||||
ret.appendLine()
|
||||
ret.appendLine("${e.javaClass.name}: ${e.message}")
|
||||
ret.appendLine(e.stackTrace.joinToString("\n"))
|
||||
ret.appendLine()
|
||||
}
|
||||
|
||||
err.lastApduResponse?.let { resp ->
|
||||
val isSuccess =
|
||||
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
|
||||
|
||||
if (isSuccess) {
|
||||
ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_response_success))
|
||||
} else {
|
||||
// Only show the full APDU response when it's a failure
|
||||
// Otherwise it's going to get very crammed
|
||||
ret.appendLine(
|
||||
getString(
|
||||
R.string.download_wizard_diagnostics_last_apdu_response,
|
||||
resp.encodeHex()
|
||||
)
|
||||
)
|
||||
ret.appendLine()
|
||||
|
||||
ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_response_fail))
|
||||
}
|
||||
}
|
||||
|
||||
err.lastApduException?.let { e ->
|
||||
ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_exception))
|
||||
ret.appendLine()
|
||||
ret.appendLine("${e.javaClass.name}: ${e.message}")
|
||||
ret.appendLine(e.stackTrace.joinToString("\n"))
|
||||
ret.appendLine()
|
||||
}
|
||||
|
||||
ret.toString()
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
data class DownloadMethod(
|
||||
val iconRes: Int,
|
||||
val titleRes: Int,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
// TODO: Maybe we should find a better barcode scanner (or an external one?)
|
||||
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||
result.contents?.let { content ->
|
||||
processLpaString(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) {
|
||||
processLpaString(it)
|
||||
}
|
||||
}
|
||||
|
||||
bmp.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val downloadMethods = arrayOf(
|
||||
DownloadMethod(R.drawable.ic_scan_black, R.string.download_wizard_method_qr_code) {
|
||||
barcodeScannerLauncher.launch(ScanOptions().apply {
|
||||
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
setOrientationLocked(false)
|
||||
})
|
||||
},
|
||||
DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) {
|
||||
gallerySelectorLauncher.launch("image/*")
|
||||
},
|
||||
DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) {
|
||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||
}
|
||||
)
|
||||
|
||||
override val hasNext: Boolean
|
||||
get() = false
|
||||
override val hasPrev: Boolean
|
||||
get() = true
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? =
|
||||
null
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardSlotSelectFragment()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_method_select, container, false)
|
||||
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_method_list)
|
||||
recyclerView.adapter = DownloadMethodAdapter()
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
return view
|
||||
}
|
||||
|
||||
private fun processLpaString(s: String) {
|
||||
val components = s.split("$")
|
||||
if (components.size < 3 || components[0] != "LPA:1") return
|
||||
state.smdp = components[1]
|
||||
state.matchingId = components[2]
|
||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||
}
|
||||
|
||||
private 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() }
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DownloadMethodAdapter : RecyclerView.Adapter<DownloadMethodViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): DownloadMethodViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.download_method_item, parent, false)
|
||||
return DownloadMethodViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = downloadMethods.size
|
||||
|
||||
override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) {
|
||||
holder.bind(downloadMethods[position])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
package im.angry.openeuicc.ui.wizard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
|
||||
class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||
companion object {
|
||||
/**
|
||||
* An array of LPA-side state types, mapping 1:1 to progressItems
|
||||
*/
|
||||
val LPA_PROGRESS_STATES = arrayOf(
|
||||
ProfileDownloadCallback.DownloadState.Preparing,
|
||||
ProfileDownloadCallback.DownloadState.Connecting,
|
||||
ProfileDownloadCallback.DownloadState.Authenticating,
|
||||
ProfileDownloadCallback.DownloadState.Downloading,
|
||||
ProfileDownloadCallback.DownloadState.Finalizing,
|
||||
)
|
||||
}
|
||||
|
||||
private enum class ProgressState {
|
||||
NotStarted,
|
||||
InProgress,
|
||||
Done,
|
||||
Error
|
||||
}
|
||||
|
||||
private data class ProgressItem(
|
||||
val titleRes: Int,
|
||||
var state: ProgressState
|
||||
)
|
||||
|
||||
private val progressItems = arrayOf(
|
||||
ProgressItem(R.string.download_wizard_progress_step_preparing, ProgressState.NotStarted),
|
||||
ProgressItem(R.string.download_wizard_progress_step_connecting, ProgressState.NotStarted),
|
||||
ProgressItem(
|
||||
R.string.download_wizard_progress_step_authenticating,
|
||||
ProgressState.NotStarted
|
||||
),
|
||||
ProgressItem(R.string.download_wizard_progress_step_downloading, ProgressState.NotStarted),
|
||||
ProgressItem(R.string.download_wizard_progress_step_finalizing, ProgressState.NotStarted)
|
||||
)
|
||||
|
||||
private val adapter = ProgressItemAdapter()
|
||||
|
||||
private var isDone = false
|
||||
|
||||
override val hasNext: Boolean
|
||||
get() = isDone
|
||||
override val hasPrev: Boolean
|
||||
get() = false
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? =
|
||||
if (state.downloadError != null) {
|
||||
DownloadWizardDiagnosticsFragment()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_download_progress, container, false)
|
||||
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_progress_list)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
lifecycleScope.launch {
|
||||
showProgressBar(-1) // set indeterminate first
|
||||
ensureEuiccChannelManager()
|
||||
|
||||
val subscriber = startDownloadOrSubscribe()
|
||||
|
||||
if (subscriber == null) {
|
||||
requireActivity().finish()
|
||||
return@launch
|
||||
}
|
||||
|
||||
subscriber.onEach {
|
||||
when (it) {
|
||||
is EuiccChannelManagerService.ForegroundTaskState.Done -> {
|
||||
hideProgressBar()
|
||||
|
||||
// Change the state of the last InProgress item to Error
|
||||
progressItems.forEachIndexed { index, progressItem ->
|
||||
if (progressItem.state == ProgressState.InProgress) {
|
||||
progressItem.state = ProgressState.Error
|
||||
}
|
||||
|
||||
adapter.notifyItemChanged(index)
|
||||
}
|
||||
|
||||
state.downloadError =
|
||||
it.error as? LocalProfileAssistant.ProfileDownloadException
|
||||
|
||||
isDone = true
|
||||
refreshButtons()
|
||||
}
|
||||
|
||||
is EuiccChannelManagerService.ForegroundTaskState.InProgress -> {
|
||||
updateProgress(it.progress)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startDownloadOrSubscribe(): EuiccChannelManagerService.ForegroundTaskSubscriberFlow? =
|
||||
if (state.downloadStarted) {
|
||||
// This will also return null if task ID is -1 (uninitialized), too
|
||||
euiccChannelManagerService.recoverForegroundTaskSubscriber(state.downloadTaskID)
|
||||
} else {
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel ->
|
||||
Pair(channel.slotId, channel.portId)
|
||||
}
|
||||
|
||||
// Set started to true even before we start -- in case we get killed in the middle
|
||||
state.downloadStarted = true
|
||||
|
||||
val ret = euiccChannelManagerService.launchProfileDownloadTask(
|
||||
slotId,
|
||||
portId,
|
||||
state.smdp,
|
||||
state.matchingId,
|
||||
state.confirmationCode,
|
||||
state.imei
|
||||
)
|
||||
|
||||
state.downloadTaskID = ret.taskId
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
private fun updateProgress(progress: Int) {
|
||||
showProgressBar(progress)
|
||||
|
||||
val lpaState = ProfileDownloadCallback.lookupStateFromProgress(progress)
|
||||
val stateIndex = LPA_PROGRESS_STATES.indexOf(lpaState)
|
||||
|
||||
if (stateIndex > 0) {
|
||||
for (i in (0..<stateIndex)) {
|
||||
if (progressItems[i].state != ProgressState.Done) {
|
||||
progressItems[i].state = ProgressState.Done
|
||||
adapter.notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (progressItems[stateIndex].state != ProgressState.InProgress) {
|
||||
progressItems[stateIndex].state = ProgressState.InProgress
|
||||
adapter.notifyItemChanged(stateIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ProgressItemHolder(val root: View) : RecyclerView.ViewHolder(root) {
|
||||
private val title = root.requireViewById<TextView>(R.id.download_progress_item_title)
|
||||
private val progressBar =
|
||||
root.requireViewById<ProgressBar>(R.id.download_progress_icon_progress)
|
||||
private val icon = root.requireViewById<ImageView>(R.id.download_progress_icon)
|
||||
|
||||
fun bind(item: ProgressItem) {
|
||||
title.text = getString(item.titleRes)
|
||||
|
||||
when (item.state) {
|
||||
ProgressState.NotStarted -> {
|
||||
progressBar.visibility = View.GONE
|
||||
icon.visibility = View.GONE
|
||||
}
|
||||
|
||||
ProgressState.InProgress -> {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
icon.visibility = View.GONE
|
||||
}
|
||||
|
||||
ProgressState.Done -> {
|
||||
progressBar.visibility = View.GONE
|
||||
icon.setImageResource(R.drawable.ic_checkmark_outline)
|
||||
icon.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
ProgressState.Error -> {
|
||||
progressBar.visibility = View.GONE
|
||||
icon.setImageResource(R.drawable.ic_error_outline)
|
||||
icon.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ProgressItemAdapter : RecyclerView.Adapter<ProgressItemHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressItemHolder {
|
||||
val root = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.download_progress_item, parent, false)
|
||||
return ProgressItemHolder(root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = progressItems.size
|
||||
|
||||
override fun onBindViewHolder(holder: ProgressItemHolder, position: Int) {
|
||||
holder.bind(progressItems[position])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,8 +26,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
val hasMultiplePorts: Boolean,
|
||||
val portId: Int,
|
||||
val eID: String,
|
||||
val freeSpace: Int,
|
||||
val imei: String,
|
||||
val enabledProfileName: String?
|
||||
)
|
||||
|
||||
|
@ -40,8 +38,9 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
override val hasPrev: Boolean
|
||||
get() = true
|
||||
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||
DownloadWizardMethodSelectFragment()
|
||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||
|
||||
|
@ -66,7 +65,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged", "MissingPermission")
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private suspend fun init() {
|
||||
ensureEuiccChannelManager()
|
||||
showProgressBar(-1)
|
||||
|
@ -78,16 +77,10 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
channel.port.card.ports.size > 1,
|
||||
channel.portId,
|
||||
channel.lpa.eID,
|
||||
channel.lpa.euiccInfo2?.freeNvram ?: 0,
|
||||
try {
|
||||
telephonyManager.getImei(channel.logicalSlotId) ?: ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
},
|
||||
channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName
|
||||
)
|
||||
}
|
||||
}.toList().sortedBy { it.logicalSlotId }
|
||||
}.toList()
|
||||
adapter.slots = slots
|
||||
|
||||
// Ensure we always have a selected slot by default
|
||||
|
@ -101,10 +94,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
0
|
||||
}
|
||||
|
||||
if (slots.isNotEmpty()) {
|
||||
state.imei = slots[adapter.currentSelectedIdx].imei
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
hideProgressBar()
|
||||
loaded = true
|
||||
|
@ -116,7 +105,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
private val type = root.requireViewById<TextView>(R.id.slot_item_type)
|
||||
private val eID = root.requireViewById<TextView>(R.id.slot_item_eid)
|
||||
private val activeProfile = root.requireViewById<TextView>(R.id.slot_item_active_profile)
|
||||
private val freeSpace = root.requireViewById<TextView>(R.id.slot_item_free_space)
|
||||
private val checkBox = root.requireViewById<CheckBox>(R.id.slot_checkbox)
|
||||
|
||||
private var curIdx = -1
|
||||
|
@ -136,7 +124,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
adapter.notifyItemChanged(curIdx)
|
||||
// Selected index isn't logical slot ID directly, needs a conversion
|
||||
state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId
|
||||
state.imei = adapter.slots[adapter.currentSelectedIdx].imei
|
||||
}
|
||||
|
||||
fun bind(item: SlotInfo, idx: Int) {
|
||||
|
@ -156,7 +143,6 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
|||
title.text = root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId)
|
||||
eID.text = item.eID
|
||||
activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown)
|
||||
freeSpace.text = formatFreeSpace(item.freeSpace)
|
||||
checkBox.isChecked = adapter.currentSelectedIdx == idx
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,74 +27,4 @@ fun formatFreeSpace(size: Int): String =
|
|||
"%.2f KiB".format(size.toDouble() / 1024)
|
||||
} else {
|
||||
"$size B"
|
||||
}
|
||||
|
||||
fun String.prettyPrintJson(): String {
|
||||
val ret = StringBuilder()
|
||||
var inQuotes = false
|
||||
var escaped = false
|
||||
val indentSymbolStack = ArrayDeque<Char>()
|
||||
|
||||
val addNewLine = {
|
||||
ret.append('\n')
|
||||
repeat(indentSymbolStack.size) {
|
||||
ret.append('\t')
|
||||
}
|
||||
}
|
||||
|
||||
var lastChar = ' '
|
||||
|
||||
for (c in this) {
|
||||
when {
|
||||
!inQuotes && (c == '{' || c == '[') -> {
|
||||
ret.append(c)
|
||||
indentSymbolStack.addLast(c)
|
||||
addNewLine()
|
||||
}
|
||||
|
||||
!inQuotes && (c == '}' || c == ']') -> {
|
||||
indentSymbolStack.removeLast()
|
||||
if (lastChar != ',') {
|
||||
addNewLine()
|
||||
}
|
||||
ret.append(c)
|
||||
}
|
||||
|
||||
!inQuotes && c == ',' -> {
|
||||
ret.append(c)
|
||||
addNewLine()
|
||||
}
|
||||
|
||||
!inQuotes && c == ':' -> {
|
||||
ret.append(c)
|
||||
ret.append(' ')
|
||||
}
|
||||
|
||||
inQuotes && c == '\\' -> {
|
||||
ret.append(c)
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
!escaped && c == '"' -> {
|
||||
ret.append(c)
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
|
||||
!inQuotes && c == ' ' -> {
|
||||
// Do nothing -- we ignore spaces outside of quotes by default
|
||||
// This is to ensure predictable formatting
|
||||
}
|
||||
|
||||
else -> ret.append(c)
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
escaped = false
|
||||
}
|
||||
|
||||
lastChar = c
|
||||
}
|
||||
|
||||
return ret.toString()
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="-100%"
|
||||
android:toXDelta="0%" />
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="100%"
|
||||
android:toXDelta="0%" />
|
|
@ -1,6 +0,0 @@
|
|||
<!-- res/anim/slide_out.xml -->
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="-100%" />
|
|
@ -1,6 +0,0 @@
|
|||
<!-- res/anim/slide_out.xml -->
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="100%" />
|
|
@ -1,5 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
|
||||
</vector>
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:padding="20dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/download_method_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
app:tint="?attr/colorAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_method_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textSize="15sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="marquee"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/download_method_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/download_method_chevron"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constrainedWidth="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/download_method_chevron"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
app:tint="?attr/colorAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_progress_item_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="20dp"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/download_progress_icon_container"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintHorizontal_bias="0.0" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/download_progress_icon_container"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/download_progress_icon_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/download_progress_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -61,20 +61,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_free_space_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="100dp"
|
||||
android:text="@string/download_wizard_slot_free_space"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/slot_item_free_space"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:id="@+id/flow1"
|
||||
android:layout_width="0dp"
|
||||
|
@ -82,7 +68,7 @@
|
|||
android:layout_marginStart="10sp"
|
||||
android:layout_marginTop="20sp"
|
||||
android:layout_marginEnd="10sp"
|
||||
app:constraint_referenced_ids="slot_item_type_label,slot_item_type,slot_item_eid_label,slot_item_eid,slot_item_active_profile_label,slot_item_active_profile,slot_item_free_space_label,slot_item_free_space"
|
||||
app:constraint_referenced_ids="slot_item_type_label,slot_item_type,slot_item_eid_label,slot_item_eid,slot_item_active_profile_label,slot_item_active_profile"
|
||||
app:flow_wrapMode="aligned"
|
||||
app:flow_horizontalAlign="start"
|
||||
app:flow_horizontalBias="1"
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_wizard_details_title"
|
||||
android:text="@string/download_wizard_details"
|
||||
android:layout_width="wrap_content"
|
||||
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"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/profile_download_server"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_download_server">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
android:inputType="text"
|
||||
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:hint="@string/profile_download_code">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
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:hint="@string/profile_download_confirmation_code">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
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:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:maxLines="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
app:constraint_referenced_ids="profile_download_server,profile_download_code,profile_download_confirmation_code,profile_download_imei"
|
||||
app:flow_verticalGap="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_wizard_details_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -1,48 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_wizard_diagnostics_title"
|
||||
android:text="@string/download_wizard_diagnostics"
|
||||
android:layout_width="wrap_content"
|
||||
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"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_wizard_diagnostics_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
android:textSize="10sp"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:longClickable="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_wizard_diagnostics_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="SmallSp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_method_select_title"
|
||||
android:text="@string/download_wizard_method_select"
|
||||
android:layout_width="wrap_content"
|
||||
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"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/download_method_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_method_select_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constrainedHeight="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_progress_title"
|
||||
android:text="@string/download_wizard_progress"
|
||||
android:layout_width="wrap_content"
|
||||
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"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/download_progress_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/download_progress_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constrainedHeight="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -9,15 +9,10 @@
|
|||
android:text="@string/download_wizard_slot_select"
|
||||
android:layout_width="wrap_content"
|
||||
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_margin="20sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<string name="download_wizard">Download Wizard</string>
|
||||
<string name="download_wizard_back">Back</string>
|
||||
<string name="download_wizard_next">Next</string>
|
||||
<string name="download_wizard_slot_select">Select or confirm the eSIM you would like to download to:</string>
|
||||
<string name="download_wizard_slot_select">Confirm the eSIM slot:</string>
|
||||
<string name="download_wizard_slot_title">Logical slot %d</string>
|
||||
<string name="download_wizard_slot_type">Type:</string>
|
||||
<string name="download_wizard_slot_type_removable">Removable</string>
|
||||
|
@ -69,26 +69,6 @@
|
|||
<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_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>
|
||||
<string name="download_wizard_method_qr_code">Scan a QR code with camera</string>
|
||||
<string name="download_wizard_method_gallery">Load a QR code from gallery</string>
|
||||
<string name="download_wizard_method_manual">Enter manually</string>
|
||||
<string name="download_wizard_details">Input or confirm details for downloading your eSIM:</string>
|
||||
<string name="download_wizard_progress">Downloading your eSIM…</string>
|
||||
<string name="download_wizard_progress_step_preparing">Preparing</string>
|
||||
<string name="download_wizard_progress_step_connecting">Establishing connection to server</string>
|
||||
<string name="download_wizard_progress_step_authenticating">Authenticating your device with server</string>
|
||||
<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_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>
|
||||
<string name="download_wizard_diagnostics_last_apdu_response">Last APDU response (from SIM): %s</string>
|
||||
<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="profile_rename_new_name">New nickname</string>
|
||||
|
||||
|
|
3
app-deps/.gitignore
vendored
3
app-deps/.gitignore
vendored
|
@ -1,2 +1 @@
|
|||
/build
|
||||
/libs
|
||||
/build
|
2
buildSrc/.gitignore
vendored
2
buildSrc/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/.gradle
|
||||
/build
|
1
libs/lpac-jni/.gitignore
vendored
1
libs/lpac-jni/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,16 +1,6 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
import net.typeblog.lpac_jni.HttpInterface.HttpResponse
|
||||
|
||||
interface LocalProfileAssistant {
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class ProfileDownloadException(
|
||||
val lastHttpResponse: HttpResponse?,
|
||||
val lastHttpException: Exception?,
|
||||
val lastApduResponse: ByteArray?,
|
||||
val lastApduException: Exception?,
|
||||
) : Exception("Failed to download profile")
|
||||
|
||||
val valid: Boolean
|
||||
val profiles: List<LocalProfileInfo>
|
||||
val notifications: List<LocalProfileNotification>
|
||||
|
@ -32,7 +22,7 @@ interface LocalProfileAssistant {
|
|||
fun deleteProfile(iccid: String): Boolean
|
||||
|
||||
fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback)
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback): Boolean
|
||||
|
||||
fun deleteNotification(seqNumber: Long): Boolean
|
||||
fun handleNotification(seqNumber: Long): Boolean
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
package net.typeblog.lpac_jni
|
||||
|
||||
interface ProfileDownloadCallback {
|
||||
companion object {
|
||||
fun lookupStateFromProgress(progress: Int): DownloadState =
|
||||
when (progress) {
|
||||
0 -> DownloadState.Preparing
|
||||
20 -> DownloadState.Connecting
|
||||
40 -> DownloadState.Authenticating
|
||||
60 -> DownloadState.Downloading
|
||||
80 -> DownloadState.Finalizing
|
||||
else -> throw IllegalArgumentException("Unknown state")
|
||||
}
|
||||
}
|
||||
|
||||
enum class DownloadState(val progress: Int) {
|
||||
Preparing(0),
|
||||
Connecting(20), // Before {server,client} authentication
|
||||
|
|
|
@ -5,76 +5,19 @@ import net.typeblog.lpac_jni.LpacJni
|
|||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.EuiccInfo2
|
||||
import net.typeblog.lpac_jni.HttpInterface
|
||||
import net.typeblog.lpac_jni.HttpInterface.HttpResponse
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import net.typeblog.lpac_jni.LocalProfileNotification
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
|
||||
class LocalProfileAssistantImpl(
|
||||
rawApduInterface: ApduInterface,
|
||||
rawHttpInterface: HttpInterface
|
||||
private val apduInterface: ApduInterface,
|
||||
httpInterface: HttpInterface
|
||||
): LocalProfileAssistant {
|
||||
companion object {
|
||||
private const val TAG = "LocalProfileAssistantImpl"
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin wrapper over ApduInterface to acquire exceptions and errors transparently
|
||||
*/
|
||||
private class ApduInterfaceWrapper(val apduInterface: ApduInterface) :
|
||||
ApduInterface by apduInterface {
|
||||
var lastApduResponse: ByteArray? = null
|
||||
var lastApduException: Exception? = null
|
||||
|
||||
override fun transmit(tx: ByteArray): ByteArray =
|
||||
try {
|
||||
apduInterface.transmit(tx).also {
|
||||
lastApduException = null
|
||||
lastApduResponse = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
lastApduResponse = null
|
||||
lastApduException = e
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same for HTTP for diagnostics
|
||||
*/
|
||||
private class HttpInterfaceWrapper(val httpInterface: HttpInterface) :
|
||||
HttpInterface by httpInterface {
|
||||
/**
|
||||
* The last HTTP response we have received from the SM-DP+ server.
|
||||
*
|
||||
* This is intended for error diagnosis. However, note that most SM-DP+ servers
|
||||
* respond with 200 even when there is an error. This needs to be taken into
|
||||
* account when designing UI.
|
||||
*/
|
||||
var lastHttpResponse: HttpResponse? = null
|
||||
|
||||
/**
|
||||
* The last exception that has been thrown during a HTTP connection
|
||||
*/
|
||||
var lastHttpException: Exception? = null
|
||||
|
||||
override fun transmit(url: String, tx: ByteArray, headers: Array<String>): HttpResponse =
|
||||
try {
|
||||
httpInterface.transmit(url, tx, headers).also {
|
||||
lastHttpException = null
|
||||
lastHttpResponse = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
lastHttpResponse = null
|
||||
lastHttpException = e
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private val apduInterface = ApduInterfaceWrapper(rawApduInterface)
|
||||
private val httpInterface = HttpInterfaceWrapper(rawHttpInterface)
|
||||
|
||||
private var finalized = false
|
||||
private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface)
|
||||
|
||||
|
@ -201,24 +144,15 @@ class LocalProfileAssistantImpl(
|
|||
|
||||
@Synchronized
|
||||
override fun downloadProfile(smdp: String, matchingId: String?, imei: String?,
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback) {
|
||||
val res = LpacJni.downloadProfile(
|
||||
confirmationCode: String?, callback: ProfileDownloadCallback): Boolean {
|
||||
return LpacJni.downloadProfile(
|
||||
contextHandle,
|
||||
smdp,
|
||||
matchingId,
|
||||
imei,
|
||||
confirmationCode,
|
||||
callback
|
||||
)
|
||||
|
||||
if (res != 0) {
|
||||
throw LocalProfileAssistant.ProfileDownloadException(
|
||||
httpInterface.lastHttpResponse,
|
||||
httpInterface.lastHttpException,
|
||||
apduInterface.lastApduResponse,
|
||||
apduInterface.lastApduException,
|
||||
)
|
||||
}
|
||||
) == 0
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
|
Loading…
Add table
Reference in a new issue