forked from PeterCxy/OpenEUICC
Compare commits
187 commits
e424b8eee0
...
0673cf370a
Author | SHA1 | Date | |
---|---|---|---|
0673cf370a | |||
bcd1295a18 | |||
50ba81f131 | |||
d0b3d54c66 | |||
3a860601a3 | |||
6b4723daee | |||
3ef78a23db | |||
31d595a6b1 | |||
e7ef370e46 | |||
653a7b32ee | |||
0f8749ee04 | |||
c0a6917645 | |||
6e3176668a | |||
66bee041a0 | |||
43f247a71b | |||
960f8855ad | |||
de3ae19a10 | |||
75d3894462 | |||
895899d03a | |||
a87f154653 | |||
b88345057c | |||
9596b8632c | |||
087c760010 | |||
24076e8fb4 | |||
f135a0da60 | |||
3b7bd8b31e | |||
74e946cc8f | |||
3430406603 | |||
24f04f54e4 | |||
0fbda7dd78 | |||
905d0c897e | |||
f395cee2e0 | |||
456754db5d | |||
7d1c7663bc | |||
9d18253e44 | |||
6039679693 | |||
5a8d92c3df | |||
55c99831f3 | |||
343dfb43f8 | |||
815d4d4324 | |||
ec334d104a | |||
70f1e00eb4 | |||
bc238c45cd | |||
14ea84c36e | |||
aefa79b18b | |||
aed2479044 | |||
f294fb5e17 | |||
55d96c6732 | |||
06873545e2 | |||
6d962a12b5 | |||
8f9c7137f6 | |||
125f1da6af | |||
4a482b9c73 | |||
ca46b578f7 | |||
23022b14be | |||
2d66c1f334 | |||
09b98b37ab | |||
fdbf9b3252 | |||
84f47cb0f0 | |||
0229ef41df | |||
15d3b701a5 | |||
700578a369 | |||
eab60bf3d3 | |||
5b80afd5fe | |||
400c2ff9f9 | |||
b4f562f90b | |||
5a000278d3 | |||
38d38523f9 | |||
022ca1da9d | |||
1140ddb249 | |||
9be1ae7cd1 | |||
a7e97378fc | |||
790cbb5a58 | |||
2247749b37 | |||
249aea482b | |||
dc0489a693 | |||
d3e54ece58 | |||
4e5bb5b11e | |||
68cc6adc9b | |||
9e637f766d | |||
acce39fd3b | |||
fb8b6de350 | |||
75221fcf79 | |||
4ae19aea3b | |||
ff6bd45ac6 | |||
858b6d55d6 | |||
78bf3612ee | |||
afeb5c5282 | |||
f74145d0b7 | |||
c6de599db0 | |||
0a78daee8b | |||
e7f58bbaaf | |||
562e5922be | |||
50c77ea467 | |||
6bb1a16aee | |||
92daa56f1a | |||
90878438f9 | |||
96bc9865ff | |||
dcae65011e | |||
1c4263a47a | |||
d7214141e6 | |||
326b39ed05 | |||
26d037048d | |||
5476e335b1 | |||
426e5c0197 | |||
74d7da35dc | |||
07072667db | |||
895cbdd53d | |||
1a3fd621d9 | |||
74489a9ae0 | |||
d68a7172de | |||
5b079c95ac | |||
f2c233fe1c | |||
3507c17834 | |||
b2abe5ee84 | |||
67c9612627 | |||
39b40f9b0d | |||
f236b40cd4 | |||
e7a0482281 | |||
81f34f9b1c | |||
8c73615fbb | |||
9cf95ad47c | |||
723ec70730 | |||
dbdadd33b3 | |||
92b7b46598 | |||
0c519af376 | |||
aaca9e807a | |||
98e16ee5aa | |||
b9d5c1c5bb | |||
c4b513fc0a | |||
6458f54db2 | |||
87f36f4166 | |||
4fb59a4b01 | |||
16636988b0 | |||
93e7297caa | |||
1087a676d4 | |||
375d13b7c4 | |||
a3d59a0761 | |||
5f0dbe3098 | |||
efa9b8bfa4 | |||
47d5c3881c | |||
e9f4d3d1f9 | |||
506b0e530a | |||
e8db3d1206 | |||
071304349a | |||
6f8aef8ea8 | |||
8e806c3ae5 | |||
42c870192c | |||
9201ee416e | |||
7105c43ae4 | |||
d846f0cdc4 | |||
5dacb75717 | |||
f28867ef2e | |||
7215a2351b | |||
837c34ba70 | |||
fe6d4264e3 | |||
13085ec202 | |||
9d8e58a95d | |||
22ec3e3baf | |||
32f5e3f71a | |||
04debd62d5 | |||
0ef435956c | |||
573dce56a6 | |||
272ab953e0 | |||
6257a03058 | |||
5e5210ae2d | |||
87eb497f40 | |||
1dc5004681 | |||
2ece6af174 | |||
59b4b9e4ab | |||
826c120ca5 | |||
5cefbc24f5 | |||
f285eacd55 | |||
481b9ce196 | |||
ce7fb29c14 | |||
c2cc8ceb2a | |||
3d4704e77b | |||
6a2d4d66dd | |||
8ac46bd778 | |||
0961ef70f4 | |||
3b868e4f9a | |||
95b24e6151 | |||
ef62274057 | |||
76e8fbd56b | |||
d54fcf2589 | |||
7cb872a664 | |||
65c9a7dc39 |
132 changed files with 4580 additions and 1430 deletions
|
@ -38,11 +38,12 @@ jobs:
|
||||||
- name: Build Debug Bundle
|
- name: Build Debug Bundle
|
||||||
run: ./gradlew --no-daemon :app-unpriv:bundleJmpDebug
|
run: ./gradlew --no-daemon :app-unpriv:bundleJmpDebug
|
||||||
|
|
||||||
|
- name: Copy Artifacts
|
||||||
|
run: find . -name 'app*-debug.apk' -exec cp {} . \;
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: https://gitea.angry.im/actions/upload-artifact@v3
|
uses: https://gitea.angry.im/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Debug APKs
|
name: Debug APKs
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
path: |
|
path: app*-debug.apk
|
||||||
app-unpriv/build/outputs/apk/jmp/debug/app-unpriv-jmp-debug.apk
|
|
||||||
app-unpriv/build/outputs/bundle/jmpDebug/app-unpriv-jmp-debug.aab
|
|
||||||
|
|
29
.gitignore
vendored
29
.gitignore
vendored
|
@ -1,20 +1,11 @@
|
||||||
*.iml
|
/.gradle
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/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
|
/captures
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
# Configuration files
|
||||||
local.properties
|
|
||||||
/libs/**/build
|
/keystore.properties
|
||||||
/buildSrc/build
|
/local.properties
|
||||||
/app-deps/libs
|
|
||||||
|
# macOS
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
14
.idea/.gitignore
generated
vendored
14
.idea/.gitignore
generated
vendored
|
@ -1,3 +1,13 @@
|
||||||
# Default ignored files
|
/shelf
|
||||||
/shelf/
|
/caches
|
||||||
|
/libraries
|
||||||
|
/assetWizardSettings.xml
|
||||||
|
/deploymentTargetDropDown.xml
|
||||||
|
/gradle.xml
|
||||||
|
/misc.xml
|
||||||
|
/modules.xml
|
||||||
|
/navEditor.xml
|
||||||
|
/runConfigurations.xml
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
|
||||||
|
**/*.iml
|
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
|
@ -0,0 +1 @@
|
||||||
|
OpenEUICC
|
12
.idea/compiler.xml
generated
12
.idea/compiler.xml
generated
|
@ -1,16 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="1.7">
|
<bytecodeTargetLevel target="1.7" />
|
||||||
<module name="OpenEUICC.app" target="17" />
|
|
||||||
<module name="OpenEUICC.app-common" target="17" />
|
|
||||||
<module name="OpenEUICC.app-deps" target="17" />
|
|
||||||
<module name="OpenEUICC.app-unpriv" target="17" />
|
|
||||||
<module name="OpenEUICC.buildSrc" target="17" />
|
|
||||||
<module name="OpenEUICC.buildSrc.main" target="17" />
|
|
||||||
<module name="OpenEUICC.buildSrc.test" target="17" />
|
|
||||||
<module name="OpenEUICC.libs.hidden-apis-shim" target="17" />
|
|
||||||
<module name="OpenEUICC.libs.lpac-jni" target="17" />
|
|
||||||
</bytecodeTargetLevel>
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
39
.idea/gradle.xml
generated
39
.idea/gradle.xml
generated
|
@ -1,39 +0,0 @@
|
||||||
<?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
25
.idea/misc.xml
generated
|
@ -1,25 +0,0 @@
|
||||||
<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>
|
|
|
@ -20,14 +20,23 @@
|
||||||
android:label="@string/profile_notifications" />
|
android:label="@string/profile_notifications" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
android:name="im.angry.openeuicc.ui.EuiccInfoActivity"
|
||||||
android:label="@string/profile_download"
|
android:label="@string/euicc_info" />
|
||||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.LogsActivity"
|
android:name="im.angry.openeuicc.ui.LogsActivity"
|
||||||
android:label="@string/pref_advanced_logs" />
|
android:label="@string/pref_advanced_logs" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:exported="true"
|
||||||
|
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||||
|
android:label="@string/download_wizard" />
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:exported="true"
|
||||||
|
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
||||||
|
android:targetActivity="im.angry.openeuicc.ui.wizard.DownloadWizardActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||||
android:screenOrientation="fullSensor"
|
android:screenOrientation="fullSensor"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
|
interface ApduInterfaceAtrProvider {
|
||||||
|
val atr: ByteArray?
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import android.hardware.usb.UsbInterface
|
||||||
import android.hardware.usb.UsbManager
|
import android.hardware.usb.UsbManager
|
||||||
import android.se.omapi.SEService
|
import android.se.omapi.SEService
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.core.usb.UsbApduInterface
|
import im.angry.openeuicc.core.usb.UsbApduInterface
|
||||||
import im.angry.openeuicc.core.usb.getIoEndpoints
|
import im.angry.openeuicc.core.usb.getIoEndpoints
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
@ -33,14 +34,17 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
|
|
||||||
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
|
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
|
||||||
try {
|
try {
|
||||||
return EuiccChannel(
|
return EuiccChannelImpl(
|
||||||
|
context.getString(R.string.omapi),
|
||||||
port,
|
port,
|
||||||
|
intrinsicChannelName = null,
|
||||||
OmapiApduInterface(
|
OmapiApduInterface(
|
||||||
seService!!,
|
seService!!,
|
||||||
port,
|
port,
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
context.preferenceRepository.verboseLoggingFlow
|
||||||
),
|
),
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
).also {
|
).also {
|
||||||
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
||||||
it.lpa.setEs10xMss(60)
|
it.lpa.setEs10xMss(60)
|
||||||
|
@ -61,15 +65,18 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
if (bulkIn == null || bulkOut == null) return null
|
if (bulkIn == null || bulkOut == null) return null
|
||||||
val conn = usbManager.openDevice(usbDevice) ?: return null
|
val conn = usbManager.openDevice(usbDevice) ?: return null
|
||||||
if (!conn.claimInterface(usbInterface, true)) return null
|
if (!conn.claimInterface(usbInterface, true)) return null
|
||||||
return EuiccChannel(
|
return EuiccChannelImpl(
|
||||||
|
context.getString(R.string.usb),
|
||||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||||
|
intrinsicChannelName = usbDevice.productName,
|
||||||
UsbApduInterface(
|
UsbApduInterface(
|
||||||
conn,
|
conn,
|
||||||
bulkIn,
|
bulkIn,
|
||||||
bulkOut,
|
bulkOut,
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
context.preferenceRepository.verboseLoggingFlow
|
||||||
),
|
),
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,10 @@ import im.angry.openeuicc.di.AppContainer
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -88,44 +91,24 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
|
protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? =
|
||||||
runBlocking {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
return@withContext usbChannel
|
||||||
return@withContext usbChannel
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (card in uiccCards) {
|
for (card in uiccCards) {
|
||||||
for (port in card.ports) {
|
for (port in card.ports) {
|
||||||
if (port.logicalSlotIndex == logicalSlotId) {
|
if (port.logicalSlotIndex == logicalSlotId) {
|
||||||
return@withContext tryOpenEuiccChannel(port)
|
return@withContext tryOpenEuiccChannel(port)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? =
|
private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
|
||||||
runBlocking {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
|
||||||
return@withContext usbChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
for (card in uiccCards) {
|
|
||||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
|
||||||
for (port in card.ports) {
|
|
||||||
tryOpenEuiccChannel(port)?.let { return@withContext it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
|
|
||||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
return usbChannel?.let { listOf(it) }
|
return usbChannel?.let { listOf(it) }
|
||||||
}
|
}
|
||||||
|
@ -138,12 +121,7 @@ open class DefaultEuiccChannelManager(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
|
private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||||
runBlocking {
|
|
||||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
return@withContext usbChannel
|
return@withContext usbChannel
|
||||||
|
@ -154,26 +132,82 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
override suspend fun findFirstAvailablePort(physicalSlotId: Int): Int =
|
||||||
runBlocking {
|
withContext(Dispatchers.IO) {
|
||||||
findEuiccChannelByPort(physicalSlotId, portId)
|
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
|
return@withContext 0
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.getOrNull(0)?.portId ?: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
|
override suspend fun findAvailablePorts(physicalSlotId: Int): List<Int> =
|
||||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return
|
withContext(Dispatchers.IO) {
|
||||||
|
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
|
return@withContext listOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
// If there is already a valid channel, we close it proactively
|
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf()
|
||||||
// Sometimes the current channel can linger on for a bit even after it should have become invalid
|
}
|
||||||
channelCache.find { it.slotId == physicalSlotId && it.portId == portId }?.apply {
|
|
||||||
if (valid) close()
|
override suspend fun <R> withEuiccChannel(
|
||||||
|
physicalSlotId: Int,
|
||||||
|
portId: Int,
|
||||||
|
fn: suspend (EuiccChannel) -> R
|
||||||
|
): R {
|
||||||
|
val channel = findEuiccChannelByPort(physicalSlotId, portId)
|
||||||
|
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||||
|
val wrapper = EuiccChannelWrapper(channel)
|
||||||
|
try {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
fn(wrapper)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
wrapper.invalidateWrapper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun <R> withEuiccChannel(
|
||||||
|
logicalSlotId: Int,
|
||||||
|
fn: suspend (EuiccChannel) -> R
|
||||||
|
): R {
|
||||||
|
val channel = findEuiccChannelByLogicalSlot(logicalSlotId)
|
||||||
|
?: throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||||
|
val wrapper = EuiccChannelWrapper(channel)
|
||||||
|
try {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
fn(wrapper)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
wrapper.invalidateWrapper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
|
||||||
|
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
|
usbChannel?.close()
|
||||||
|
usbChannel = null
|
||||||
|
} else {
|
||||||
|
// If there is already a valid channel, we close it proactively
|
||||||
|
// Sometimes the current channel can linger on for a bit even after it should have become invalid
|
||||||
|
channelCache.find { it.slotId == physicalSlotId && it.portId == portId }?.apply {
|
||||||
|
if (valid) close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withTimeout(timeoutMillis) {
|
withTimeout(timeoutMillis) {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
val channel = if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
// and recreate when needed
|
// tryOpenUsbEuiccChannel() will always try to reopen the channel, even if
|
||||||
val channel = findEuiccChannelByPort(physicalSlotId, portId)!!
|
// a USB channel already exists
|
||||||
|
tryOpenUsbEuiccChannel()
|
||||||
|
usbChannel!!
|
||||||
|
} else {
|
||||||
|
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
||||||
|
// and recreate when needed
|
||||||
|
findEuiccChannelByPort(physicalSlotId, portId)!!
|
||||||
|
}
|
||||||
check(channel.valid) { "Invalid channel" }
|
check(channel.valid) { "Invalid channel" }
|
||||||
break
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -184,34 +218,42 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun enumerateEuiccChannels(): List<EuiccChannel> =
|
override fun flowInternalEuiccPorts(): Flow<Pair<Int, Int>> = flow {
|
||||||
withContext(Dispatchers.IO) {
|
uiccCards.forEach { info ->
|
||||||
uiccCards.flatMap { info ->
|
info.ports.forEach { port ->
|
||||||
info.ports.mapNotNull { port ->
|
tryOpenEuiccChannel(port)?.also {
|
||||||
tryOpenEuiccChannel(port)?.also {
|
Log.d(
|
||||||
Log.d(
|
TAG,
|
||||||
TAG,
|
"Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}"
|
||||||
"Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}"
|
)
|
||||||
)
|
|
||||||
}
|
emit(Pair(info.physicalSlotIndex, port.portIndex))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
override suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> =
|
override fun flowAllOpenEuiccPorts(): Flow<Pair<Int, Int>> =
|
||||||
|
merge(flowInternalEuiccPorts(), flow {
|
||||||
|
if (tryOpenUsbEuiccChannel().second) {
|
||||||
|
emit(Pair(EuiccChannelManager.USB_CHANNEL_ID, 0))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
override suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
usbManager.deviceList.values.forEach { device ->
|
usbManager.deviceList.values.forEach { device ->
|
||||||
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
|
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
|
||||||
val iface = device.getSmartCardInterface() ?: return@forEach
|
val iface = device.getSmartCardInterface() ?: return@forEach
|
||||||
// If we don't have permission, tell UI code that we found a candidate device, but we
|
// If we don't have permission, tell UI code that we found a candidate device, but we
|
||||||
// need permission to be able to do anything with it
|
// need permission to be able to do anything with it
|
||||||
if (!usbManager.hasPermission(device)) return@withContext Pair(device, null)
|
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
|
||||||
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
|
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
|
||||||
try {
|
try {
|
||||||
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
|
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
|
||||||
if (channel != null && channel.lpa.valid) {
|
if (channel != null && channel.lpa.valid) {
|
||||||
usbChannel = channel
|
usbChannel = channel
|
||||||
return@withContext Pair(device, channel)
|
return@withContext Pair(device, true)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignored -- skip forward
|
// Ignored -- skip forward
|
||||||
|
@ -219,7 +261,7 @@ open class DefaultEuiccChannelManager(
|
||||||
}
|
}
|
||||||
Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
|
Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
|
||||||
}
|
}
|
||||||
return@withContext Pair(null, null)
|
return@withContext Pair(null, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() {
|
override fun invalidate() {
|
||||||
|
|
|
@ -1,26 +1,32 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import net.typeblog.lpac_jni.ApduInterface
|
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
|
||||||
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
|
|
||||||
|
|
||||||
class EuiccChannel(
|
interface EuiccChannel {
|
||||||
val port: UiccPortInfoCompat,
|
val type: String
|
||||||
apduInterface: ApduInterface,
|
|
||||||
verboseLoggingFlow: Flow<Boolean>
|
|
||||||
) {
|
|
||||||
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
|
|
||||||
val logicalSlotId = port.logicalSlotIndex
|
|
||||||
val portId = port.portIndex
|
|
||||||
|
|
||||||
val lpa: LocalProfileAssistant =
|
val port: UiccPortInfoCompat
|
||||||
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow))
|
|
||||||
|
val slotId: Int // PHYSICAL slot
|
||||||
|
val logicalSlotId: Int
|
||||||
|
val portId: Int
|
||||||
|
|
||||||
|
val lpa: LocalProfileAssistant
|
||||||
|
|
||||||
val valid: Boolean
|
val valid: Boolean
|
||||||
get() = lpa.valid
|
|
||||||
|
|
||||||
fun close() = lpa.close()
|
/**
|
||||||
|
* Answer to Reset (ATR) value of the underlying interface, if any
|
||||||
|
*/
|
||||||
|
val atr: ByteArray?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intrinsic name of this channel. For device-internal SIM slots,
|
||||||
|
* this should be null; for USB readers, this should be the name of
|
||||||
|
* the reader device.
|
||||||
|
*/
|
||||||
|
val intrinsicChannelName: String?
|
||||||
|
|
||||||
|
fun close()
|
||||||
}
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
||||||
|
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
|
||||||
|
|
||||||
|
class EuiccChannelImpl(
|
||||||
|
override val type: String,
|
||||||
|
override val port: UiccPortInfoCompat,
|
||||||
|
override val intrinsicChannelName: String?,
|
||||||
|
private val apduInterface: ApduInterface,
|
||||||
|
verboseLoggingFlow: Flow<Boolean>,
|
||||||
|
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||||
|
) : EuiccChannel {
|
||||||
|
override val slotId = port.card.physicalSlotIndex
|
||||||
|
override val logicalSlotId = port.logicalSlotIndex
|
||||||
|
override val portId = port.portIndex
|
||||||
|
|
||||||
|
override val lpa: LocalProfileAssistant =
|
||||||
|
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow))
|
||||||
|
|
||||||
|
override val atr: ByteArray?
|
||||||
|
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
|
||||||
|
|
||||||
|
override val valid: Boolean
|
||||||
|
get() = lpa.valid
|
||||||
|
|
||||||
|
override fun close() = lpa.close()
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EuiccChannelManager holds references to, and manages the lifecycles of, individual
|
* EuiccChannelManager holds references to, and manages the lifecycles of, individual
|
||||||
|
@ -18,19 +19,35 @@ interface EuiccChannelManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan all possible _device internal_ sources for EuiccChannels, return them and have all
|
* Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical
|
||||||
* scanned channels cached; these channels will remain open for the entire lifetime of
|
* (slotId, portId) and have all scanned channels cached; these channels will remain open
|
||||||
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d
|
* for the entire lifetime of this EuiccChannelManager object, unless disconnected externally
|
||||||
|
* or invalidate()'d.
|
||||||
|
*
|
||||||
|
* To obtain a temporary reference to a EuiccChannel, use `withEuiccChannel()`.
|
||||||
*/
|
*/
|
||||||
suspend fun enumerateEuiccChannels(): List<EuiccChannel>
|
fun flowInternalEuiccPorts(): Flow<Pair<Int, Int>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as flowInternalEuiccPorts(), except that this includes non-device internal eUICC chips
|
||||||
|
* as well. Namely, this includes the USB reader.
|
||||||
|
*
|
||||||
|
* Non-internal readers will only be included if they have been opened properly, i.e. with permissions
|
||||||
|
* granted by the user.
|
||||||
|
*/
|
||||||
|
fun flowAllOpenEuiccPorts(): Flow<Pair<Int, Int>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan all possible USB devices for CCID readers that may contain eUICC cards.
|
* Scan all possible USB devices for CCID readers that may contain eUICC cards.
|
||||||
* If found, try to open it for access, and add it to the internal EuiccChannel cache
|
* If found, try to open it for access, and add it to the internal EuiccChannel cache
|
||||||
* as a "port" with id 99. When user interaction is required to obtain permission
|
* as a "port" with id 99. When user interaction is required to obtain permission
|
||||||
* to interact with the device, the second return value (EuiccChannel) will be null.
|
* to interact with the device, the second return value will be false.
|
||||||
|
*
|
||||||
|
* Returns (usbDevice, canOpen). canOpen is false if either (1) no usb reader is found;
|
||||||
|
* or (2) usb reader is found, but user interaction is required for access;
|
||||||
|
* or (3) usb reader is found, but we are unable to open ISD-R.
|
||||||
*/
|
*/
|
||||||
suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?>
|
suspend fun tryOpenUsbEuiccChannel(): Pair<UsbDevice?, Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for a slot + port to reconnect (i.e. become valid again)
|
* Wait for a slot + port to reconnect (i.e. become valid again)
|
||||||
|
@ -40,29 +57,40 @@ interface EuiccChannelManager {
|
||||||
suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000)
|
suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the EuiccChannel corresponding to a **logical** slot
|
* Returns the first mapped & available port ID for a physical slot, or -1 if
|
||||||
|
* not found.
|
||||||
*/
|
*/
|
||||||
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel?
|
suspend fun findFirstAvailablePort(physicalSlotId: Int): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the first EuiccChannel corresponding to a **physical** slot
|
* Returns all mapped & available port IDs for a physical slot.
|
||||||
* If the physical slot supports MEP and has multiple ports, it is undefined
|
|
||||||
* which of the two channels will be returned.
|
|
||||||
*/
|
*/
|
||||||
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel?
|
suspend fun findAvailablePorts(physicalSlotId: Int): List<Int>
|
||||||
|
|
||||||
|
class EuiccChannelNotFoundException: Exception("EuiccChannel not found")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all EuiccChannels corresponding to a **physical** slot
|
* Find a EuiccChannel by its slot and port, then run a callback with a reference to it.
|
||||||
* Multiple channels are possible in the case of MEP
|
* The reference is not supposed to be held outside of the callback. This is enforced via
|
||||||
|
* a wrapper object.
|
||||||
|
*
|
||||||
|
* The callback is run on Dispatchers.IO by default.
|
||||||
|
*
|
||||||
|
* If a channel for that slot / port is not found, EuiccChannelNotFoundException is thrown
|
||||||
*/
|
*/
|
||||||
suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>?
|
suspend fun <R> withEuiccChannel(
|
||||||
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>?
|
physicalSlotId: Int,
|
||||||
|
portId: Int,
|
||||||
|
fn: suspend (EuiccChannel) -> R
|
||||||
|
): R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the EuiccChannel corresponding to a **physical** slot and a port ID
|
* Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID
|
||||||
*/
|
*/
|
||||||
suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel?
|
suspend fun <R> withEuiccChannel(
|
||||||
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
|
logicalSlotId: Int,
|
||||||
|
fn: suspend (EuiccChannel) -> R
|
||||||
|
): R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate all EuiccChannels previously cached by this Manager
|
* Invalidate all EuiccChannels previously cached by this Manager
|
||||||
|
@ -74,7 +102,7 @@ interface EuiccChannelManager {
|
||||||
* This is only expected to be implemented when the application is privileged
|
* This is only expected to be implemented when the application is privileged
|
||||||
* TODO: Remove this from the common interface
|
* TODO: Remove this from the common interface
|
||||||
*/
|
*/
|
||||||
fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||||
// no-op by default
|
// no-op by default
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
|
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
||||||
|
private var _inner: EuiccChannel? = orig
|
||||||
|
|
||||||
|
private val channel: EuiccChannel
|
||||||
|
get() {
|
||||||
|
if (_inner == null) {
|
||||||
|
throw IllegalStateException("This wrapper has been invalidated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return _inner!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override val type: String
|
||||||
|
get() = channel.type
|
||||||
|
override val port: UiccPortInfoCompat
|
||||||
|
get() = channel.port
|
||||||
|
override val slotId: Int
|
||||||
|
get() = channel.slotId
|
||||||
|
override val logicalSlotId: Int
|
||||||
|
get() = channel.logicalSlotId
|
||||||
|
override val portId: Int
|
||||||
|
get() = channel.portId
|
||||||
|
private val lpaDelegate = lazy {
|
||||||
|
LocalProfileAssistantWrapper(channel.lpa)
|
||||||
|
}
|
||||||
|
override val lpa: LocalProfileAssistant by lpaDelegate
|
||||||
|
override val valid: Boolean
|
||||||
|
get() = channel.valid
|
||||||
|
override val intrinsicChannelName: String?
|
||||||
|
get() = channel.intrinsicChannelName
|
||||||
|
override val atr: ByteArray?
|
||||||
|
get() = channel.atr
|
||||||
|
|
||||||
|
override fun close() = channel.close()
|
||||||
|
|
||||||
|
fun invalidateWrapper() {
|
||||||
|
_inner = null
|
||||||
|
|
||||||
|
if (lpaDelegate.isInitialized()) {
|
||||||
|
(lpa as LocalProfileAssistantWrapper).invalidateWrapper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
|
import net.typeblog.lpac_jni.EuiccInfo2
|
||||||
|
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 LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
|
||||||
|
LocalProfileAssistant {
|
||||||
|
private var _inner: LocalProfileAssistant? = orig
|
||||||
|
|
||||||
|
private val lpa: LocalProfileAssistant
|
||||||
|
get() {
|
||||||
|
if (_inner == null) {
|
||||||
|
throw IllegalStateException("This wrapper has been invalidated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return _inner!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override val valid: Boolean
|
||||||
|
get() = lpa.valid
|
||||||
|
override val profiles: List<LocalProfileInfo>
|
||||||
|
get() = lpa.profiles
|
||||||
|
override val notifications: List<LocalProfileNotification>
|
||||||
|
get() = lpa.notifications
|
||||||
|
override val eID: String
|
||||||
|
get() = lpa.eID
|
||||||
|
override val euiccInfo2: EuiccInfo2?
|
||||||
|
get() = lpa.euiccInfo2
|
||||||
|
|
||||||
|
override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss)
|
||||||
|
|
||||||
|
override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
|
||||||
|
lpa.enableProfile(iccid, refresh)
|
||||||
|
|
||||||
|
override fun disableProfile(iccid: String, refresh: Boolean): Boolean =
|
||||||
|
lpa.disableProfile(iccid, refresh)
|
||||||
|
|
||||||
|
override fun deleteProfile(iccid: String): Boolean = lpa.deleteProfile(iccid)
|
||||||
|
|
||||||
|
override fun downloadProfile(
|
||||||
|
smdp: String,
|
||||||
|
matchingId: String?,
|
||||||
|
imei: String?,
|
||||||
|
confirmationCode: String?,
|
||||||
|
callback: ProfileDownloadCallback
|
||||||
|
) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback)
|
||||||
|
|
||||||
|
override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber)
|
||||||
|
|
||||||
|
override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber)
|
||||||
|
|
||||||
|
override fun euiccMemoryReset() = lpa.euiccMemoryReset()
|
||||||
|
|
||||||
|
override fun setNickname(iccid: String, nickname: String) {
|
||||||
|
lpa.setNickname(iccid, nickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = lpa.close()
|
||||||
|
|
||||||
|
fun invalidateWrapper() {
|
||||||
|
_inner = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ class OmapiApduInterface(
|
||||||
private val service: SEService,
|
private val service: SEService,
|
||||||
private val port: UiccPortInfoCompat,
|
private val port: UiccPortInfoCompat,
|
||||||
private val verboseLoggingFlow: Flow<Boolean>
|
private val verboseLoggingFlow: Flow<Boolean>
|
||||||
): ApduInterface {
|
): ApduInterface, ApduInterfaceAtrProvider {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "OmapiApduInterface"
|
const val TAG = "OmapiApduInterface"
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,9 @@ class OmapiApduInterface(
|
||||||
override val valid: Boolean
|
override val valid: Boolean
|
||||||
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
||||||
|
|
||||||
|
override val atr: ByteArray?
|
||||||
|
get() = session.atr
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession()
|
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession()
|
||||||
}
|
}
|
||||||
|
@ -38,8 +41,8 @@ class OmapiApduInterface(
|
||||||
check(!this::lastChannel.isInitialized) {
|
check(!this::lastChannel.isInitialized) {
|
||||||
"Can only open one channel"
|
"Can only open one channel"
|
||||||
}
|
}
|
||||||
lastChannel = session.openLogicalChannel(aid)!!;
|
lastChannel = session.openLogicalChannel(aid)!!
|
||||||
return 1;
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logicalChannelClose(handle: Int) {
|
override fun logicalChannelClose(handle: Int) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package im.angry.openeuicc.core.usb
|
||||||
import android.hardware.usb.UsbDeviceConnection
|
import android.hardware.usb.UsbDeviceConnection
|
||||||
import android.hardware.usb.UsbEndpoint
|
import android.hardware.usb.UsbEndpoint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import net.typeblog.lpac_jni.ApduInterface
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
|
@ -12,7 +13,7 @@ class UsbApduInterface(
|
||||||
private val bulkIn: UsbEndpoint,
|
private val bulkIn: UsbEndpoint,
|
||||||
private val bulkOut: UsbEndpoint,
|
private val bulkOut: UsbEndpoint,
|
||||||
private val verboseLoggingFlow: Flow<Boolean>
|
private val verboseLoggingFlow: Flow<Boolean>
|
||||||
): ApduInterface {
|
) : ApduInterface, ApduInterfaceAtrProvider {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "UsbApduInterface"
|
private const val TAG = "UsbApduInterface"
|
||||||
}
|
}
|
||||||
|
@ -22,6 +23,8 @@ class UsbApduInterface(
|
||||||
|
|
||||||
private var channelId = -1
|
private var channelId = -1
|
||||||
|
|
||||||
|
override var atr: ByteArray? = null
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
||||||
|
|
||||||
|
@ -32,7 +35,9 @@ class UsbApduInterface(
|
||||||
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
|
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
transceiver.iccPowerOn()
|
// 6.1.1.1 PC_to_RDR_IccPowerOn (Page 20 of 40)
|
||||||
|
// https://www.usb.org/sites/default/files/DWG_Smart-Card_USB-ICC_ICCD_rev10.pdf
|
||||||
|
atr = transceiver.iccPowerOn().data
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
throw e
|
throw e
|
||||||
|
|
|
@ -15,4 +15,5 @@ interface AppContainer {
|
||||||
val preferenceRepository: PreferenceRepository
|
val preferenceRepository: PreferenceRepository
|
||||||
val uiComponentFactory: UiComponentFactory
|
val uiComponentFactory: UiComponentFactory
|
||||||
val euiccChannelFactory: EuiccChannelFactory
|
val euiccChannelFactory: EuiccChannelFactory
|
||||||
|
val customizableTextProvider: CustomizableTextProvider
|
||||||
}
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
interface CustomizableTextProvider {
|
||||||
|
/**
|
||||||
|
* Explanation string for when no eUICC is found on the device.
|
||||||
|
* This could be different depending on whether the app is privileged or not.
|
||||||
|
*/
|
||||||
|
val noEuiccExplanation: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown when we timed out switching between profiles.
|
||||||
|
*/
|
||||||
|
val profileSwitchingTimeoutMessage: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the name of a logical slot; internal only -- not intended for
|
||||||
|
* other channels such as USB.
|
||||||
|
*/
|
||||||
|
fun formatInternalChannelName(logicalSlotId: Int): String
|
||||||
|
}
|
|
@ -38,4 +38,8 @@ open class DefaultAppContainer(context: Context) : AppContainer {
|
||||||
override val euiccChannelFactory by lazy {
|
override val euiccChannelFactory by lazy {
|
||||||
DefaultEuiccChannelFactory(context)
|
DefaultEuiccChannelFactory(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val customizableTextProvider by lazy {
|
||||||
|
DefaultCustomizableTextProvider(context)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import im.angry.openeuicc.common.R
|
||||||
|
|
||||||
|
open class DefaultCustomizableTextProvider(private val context: Context) : CustomizableTextProvider {
|
||||||
|
override val noEuiccExplanation: String
|
||||||
|
get() = context.getString(R.string.no_euicc)
|
||||||
|
|
||||||
|
override val profileSwitchingTimeoutMessage: String
|
||||||
|
get() = context.getString(R.string.enable_disable_timeout)
|
||||||
|
|
||||||
|
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||||
|
context.getString(R.string.channel_name_format, logicalSlotId)
|
||||||
|
}
|
|
@ -1,13 +1,16 @@
|
||||||
package im.angry.openeuicc.di
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||||
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
|
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
|
||||||
|
import im.angry.openeuicc.ui.SettingsFragment
|
||||||
|
|
||||||
open class DefaultUiComponentFactory : UiComponentFactory {
|
open class DefaultUiComponentFactory : UiComponentFactory {
|
||||||
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||||
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
EuiccManagementFragment.newInstance(slotId, portId)
|
||||||
|
|
||||||
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
||||||
|
|
||||||
|
override fun createSettingsFragment(): Fragment = SettingsFragment()
|
||||||
}
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
package im.angry.openeuicc.di
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||||
|
|
||||||
interface UiComponentFactory {
|
interface UiComponentFactory {
|
||||||
fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment
|
fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
|
||||||
fun createNoEuiccPlaceholderFragment(): Fragment
|
fun createNoEuiccPlaceholderFragment(): Fragment
|
||||||
|
fun createSettingsFragment(): Fragment
|
||||||
}
|
}
|
|
@ -15,14 +15,19 @@ import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
@ -55,7 +60,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
private const val TAG = "EuiccChannelManagerService"
|
private const val TAG = "EuiccChannelManagerService"
|
||||||
private const val CHANNEL_ID = "tasks"
|
private const val CHANNEL_ID = "tasks"
|
||||||
private const val FOREGROUND_ID = 1000
|
private const val FOREGROUND_ID = 1000
|
||||||
private const val TASK_FAILURE_ID = 1001
|
private const val TASK_FAILURE_ID = 1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to wait for a foreground task to be done, return its
|
||||||
|
* error if any, or null on success.
|
||||||
|
*/
|
||||||
|
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() {
|
inner class LocalBinder : Binder() {
|
||||||
|
@ -89,6 +113,25 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
private val foregroundTaskState: MutableStateFlow<ForegroundTaskState> =
|
private val foregroundTaskState: MutableStateFlow<ForegroundTaskState> =
|
||||||
MutableStateFlow(ForegroundTaskState.Idle)
|
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 {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
super.onBind(intent)
|
super.onBind(intent)
|
||||||
return LocalBinder()
|
return LocalBinder()
|
||||||
|
@ -166,12 +209,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification)
|
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.
|
* Launch a potentially blocking foreground task in this service's lifecycle context.
|
||||||
* This function does not block, but returns a Flow that emits ForegroundTaskState
|
* 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
|
* updates associated with this task. The last update the returned flow will emit is
|
||||||
* always ForegroundTaskState.Done. The returned flow MUST be started in order for the
|
* always ForegroundTaskState.Done.
|
||||||
* foreground task to run.
|
*
|
||||||
|
* 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.
|
||||||
*
|
*
|
||||||
* The task closure is expected to update foregroundTaskState whenever appropriate.
|
* The task closure is expected to update foregroundTaskState whenever appropriate.
|
||||||
* If a foreground task is already running, this function returns null.
|
* If a foreground task is already running, this function returns null.
|
||||||
|
@ -185,7 +242,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
failureTitle: String,
|
failureTitle: String,
|
||||||
iconRes: Int,
|
iconRes: Int,
|
||||||
task: suspend EuiccChannelManagerService.() -> Unit
|
task: suspend EuiccChannelManagerService.() -> Unit
|
||||||
): Flow<ForegroundTaskState>? {
|
): ForegroundTaskSubscriberFlow {
|
||||||
|
val taskID = System.currentTimeMillis()
|
||||||
|
|
||||||
// Atomically set the state to InProgress. If this returns true, we are
|
// Atomically set the state to InProgress. If this returns true, we are
|
||||||
// the only task currently in progress.
|
// the only task currently in progress.
|
||||||
if (!foregroundTaskState.compareAndSet(
|
if (!foregroundTaskState.compareAndSet(
|
||||||
|
@ -193,7 +252,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
ForegroundTaskState.InProgress(0)
|
ForegroundTaskState.InProgress(0)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return null
|
return ForegroundTaskSubscriberFlow(
|
||||||
|
taskID,
|
||||||
|
flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) })
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
@ -235,38 +296,71 @@ 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
|
// We should be the only task running, so we can subscribe to foregroundTaskState
|
||||||
// until we encounter ForegroundTaskState.Done.
|
// until we encounter ForegroundTaskState.Done.
|
||||||
// Then, we complete the returned flow, but we also set the state back to Idle.
|
// 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
|
// The state update back to Idle won't show up in the returned stream, because
|
||||||
// it has been completed by that point.
|
// it has been completed by that point.
|
||||||
return foregroundTaskState.transformWhile {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
// Also update our notification when we see an update
|
foregroundTaskState
|
||||||
// But ignore the first progress = 0 update -- that is the current value.
|
.applyCompletionTransform()
|
||||||
// we need that to be handled by the main coroutine after it finishes.
|
.onEach {
|
||||||
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
|
// Also update our notification when we see an update
|
||||||
withContext(Dispatchers.Main) {
|
// But ignore the first progress = 0 update -- that is the current value.
|
||||||
updateForegroundNotification(title, iconRes)
|
// we need that to be handled by the main coroutine after it finishes.
|
||||||
}
|
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
|
||||||
}
|
updateForegroundNotification(title, iconRes)
|
||||||
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
|
subscriberFlow.emit(it)
|
||||||
get() = foregroundTaskState.value != ForegroundTaskState.Idle
|
}
|
||||||
|
.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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun waitForForegroundTask() {
|
suspend fun waitForForegroundTask() {
|
||||||
foregroundTaskState.takeWhile { it != ForegroundTaskState.Idle }
|
foregroundTaskState.takeWhile { it != ForegroundTaskState.Idle }
|
||||||
|
@ -280,30 +374,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
matchingId: String?,
|
matchingId: String?,
|
||||||
confirmationCode: String?,
|
confirmationCode: String?,
|
||||||
imei: String?
|
imei: String?
|
||||||
): Flow<ForegroundTaskState>? =
|
): ForegroundTaskSubscriberFlow =
|
||||||
launchForegroundTask(
|
launchForegroundTask(
|
||||||
getString(R.string.task_profile_download),
|
getString(R.string.task_profile_download),
|
||||||
getString(R.string.task_profile_download_failure),
|
getString(R.string.task_profile_download_failure),
|
||||||
R.drawable.ic_task_sim_card_download
|
R.drawable.ic_task_sim_card_download
|
||||||
) {
|
) {
|
||||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||||
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)
|
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||||
val res = channel!!.lpa.downloadProfile(
|
channel.lpa.downloadProfile(
|
||||||
smdp,
|
smdp,
|
||||||
matchingId,
|
matchingId,
|
||||||
imei,
|
imei,
|
||||||
confirmationCode,
|
confirmationCode,
|
||||||
object : ProfileDownloadCallback {
|
object : ProfileDownloadCallback {
|
||||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||||
if (state.progress == 0) return
|
if (state.progress == 0) return
|
||||||
foregroundTaskState.value =
|
foregroundTaskState.value =
|
||||||
ForegroundTaskState.InProgress(state.progress)
|
ForegroundTaskState.InProgress(state.progress)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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()
|
preferenceRepository.notificationDownloadFlow.first()
|
||||||
|
@ -315,19 +405,17 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
portId: Int,
|
portId: Int,
|
||||||
iccid: String,
|
iccid: String,
|
||||||
name: String
|
name: String
|
||||||
): Flow<ForegroundTaskState>? =
|
): ForegroundTaskSubscriberFlow =
|
||||||
launchForegroundTask(
|
launchForegroundTask(
|
||||||
getString(R.string.task_profile_rename),
|
getString(R.string.task_profile_rename),
|
||||||
getString(R.string.task_profile_rename_failure),
|
getString(R.string.task_profile_rename_failure),
|
||||||
R.drawable.ic_task_rename
|
R.drawable.ic_task_rename
|
||||||
) {
|
) {
|
||||||
val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname(
|
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||||
iccid,
|
channel.lpa.setNickname(
|
||||||
name
|
iccid,
|
||||||
)
|
name
|
||||||
|
)
|
||||||
if (!res) {
|
|
||||||
throw RuntimeException("Profile not renamed")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,17 +423,16 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
slotId: Int,
|
slotId: Int,
|
||||||
portId: Int,
|
portId: Int,
|
||||||
iccid: String
|
iccid: String
|
||||||
): Flow<ForegroundTaskState>? =
|
): ForegroundTaskSubscriberFlow =
|
||||||
launchForegroundTask(
|
launchForegroundTask(
|
||||||
getString(R.string.task_profile_delete),
|
getString(R.string.task_profile_delete),
|
||||||
getString(R.string.task_profile_delete_failure),
|
getString(R.string.task_profile_delete_failure),
|
||||||
R.drawable.ic_task_delete
|
R.drawable.ic_task_delete
|
||||||
) {
|
) {
|
||||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||||
euiccChannelManager.findEuiccChannelByPort(
|
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||||
slotId,
|
channel.lpa.deleteProfile(iccid)
|
||||||
portId
|
}
|
||||||
)!!.lpa.deleteProfile(iccid)
|
|
||||||
|
|
||||||
preferenceRepository.notificationDeleteFlow.first()
|
preferenceRepository.notificationDeleteFlow.first()
|
||||||
}
|
}
|
||||||
|
@ -358,16 +445,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
portId: Int,
|
portId: Int,
|
||||||
iccid: String,
|
iccid: String,
|
||||||
enable: Boolean, // Enable or disable the profile indicated in iccid
|
enable: Boolean, // Enable or disable the profile indicated in iccid
|
||||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers
|
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
||||||
): Flow<ForegroundTaskState>? =
|
): ForegroundTaskSubscriberFlow =
|
||||||
launchForegroundTask(
|
launchForegroundTask(
|
||||||
getString(R.string.task_profile_switch),
|
getString(R.string.task_profile_switch),
|
||||||
getString(R.string.task_profile_switch_failure),
|
getString(R.string.task_profile_switch_failure),
|
||||||
R.drawable.ic_task_switch
|
R.drawable.ic_task_switch
|
||||||
) {
|
) {
|
||||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||||
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!
|
val (res, refreshed) = euiccChannelManager.withEuiccChannel(
|
||||||
val (res, refreshed) =
|
slotId,
|
||||||
|
portId
|
||||||
|
) { channel ->
|
||||||
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
|
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
|
||||||
// Sometimes, we *can* enable or disable the profile, but we cannot
|
// Sometimes, we *can* enable or disable the profile, but we cannot
|
||||||
// send the refresh command to the modem because the profile somehow
|
// send the refresh command to the modem because the profile somehow
|
||||||
|
@ -378,13 +467,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
} else {
|
} else {
|
||||||
Pair(true, true)
|
Pair(true, true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
throw RuntimeException("Could not switch profile")
|
throw RuntimeException("Could not switch profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!refreshed) {
|
if (!refreshed && slotId != EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
// We may have switched the profile, but we could not refresh. Tell the caller about this
|
// We may have switched the profile, but we could not refresh. Tell the caller about this
|
||||||
|
// but only if we are talking to a modem and not a USB reader
|
||||||
throw SwitchingProfilesRefreshException()
|
throw SwitchingProfilesRefreshException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package im.angry.openeuicc.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import im.angry.openeuicc.util.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class DirectProfileDownloadActivity : BaseEuiccAccessActivity(), SlotSelectFragment.SlotSelectedListener, OpenEuiccContextMarker {
|
|
||||||
override fun onInit() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val knownChannels = withContext(Dispatchers.IO) {
|
|
||||||
euiccChannelManager.enumerateEuiccChannels()
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
knownChannels.isEmpty() -> {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
knownChannels.hasMultipleChips -> {
|
|
||||||
SlotSelectFragment.newInstance(knownChannels.sortedBy { it.logicalSlotId })
|
|
||||||
.show(supportFragmentManager, SlotSelectFragment.TAG)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// If the device has only one eSIM "chip" (but may be mapped to multiple slots),
|
|
||||||
// we can skip the slot selection dialog since there is only one chip to save to.
|
|
||||||
onSlotSelected(knownChannels[0].slotId,
|
|
||||||
knownChannels[0].portId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSlotSelected(slotId: Int, portId: Int) {
|
|
||||||
ProfileDownloadFragment.newInstance(slotId, portId, finishWhenDone = true)
|
|
||||||
.show(supportFragmentManager, ProfileDownloadFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSlotSelectCancelled() = finish()
|
|
||||||
}
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
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.launch
|
||||||
|
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
|
||||||
|
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
|
||||||
|
|
||||||
|
class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
|
companion object {
|
||||||
|
private val YES_NO = Pair(R.string.yes, R.string.no)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||||
|
private lateinit var infoList: RecyclerView
|
||||||
|
|
||||||
|
private var logicalSlotId: Int = -1
|
||||||
|
|
||||||
|
data class Item(
|
||||||
|
@StringRes
|
||||||
|
val titleResId: Int,
|
||||||
|
val content: String?,
|
||||||
|
val copiedToastResId: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_euicc_info)
|
||||||
|
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||||
|
setupToolbarInsets()
|
||||||
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||||
|
infoList = requireViewById<RecyclerView>(R.id.recycler_view).also {
|
||||||
|
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||||
|
it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||||
|
it.adapter = EuiccInfoAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||||
|
|
||||||
|
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
|
getString(R.string.usb)
|
||||||
|
} else {
|
||||||
|
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||||
|
}
|
||||||
|
|
||||||
|
title = getString(R.string.euicc_info_activity_title, channelTitle)
|
||||||
|
|
||||||
|
swipeRefresh.setOnRefreshListener { refresh() }
|
||||||
|
|
||||||
|
setupRootViewInsets(infoList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInit() {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
swipeRefresh.isRefreshing = true
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
|
||||||
|
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
|
||||||
|
|
||||||
|
swipeRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
|
||||||
|
add(Item(R.string.euicc_info_access_mode, channel.type))
|
||||||
|
add(
|
||||||
|
Item(
|
||||||
|
R.string.euicc_info_removable,
|
||||||
|
formatByBoolean(channel.port.card.isRemovable, YES_NO)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
Item(
|
||||||
|
R.string.euicc_info_eid,
|
||||||
|
channel.lpa.eID,
|
||||||
|
copiedToastResId = R.string.toast_eid_copied
|
||||||
|
)
|
||||||
|
)
|
||||||
|
channel.lpa.euiccInfo2.let { info ->
|
||||||
|
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version))
|
||||||
|
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
||||||
|
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
||||||
|
add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
|
||||||
|
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
|
||||||
|
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
|
||||||
|
}
|
||||||
|
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
|
||||||
|
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
|
||||||
|
// https://www.gsma.com/newsroom/wp-content/uploads/SGP.28-v1.0.pdf#page=5
|
||||||
|
// FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
|
||||||
|
// https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
|
||||||
|
val resId = when {
|
||||||
|
signers.isEmpty() -> R.string.unknown // the case is not mp, but it's is not common
|
||||||
|
PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
|
||||||
|
PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
|
||||||
|
else -> R.string.euicc_info_ci_unknown
|
||||||
|
}
|
||||||
|
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
||||||
|
}
|
||||||
|
add(
|
||||||
|
Item(
|
||||||
|
R.string.euicc_info_atr,
|
||||||
|
channel.atr?.encodeHex() ?: getString(R.string.information_unavailable),
|
||||||
|
copiedToastResId = R.string.toast_atr_copied,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
|
||||||
|
getString(
|
||||||
|
if (b) {
|
||||||
|
res.first
|
||||||
|
} else {
|
||||||
|
res.second
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
|
||||||
|
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
|
||||||
|
private val content: TextView = root.requireViewById(R.id.euicc_info_content)
|
||||||
|
private var copiedToastResId: Int? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
root.setOnClickListener {
|
||||||
|
if (copiedToastResId != null) {
|
||||||
|
val label = title.text.toString()
|
||||||
|
getSystemService(ClipboardManager::class.java)!!
|
||||||
|
.setPrimaryClip(ClipData.newPlainText(label, content.text))
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@EuiccInfoActivity,
|
||||||
|
copiedToastResId!!,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(item: Item) {
|
||||||
|
copiedToastResId = item.copiedToastResId
|
||||||
|
title.setText(item.titleResId)
|
||||||
|
content.text = item.content ?: getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
|
||||||
|
var euiccInfoItems: List<Item> = listOf()
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
set(newVal) {
|
||||||
|
field = newVal
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EuiccInfoViewHolder {
|
||||||
|
val root = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.euicc_info_item, parent, false)
|
||||||
|
return EuiccInfoViewHolder(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = euiccInfoItems.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EuiccInfoViewHolder, position: Int) {
|
||||||
|
holder.bind(euiccInfoItems[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,9 @@ import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.PasswordTransformationMethod
|
import android.text.method.PasswordTransformationMethod
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
@ -21,6 +21,7 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -31,11 +32,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||||
|
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||||
|
import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.last
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -52,6 +54,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||||
private lateinit var fab: FloatingActionButton
|
private lateinit var fab: FloatingActionButton
|
||||||
private lateinit var profileList: RecyclerView
|
private lateinit var profileList: RecyclerView
|
||||||
|
private var logicalSlotId: Int = -1
|
||||||
|
|
||||||
private val adapter = EuiccProfileAdapter()
|
private val adapter = EuiccProfileAdapter()
|
||||||
|
|
||||||
|
@ -63,6 +66,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
// This gives us access to the "latest" state without having to launch coroutines
|
// This gives us access to the "latest" state without having to launch coroutines
|
||||||
private lateinit var disableSafeguardFlow: StateFlow<Boolean>
|
private lateinit var disableSafeguardFlow: StateFlow<Boolean>
|
||||||
|
|
||||||
|
private lateinit var unfilteredProfileListFlow: StateFlow<Boolean>
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
@ -105,8 +110,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
|
||||||
fab.setOnClickListener {
|
fab.setOnClickListener {
|
||||||
ProfileDownloadFragment.newInstance(slotId, portId)
|
Intent(requireContext(), DownloadWizardActivity::class.java).apply {
|
||||||
.show(childFragmentManager, ProfileDownloadFragment.TAG)
|
putExtra("selectedLogicalSlot", logicalSlotId)
|
||||||
|
startActivity(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +134,21 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.show_notifications -> {
|
R.id.show_notifications -> {
|
||||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
if (logicalSlotId != -1) {
|
||||||
putExtra("logicalSlotId", channel.logicalSlotId)
|
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||||
startActivity(this)
|
putExtra("logicalSlotId", logicalSlotId)
|
||||||
|
startActivity(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.euicc_info -> {
|
||||||
|
if (logicalSlotId != -1) {
|
||||||
|
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
||||||
|
putExtra("logicalSlotId", logicalSlotId)
|
||||||
|
startActivity(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -148,31 +167,43 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
listOf()
|
listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun refresh() {
|
private fun refresh() {
|
||||||
if (invalid) return
|
if (invalid) return
|
||||||
swipeRefresh.isRefreshing = true
|
swipeRefresh.isRefreshing = true
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
ensureEuiccChannelManager()
|
doRefresh()
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) {
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
disableSafeguardFlow =
|
protected open suspend fun doRefresh() {
|
||||||
preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope)
|
ensureEuiccChannelManager()
|
||||||
}
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
|
|
||||||
val profiles = withContext(Dispatchers.IO) {
|
if (!::disableSafeguardFlow.isInitialized) {
|
||||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
disableSafeguardFlow =
|
||||||
|
preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
if (!::unfilteredProfileListFlow.isInitialized) {
|
||||||
|
unfilteredProfileListFlow =
|
||||||
|
preferenceRepository.unfilteredProfileListFlow.stateIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
val profiles = withEuiccChannel { channel ->
|
||||||
|
logicalSlotId = channel.logicalSlotId
|
||||||
|
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||||
|
if (unfilteredProfileListFlow.value)
|
||||||
|
channel.lpa.profiles
|
||||||
|
else
|
||||||
channel.lpa.profiles.operational
|
channel.lpa.profiles.operational
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
adapter.profiles = profiles
|
adapter.profiles = profiles
|
||||||
adapter.footerViews = onCreateFooterViews(profileList, profiles)
|
adapter.footerViews = onCreateFooterViews(profileList, profiles)
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
swipeRefresh.isRefreshing = false
|
swipeRefresh.isRefreshing = false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,24 +223,15 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
ensureEuiccChannelManager()
|
ensureEuiccChannelManager()
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
|
|
||||||
val res = euiccChannelManagerService.launchProfileSwitchTask(
|
val err = euiccChannelManagerService.launchProfileSwitchTask(
|
||||||
slotId,
|
slotId,
|
||||||
portId,
|
portId,
|
||||||
iccid,
|
iccid,
|
||||||
enable,
|
enable,
|
||||||
reconnectTimeoutMillis = if (isUsb) {
|
reconnectTimeoutMillis = 30 * 1000
|
||||||
0
|
).waitDone()
|
||||||
} else {
|
|
||||||
30 * 1000
|
|
||||||
}
|
|
||||||
)?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done
|
|
||||||
|
|
||||||
if (res == null) {
|
when (err) {
|
||||||
showSwitchFailureText()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
when (res.error) {
|
|
||||||
null -> {}
|
null -> {}
|
||||||
is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
|
is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
|
||||||
// This is only really fatal for internal eSIMs
|
// This is only really fatal for internal eSIMs
|
||||||
|
@ -236,7 +258,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
invalid = true
|
invalid = true
|
||||||
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
setMessage(R.string.enable_disable_timeout)
|
setMessage(appContainer.customizableTextProvider.profileSwitchingTimeoutMessage)
|
||||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
|
@ -279,7 +301,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int) =
|
fun fromInt(value: Int) =
|
||||||
Type.values().first { it.value == value }
|
entries.first { it.value == value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,6 +329,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
private val name: TextView = root.requireViewById(R.id.name)
|
private val name: TextView = root.requireViewById(R.id.name)
|
||||||
private val state: TextView = root.requireViewById(R.id.state)
|
private val state: TextView = root.requireViewById(R.id.state)
|
||||||
private val provider: TextView = root.requireViewById(R.id.provider)
|
private val provider: TextView = root.requireViewById(R.id.provider)
|
||||||
|
private val profileClassLabel: TextView = root.requireViewById(R.id.profile_class_label)
|
||||||
|
private val profileClass: TextView = root.requireViewById(R.id.profile_class)
|
||||||
private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
|
private val profileMenu: ImageButton = root.requireViewById(R.id.profile_menu)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -321,7 +345,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
iccid.setOnLongClickListener {
|
iccid.setOnLongClickListener {
|
||||||
requireContext().getSystemService(ClipboardManager::class.java)!!
|
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||||
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text))
|
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text))
|
||||||
Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast
|
||||||
|
.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -343,6 +368,15 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
provider.text = profile.providerName
|
provider.text = profile.providerName
|
||||||
|
profileClassLabel.isVisible = unfilteredProfileListFlow.value
|
||||||
|
profileClass.isVisible = unfilteredProfileListFlow.value
|
||||||
|
profileClass.setText(
|
||||||
|
when (profile.profileClass) {
|
||||||
|
LocalProfileInfo.Clazz.Testing -> R.string.profile_class_testing
|
||||||
|
LocalProfileInfo.Clazz.Provisioning -> R.string.profile_class_provisioning
|
||||||
|
LocalProfileInfo.Clazz.Operational -> R.string.profile_class_operational
|
||||||
|
}
|
||||||
|
)
|
||||||
iccid.text = profile.iccid
|
iccid.text = profile.iccid
|
||||||
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
|
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.icu.text.SimpleDateFormat
|
import android.icu.text.SimpleDateFormat
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -8,7 +9,6 @@ import android.view.View
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
@ -17,7 +17,6 @@ import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LogsActivity : AppCompatActivity() {
|
class LogsActivity : AppCompatActivity() {
|
||||||
|
@ -27,15 +26,25 @@ class LogsActivity : AppCompatActivity() {
|
||||||
private lateinit var logStr: String
|
private lateinit var logStr: String
|
||||||
|
|
||||||
private val saveLogs =
|
private val saveLogs =
|
||||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
setupLogSaving(
|
||||||
if (uri == null) return@registerForActivityResult
|
getLogFileName = {
|
||||||
if (!this::logStr.isInitialized) return@registerForActivityResult
|
getString(
|
||||||
contentResolver.openFileDescriptor(uri, "w")?.use {
|
R.string.logs_filename_template,
|
||||||
FileOutputStream(it.fileDescriptor).use { os ->
|
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||||
os.write(logStr.encodeToByteArray())
|
)
|
||||||
}
|
},
|
||||||
}
|
getLogText = ::buildLogText
|
||||||
}
|
)
|
||||||
|
|
||||||
|
private fun buildLogText() = buildString {
|
||||||
|
appendLine("Manufacturer: ${Build.MANUFACTURER}")
|
||||||
|
appendLine("Brand: ${Build.BRAND}")
|
||||||
|
appendLine("Model: ${Build.MODEL}")
|
||||||
|
appendLine("SDK Version: ${Build.VERSION.SDK_INT}")
|
||||||
|
appendLine("App Version: $selfAppVersion")
|
||||||
|
appendLine("-".repeat(10))
|
||||||
|
appendLine(logStr)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
@ -76,9 +85,7 @@ class LogsActivity : AppCompatActivity() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.save -> {
|
R.id.save -> {
|
||||||
saveLogs.launch(getString(R.string.logs_filename_template,
|
saveLogs()
|
||||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
|
||||||
))
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
|
|
@ -23,9 +23,12 @@ import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ -44,6 +47,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
private var refreshing = false
|
private var refreshing = false
|
||||||
|
|
||||||
private data class Page(
|
private data class Page(
|
||||||
|
val logicalSlotId: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val createFragment: () -> Fragment
|
val createFragment: () -> Fragment
|
||||||
)
|
)
|
||||||
|
@ -105,7 +109,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.settings -> {
|
R.id.settings -> {
|
||||||
startActivity(Intent(this, SettingsActivity::class.java));
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.reload -> {
|
R.id.reload -> {
|
||||||
|
@ -122,7 +126,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureNotificationPermissions() {
|
private fun ensureNotificationPermissions() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
val needsNotificationPerms = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
|
||||||
|
val notificationPermsGranted =
|
||||||
|
needsNotificationPerms && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
if (needsNotificationPerms && !notificationPermsGranted) {
|
||||||
requestPermissions(
|
requestPermissions(
|
||||||
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
|
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
|
||||||
PERMISSION_REQUEST_CODE
|
PERMISSION_REQUEST_CODE
|
||||||
|
@ -138,65 +145,75 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
// Prevent concurrent access with any running foreground task
|
// Prevent concurrent access with any running foreground task
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
|
|
||||||
val knownChannels = withContext(Dispatchers.IO) {
|
val (usbDevice, _) = withContext(Dispatchers.IO) {
|
||||||
euiccChannelManager.enumerateEuiccChannels().onEach {
|
euiccChannelManager.tryOpenUsbEuiccChannel()
|
||||||
Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
|
}
|
||||||
|
|
||||||
|
val newPages: MutableList<Page> = mutableListOf()
|
||||||
|
|
||||||
|
euiccChannelManager.flowInternalEuiccPorts().onEach { (slotId, portId) ->
|
||||||
|
Log.d(TAG, "slot $slotId port $portId")
|
||||||
|
|
||||||
|
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||||
if (preferenceRepository.verboseLoggingFlow.first()) {
|
if (preferenceRepository.verboseLoggingFlow.first()) {
|
||||||
Log.d(TAG, it.lpa.eID)
|
Log.d(TAG, channel.lpa.eID)
|
||||||
}
|
}
|
||||||
// Request the system to refresh the list of profiles every time we start
|
// Request the system to refresh the list of profiles every time we start
|
||||||
// Note that this is currently supposed to be no-op when unprivileged,
|
// Note that this is currently supposed to be no-op when unprivileged,
|
||||||
// but it could change in the future
|
// but it could change in the future
|
||||||
euiccChannelManager.notifyEuiccProfilesChanged(it.logicalSlotId)
|
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||||
|
|
||||||
|
val channelName =
|
||||||
|
appContainer.customizableTextProvider.formatInternalChannelName(channel.logicalSlotId)
|
||||||
|
newPages.add(Page(channel.logicalSlotId, channelName) {
|
||||||
|
appContainer.uiComponentFactory.createEuiccManagementFragment(slotId, portId)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}.collect()
|
||||||
|
|
||||||
|
// If USB readers exist, add them at the very last
|
||||||
|
// We use a wrapper fragment to handle logic specific to USB readers
|
||||||
|
usbDevice?.let {
|
||||||
|
val productName = it.productName ?: getString(R.string.usb)
|
||||||
|
newPages.add(Page(EuiccChannelManager.USB_CHANNEL_ID, productName) {
|
||||||
|
UsbCcidReaderFragment()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
viewPager.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
if (newPages.size > 1) {
|
||||||
|
tabs.visibility = View.VISIBLE
|
||||||
|
} else if (newPages.isEmpty()) {
|
||||||
|
newPages.add(Page(-1, "") {
|
||||||
|
appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
val (usbDevice, _) = withContext(Dispatchers.IO) {
|
newPages.sortBy { it.logicalSlotId }
|
||||||
euiccChannelManager.enumerateUsbEuiccChannel()
|
|
||||||
|
pages.clear()
|
||||||
|
pages.addAll(newPages)
|
||||||
|
|
||||||
|
loadingProgress.visibility = View.GONE
|
||||||
|
pagerAdapter.notifyDataSetChanged()
|
||||||
|
// Reset the adapter so that the current view actually gets cleared
|
||||||
|
// notifyDataSetChanged() doesn't cause the current view to be removed.
|
||||||
|
viewPager.adapter = pagerAdapter
|
||||||
|
|
||||||
|
if (fromUsbEvent && usbDevice != null) {
|
||||||
|
// If this refresh was triggered by a USB insertion while active, scroll to that page
|
||||||
|
viewPager.post {
|
||||||
|
viewPager.setCurrentItem(pages.size - 1, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewPager.currentItem = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
if (pages.size > 0) {
|
||||||
loadingProgress.visibility = View.GONE
|
ensureNotificationPermissions()
|
||||||
|
|
||||||
knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
|
|
||||||
pages.add(Page(
|
|
||||||
getString(R.string.channel_name_format, channel.logicalSlotId)
|
|
||||||
) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// If USB readers exist, add them at the very last
|
|
||||||
// We use a wrapper fragment to handle logic specific to USB readers
|
|
||||||
usbDevice?.let {
|
|
||||||
pages.add(Page(it.productName ?: getString(R.string.usb)) { UsbCcidReaderFragment() })
|
|
||||||
}
|
|
||||||
viewPager.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
if (pages.size > 1) {
|
|
||||||
tabs.visibility = View.VISIBLE
|
|
||||||
} else if (pages.isEmpty()) {
|
|
||||||
pages.add(Page("") { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
|
|
||||||
}
|
|
||||||
|
|
||||||
pagerAdapter.notifyDataSetChanged()
|
|
||||||
// Reset the adapter so that the current view actually gets cleared
|
|
||||||
// notifyDataSetChanged() doesn't cause the current view to be removed.
|
|
||||||
viewPager.adapter = pagerAdapter
|
|
||||||
|
|
||||||
if (fromUsbEvent && usbDevice != null) {
|
|
||||||
// If this refresh was triggered by a USB insertion while active, scroll to that page
|
|
||||||
viewPager.post {
|
|
||||||
viewPager.setCurrentItem(pages.size - 1, true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
viewPager.currentItem = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pages.size > 0) {
|
|
||||||
ensureNotificationPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshing = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh(fromUsbEvent: Boolean = false) {
|
private fun refresh(fromUsbEvent: Boolean = false) {
|
||||||
|
|
|
@ -4,15 +4,20 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
|
||||||
class NoEuiccPlaceholderFragment : Fragment() {
|
class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
return inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false)
|
val view = inflater.inflate(R.layout.fragment_no_euicc_placeholder, container, false)
|
||||||
|
val textView = view.requireViewById<TextView>(R.id.no_euicc_placeholder)
|
||||||
|
textView.text = appContainer.customizableTextProvider.noEuiccExplanation
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -33,7 +32,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
private lateinit var notificationList: RecyclerView
|
private lateinit var notificationList: RecyclerView
|
||||||
private val notificationAdapter = NotificationAdapter()
|
private val notificationAdapter = NotificationAdapter()
|
||||||
|
|
||||||
private lateinit var euiccChannel: EuiccChannel
|
private var logicalSlotId = -1
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
@ -56,14 +55,14 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
notificationList.adapter = notificationAdapter
|
notificationList.adapter = notificationAdapter
|
||||||
registerForContextMenu(notificationList)
|
registerForContextMenu(notificationList)
|
||||||
|
|
||||||
val logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||||
|
|
||||||
// This is slightly different from the MainActivity logic
|
// This is slightly different from the MainActivity logic
|
||||||
// due to the length (we don't want to display the full USB product name)
|
// due to the length (we don't want to display the full USB product name)
|
||||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
getString(R.string.usb)
|
getString(R.string.usb)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.channel_name_format, logicalSlotId)
|
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||||
}
|
}
|
||||||
|
|
||||||
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
||||||
|
@ -104,16 +103,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
swipeRefresh.isRefreshing = true
|
swipeRefresh.isRefreshing = true
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
if (!this@NotificationsActivity::euiccChannel.isInitialized) {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
euiccChannelManagerLoaded.await()
|
||||||
euiccChannelManagerLoaded.await()
|
|
||||||
euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking(
|
|
||||||
intent.getIntExtra(
|
|
||||||
"logicalSlotId",
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)!!
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task()
|
task()
|
||||||
|
@ -124,15 +115,16 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
|
|
||||||
private fun refresh() {
|
private fun refresh() {
|
||||||
launchTask {
|
launchTask {
|
||||||
val profiles = withContext(Dispatchers.IO) {
|
|
||||||
euiccChannel.lpa.profiles
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationAdapter.notifications =
|
notificationAdapter.notifications =
|
||||||
withContext(Dispatchers.IO) {
|
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||||
euiccChannel.lpa.notifications.map {
|
val nameMap = buildMap {
|
||||||
val profile = profiles.find { p -> p.iccid == it.iccid }
|
for (profile in channel.lpa.profiles) {
|
||||||
LocalProfileNotificationWrapper(it, profile?.displayName ?: "???")
|
put(profile.iccid, profile.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.lpa.notifications.map {
|
||||||
|
LocalProfileNotificationWrapper(it, nameMap[it.iccid] ?: "???")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +139,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
inner class NotificationViewHolder(private val root: View):
|
inner class NotificationViewHolder(private val root: View):
|
||||||
RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
|
RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
|
||||||
private val address: TextView = root.requireViewById(R.id.notification_address)
|
private val address: TextView = root.requireViewById(R.id.notification_address)
|
||||||
|
private val sequenceNumber: TextView =
|
||||||
|
root.requireViewById(R.id.notification_sequence_number)
|
||||||
private val profileName: TextView = root.requireViewById(R.id.notification_profile_name)
|
private val profileName: TextView = root.requireViewById(R.id.notification_profile_name)
|
||||||
|
|
||||||
private lateinit var notification: LocalProfileNotificationWrapper
|
private lateinit var notification: LocalProfileNotificationWrapper
|
||||||
|
@ -168,6 +162,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun operationToLocalizedText(operation: LocalProfileNotification.Operation) =
|
private fun operationToLocalizedText(operation: LocalProfileNotification.Operation) =
|
||||||
root.context.getText(
|
root.context.getText(
|
||||||
when (operation) {
|
when (operation) {
|
||||||
|
@ -181,6 +176,10 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
notification = value
|
notification = value
|
||||||
|
|
||||||
address.text = value.inner.notificationAddress
|
address.text = value.inner.notificationAddress
|
||||||
|
sequenceNumber.text = root.context.getString(
|
||||||
|
R.string.profile_notification_sequence_number_format,
|
||||||
|
value.inner.seqNumber
|
||||||
|
)
|
||||||
profileName.text = Html.fromHtml(
|
profileName.text = Html.fromHtml(
|
||||||
root.context.getString(R.string.profile_notification_name_format,
|
root.context.getString(R.string.profile_notification_name_format,
|
||||||
operationToLocalizedText(value.inner.profileManagementOperation),
|
operationToLocalizedText(value.inner.profileManagementOperation),
|
||||||
|
@ -205,7 +204,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
R.id.notification_process -> {
|
R.id.notification_process -> {
|
||||||
launchTask {
|
launchTask {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
euiccChannel.lpa.handleNotification(notification.inner.seqNumber)
|
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||||
|
channel.lpa.handleNotification(notification.inner.seqNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
|
@ -215,7 +216,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
R.id.notification_delete -> {
|
R.id.notification_delete -> {
|
||||||
launchTask {
|
launchTask {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
euiccChannel.lpa.deleteNotification(notification.inner.seqNumber)
|
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||||
|
channel.lpa.deleteNotification(notification.inner.seqNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
|
|
|
@ -4,54 +4,69 @@ import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ProfileDeleteFragment"
|
const val TAG = "ProfileDeleteFragment"
|
||||||
|
private const val FIELD_ICCID = "iccid"
|
||||||
|
private const val FIELD_NAME = "name"
|
||||||
|
|
||||||
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
|
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
|
||||||
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
|
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
|
||||||
instance.requireArguments().apply {
|
instance.requireArguments().apply {
|
||||||
putString("iccid", iccid)
|
putString(FIELD_ICCID, iccid)
|
||||||
putString("name", name)
|
putString(FIELD_NAME, name)
|
||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val iccid by lazy {
|
||||||
|
requireArguments().getString(FIELD_ICCID)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private val name by lazy {
|
||||||
|
requireArguments().getString(FIELD_NAME)!!
|
||||||
|
}
|
||||||
|
|
||||||
private val editText by lazy {
|
private val editText by lazy {
|
||||||
EditText(requireContext()).apply {
|
EditText(requireContext()).apply {
|
||||||
hint = Editable.Factory.getInstance().newEditable(
|
hint = Editable.Factory.getInstance()
|
||||||
getString(R.string.profile_delete_confirm_input, requireArguments().getString("name")!!)
|
.newEditable(getString(R.string.profile_delete_confirm_input, name))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val inputMatchesName: Boolean
|
private val inputMatchesName: Boolean
|
||||||
get() = editText.text.toString() == requireArguments().getString("name")!!
|
get() = editText.text.toString() == name
|
||||||
|
|
||||||
|
private var toast: Toast? = null
|
||||||
|
|
||||||
private var deleting = false
|
private var deleting = false
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
private val alertDialog: AlertDialog
|
||||||
return AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply {
|
get() = requireDialog() as AlertDialog
|
||||||
setMessage(getString(R.string.profile_delete_confirm, requireArguments().getString("name")))
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||||
|
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply {
|
||||||
|
setMessage(getString(R.string.profile_delete_confirm, name))
|
||||||
setView(editText)
|
setView(editText)
|
||||||
setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing
|
setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
setNegativeButton(android.R.string.cancel, null)
|
||||||
}.create()
|
}.create()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
val alertDialog = dialog!! as AlertDialog
|
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
if (!deleting && inputMatchesName) delete()
|
if (!deleting) delete()
|
||||||
}
|
}
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||||
if (!deleting) dismiss()
|
if (!deleting) dismiss()
|
||||||
|
@ -59,8 +74,15 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun delete() {
|
private fun delete() {
|
||||||
|
toast?.cancel()
|
||||||
|
if (!inputMatchesName) {
|
||||||
|
val resId = R.string.toast_profile_delete_confirm_text_mismatched
|
||||||
|
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).also {
|
||||||
|
it.show()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
deleting = true
|
deleting = true
|
||||||
val alertDialog = dialog!! as AlertDialog
|
|
||||||
alertDialog.setCanceledOnTouchOutside(false)
|
alertDialog.setCanceledOnTouchOutside(false)
|
||||||
alertDialog.setCancelable(false)
|
alertDialog.setCancelable(false)
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||||
|
@ -69,12 +91,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||||
requireParentFragment().lifecycleScope.launch {
|
requireParentFragment().lifecycleScope.launch {
|
||||||
ensureEuiccChannelManager()
|
ensureEuiccChannelManager()
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
|
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
|
||||||
euiccChannelManagerService.launchProfileDeleteTask(
|
|
||||||
slotId,
|
|
||||||
portId,
|
|
||||||
requireArguments().getString("iccid")!!
|
|
||||||
)!!.onStart {
|
|
||||||
if (parentFragment is EuiccProfilesChangedListener) {
|
if (parentFragment is EuiccProfilesChangedListener) {
|
||||||
// Trigger a refresh in the parent fragment -- it should wait until
|
// Trigger a refresh in the parent fragment -- it should wait until
|
||||||
// any foreground task is completed before actually doing a refresh
|
// any foreground task is completed before actually doing a refresh
|
||||||
|
@ -86,7 +103,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
// Ignored
|
// Ignored
|
||||||
}
|
}
|
||||||
}.collect()
|
}.waitDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,282 +0,0 @@
|
||||||
package im.angry.openeuicc.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
|
||||||
import im.angry.openeuicc.common.R
|
|
||||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
|
||||||
import im.angry.openeuicc.util.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.last
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.Exception
|
|
||||||
|
|
||||||
class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|
||||||
Toolbar.OnMenuItemClickListener, EuiccChannelFragmentMarker {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "ProfileDownloadFragment"
|
|
||||||
|
|
||||||
fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment =
|
|
||||||
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) {
|
|
||||||
putBoolean("finishWhenDone", finishWhenDone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var toolbar: Toolbar
|
|
||||||
private lateinit var profileDownloadServer: TextInputLayout
|
|
||||||
private lateinit var profileDownloadCode: TextInputLayout
|
|
||||||
private lateinit var profileDownloadConfirmationCode: TextInputLayout
|
|
||||||
private lateinit var profileDownloadIMEI: TextInputLayout
|
|
||||||
private lateinit var profileDownloadFreeSpace: TextView
|
|
||||||
private lateinit var progress: ProgressBar
|
|
||||||
|
|
||||||
private var freeNvram: Int = -1
|
|
||||||
|
|
||||||
private var downloading = false
|
|
||||||
|
|
||||||
private val finishWhenDone by lazy {
|
|
||||||
requireArguments().getBoolean("finishWhenDone", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
|
|
||||||
result.contents?.let { content ->
|
|
||||||
onScanResult(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val gallerySelectorLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
|
|
||||||
if (result == null) return@registerForActivityResult
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
runCatching {
|
|
||||||
requireContext().contentResolver.openInputStream(result)?.let { input ->
|
|
||||||
val bmp = BitmapFactory.decodeStream(input)
|
|
||||||
input.close()
|
|
||||||
|
|
||||||
decodeQrFromBitmap(bmp)?.let {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onScanResult(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bmp.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onScanResult(result: String) {
|
|
||||||
val components = result.split("$")
|
|
||||||
if (components.size < 3 || components[0] != "LPA:1") return
|
|
||||||
profileDownloadServer.editText?.setText(components[1])
|
|
||||||
profileDownloadCode.editText?.setText(components[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val view = inflater.inflate(R.layout.fragment_profile_download, container, false)
|
|
||||||
|
|
||||||
toolbar = view.requireViewById(R.id.toolbar)
|
|
||||||
profileDownloadServer = view.requireViewById(R.id.profile_download_server)
|
|
||||||
profileDownloadCode = view.requireViewById(R.id.profile_download_code)
|
|
||||||
profileDownloadConfirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
|
|
||||||
profileDownloadIMEI = view.requireViewById(R.id.profile_download_imei)
|
|
||||||
profileDownloadFreeSpace = view.requireViewById(R.id.profile_download_free_space)
|
|
||||||
progress = view.requireViewById(R.id.progress)
|
|
||||||
|
|
||||||
toolbar.inflateMenu(R.menu.fragment_profile_download)
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
toolbar.apply {
|
|
||||||
setTitle(R.string.profile_download)
|
|
||||||
setNavigationOnClickListener {
|
|
||||||
if (!downloading) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setOnMenuItemClickListener(this@ProfileDownloadFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean = downloading ||
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.scan -> {
|
|
||||||
barcodeScannerLauncher.launch(ScanOptions().apply {
|
|
||||||
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
|
||||||
setOrientationLocked(false)
|
|
||||||
})
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.scan_from_gallery -> {
|
|
||||||
gallerySelectorLauncher.launch("image/*")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.ok -> {
|
|
||||||
startDownloadProfile()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
setWidthPercent(95)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
ensureEuiccChannelManager()
|
|
||||||
if (euiccChannelManagerService.isForegroundTaskRunning) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val imei = try {
|
|
||||||
telephonyManager.getImei(channel.logicalSlotId) ?: ""
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch remaining NVRAM
|
|
||||||
val str = channel.lpa.euiccInfo2?.freeNvram?.also {
|
|
||||||
freeNvram = it
|
|
||||||
}?.let { formatFreeSpace(it) }
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space,
|
|
||||||
str ?: getText(R.string.unknown))
|
|
||||||
profileDownloadIMEI.editText!!.text =
|
|
||||||
Editable.Factory.getInstance().newEditable(imei)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return super.onCreateDialog(savedInstanceState).also {
|
|
||||||
it.setCanceledOnTouchOutside(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startDownloadProfile() {
|
|
||||||
val server = profileDownloadServer.editText!!.let {
|
|
||||||
it.text.toString().trim().apply {
|
|
||||||
if (isEmpty()) {
|
|
||||||
it.requestFocus()
|
|
||||||
return@startDownloadProfile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val code = profileDownloadCode.editText!!.text.toString().trim()
|
|
||||||
.ifBlank { null }
|
|
||||||
val confirmationCode = profileDownloadConfirmationCode.editText!!.text.toString().trim()
|
|
||||||
.ifBlank { null }
|
|
||||||
val imei = profileDownloadIMEI.editText!!.text.toString().trim()
|
|
||||||
.ifBlank { null }
|
|
||||||
|
|
||||||
downloading = true
|
|
||||||
|
|
||||||
profileDownloadServer.editText!!.isEnabled = false
|
|
||||||
profileDownloadCode.editText!!.isEnabled = false
|
|
||||||
profileDownloadConfirmationCode.editText!!.isEnabled = false
|
|
||||||
profileDownloadIMEI.editText!!.isEnabled = false
|
|
||||||
|
|
||||||
progress.isIndeterminate = true
|
|
||||||
progress.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
ensureEuiccChannelManager()
|
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
|
||||||
val res = doDownloadProfile(server, code, confirmationCode, imei)
|
|
||||||
|
|
||||||
if (res == null || res.error != null) {
|
|
||||||
Log.d(TAG, "Error downloading profile")
|
|
||||||
|
|
||||||
if (res?.error != null) {
|
|
||||||
Log.d(TAG, Log.getStackTraceString(res.error))
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentFragment is EuiccProfilesChangedListener) {
|
|
||||||
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
dismiss()
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun doDownloadProfile(
|
|
||||||
server: String,
|
|
||||||
code: String?,
|
|
||||||
confirmationCode: String?,
|
|
||||||
imei: String?
|
|
||||||
) = withContext(Dispatchers.Main) {
|
|
||||||
// The service is responsible for launching the actual blocking part on the IO context
|
|
||||||
val res = euiccChannelManagerService.launchProfileDownloadTask(
|
|
||||||
slotId,
|
|
||||||
portId,
|
|
||||||
server,
|
|
||||||
code,
|
|
||||||
confirmationCode,
|
|
||||||
imei
|
|
||||||
)!!.onEach {
|
|
||||||
if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) {
|
|
||||||
progress.progress = it.progress
|
|
||||||
progress.isIndeterminate = it.progress == 0
|
|
||||||
} else {
|
|
||||||
progress.progress = 100
|
|
||||||
progress.isIndeterminate = false
|
|
||||||
}
|
|
||||||
}.last()
|
|
||||||
|
|
||||||
res as? EuiccChannelManagerService.ForegroundTaskState.Done
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
if (finishWhenDone) {
|
|
||||||
activity?.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(dialog: DialogInterface) {
|
|
||||||
super.onCancel(dialog)
|
|
||||||
if (finishWhenDone) {
|
|
||||||
activity?.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,9 +11,10 @@ import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -53,6 +54,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
|
||||||
toolbar.apply {
|
toolbar.apply {
|
||||||
setTitle(R.string.rename)
|
setTitle(R.string.rename)
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener {
|
||||||
|
@ -65,11 +67,6 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
setWidthPercent(95)
|
setWidthPercent(95)
|
||||||
|
@ -81,13 +78,18 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rename() {
|
private fun showErrorAndCancel(errorStrRes: Int) {
|
||||||
val name = profileRenameNewName.editText!!.text.toString().trim()
|
Toast.makeText(
|
||||||
if (name.length >= 64) {
|
requireContext(),
|
||||||
Toast.makeText(context, R.string.toast_profile_name_too_long, Toast.LENGTH_LONG).show()
|
errorStrRes,
|
||||||
return
|
Toast.LENGTH_LONG
|
||||||
}
|
).show()
|
||||||
|
|
||||||
|
renaming = false
|
||||||
|
progress.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rename() {
|
||||||
renaming = true
|
renaming = true
|
||||||
progress.isIndeterminate = true
|
progress.isIndeterminate = true
|
||||||
progress.visibility = View.VISIBLE
|
progress.visibility = View.VISIBLE
|
||||||
|
@ -95,21 +97,37 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
ensureEuiccChannelManager()
|
ensureEuiccChannelManager()
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
euiccChannelManagerService.launchProfileRenameTask(
|
val res = euiccChannelManagerService.launchProfileRenameTask(
|
||||||
slotId,
|
slotId,
|
||||||
portId,
|
portId,
|
||||||
requireArguments().getString("iccid")!!,
|
requireArguments().getString("iccid")!!,
|
||||||
name
|
profileRenameNewName.editText!!.text.toString().trim()
|
||||||
)?.collect()
|
).waitDone()
|
||||||
|
|
||||||
if (parentFragment is EuiccProfilesChangedListener) {
|
when (res) {
|
||||||
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
is LocalProfileAssistant.ProfileNameTooLongException -> {
|
||||||
}
|
showErrorAndCancel(R.string.profile_rename_too_long)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
is LocalProfileAssistant.ProfileNameIsInvalidUTF8Exception -> {
|
||||||
dismiss()
|
showErrorAndCancel(R.string.profile_rename_encoding_error)
|
||||||
} catch (e: IllegalStateException) {
|
}
|
||||||
// Ignored
|
|
||||||
|
is Throwable -> {
|
||||||
|
showErrorAndCancel(R.string.profile_rename_failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
if (parentFragment is EuiccProfilesChangedListener) {
|
||||||
|
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dismiss()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,14 @@ import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
|
||||||
class SettingsActivity: AppCompatActivity() {
|
class SettingsActivity: AppCompatActivity() {
|
||||||
|
private val appContainer
|
||||||
|
get() = (application as OpenEuiccApplication).appContainer
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -15,8 +19,9 @@ class SettingsActivity: AppCompatActivity() {
|
||||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||||
setupToolbarInsets()
|
setupToolbarInsets()
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val settingsFragment = appContainer.uiComponentFactory.createSettingsFragment()
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.settings_container, SettingsFragment())
|
.replace(R.id.settings_container, settingsFragment)
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,71 +2,154 @@ package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.provider.Settings
|
||||||
import android.view.View
|
import android.widget.Toast
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class SettingsFragment: PreferenceFragmentCompat() {
|
open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
|
private lateinit var developerPref: PreferenceCategory
|
||||||
|
|
||||||
|
// Hidden developer options switch
|
||||||
|
private var numClicks = 0
|
||||||
|
private var lastClickTimestamp = -1L
|
||||||
|
private var lastToast: Toast? = null
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.pref_settings, rootKey)
|
setPreferencesFromResource(R.xml.pref_settings, rootKey)
|
||||||
|
|
||||||
findPreference<Preference>("pref_info_app_version")
|
developerPref = requirePreference("pref_developer")
|
||||||
?.summary = requireContext().selfAppVersion
|
|
||||||
|
|
||||||
findPreference<Preference>("pref_info_source_code")
|
// Show / hide developer preference based on whether it is enabled
|
||||||
?.setOnPreferenceClickListener {
|
lifecycleScope.launch {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.summary.toString())))
|
preferenceRepository.developerOptionsEnabledFlow
|
||||||
true
|
.onEach { developerPref.isVisible = it }
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
requirePreference<Preference>("pref_info_app_version").apply {
|
||||||
|
summary = requireContext().selfAppVersion
|
||||||
|
|
||||||
|
// Enable developer options when this is clicked for 7 times
|
||||||
|
setOnPreferenceClickListener(::onAppVersionClicked)
|
||||||
|
}
|
||||||
|
|
||||||
|
requirePreference<Preference>("pref_advanced_language").apply {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return@apply
|
||||||
|
isVisible = true
|
||||||
|
intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", requireContext().packageName, null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findPreference<Preference>("pref_advanced_logs")
|
requirePreference<Preference>("pref_advanced_logs").apply {
|
||||||
?.setOnPreferenceClickListener {
|
intent = Intent(requireContext(), LogsActivity::class.java)
|
||||||
startActivity(Intent(requireContext(), LogsActivity::class.java))
|
}
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_notifications_download")
|
requirePreference<CheckBoxPreference>("pref_notifications_download")
|
||||||
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
|
.bindBooleanFlow(preferenceRepository.notificationDownloadFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_notifications_delete")
|
requirePreference<CheckBoxPreference>("pref_notifications_delete")
|
||||||
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
|
.bindBooleanFlow(preferenceRepository.notificationDeleteFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_notifications_switch")
|
requirePreference<CheckBoxPreference>("pref_notifications_switch")
|
||||||
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH)
|
.bindBooleanFlow(preferenceRepository.notificationSwitchFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
requirePreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
||||||
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM)
|
.bindBooleanFlow(preferenceRepository.disableSafeguardFlow)
|
||||||
|
|
||||||
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
requirePreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
||||||
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
|
.bindBooleanFlow(preferenceRepository.verboseLoggingFlow)
|
||||||
|
|
||||||
|
requirePreference<CheckBoxPreference>("pref_developer_unfiltered_profile_list")
|
||||||
|
.bindBooleanFlow(preferenceRepository.unfilteredProfileListFlow)
|
||||||
|
|
||||||
|
requirePreference<CheckBoxPreference>("pref_developer_ignore_tls_certificate")
|
||||||
|
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
||||||
|
findPreference<T>(key)!!
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
setupRootViewInsets(requireView().requireViewById(androidx.preference.R.id.recycler_view))
|
setupRootViewInsets(requireView().requireViewById(R.id.recycler_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
private fun onAppVersionClicked(pref: Preference): Boolean {
|
||||||
|
if (developerPref.isVisible) return false
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastClickTimestamp >= 1000) {
|
||||||
|
numClicks = 1
|
||||||
|
} else {
|
||||||
|
numClicks++
|
||||||
|
}
|
||||||
|
lastClickTimestamp = now
|
||||||
|
|
||||||
|
if (numClicks == 7) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
preferenceRepository.developerOptionsEnabledFlow.updatePreference(true)
|
||||||
|
|
||||||
|
lastToast?.cancel()
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.developer_options_enabled,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else if (numClicks > 1) {
|
||||||
|
lastToast?.cancel()
|
||||||
|
lastToast = Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
getString(R.string.developer_options_steps, 7 - numClicks),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
lastToast!!.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
flow.collect { isChecked = it }
|
flow.collect { isChecked = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
runBlocking {
|
runBlocking {
|
||||||
preferenceRepository.updatePreference(key, newValue as Boolean)
|
flow.updatePreference(newValue as Boolean)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun mergePreferenceOverlay(overlayKey: String, targetKey: String) {
|
||||||
|
val overlayCat = requirePreference<PreferenceCategory>(overlayKey)
|
||||||
|
val targetCat = requirePreference<PreferenceCategory>(targetKey)
|
||||||
|
|
||||||
|
val prefs = buildList {
|
||||||
|
for (i in 0..<overlayCat.preferenceCount) {
|
||||||
|
add(overlayCat.getPreference(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.forEach {
|
||||||
|
overlayCat.removePreference(it)
|
||||||
|
targetCat.addPreference(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayCat.parent?.removePreference(overlayCat)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,93 +0,0 @@
|
||||||
package im.angry.openeuicc.ui
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import im.angry.openeuicc.common.R
|
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
|
||||||
import im.angry.openeuicc.util.*
|
|
||||||
|
|
||||||
class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "SlotSelectFragment"
|
|
||||||
|
|
||||||
fun newInstance(knownChannels: List<EuiccChannel>): SlotSelectFragment {
|
|
||||||
return SlotSelectFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putIntArray("slotIds", knownChannels.map { it.slotId }.toIntArray())
|
|
||||||
putIntArray("logicalSlotIds", knownChannels.map { it.logicalSlotId }.toIntArray())
|
|
||||||
putIntArray("portIds", knownChannels.map { it.portId }.toIntArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SlotSelectedListener {
|
|
||||||
fun onSlotSelected(slotId: Int, portId: Int)
|
|
||||||
fun onSlotSelectCancelled()
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var toolbar: Toolbar
|
|
||||||
private lateinit var spinner: Spinner
|
|
||||||
private lateinit var adapter: ArrayAdapter<String>
|
|
||||||
private lateinit var slotIds: IntArray
|
|
||||||
private lateinit var logicalSlotIds: IntArray
|
|
||||||
private lateinit var portIds: IntArray
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val view = inflater.inflate(R.layout.fragment_slot_select, container, false)
|
|
||||||
|
|
||||||
toolbar = view.requireViewById(R.id.toolbar)
|
|
||||||
toolbar.setTitle(R.string.slot_select)
|
|
||||||
toolbar.inflateMenu(R.menu.fragment_slot_select)
|
|
||||||
|
|
||||||
adapter = ArrayAdapter<String>(inflater.context, R.layout.spinner_item)
|
|
||||||
|
|
||||||
spinner = view.requireViewById(R.id.spinner)
|
|
||||||
spinner.adapter = adapter
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
slotIds = requireArguments().getIntArray("slotIds")!!
|
|
||||||
logicalSlotIds = requireArguments().getIntArray("logicalSlotIds")!!
|
|
||||||
portIds = requireArguments().getIntArray("portIds")!!
|
|
||||||
|
|
||||||
logicalSlotIds.forEach { id ->
|
|
||||||
adapter.add(getString(R.string.channel_name_format, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbar.setNavigationOnClickListener {
|
|
||||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
|
||||||
}
|
|
||||||
toolbar.setOnMenuItemClickListener {
|
|
||||||
val slotId = slotIds[spinner.selectedItemPosition]
|
|
||||||
val portId = portIds[spinner.selectedItemPosition]
|
|
||||||
(requireActivity() as SlotSelectedListener).onSlotSelected(slotId, portId)
|
|
||||||
dismiss()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
setWidthPercent(75)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(dialog: DialogInterface) {
|
|
||||||
super.onCancel(dialog)
|
|
||||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,7 +20,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -73,7 +72,6 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
private lateinit var loadingProgress: ProgressBar
|
private lateinit var loadingProgress: ProgressBar
|
||||||
|
|
||||||
private var usbDevice: UsbDevice? = null
|
private var usbDevice: UsbDevice? = null
|
||||||
private var usbChannel: EuiccChannel? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -122,7 +120,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
try {
|
try {
|
||||||
requireContext().unregisterReceiver(usbPermissionReceiver)
|
requireContext().unregisterReceiver(usbPermissionReceiver)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +129,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
try {
|
try {
|
||||||
requireContext().unregisterReceiver(usbPermissionReceiver)
|
requireContext().unregisterReceiver(usbPermissionReceiver)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,24 +138,26 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
permissionButton.visibility = View.GONE
|
permissionButton.visibility = View.GONE
|
||||||
loadingProgress.visibility = View.VISIBLE
|
loadingProgress.visibility = View.VISIBLE
|
||||||
|
|
||||||
val (device, channel) = withContext(Dispatchers.IO) {
|
val (device, canOpen) = withContext(Dispatchers.IO) {
|
||||||
euiccChannelManager.enumerateUsbEuiccChannel()
|
euiccChannelManager.tryOpenUsbEuiccChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingProgress.visibility = View.GONE
|
loadingProgress.visibility = View.GONE
|
||||||
|
|
||||||
usbDevice = device
|
usbDevice = device
|
||||||
usbChannel = channel
|
|
||||||
|
|
||||||
if (device != null && channel == null && !usbManager.hasPermission(device)) {
|
if (device != null && !canOpen && !usbManager.hasPermission(device)) {
|
||||||
text.text = getString(R.string.usb_permission_needed)
|
text.text = getString(R.string.usb_permission_needed)
|
||||||
text.visibility = View.VISIBLE
|
text.visibility = View.VISIBLE
|
||||||
permissionButton.visibility = View.VISIBLE
|
permissionButton.visibility = View.VISIBLE
|
||||||
} else if (device != null && channel != null) {
|
} else if (device != null && canOpen) {
|
||||||
childFragmentManager.commit {
|
childFragmentManager.commit {
|
||||||
replace(
|
replace(
|
||||||
R.id.child_container,
|
R.id.child_container,
|
||||||
appContainer.uiComponentFactory.createEuiccManagementFragment(channel)
|
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||||
|
slotId = EuiccChannelManager.USB_CHANNEL_ID,
|
||||||
|
portId = 0
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
|
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() {
|
||||||
|
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?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private lateinit var state: DownloadWizardState
|
||||||
|
|
||||||
|
private lateinit var progressBar: ProgressBar
|
||||||
|
private lateinit var nextButton: Button
|
||||||
|
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()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_download_wizard)
|
||||||
|
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
// Make back == prev
|
||||||
|
onPrevPressed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
state = DownloadWizardState(
|
||||||
|
null,
|
||||||
|
intent.getIntExtra("selectedLogicalSlot", 0),
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(navigation) { v, insets ->
|
||||||
|
val bars = insets.getInsets(
|
||||||
|
WindowInsetsCompat.Type.systemBars()
|
||||||
|
or WindowInsetsCompat.Type.displayCutout()
|
||||||
|
or WindowInsetsCompat.Type.ime()
|
||||||
|
)
|
||||||
|
v.updatePadding(bars.left, 0, bars.right, bars.bottom)
|
||||||
|
val newParams = navigation.layoutParams
|
||||||
|
newParams.height = origHeight + bars.bottom
|
||||||
|
navigation.layoutParams = newParams
|
||||||
|
WindowInsetsCompat.CONSUMED
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentRoot = requireViewById<View>(R.id.step_fragment_container)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(fragmentRoot) { v, insets ->
|
||||||
|
val bars = insets.getInsets(
|
||||||
|
WindowInsetsCompat.Type.systemBars()
|
||||||
|
or WindowInsetsCompat.Type.displayCutout()
|
||||||
|
)
|
||||||
|
v.updatePadding(bars.left, bars.top, bars.right, 0)
|
||||||
|
WindowInsetsCompat.CONSUMED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
hideIme()
|
||||||
|
|
||||||
|
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() {
|
||||||
|
hideIme()
|
||||||
|
|
||||||
|
nextButton.isEnabled = false
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
progressBar.isIndeterminate = true
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
if (state.selectedLogicalSlot >= 0) {
|
||||||
|
try {
|
||||||
|
// This is run on IO by default
|
||||||
|
euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel ->
|
||||||
|
// Be _very_ sure that the channel we got is valid
|
||||||
|
if (!channel.valid) throw EuiccChannelManager.EuiccChannelNotFoundException()
|
||||||
|
}
|
||||||
|
} catch (e: EuiccChannelManager.EuiccChannelNotFoundException) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@DownloadWizardActivity,
|
||||||
|
R.string.download_wizard_slot_removed,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
nextButton.isEnabled = true
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFragment(
|
||||||
|
nextFrag: DownloadWizardStepFragment,
|
||||||
|
enterAnim: Int = 0,
|
||||||
|
exitAnim: Int = 0
|
||||||
|
) {
|
||||||
|
currentFragment = nextFrag
|
||||||
|
supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim)
|
||||||
|
.replace(R.id.step_fragment_container, nextFrag)
|
||||||
|
.commit()
|
||||||
|
refreshButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshButtons() {
|
||||||
|
currentFragment?.let {
|
||||||
|
nextButton.visibility = if (it.hasNext) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
prevButton.visibility = if (it.hasPrev) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideIme() {
|
||||||
|
currentFocus?.let {
|
||||||
|
val imm = getSystemService(InputMethodManager::class.java)
|
||||||
|
imm.hideSoftInputFromWindow(it.windowToken, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class DownloadWizardStepFragment : Fragment(), OpenEuiccContextMarker {
|
||||||
|
protected val state: DownloadWizardState
|
||||||
|
get() = (requireActivity() as DownloadWizardActivity).state
|
||||||
|
|
||||||
|
abstract val hasNext: Boolean
|
||||||
|
abstract val hasPrev: Boolean
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun showProgressBar(progressValue: Int) {
|
||||||
|
(requireActivity() as DownloadWizardActivity).progressBar.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
if (progressValue >= 0) {
|
||||||
|
isIndeterminate = false
|
||||||
|
progress = progressValue
|
||||||
|
} else {
|
||||||
|
isIndeterminate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun refreshButtons() {
|
||||||
|
(requireActivity() as DownloadWizardActivity).refreshButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun beforeNext() {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
private fun saveState() {
|
||||||
|
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 beforeNext() = saveState()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
saveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateInputCompleteness() {
|
||||||
|
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
|
||||||
|
refreshButtons()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
|
import android.icu.text.SimpleDateFormat
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||||
|
override val hasNext: Boolean
|
||||||
|
get() = true
|
||||||
|
override val hasPrev: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
private lateinit var diagnosticTextView: TextView
|
||||||
|
|
||||||
|
private val saveDiagnostics =
|
||||||
|
setupLogSaving(
|
||||||
|
getLogFileName = {
|
||||||
|
getString(
|
||||||
|
R.string.download_wizard_diagnostics_file_template,
|
||||||
|
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
getLogText = { diagnosticTextView.text.toString() }
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
view.requireViewById<View>(R.id.download_wizard_diagnostics_save).setOnClickListener {
|
||||||
|
saveDiagnostics()
|
||||||
|
}
|
||||||
|
diagnosticTextView = view.requireViewById(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()
|
||||||
|
|
||||||
|
ret.appendLine(
|
||||||
|
getString(
|
||||||
|
R.string.download_wizard_diagnostics_error_code,
|
||||||
|
err.lpaErrorReason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ret.appendLine()
|
||||||
|
|
||||||
|
err.lastHttpResponse?.let { resp ->
|
||||||
|
if (resp.rcode != 200) {
|
||||||
|
// Only show the status if it's not 200
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
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 android.widget.Toast
|
||||||
|
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_paste_go, R.string.download_wizard_method_clipboard) {
|
||||||
|
handleLoadFromClipboard()
|
||||||
|
},
|
||||||
|
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 handleLoadFromClipboard() {
|
||||||
|
val clipboard = requireContext().getSystemService(ClipboardManager::class.java)
|
||||||
|
val text = clipboard.primaryClip?.getItemAt(0)?.text
|
||||||
|
|
||||||
|
if (text == null) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.profile_download_no_lpa_string,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processLpaString(text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processLpaString(s: String) {
|
||||||
|
val components = s.split("$")
|
||||||
|
if (components.size < 3 || components[0] != "LPA:1") {
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.profile_download_incorrect_lpa_string)
|
||||||
|
setMessage(R.string.profile_download_incorrect_lpa_string_message)
|
||||||
|
setCancelable(true)
|
||||||
|
setNegativeButton(android.R.string.cancel, null)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
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()
|
||||||
|
|
||||||
|
state.downloadError =
|
||||||
|
it.error as? LocalProfileAssistant.ProfileDownloadException
|
||||||
|
|
||||||
|
// Change the state of the last InProgress item to success (or error)
|
||||||
|
progressItems.forEachIndexed { index, progressItem ->
|
||||||
|
if (progressItem.state == ProgressState.InProgress) {
|
||||||
|
progressItem.state =
|
||||||
|
if (state.downloadError == null) ProgressState.Done else ProgressState.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import im.angry.openeuicc.common.R
|
||||||
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||||
|
|
||||||
|
class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() {
|
||||||
|
companion object {
|
||||||
|
const val LOW_NVRAM_THRESHOLD =
|
||||||
|
30 * 1024 // < 30 KiB, alert about potential download failure
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SlotInfo(
|
||||||
|
val logicalSlotId: Int,
|
||||||
|
val isRemovable: Boolean,
|
||||||
|
val hasMultiplePorts: Boolean,
|
||||||
|
val portId: Int,
|
||||||
|
val eID: String,
|
||||||
|
val freeSpace: Int,
|
||||||
|
val imei: String,
|
||||||
|
val enabledProfileName: String?,
|
||||||
|
val intrinsicChannelName: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private var loaded = false
|
||||||
|
|
||||||
|
private val adapter = SlotInfoAdapter()
|
||||||
|
|
||||||
|
override val hasNext: Boolean
|
||||||
|
get() = loaded && adapter.slots.isNotEmpty()
|
||||||
|
override val hasPrev: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||||
|
DownloadWizardMethodSelectFragment()
|
||||||
|
|
||||||
|
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||||
|
|
||||||
|
override fun beforeNext() {
|
||||||
|
super.beforeNext()
|
||||||
|
|
||||||
|
if (adapter.selected.freeSpace < LOW_NVRAM_THRESHOLD) {
|
||||||
|
val activity = requireActivity()
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.profile_download_low_nvram_title)
|
||||||
|
setMessage(R.string.profile_download_low_nvram_message)
|
||||||
|
setCancelable(true)
|
||||||
|
setPositiveButton(android.R.string.ok, null)
|
||||||
|
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_download_slot_select, container, false)
|
||||||
|
val recyclerView = view.requireViewById<RecyclerView>(R.id.download_slot_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()
|
||||||
|
if (!loaded) {
|
||||||
|
lifecycleScope.launch { init() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged", "MissingPermission")
|
||||||
|
private suspend fun init() {
|
||||||
|
ensureEuiccChannelManager()
|
||||||
|
showProgressBar(-1)
|
||||||
|
val slots = euiccChannelManager.flowAllOpenEuiccPorts().map { (slotId, portId) ->
|
||||||
|
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||||
|
SlotInfo(
|
||||||
|
channel.logicalSlotId,
|
||||||
|
channel.port.card.isRemovable,
|
||||||
|
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.enabled?.displayName,
|
||||||
|
channel.intrinsicChannelName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.toList().sortedBy { it.logicalSlotId }
|
||||||
|
adapter.slots = slots
|
||||||
|
|
||||||
|
// Ensure we always have a selected slot by default
|
||||||
|
val selectedIdx = slots.indexOfFirst { it.logicalSlotId == state.selectedLogicalSlot }
|
||||||
|
adapter.currentSelectedIdx = if (selectedIdx > 0) {
|
||||||
|
selectedIdx
|
||||||
|
} else {
|
||||||
|
if (slots.isNotEmpty()) {
|
||||||
|
state.selectedLogicalSlot = slots[0].logicalSlotId
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slots.isNotEmpty()) {
|
||||||
|
state.imei = slots[adapter.currentSelectedIdx].imei
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
hideProgressBar()
|
||||||
|
loaded = true
|
||||||
|
refreshButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SlotItemHolder(val root: View) : ViewHolder(root) {
|
||||||
|
private val title = root.requireViewById<TextView>(R.id.slot_item_title)
|
||||||
|
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
|
||||||
|
|
||||||
|
init {
|
||||||
|
root.setOnClickListener(this::onSelect)
|
||||||
|
checkBox.setOnClickListener(this::onSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun onSelect(view: View) {
|
||||||
|
if (curIdx < 0) return
|
||||||
|
checkBox.isChecked = true
|
||||||
|
if (adapter.currentSelectedIdx == curIdx) return
|
||||||
|
val lastIdx = adapter.currentSelectedIdx
|
||||||
|
adapter.currentSelectedIdx = curIdx
|
||||||
|
adapter.notifyItemChanged(lastIdx)
|
||||||
|
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) {
|
||||||
|
curIdx = idx
|
||||||
|
|
||||||
|
type.text = if (item.isRemovable) {
|
||||||
|
root.context.getString(R.string.download_wizard_slot_type_removable)
|
||||||
|
} else if (!item.hasMultiplePorts) {
|
||||||
|
root.context.getString(R.string.download_wizard_slot_type_internal)
|
||||||
|
} else {
|
||||||
|
root.context.getString(
|
||||||
|
R.string.download_wizard_slot_type_internal_port,
|
||||||
|
item.portId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
title.text = if (item.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
|
item.intrinsicChannelName ?: root.context.getString(R.string.usb)
|
||||||
|
} else {
|
||||||
|
appContainer.customizableTextProvider.formatInternalChannelName(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SlotInfoAdapter : RecyclerView.Adapter<SlotItemHolder>() {
|
||||||
|
var slots: List<SlotInfo> = listOf()
|
||||||
|
var currentSelectedIdx = -1
|
||||||
|
|
||||||
|
val selected: SlotInfo
|
||||||
|
get() = slots[currentSelectedIdx]
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SlotItemHolder {
|
||||||
|
val root = LayoutInflater.from(parent.context).inflate(R.layout.download_slot_item, parent, false)
|
||||||
|
return SlotItemHolder(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = slots.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: SlotItemHolder, position: Int) {
|
||||||
|
holder.bind(slots[position], position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,15 +32,17 @@ val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
|
||||||
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
|
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
|
||||||
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
|
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
|
||||||
|
|
||||||
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
|
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
|
||||||
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
|
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
|
||||||
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker
|
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
|
||||||
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
|
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
|
||||||
val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
|
|
||||||
get() =
|
|
||||||
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
|
|
||||||
|
|
||||||
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
|
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
|
||||||
|
ensureEuiccChannelManager()
|
||||||
|
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
|
||||||
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
|
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
|
||||||
|
|
||||||
interface EuiccProfilesChangedListener {
|
interface EuiccProfilesChangedListener {
|
||||||
|
|
|
@ -3,9 +3,6 @@ package im.angry.openeuicc.util
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
import im.angry.openeuicc.core.EuiccChannel
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||||
|
|
||||||
|
@ -19,9 +16,10 @@ val LocalProfileInfo.isEnabled: Boolean
|
||||||
get() = state == LocalProfileInfo.State.Enabled
|
get() = state == LocalProfileInfo.State.Enabled
|
||||||
|
|
||||||
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
|
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
|
||||||
get() = filter {
|
get() = filter { it.profileClass == LocalProfileInfo.Clazz.Operational }
|
||||||
it.profileClass == LocalProfileInfo.Clazz.Operational
|
|
||||||
}
|
val List<LocalProfileInfo>.enabled: LocalProfileInfo?
|
||||||
|
get() = find { it.isEnabled }
|
||||||
|
|
||||||
val List<EuiccChannel>.hasMultipleChips: Boolean
|
val List<EuiccChannel>.hasMultipleChips: Boolean
|
||||||
get() = distinctBy { it.slotId }.size > 1
|
get() = distinctBy { it.slotId }.size > 1
|
||||||
|
@ -42,22 +40,27 @@ fun LocalProfileAssistant.switchProfile(
|
||||||
* See EuiccManager.waitForReconnect()
|
* See EuiccManager.waitForReconnect()
|
||||||
*/
|
*/
|
||||||
fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
|
fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
|
||||||
profiles.find { it.isEnabled }?.let {
|
profiles.enabled?.let {
|
||||||
Log.i(TAG, "Disabling active profile ${it.iccid}")
|
Log.i(TAG, "Disabling active profile ${it.iccid}")
|
||||||
disableProfile(it.iccid, refresh)
|
disableProfile(it.iccid, refresh)
|
||||||
} ?: true
|
} ?: true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable the active profile, return a lambda that reverts this action when called.
|
* Disable the current active profile if any. If refresh is true, also cause a refresh command.
|
||||||
* If refreshOnDisable is true, also cause a eUICC refresh command. Note that refreshing
|
|
||||||
* will disconnect the eUICC and might need some time before being operational again.
|
|
||||||
* See EuiccManager.waitForReconnect()
|
* See EuiccManager.waitForReconnect()
|
||||||
|
*
|
||||||
|
* Return the iccid of the profile being disabled, or null if no active profile found or failed to
|
||||||
|
* disable.
|
||||||
*/
|
*/
|
||||||
fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean): () -> Unit =
|
fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? =
|
||||||
profiles.find { it.isEnabled }?.let {
|
profiles.enabled?.let {
|
||||||
disableProfile(it.iccid, refreshOnDisable)
|
Log.i(TAG, "Disabling active profile ${it.iccid}")
|
||||||
return { enableProfile(it.iccid) }
|
if (disableProfile(it.iccid, refresh)) {
|
||||||
} ?: { }
|
it.iccid
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begin a "tracked" operation where notifications may be generated by the eSIM
|
* Begin a "tracked" operation where notifications may be generated by the eSIM
|
||||||
|
@ -78,60 +81,21 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
||||||
portId: Int,
|
portId: Int,
|
||||||
op: () -> Boolean
|
op: () -> Boolean
|
||||||
) {
|
) {
|
||||||
val latestSeq =
|
val latestSeq = withEuiccChannel(slotId, portId) { channel ->
|
||||||
findEuiccChannelByPort(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber
|
channel.lpa.notifications.firstOrNull()?.seqNumber
|
||||||
?: 0
|
?: 0
|
||||||
|
}
|
||||||
Log.d(TAG, "Latest notification is $latestSeq before operation")
|
Log.d(TAG, "Latest notification is $latestSeq before operation")
|
||||||
if (op()) {
|
if (op()) {
|
||||||
Log.d(TAG, "Operation has requested notification handling")
|
Log.d(TAG, "Operation has requested notification handling")
|
||||||
try {
|
try {
|
||||||
// Note that the exact instance of "channel" might have changed here if reconnected;
|
// Note that the exact instance of "channel" might have changed here if reconnected;
|
||||||
// so we MUST use the automatic getter for "channel"
|
// this is why we need to use two distinct calls to withEuiccChannel()
|
||||||
findEuiccChannelByPort(
|
withEuiccChannel(slotId, portId) { channel ->
|
||||||
slotId,
|
channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
|
||||||
portId
|
Log.d(TAG, "Handling notification $it")
|
||||||
)?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach {
|
channel.lpa.handleNotification(it.seqNumber)
|
||||||
Log.d(TAG, "Handling notification $it")
|
}
|
||||||
findEuiccChannelByPort(
|
|
||||||
slotId,
|
|
||||||
portId
|
|
||||||
)?.lpa?.handleNotification(it.seqNumber)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Ignore any error during notification handling
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Operation complete")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as beginTrackedOperation but uses blocking primitives.
|
|
||||||
* TODO: This function needs to be phased out of use.
|
|
||||||
*/
|
|
||||||
inline fun EuiccChannelManager.beginTrackedOperationBlocking(
|
|
||||||
slotId: Int,
|
|
||||||
portId: Int,
|
|
||||||
op: () -> Boolean
|
|
||||||
) {
|
|
||||||
val latestSeq =
|
|
||||||
findEuiccChannelByPortBlocking(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber
|
|
||||||
?: 0
|
|
||||||
Log.d(TAG, "Latest notification is $latestSeq before operation")
|
|
||||||
if (op()) {
|
|
||||||
Log.d(TAG, "Operation has requested notification handling")
|
|
||||||
try {
|
|
||||||
// Note that the exact instance of "channel" might have changed here if reconnected;
|
|
||||||
// so we MUST use the automatic getter for "channel"
|
|
||||||
findEuiccChannelByPortBlocking(
|
|
||||||
slotId,
|
|
||||||
portId
|
|
||||||
)?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach {
|
|
||||||
Log.d(TAG, "Handling notification $it")
|
|
||||||
findEuiccChannelByPortBlocking(
|
|
||||||
slotId,
|
|
||||||
portId
|
|
||||||
)?.lpa?.handleNotification(it.seqNumber)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignore any error during notification handling
|
// Ignore any error during notification handling
|
||||||
|
|
|
@ -19,38 +19,54 @@ val Context.preferenceRepository: PreferenceRepository
|
||||||
val Fragment.preferenceRepository: PreferenceRepository
|
val Fragment.preferenceRepository: PreferenceRepository
|
||||||
get() = requireContext().preferenceRepository
|
get() = requireContext().preferenceRepository
|
||||||
|
|
||||||
object PreferenceKeys {
|
internal object PreferenceKeys {
|
||||||
|
// ---- Profile Notifications ----
|
||||||
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
||||||
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
||||||
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
|
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
|
||||||
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
|
|
||||||
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreferenceRepository(context: Context) {
|
|
||||||
private val dataStore = context.dataStore
|
|
||||||
|
|
||||||
// Expose flows so that we can also handle default values
|
|
||||||
// ---- Profile Notifications ----
|
|
||||||
val notificationDownloadFlow: Flow<Boolean> =
|
|
||||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DOWNLOAD] ?: true }
|
|
||||||
|
|
||||||
val notificationDeleteFlow: Flow<Boolean> =
|
|
||||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DELETE] ?: true }
|
|
||||||
|
|
||||||
val notificationSwitchFlow: Flow<Boolean> =
|
|
||||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_SWITCH] ?: false }
|
|
||||||
|
|
||||||
// ---- Advanced ----
|
// ---- Advanced ----
|
||||||
val disableSafeguardFlow: Flow<Boolean> =
|
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
|
||||||
dataStore.data.map { it[PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM] ?: false }
|
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
|
||||||
|
|
||||||
val verboseLoggingFlow: Flow<Boolean> =
|
// ---- Developer Options ----
|
||||||
dataStore.data.map { it[PreferenceKeys.VERBOSE_LOGGING] ?: false }
|
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
||||||
|
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
||||||
|
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
|
class PreferenceRepository(private val context: Context) {
|
||||||
dataStore.edit {
|
// Expose flows so that we can also handle default values
|
||||||
it[key] = value
|
// ---- Profile Notifications ----
|
||||||
}
|
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
|
||||||
|
val notificationDeleteFlow = bindFlow(PreferenceKeys.NOTIFICATION_DELETE, true)
|
||||||
|
val notificationSwitchFlow = bindFlow(PreferenceKeys.NOTIFICATION_SWITCH, false)
|
||||||
|
|
||||||
|
// ---- Advanced ----
|
||||||
|
val disableSafeguardFlow = bindFlow(PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM, false)
|
||||||
|
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
|
||||||
|
|
||||||
|
// ---- Developer Options ----
|
||||||
|
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
||||||
|
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||||
|
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||||
|
|
||||||
|
private 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>
|
||||||
|
) : Flow<T> by inner {
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,3 +28,73 @@ fun formatFreeSpace(size: Int): String =
|
||||||
} else {
|
} else {
|
||||||
"$size B"
|
"$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()
|
||||||
|
}
|
|
@ -45,6 +45,8 @@ fun SEService.getUiccReaderCompat(slotNumber: Int): Reader {
|
||||||
interface UiccCardInfoCompat {
|
interface UiccCardInfoCompat {
|
||||||
val physicalSlotIndex: Int
|
val physicalSlotIndex: Int
|
||||||
val ports: Collection<UiccPortInfoCompat>
|
val ports: Collection<UiccPortInfoCompat>
|
||||||
|
val isRemovable: Boolean
|
||||||
|
get() = true // This defaults to removable unless overridden
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UiccPortInfoCompat {
|
interface UiccPortInfoCompat {
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultCaller
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
||||||
/**
|
/**
|
||||||
|
@ -70,3 +77,48 @@ fun setupRootViewInsets(view: ViewGroup) {
|
||||||
WindowInsetsCompat.CONSUMED
|
WindowInsetsCompat.CONSUMED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : ActivityResultCaller> T.setupLogSaving(
|
||||||
|
getLogFileName: () -> String,
|
||||||
|
getLogText: () -> String
|
||||||
|
): () -> Unit {
|
||||||
|
var lastFileName = "untitled"
|
||||||
|
|
||||||
|
val launchSaveIntent =
|
||||||
|
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||||
|
if (uri == null) return@registerForActivityResult
|
||||||
|
|
||||||
|
val context = when (this@setupLogSaving) {
|
||||||
|
is Context -> this@setupLogSaving
|
||||||
|
is Fragment -> requireContext()
|
||||||
|
else -> throw IllegalArgumentException("Must be either Context or Fragment!")
|
||||||
|
}
|
||||||
|
|
||||||
|
context.contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||||
|
FileOutputStream(it.fileDescriptor).use { os ->
|
||||||
|
os.write(getLogText().encodeToByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(context).apply {
|
||||||
|
setMessage(R.string.logs_saved_message)
|
||||||
|
setNegativeButton(R.string.no) { _, _ -> }
|
||||||
|
setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
clipData = ClipData.newUri(context.contentResolver, lastFileName, uri)
|
||||||
|
putExtra(Intent.EXTRA_TITLE, lastFileName)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(Intent.createChooser(intent, null))
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastFileName = getLogFileName()
|
||||||
|
launchSaveIntent.launch(lastFileName)
|
||||||
|
}
|
||||||
|
}
|
6
app-common/src/main/res/anim/slide_in_left.xml
Normal file
6
app-common/src/main/res/anim/slide_in_left.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?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%" />
|
6
app-common/src/main/res/anim/slide_in_right.xml
Normal file
6
app-common/src/main/res/anim/slide_in_right.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?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%" />
|
6
app-common/src/main/res/anim/slide_out_left.xml
Normal file
6
app-common/src/main/res/anim/slide_out_left.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<!-- 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%" />
|
6
app-common/src/main/res/anim/slide_out_right.xml
Normal file
6
app-common/src/main/res/anim/slide_out_right.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<!-- 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%" />
|
5
app-common/src/main/res/drawable/ic_chevron_left.xml
Normal file
5
app-common/src/main/res/drawable/ic_chevron_left.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<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="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
|
||||||
|
|
||||||
|
</vector>
|
5
app-common/src/main/res/drawable/ic_chevron_right.xml
Normal file
5
app-common/src/main/res/drawable/ic_chevron_right.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<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="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
|
||||||
|
|
||||||
|
</vector>
|
5
app-common/src/main/res/drawable/ic_edit.xml
Normal file
5
app-common/src/main/res/drawable/ic_edit.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
7
app-common/src/main/res/drawable/ic_paste_go.xml
Normal file
7
app-common/src/main/res/drawable/ic_paste_go.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M5,5h2v3h10V5h2v6h2V5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1S9.6,1.84 9.18,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h5v-2H5V5zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,3 12,3z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18.01,13l-1.42,1.41l1.58,1.58l-6.17,0l0,2l6.17,0l-1.58,1.59l1.42,1.41l3.99,-4z"/>
|
||||||
|
|
||||||
|
</vector>
|
74
app-common/src/main/res/layout/activity_download_wizard.xml
Normal file
74
app-common/src/main/res/layout/activity_download_wizard.xml
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/step_fragment_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/guideline"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/guideline"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/download_wizard_navigation"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/download_wizard_navigation"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/colorSurfaceContainer"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/download_wizard_back"
|
||||||
|
android:text="@string/download_wizard_back"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:textColor="?attr/colorPrimary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
app:icon="@drawable/ic_chevron_left"
|
||||||
|
app:iconGravity="start"
|
||||||
|
app:iconTint="?attr/colorPrimary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/download_wizard_next"
|
||||||
|
android:text="@string/download_wizard_next"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:textColor="?attr/colorPrimary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
app:icon="@drawable/ic_chevron_right"
|
||||||
|
app:iconGravity="end"
|
||||||
|
app:iconTint="?attr/colorPrimary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
25
app-common/src/main/res/layout/activity_euicc_info.xml
Normal file
25
app-common/src/main/res/layout/activity_euicc_info.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<include layout="@layout/toolbar_activity" />
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipe_refresh"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
44
app-common/src/main/res/layout/download_method_item.xml
Normal file
44
app-common/src/main/res/layout/download_method_item.xml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?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>
|
45
app-common/src/main/res/layout/download_progress_item.xml
Normal file
45
app-common/src/main/res/layout/download_progress_item.xml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?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>
|
108
app-common/src/main/res/layout/download_slot_item.xml
Normal file
108
app-common/src/main/res/layout/download_slot_item.xml
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="20sp"
|
||||||
|
android:paddingTop="10sp"
|
||||||
|
android:paddingStart="20sp"
|
||||||
|
android:paddingEnd="20sp"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="10sp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_type_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="100dp"
|
||||||
|
android:text="@string/download_wizard_slot_type"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_type"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_eid_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="100dp"
|
||||||
|
android:text="@string/download_wizard_slot_eid"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_eid"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_active_profile_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="100dp"
|
||||||
|
android:text="@string/download_wizard_slot_active_profile"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/slot_item_active_profile"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
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"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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:flow_wrapMode="aligned"
|
||||||
|
app:flow_horizontalAlign="start"
|
||||||
|
app:flow_horizontalBias="1"
|
||||||
|
app:flow_horizontalGap="10sp"
|
||||||
|
app:flow_horizontalStyle="packed"
|
||||||
|
app:flow_maxElementsWrap="2"
|
||||||
|
app:flow_verticalBias="0"
|
||||||
|
app:flow_verticalGap="16sp"
|
||||||
|
app:flow_verticalStyle="packed"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/slot_checkbox"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/slot_item_title" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/slot_checkbox"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/flow1"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
30
app-common/src/main/res/layout/euicc_info_item.xml
Normal file
30
app-common/src/main/res/layout/euicc_info_item.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?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"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/euicc_info_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/euicc_info_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/euicc_info_title"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -28,7 +28,8 @@
|
||||||
app:layout_constraintRight_toLeftOf="@+id/profile_menu"
|
app:layout_constraintRight_toLeftOf="@+id/profile_menu"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/state"
|
app:layout_constraintBottom_toTopOf="@+id/state"
|
||||||
app:layout_constraintHorizontal_bias="0" />
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constrainedWidth="true" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/profile_menu"
|
android:id="@+id/profile_menu"
|
||||||
|
@ -62,18 +63,45 @@
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/state"
|
app:layout_constraintTop_toBottomOf="@id/state"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/iccid_label"/>
|
app:layout_constraintBottom_toTopOf="@+id/profile_class_label"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/provider"
|
android:id="@+id/provider"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:layout_marginLeft="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
app:layout_constraintLeft_toRightOf="@id/provider_label"
|
app:layout_constraintLeft_toRightOf="@id/provider_label"
|
||||||
app:layout_constraintTop_toBottomOf="@id/state"
|
app:layout_constraintTop_toBottomOf="@id/state"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/profile_class"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/profile_class_label"
|
||||||
|
android:text="@string/profile_class"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/provider_label"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/iccid_label"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/profile_class"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/profile_class_label"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/provider"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/iccid"/>
|
app:layout_constraintBottom_toTopOf="@+id/iccid"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -86,7 +114,7 @@
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/provider_label"
|
app:layout_constraintTop_toBottomOf="@id/profile_class_label"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -94,11 +122,11 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:layout_marginLeft="7dp"
|
android:layout_marginStart="7dp"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
app:layout_constraintLeft_toRightOf="@id/iccid_label"
|
app:layout_constraintLeft_toRightOf="@id/iccid_label"
|
||||||
app:layout_constraintTop_toBottomOf="@id/provider"
|
app:layout_constraintTop_toBottomOf="@id/profile_class"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
104
app-common/src/main/res/layout/fragment_download_details.xml
Normal file
104
app-common/src/main/res/layout/fragment_download_details.xml
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<?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="20dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:layout_marginEnd="60dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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="numberPassword" />
|
||||||
|
|
||||||
|
</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>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?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="20dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:layout_marginEnd="60dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/download_wizard_diagnostics_save"
|
||||||
|
android:src="@drawable/ic_save_as_black"
|
||||||
|
android:layout_margin="20dp"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="@string/download_wizard_diagnostics_save"
|
||||||
|
app:tint="?attr/colorAccent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_wizard_diagnostics_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
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>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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="20dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:layout_marginEnd="60dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<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>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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="20dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:layout_marginEnd="60dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<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>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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_slot_select_title"
|
||||||
|
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="20dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:layout_marginEnd="60dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/download_slot_list"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/download_slot_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,126 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/colorSurface">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintWidth_percent="1"
|
|
||||||
app:navigationIcon="?homeAsUpIndicator" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/guideline"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/toolbar"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progress"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/guideline"
|
|
||||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/profile_download_server"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="15dp"
|
|
||||||
android:hint="@string/profile_download_server"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintWidth_percent=".8">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/profile_download_code"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginVertical="15dp"
|
|
||||||
android:hint="@string/profile_download_code"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/profile_download_server"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintWidth_percent=".8"
|
|
||||||
app:passwordToggleEnabled="true">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/profile_download_confirmation_code"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginVertical="15dp"
|
|
||||||
android:hint="@string/profile_download_confirmation_code"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/profile_download_code"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintWidth_percent=".8"
|
|
||||||
app:passwordToggleEnabled="true">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/profile_download_imei"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="15dp"
|
|
||||||
android:layout_marginBottom="6dp"
|
|
||||||
android:hint="@string/profile_download_imei"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/profile_download_confirmation_code"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/profile_download_free_space"
|
|
||||||
app:layout_constraintWidth_percent=".8"
|
|
||||||
app:passwordToggleEnabled="true">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:inputType="textPassword" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/profile_download_free_space"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="11sp"
|
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/profile_download_imei"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,27 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/colorSurface">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintWidth_percent="1"
|
|
||||||
app:navigationIcon="?homeAsUpIndicator" />
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/spinner"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginVertical="48dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -15,6 +15,15 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_sequence_number"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/notification_profile_name"
|
android:id="@+id/notification_profile_name"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -5,4 +5,9 @@
|
||||||
android:id="@+id/show_notifications"
|
android:id="@+id/show_notifications"
|
||||||
android:title="@string/profile_notifications_show"
|
android:title="@string/profile_notifications_show"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/euicc_info"
|
||||||
|
android:title="@string/euicc_info"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
|
@ -1,21 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
android:id="@+id/scan"
|
|
||||||
android:icon="@drawable/ic_scan_black"
|
|
||||||
android:title="@string/profile_download_scan"
|
|
||||||
app:showAsAction="ifRoom"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/scan_from_gallery"
|
|
||||||
android:icon="@drawable/ic_gallery_black"
|
|
||||||
android:title="@string/profile_download_scan_from_gallery"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ok"
|
|
||||||
android:icon="@drawable/ic_check_black"
|
|
||||||
android:title="@string/profile_download_ok"
|
|
||||||
app:showAsAction="always"/>
|
|
||||||
</menu>
|
|
147
app-common/src/main/res/values-ja/strings.xml
Normal file
147
app-common/src/main/res/values-ja/strings.xml
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="no_euicc">このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。</string>
|
||||||
|
<string name="no_profile">この eSIM にはプロファイルがありません。</string>
|
||||||
|
<string name="unknown">不明</string>
|
||||||
|
<string name="information_unavailable">情報なし</string>
|
||||||
|
<string name="help">ヘルプ</string>
|
||||||
|
<string name="reload">スロットを再読み込み</string>
|
||||||
|
<string name="channel_name_format">論理スロット %d</string>
|
||||||
|
<string name="enabled">有効済み</string>
|
||||||
|
<string name="disabled">無効済み</string>
|
||||||
|
<string name="provider">プロバイダー:</string>
|
||||||
|
<string name="enable">有効化</string>
|
||||||
|
<string name="disable">無効化</string>
|
||||||
|
<string name="delete">削除</string>
|
||||||
|
<string name="rename">名前を変更</string>
|
||||||
|
<string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string>
|
||||||
|
<string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string>
|
||||||
|
<string name="toast_profile_enable_failed">新しい eSIM プロファイルに切り替えることができません。</string>
|
||||||
|
<string name="toast_profile_delete_confirm_text_mismatched">入力した確認用テキストは一致していません</string>
|
||||||
|
<string name="toast_iccid_copied">ICCID をクリップボードにコピーしました</string>
|
||||||
|
<string name="toast_eid_copied">EID をクリップボードにコピーしました</string>
|
||||||
|
<string name="toast_atr_copied">ATR をクリップボードにコピーしました</string>
|
||||||
|
<string name="usb_permission">USB の権限を許可</string>
|
||||||
|
<string name="usb_permission_needed">USB スマートカードリーダーにアクセスするには許可が必要です。</string>
|
||||||
|
<string name="usb_failed">USB スマートカードリーダー経由で eSIM に接続できません。</string>
|
||||||
|
<string name="task_notification">長時間実行されるタスク</string>
|
||||||
|
<string name="task_profile_download">eSIM プロファイルをダウンロード中です</string>
|
||||||
|
<string name="task_profile_download_failure">eSIM プロファイルのダウンロードに失敗しました</string>
|
||||||
|
<string name="task_profile_rename">eSIM プロファイルの名前を変更中です</string>
|
||||||
|
<string name="task_profile_rename_failure">eSIM プロファイルの名前の変更に失敗しました</string>
|
||||||
|
<string name="task_profile_delete">eSIM プロファイルを削除中です</string>
|
||||||
|
<string name="task_profile_delete_failure">eSIM プロファイルの削除に失敗しました</string>
|
||||||
|
<string name="task_profile_switch">eSIM プロファイルを切り替え中</string>
|
||||||
|
<string name="task_profile_switch_failure">eSIM プロファイルの切り替えに失敗しました</string>
|
||||||
|
<string name="profile_download">新しい eSIM</string>
|
||||||
|
<string name="profile_download_server">サーバー (RSP / SM-DP+)</string>
|
||||||
|
<string name="profile_download_code">アクティベーションコード</string>
|
||||||
|
<string name="profile_download_confirmation_code">確認コード (オプション)</string>
|
||||||
|
<string name="profile_download_imei">IMEI (オプション)</string>
|
||||||
|
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
|
||||||
|
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
|
||||||
|
<string name="profile_download_no_lpa_string">クリップボードに LPA コードが見つかりません</string>
|
||||||
|
<string name="profile_download_incorrect_lpa_string">LPA コードを解析できません</string>
|
||||||
|
<string name="profile_download_incorrect_lpa_string_message">クリップボードまたは QR コードの内容を LPA コードとして解析できません</string>
|
||||||
|
<string name="download_wizard">ダウンロードウィザード</string>
|
||||||
|
<string name="download_wizard_back">戻る</string>
|
||||||
|
<string name="download_wizard_next">次へ</string>
|
||||||
|
<string name="download_wizard_slot_removed">選択された SIM が取り外されました</string>
|
||||||
|
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</string>
|
||||||
|
<string name="download_wizard_slot_type">タイプ:</string>
|
||||||
|
<string name="download_wizard_slot_type_removable">リムーバブル</string>
|
||||||
|
<string name="download_wizard_slot_type_internal">内部</string>
|
||||||
|
<string name="download_wizard_slot_type_internal_port">内部 - ポート: %d</string>
|
||||||
|
<string name="download_wizard_slot_active_profile">有効なプロファイル:</string>
|
||||||
|
<string name="download_wizard_slot_free_space">空き容量:</string>
|
||||||
|
<string name="download_wizard_method_select">eSIM プロファイルをどの方法でダウンロードしますか?</string>
|
||||||
|
<string name="download_wizard_method_qr_code">カメラで QR コードをスキャン</string>
|
||||||
|
<string name="download_wizard_method_gallery">ギャラリーから QR コードをスキャン</string>
|
||||||
|
<string name="download_wizard_method_clipboard">クリップボードから読み込む</string>
|
||||||
|
<string name="download_wizard_method_manual">手動で入力</string>
|
||||||
|
<string name="download_wizard_details">eSIM をダウンロードするための詳細情報を入力または確認:</string>
|
||||||
|
<string name="download_wizard_progress">eSIM をダウンロード中です…</string>
|
||||||
|
<string name="download_wizard_progress_step_preparing">準備中</string>
|
||||||
|
<string name="download_wizard_progress_step_connecting">サーバーへの接続を確立しています</string>
|
||||||
|
<string name="download_wizard_progress_step_authenticating">サーバーでデバイスを認証中です</string>
|
||||||
|
<string name="download_wizard_progress_step_downloading">eSIM プロファイルをダウンロード中です</string>
|
||||||
|
<string name="download_wizard_progress_step_finalizing">eSIM プロファイルをストレージに読み込み中です</string>
|
||||||
|
<string name="download_wizard_diagnostics">エラー診断</string>
|
||||||
|
<string name="download_wizard_diagnostics_error_code">エラーコード: %s</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_status">最終の HTTP ステータス (サーバー): %d</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_response">最終の HTTP レスポンス (サーバー):</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_exception">最終の HTTP 例外:</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_response">最終の APDU レスポンス (SIM): %s</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_response_success">最終の APDU レスポンス (SIM) は成功しました</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_response_fail">最終の APDU レスポンス (SIM) は失敗しました</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_exception">最終の APDU 例外:</string>
|
||||||
|
<string name="download_wizard_diagnostics_save">保存</string>
|
||||||
|
<string name="download_wizard_diagnostics_file_template">%s のエラー診断</string>
|
||||||
|
<string name="logs_saved_message">ログは指定されたパスに保存しました。他のアプリにシェアしますか?</string>
|
||||||
|
<string name="profile_rename_new_name">新しいニックネーム</string>
|
||||||
|
<string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできません</string>
|
||||||
|
<string name="profile_rename_too_long">ニックネームは 64 文字以内にしてください</string>
|
||||||
|
<string name="profile_rename_failure">ニックネームの変更で予期せぬエラーが発生しました</string>
|
||||||
|
<string name="profile_delete_confirm">%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。</string>
|
||||||
|
<string name="profile_delete_confirm_input">削除を確認するには「%s」を入力してください</string>
|
||||||
|
<string name="profile_notifications">通知</string>
|
||||||
|
<string name="profile_notifications_detailed_format">通知 (%s)</string>
|
||||||
|
<string name="profile_notifications_show">通知の管理</string>
|
||||||
|
<string name="profile_notifications_help">eSIM プロファイルはダウンロードや削除、有効化や無効化されたときに通信事業者に通知を送信できます。送信されるこれらの通知のキューはここにリストされます。\n\n設定では、各タイプの通知を自動的に送信するかどうかを指定できます。通知が送信された場合でもキューのスペースが不足していない限り、記録から自動的に削除されることはありません。\n\nここでは保留中の各通知を手動で送信または削除できます。</string>
|
||||||
|
<string name="profile_notification_operation_download">ダウンロードしました</string>
|
||||||
|
<string name="profile_notification_operation_delete">削除しました</string>
|
||||||
|
<string name="profile_notification_operation_enable">有効化しました</string>
|
||||||
|
<string name="profile_notification_operation_disable">無効化しました</string>
|
||||||
|
<string name="profile_notification_process">処理</string>
|
||||||
|
<string name="profile_notification_delete">削除</string>
|
||||||
|
<string name="euicc_info">eUICC 情報</string>
|
||||||
|
<string name="euicc_info_activity_title">eUICC 情報 (%s)</string>
|
||||||
|
<string name="euicc_info_access_mode">アクセスモード</string>
|
||||||
|
<string name="euicc_info_removable">リムーバブル</string>
|
||||||
|
<string name="euicc_info_sgp22_version">SGP.22 バージョン</string>
|
||||||
|
<string name="euicc_info_firmware_version">eUICC OS のバージョン</string>
|
||||||
|
<string name="euicc_info_globalplatform_version">グローバルプラットフォームのバージョン</string>
|
||||||
|
<string name="euicc_info_sas_accreditation_number">SAS 認定番号</string>
|
||||||
|
<string name="euicc_info_pp_version">Protected Profileのバージョン</string>
|
||||||
|
<string name="euicc_info_free_nvram">NVRAM の空き容量 (eSIM プロファイルストレージ)</string>
|
||||||
|
<string name="euicc_info_ci_type">証明書の発行者 (CI)</string>
|
||||||
|
<string name="euicc_info_ci_gsma_live">GSMA プロダクション CI</string>
|
||||||
|
<string name="euicc_info_ci_gsma_test">GSMA テスト CI</string>
|
||||||
|
<string name="euicc_info_ci_unknown">未知の eSIM CI</string>
|
||||||
|
<string name="yes">はい</string>
|
||||||
|
<string name="no">いいえ</string>
|
||||||
|
<string name="logs_save">保存</string>
|
||||||
|
<string name="logs_filename_template">%s のログ</string>
|
||||||
|
<string name="developer_options_steps">開発者になるまであと %d ステップです。</string>
|
||||||
|
<string name="developer_options_enabled">あなたは開発者になりました!</string>
|
||||||
|
<string name="pref_settings">設定</string>
|
||||||
|
<string name="pref_notifications">通知</string>
|
||||||
|
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。ここでは、どのタイプの通知を送信するのかを微調整できます。</string>
|
||||||
|
<string name="pref_notifications_download">ダウンロード</string>
|
||||||
|
<string name="pref_notifications_download_desc">プロファイルの<i>ダウンロード済み</i>の通知を送信します</string>
|
||||||
|
<string name="pref_notifications_delete">削除</string>
|
||||||
|
<string name="pref_notifications_delete_desc">プロファイルの<i>削除済み</i>の通知を送信します</string>
|
||||||
|
<string name="pref_notifications_switch">切り替え</string>
|
||||||
|
<string name="pref_notifications_switch_desc">プロファイルの<i>切り替え済み</i>の通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。</string>
|
||||||
|
<string name="pref_advanced">高度な設定</string>
|
||||||
|
<string name="pref_advanced_disable_safeguard_removable_esim">有効なプロファイルの無効化と削除を許可する</string>
|
||||||
|
<string name="pref_advanced_disable_safeguard_removable_esim_desc">デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと<i>時々</i>アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を<i>解除</i>します。</string>
|
||||||
|
<string name="pref_advanced_verbose_logging">詳細ログ</string>
|
||||||
|
<string name="pref_advanced_verbose_logging_desc">詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。</string>
|
||||||
|
<string name="pref_advanced_logs">ログ</string>
|
||||||
|
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string>
|
||||||
|
<string name="pref_developer">開発者オプション</string>
|
||||||
|
<string name="pref_developer_ignore_tls_certificate">SM-DP+ TLS 証明書を無視する</string>
|
||||||
|
<string name="pref_developer_ignore_tls_certificate_desc">SM-DP+ TLS 証明書を無視して任意の RSP を許可します</string>
|
||||||
|
<string name="pref_info">情報</string>
|
||||||
|
<string name="pref_info_app_version">アプリバージョン</string>
|
||||||
|
<string name="pref_info_source_code">ソースコード</string>
|
||||||
|
<string name="pref_advanced_language">言語</string>
|
||||||
|
<string name="pref_advanced_language_desc">アプリの言語を選択</string>
|
||||||
|
<string name="pref_developer_unfiltered_profile_list">すべてのプロファイルを表示</string>
|
||||||
|
<string name="pref_developer_unfiltered_profile_list_desc">プロダクション以外のプロファイルも表示する</string>
|
||||||
|
<string name="profile_class">タイプ:</string>
|
||||||
|
<string name="profile_class_testing">テスティング</string>
|
||||||
|
<string name="profile_class_provisioning">準備中</string>
|
||||||
|
<string name="profile_class_operational">動作中</string>
|
||||||
|
</resources>
|
147
app-common/src/main/res/values-zh-rCN/strings.xml
Normal file
147
app-common/src/main/res/values-zh-rCN/strings.xml
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="no_euicc">在此设备上未检测到此应用程序可访问的可插拔 eUICC 卡。请插入兼容卡或 USB 读卡器。</string>
|
||||||
|
<string name="no_profile">此 eSIM 上还没有配置文件</string>
|
||||||
|
<string name="unknown">未知</string>
|
||||||
|
<string name="help">帮助</string>
|
||||||
|
<string name="reload">重新加载卡槽</string>
|
||||||
|
<string name="channel_name_format">逻辑卡槽 %d</string>
|
||||||
|
<string name="enabled">已启用</string>
|
||||||
|
<string name="disabled">已禁用</string>
|
||||||
|
<string name="provider">提供商:</string>
|
||||||
|
<string name="profile_class">类型:</string>
|
||||||
|
<string name="enable">启用</string>
|
||||||
|
<string name="disable">禁用</string>
|
||||||
|
<string name="delete">删除</string>
|
||||||
|
<string name="rename">重命名</string>
|
||||||
|
<string name="enable_disable_timeout">等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机</string>
|
||||||
|
<string name="switch_did_not_refresh">操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。</string>
|
||||||
|
<string name="toast_profile_enable_failed">无法切换到新的 eSIM 配置文件。</string>
|
||||||
|
<string name="toast_profile_delete_confirm_text_mismatched">输入的确认文本不匹配</string>
|
||||||
|
<string name="toast_iccid_copied">已复制 ICCID 到剪贴板</string>
|
||||||
|
<string name="toast_eid_copied">已复制 EID 到剪贴板</string>
|
||||||
|
<string name="toast_atr_copied">已复制 ATR 到剪贴板</string>
|
||||||
|
<string name="usb_permission">授予 USB 权限</string>
|
||||||
|
<string name="usb_permission_needed">需要获得访问 USB 智能卡读卡器的权限。</string>
|
||||||
|
<string name="usb_failed">无法通过 USB 智能卡读卡器连接到 eSIM。</string>
|
||||||
|
<string name="task_notification">长时间运行的后台任务</string>
|
||||||
|
<string name="task_profile_download">正在下载 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_download_failure">无法下载 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_rename">正在重命名 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_rename_failure">无法重命名 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_delete">正在删除 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_delete_failure">无法删除 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_switch">正在切换 eSIM 配置文件</string>
|
||||||
|
<string name="task_profile_switch_failure">无法切换 eSIM 配置文件</string>
|
||||||
|
<string name="profile_download">添加新 eSIM</string>
|
||||||
|
<string name="profile_download_server">服务器 (RSP / SM-DP+)</string>
|
||||||
|
<string name="profile_download_code">激活码</string>
|
||||||
|
<string name="profile_download_confirmation_code">确认码 (可选)</string>
|
||||||
|
<string name="profile_download_imei">IMEI (可选)</string>
|
||||||
|
<string name="profile_download_low_nvram_title">本次下载可能会失败</string>
|
||||||
|
<string name="profile_download_low_nvram_message">当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?</string>
|
||||||
|
<string name="logs_saved_message">日志已保存到指定路径。需要通过其他 App 分享吗?</string>
|
||||||
|
<string name="profile_rename_new_name">新昵称</string>
|
||||||
|
<string name="profile_rename_encoding_error">无法将昵称编码为 UTF-8</string>
|
||||||
|
<string name="profile_rename_too_long">昵称长于 64 字符</string>
|
||||||
|
<string name="profile_rename_failure">重命名配置文件时发生了未知错误</string>
|
||||||
|
<string name="profile_delete_confirm">您确定要删除 %s 吗?此操作是不可逆的。</string>
|
||||||
|
<string name="profile_delete_confirm_input">请输入\'%s\'以确认删除</string>
|
||||||
|
<string name="profile_notifications">通知列表</string>
|
||||||
|
<string name="profile_notifications_detailed_format">通知列表 (%s)</string>
|
||||||
|
<string name="profile_notifications_show">管理通知</string>
|
||||||
|
<string name="profile_notifications_help">eSIM 配置文件可以在下载、删除、启用或禁用时向运营商发送通知。此处列出了要发送的这些通知的队列。\n\n在\"设置\"中,您可以指定是否自动发送每种类型的通知。请注意,即使通知已发送,也不会自动从记录中删除,除非队列空间不足。\n\n在这里,您可以手动发送或删除每个待处理的通知。</string>
|
||||||
|
<string name="profile_notification_operation_download">已下载</string>
|
||||||
|
<string name="profile_notification_operation_delete">已删除</string>
|
||||||
|
<string name="profile_notification_operation_enable">已启用</string>
|
||||||
|
<string name="profile_notification_operation_disable">已禁用</string>
|
||||||
|
<string name="profile_notification_process">处理</string>
|
||||||
|
<string name="profile_notification_delete">删除</string>
|
||||||
|
<string name="logs_save">保存日志</string>
|
||||||
|
<string name="logs_filename_template">%s 的日志</string>
|
||||||
|
<string name="pref_settings">设置</string>
|
||||||
|
<string name="pref_notifications">通知</string>
|
||||||
|
<string name="pref_notifications_desc">操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。</string>
|
||||||
|
<string name="pref_notifications_download">下载</string>
|
||||||
|
<string name="pref_notifications_download_desc">发送 <i>下载</i> 配置文件的通知</string>
|
||||||
|
<string name="pref_notifications_delete">删除</string>
|
||||||
|
<string name="pref_notifications_delete_desc">发送 <i>删除</i> 配置文件的通知</string>
|
||||||
|
<string name="pref_notifications_switch">切换</string>
|
||||||
|
<string name="pref_notifications_switch_desc">发送 <i>切换</i> 配置文件的通知\n注意,这种类型的通知是不可靠的。</string>
|
||||||
|
<string name="pref_advanced">高级</string>
|
||||||
|
<string name="pref_advanced_disable_safeguard_removable_esim">允许 禁用/删除 已启用的配置文件</string>
|
||||||
|
<string name="pref_advanced_disable_safeguard_removable_esim_desc">默认情况下,此应用程序会阻止您禁用可插拔 eSIM 中已启用的配置文件。\n因为这样做 <i>有时</i> 会使其无法访问。\n勾选此框以 <i>移除</i> 此保护措施。</string>
|
||||||
|
<string name="pref_advanced_verbose_logging">记录详细日志</string>
|
||||||
|
<string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
|
||||||
|
<string name="pref_advanced_logs">日志</string>
|
||||||
|
<string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
|
||||||
|
<string name="pref_info">信息</string>
|
||||||
|
<string name="pref_info_app_version">App 版本</string>
|
||||||
|
<string name="pref_info_source_code">源码</string>
|
||||||
|
<string name="profile_class_testing">测试</string>
|
||||||
|
<string name="profile_class_provisioning">准备中</string>
|
||||||
|
<string name="profile_class_operational">可用</string>
|
||||||
|
<string name="profile_download_no_lpa_string">未在剪贴板上发现 LPA 码</string>
|
||||||
|
<string name="profile_download_incorrect_lpa_string">LPA 码解析错误</string>
|
||||||
|
<string name="profile_download_incorrect_lpa_string_message">无法将二维码或剪贴板内容解析为 LPA 码</string>
|
||||||
|
<string name="download_wizard">下载向导</string>
|
||||||
|
<string name="download_wizard_back">返回</string>
|
||||||
|
<string name="download_wizard_next">下一步</string>
|
||||||
|
<string name="download_wizard_slot_removed">您选择的 SIM 已被移除</string>
|
||||||
|
<string name="download_wizard_slot_select">请选择或确认下载目标 eSIM 卡槽:</string>
|
||||||
|
<string name="download_wizard_slot_type">类型:</string>
|
||||||
|
<string name="download_wizard_slot_type_removable">可插拔</string>
|
||||||
|
<string name="download_wizard_slot_type_internal">内置</string>
|
||||||
|
<string name="download_wizard_slot_type_internal_port">内置, 端口 %d</string>
|
||||||
|
<string name="download_wizard_slot_active_profile">当前配置文件:</string>
|
||||||
|
<string name="download_wizard_slot_free_space">剩余空间:</string>
|
||||||
|
<string name="download_wizard_method_select">您想要如何下载 eSIM 配置文件?</string>
|
||||||
|
<string name="download_wizard_method_qr_code">用相机扫描二维码</string>
|
||||||
|
<string name="download_wizard_method_gallery">从图库选择二维码</string>
|
||||||
|
<string name="download_wizard_method_clipboard">从剪贴板读取</string>
|
||||||
|
<string name="download_wizard_method_manual">手动输入</string>
|
||||||
|
<string name="download_wizard_details">请输入或确认下载 eSIM 的详细信息:</string>
|
||||||
|
<string name="download_wizard_progress">正在下载您的 eSIM...</string>
|
||||||
|
<string name="download_wizard_progress_step_preparing">准备中</string>
|
||||||
|
<string name="download_wizard_progress_step_connecting">正在连接服务器</string>
|
||||||
|
<string name="download_wizard_progress_step_authenticating">正在向服务器认证您的设备</string>
|
||||||
|
<string name="download_wizard_progress_step_downloading">正在下载 eSIM 配置文件</string>
|
||||||
|
<string name="download_wizard_progress_step_finalizing">正在写入 eSIM 配置文件</string>
|
||||||
|
<string name="download_wizard_diagnostics">错误诊断</string>
|
||||||
|
<string name="download_wizard_diagnostics_error_code">错误代码: %s</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_status">上次 HTTP 状态码 (来自服务器): %d</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_response">上次 HTTP 应答 (来自服务器):</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_exception">上次 HTTP 错误:</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_response">上次 APDU 应答 (来自 SIM): %s</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_response_success">上次 APDU 应答 (来自 SIM) 是成功的</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_response_fail">上次 APDU 应答 (来自 SIM) 是失败的</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_apdu_exception">上次 APDU 错误:</string>
|
||||||
|
<string name="download_wizard_diagnostics_save">保存</string>
|
||||||
|
<string name="download_wizard_diagnostics_file_template">%s 的错误诊断</string>
|
||||||
|
<string name="euicc_info">eUICC 详情</string>
|
||||||
|
<string name="euicc_info_activity_title">eUICC 详情 (%s)</string>
|
||||||
|
<string name="euicc_info_access_mode">访问方式</string>
|
||||||
|
<string name="euicc_info_removable">可插拔</string>
|
||||||
|
<string name="euicc_info_sgp22_version">SGP.22 版本</string>
|
||||||
|
<string name="euicc_info_firmware_version">eUICC OS 版本</string>
|
||||||
|
<string name="euicc_info_globalplatform_version">GlobalPlatform 版本</string>
|
||||||
|
<string name="euicc_info_sas_accreditation_number">SAS 认证号码</string>
|
||||||
|
<string name="euicc_info_pp_version">Protected Profile 版本</string>
|
||||||
|
<string name="euicc_info_free_nvram">NVRAM 剩余空间 (eSIM 存储容量)</string>
|
||||||
|
<string name="euicc_info_ci_type">证书签发者 (CI)</string>
|
||||||
|
<string name="euicc_info_ci_gsma_live">GSMA 生产环境 CI</string>
|
||||||
|
<string name="euicc_info_ci_gsma_test">GSMA 测试 CI</string>
|
||||||
|
<string name="euicc_info_ci_unknown">未知 eSIM CI</string>
|
||||||
|
<string name="yes">是</string>
|
||||||
|
<string name="no">否</string>
|
||||||
|
<string name="developer_options_steps">还有 %d 步成为开发者</string>
|
||||||
|
<string name="developer_options_enabled">你现在是开发者了!</string>
|
||||||
|
<string name="pref_advanced_language">语言</string>
|
||||||
|
<string name="pref_advanced_language_desc">选择 App 语言</string>
|
||||||
|
<string name="pref_developer">开发者选项</string>
|
||||||
|
<string name="pref_developer_unfiltered_profile_list">显示未经过滤的配置文件列表</string>
|
||||||
|
<string name="pref_developer_unfiltered_profile_list_desc">在配置文件列表中包括非生产环境的配置文件</string>
|
||||||
|
<string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
|
||||||
|
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
|
||||||
|
<string name="information_unavailable">无信息</string>
|
||||||
|
</resources>
|
|
@ -3,16 +3,22 @@
|
||||||
<string name="no_euicc">No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader.</string>
|
<string name="no_euicc">No removable eUICC card accessible by this app is detected on this device. Insert a compatible card or a USB reader.</string>
|
||||||
<string name="no_profile">No profiles (yet) on this eSIM.</string>
|
<string name="no_profile">No profiles (yet) on this eSIM.</string>
|
||||||
<string name="unknown">Unknown</string>
|
<string name="unknown">Unknown</string>
|
||||||
|
<string name="information_unavailable">Information Unavailable</string>
|
||||||
<string name="help">Help</string>
|
<string name="help">Help</string>
|
||||||
<string name="reload">Reload Slots</string>
|
<string name="reload">Reload Slots</string>
|
||||||
|
|
||||||
<string name="channel_name_format">Logical Slot %d</string>
|
<string name="channel_name_format">Logical Slot %d</string>
|
||||||
<string name="usb">USB</string>
|
<string name="usb" translatable="false">USB</string>
|
||||||
|
<string name="omapi" translatable="false">OpenMobile API (OMAPI)</string>
|
||||||
|
|
||||||
<string name="enabled">Enabled</string>
|
<string name="enabled">Enabled</string>
|
||||||
<string name="disabled">Disabled</string>
|
<string name="disabled">Disabled</string>
|
||||||
<string name="provider">Provider:</string>
|
<string name="provider">Provider:</string>
|
||||||
<string name="iccid">ICCID:</string>
|
<string name="profile_class">Class:</string>
|
||||||
|
<string name="profile_class_testing">Testing</string>
|
||||||
|
<string name="profile_class_provisioning">Provisioning</string>
|
||||||
|
<string name="profile_class_operational">Operational</string>
|
||||||
|
<string name="iccid" translatable="false">ICCID:</string>
|
||||||
|
|
||||||
<string name="enable">Enable</string>
|
<string name="enable">Enable</string>
|
||||||
<string name="disable">Disable</string>
|
<string name="disable">Disable</string>
|
||||||
|
@ -23,11 +29,10 @@
|
||||||
<string name="switch_did_not_refresh">The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.</string>
|
<string name="switch_did_not_refresh">The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile.</string>
|
||||||
|
|
||||||
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
||||||
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
|
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||||
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
||||||
|
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||||
<string name="slot_select">Select Slot</string>
|
<string name="toast_atr_copied">ATR copied to clipboard</string>
|
||||||
<string name="slot_select_select">Select</string>
|
|
||||||
|
|
||||||
<string name="usb_permission">Grant USB permission</string>
|
<string name="usb_permission">Grant USB permission</string>
|
||||||
<string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string>
|
<string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string>
|
||||||
|
@ -48,13 +53,55 @@
|
||||||
<string name="profile_download_code">Activation Code</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">Confirmation Code (Optional)</string>
|
||||||
<string name="profile_download_imei">IMEI (Optional)</string>
|
<string name="profile_download_imei">IMEI (Optional)</string>
|
||||||
<string name="profile_download_free_space">Space remaining: %s</string>
|
|
||||||
<string name="profile_download_scan">Scan QR Code</string>
|
<string name="profile_download_low_nvram_title">This download may fail</string>
|
||||||
<string name="profile_download_scan_from_gallery">Scan QR Code from Gallery</string>
|
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
|
||||||
<string name="profile_download_ok">Download</string>
|
<string name="profile_download_no_lpa_string">No LPA code found in clipboard</string>
|
||||||
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</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>
|
||||||
|
|
||||||
|
<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_removed">Selected SIM has been removed</string>
|
||||||
|
<string name="download_wizard_slot_select">Select or confirm the eSIM you would like to download to:</string>
|
||||||
|
<string name="download_wizard_slot_type">Type:</string>
|
||||||
|
<string name="download_wizard_slot_type_removable">Removable</string>
|
||||||
|
<string name="download_wizard_slot_type_internal">Internal</string>
|
||||||
|
<string name="download_wizard_slot_type_internal_port">Internal, port %d</string>
|
||||||
|
<string name="download_wizard_slot_eid" translatable="false">eID:</string>
|
||||||
|
<string name="download_wizard_slot_active_profile">Active Profile:</string>
|
||||||
|
<string name="download_wizard_slot_free_space">Free Space:</string>
|
||||||
|
<string name="download_wizard_method_select">How would you like to download the eSIM profile?</string>
|
||||||
|
<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_clipboard">Load from Clipboard</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_error_code">Error code: %s</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_status">Last HTTP status (from server): %d</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_response">Last HTTP response (from server):</string>
|
||||||
|
<string name="download_wizard_diagnostics_last_http_exception">Last HTTP exception:</string>
|
||||||
|
<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="download_wizard_diagnostics_save">Save</string>
|
||||||
|
<string name="download_wizard_diagnostics_file_template">Diagnostics at %s</string>
|
||||||
|
|
||||||
|
<string name="logs_saved_message">Logs have been saved to the selected path. Would you like to share the log through another app?</string>
|
||||||
|
|
||||||
<string name="profile_rename_new_name">New nickname</string>
|
<string name="profile_rename_new_name">New nickname</string>
|
||||||
|
<string name="profile_rename_encoding_error">Failed to encode nickname as UTF-8</string>
|
||||||
|
<string name="profile_rename_too_long">Nickname is longer than 64 characters</string>
|
||||||
|
<string name="profile_rename_failure">Unknown failure when renaming profile</string>
|
||||||
|
|
||||||
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
|
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
|
||||||
<string name="profile_delete_confirm_input">Type \'%s\' here to confirm deletion</string>
|
<string name="profile_delete_confirm_input">Type \'%s\' here to confirm deletion</string>
|
||||||
|
@ -67,13 +114,37 @@
|
||||||
<string name="profile_notification_operation_delete">Deleted</string>
|
<string name="profile_notification_operation_delete">Deleted</string>
|
||||||
<string name="profile_notification_operation_enable">Enabled</string>
|
<string name="profile_notification_operation_enable">Enabled</string>
|
||||||
<string name="profile_notification_operation_disable">Disabled</string>
|
<string name="profile_notification_operation_disable">Disabled</string>
|
||||||
<string name="profile_notification_name_format"><b>%1$s</b> %2$s (%3$s)</string>
|
<string name="profile_notification_name_format" translatable="false"><b>%1$s</b> %2$s (%3$s)</string>
|
||||||
|
<string name="profile_notification_sequence_number_format" translatable="false">#%d</string>
|
||||||
<string name="profile_notification_process">Process</string>
|
<string name="profile_notification_process">Process</string>
|
||||||
<string name="profile_notification_delete">Delete</string>
|
<string name="profile_notification_delete">Delete</string>
|
||||||
|
|
||||||
|
<string name="euicc_info">eUICC Info</string>
|
||||||
|
<string name="euicc_info_activity_title">eUICC Info (%s)</string>
|
||||||
|
<string name="euicc_info_access_mode">Access Mode</string>
|
||||||
|
<string name="euicc_info_removable">Removable</string>
|
||||||
|
<string name="euicc_info_eid" translatable="false">EID</string>
|
||||||
|
<string name="euicc_info_sgp22_version">SGP.22 Version</string>
|
||||||
|
<string name="euicc_info_firmware_version">eUICC OS Version</string>
|
||||||
|
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>
|
||||||
|
<string name="euicc_info_sas_accreditation_number">SAS Accreditation Number</string>
|
||||||
|
<string name="euicc_info_pp_version">Protected Profile Version</string>
|
||||||
|
<string name="euicc_info_free_nvram">Free NVRAM (eSIM profile storage)</string>
|
||||||
|
<string name="euicc_info_ci_type">Certificate Issuer (CI)</string>
|
||||||
|
<string name="euicc_info_ci_gsma_live">GSMA Live CI</string>
|
||||||
|
<string name="euicc_info_ci_gsma_test">GSMA Test CI</string>
|
||||||
|
<string name="euicc_info_ci_unknown">Unknown eSIM CI</string>
|
||||||
|
<string name="euicc_info_atr" translatable="false">Answer To Reset (ATR)</string>
|
||||||
|
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
|
||||||
<string name="logs_save">Save</string>
|
<string name="logs_save">Save</string>
|
||||||
<string name="logs_filename_template">Logs at %s</string>
|
<string name="logs_filename_template">Logs at %s</string>
|
||||||
|
|
||||||
|
<string name="developer_options_steps">You are %d steps away from being a developer.</string>
|
||||||
|
<string name="developer_options_enabled">You are now a developer!</string>
|
||||||
|
|
||||||
<string name="pref_settings">Settings</string>
|
<string name="pref_settings">Settings</string>
|
||||||
<string name="pref_notifications">Notifications</string>
|
<string name="pref_notifications">Notifications</string>
|
||||||
<string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
|
<string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
|
||||||
|
@ -88,8 +159,15 @@
|
||||||
<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_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>
|
<string name="pref_advanced_verbose_logging">Verbose Logging</string>
|
||||||
<string name="pref_advanced_verbose_logging_desc">Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.</string>
|
<string name="pref_advanced_verbose_logging_desc">Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.</string>
|
||||||
|
<string name="pref_advanced_language">Language</string>
|
||||||
|
<string name="pref_advanced_language_desc">Select app language</string>
|
||||||
<string name="pref_advanced_logs">Logs</string>
|
<string name="pref_advanced_logs">Logs</string>
|
||||||
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
||||||
|
<string name="pref_developer">Developer Options</string>
|
||||||
|
<string name="pref_developer_unfiltered_profile_list">Show unfiltered profile list</string>
|
||||||
|
<string name="pref_developer_unfiltered_profile_list_desc">Include non-production profiles in the list</string>
|
||||||
|
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
|
||||||
|
<string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
|
||||||
<string name="pref_info">Info</string>
|
<string name="pref_info">Info</string>
|
||||||
<string name="pref_info_app_version">App Version</string>
|
<string name="pref_info_app_version">App Version</string>
|
||||||
<string name="pref_info_source_code">Source Code</string>
|
<string name="pref_info_source_code">Source Code</string>
|
||||||
|
|
6
app-common/src/main/res/xml/locale_config.xml
Normal file
6
app-common/src/main/res/xml/locale_config.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<locale android:name="en-US" />
|
||||||
|
<locale android:name="ja" />
|
||||||
|
<locale android:name="zh-CN" />
|
||||||
|
</locale-config>
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
|
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
|
||||||
app:title="@string/pref_notifications"
|
app:title="@string/pref_notifications"
|
||||||
app:summary="@string/pref_notifications_desc"
|
app:summary="@string/pref_notifications_desc"
|
||||||
|
@ -36,14 +37,42 @@
|
||||||
app:title="@string/pref_advanced_verbose_logging"
|
app:title="@string/pref_advanced_verbose_logging"
|
||||||
app:summary="@string/pref_advanced_verbose_logging_desc" />
|
app:summary="@string/pref_advanced_verbose_logging_desc" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:isPreferenceVisible="false"
|
||||||
|
app:key="pref_advanced_language"
|
||||||
|
app:summary="@string/pref_advanced_language_desc"
|
||||||
|
app:title="@string/pref_advanced_language" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:key="pref_advanced_logs"
|
app:key="pref_advanced_logs"
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:title="@string/pref_advanced_logs"
|
app:title="@string/pref_advanced_logs"
|
||||||
app:summary="@string/pref_advanced_logs_desc" />
|
app:summary="@string/pref_advanced_logs_desc" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
|
app:key="pref_developer"
|
||||||
|
app:title="@string/pref_developer"
|
||||||
|
app:iconSpaceReserved="false">
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:key="pref_developer_unfiltered_profile_list"
|
||||||
|
app:summary="@string/pref_developer_unfiltered_profile_list_desc"
|
||||||
|
app:title="@string/pref_developer_unfiltered_profile_list" />
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:key="pref_developer_ignore_tls_certificate"
|
||||||
|
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
|
||||||
|
app:title="@string/pref_developer_ignore_tls_certificate" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="pref_info"
|
||||||
app:title="@string/pref_info"
|
app:title="@string/pref_info"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false">
|
||||||
<Preference
|
<Preference
|
||||||
|
@ -55,6 +84,10 @@
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:title="@string/pref_info_source_code"
|
app:title="@string/pref_info_source_code"
|
||||||
app:summary="@string/pref_info_source_code_url"
|
app:summary="@string/pref_info_source_code_url"
|
||||||
app:key="pref_info_source_code"/>
|
app:key="pref_info_source_code">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:data="@string/pref_info_source_code_url" />
|
||||||
|
</Preference>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
1
app-deps/.gitignore
vendored
1
app-deps/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/build
|
/build
|
||||||
|
/libs
|
|
@ -9,5 +9,6 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_jmp"
|
android:roundIcon="@mipmap/ic_launcher_jmp"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:localeConfig="@xml/locale_config"
|
||||||
android:theme="@style/Theme.OpenEUICC" />
|
android:theme="@style/Theme.OpenEUICC" />
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="im.angry.openeuicc.UnprivilegedOpenEuiccApplication"
|
android:name="im.angry.openeuicc.UnprivilegedOpenEuiccApplication"
|
||||||
|
@ -8,7 +9,9 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.OpenEUICC">
|
android:localeConfig="@xml/locale_config"
|
||||||
|
android:theme="@style/Theme.OpenEUICC"
|
||||||
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
|
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
|
||||||
|
@ -22,9 +25,22 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
||||||
android:label="@string/compatibility_check"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:label="@string/compatibility_check" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data
|
||||||
|
android:name="autoStoreLocales"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<package android:name="com.android.stk" />
|
||||||
|
<package android:name="com.android.stk1" />
|
||||||
|
<package android:name="com.android.stk2" />
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
|
@ -6,4 +6,8 @@ open class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(cont
|
||||||
override val uiComponentFactory by lazy {
|
override val uiComponentFactory by lazy {
|
||||||
UnprivilegedUiComponentFactory()
|
UnprivilegedUiComponentFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val customizableTextProvider by lazy {
|
||||||
|
UnprivilegedCustomizableTextProvider(context)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
|
||||||
|
class UnprivilegedCustomizableTextProvider(private val context: Context) :
|
||||||
|
DefaultCustomizableTextProvider(context) {
|
||||||
|
override fun formatInternalChannelName(logicalSlotId: Int): String =
|
||||||
|
context.getString(R.string.channel_name_format_unpriv, logicalSlotId)
|
||||||
|
}
|
|
@ -1,9 +1,19 @@
|
||||||
package im.angry.openeuicc.di
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||||
|
import im.angry.openeuicc.ui.SettingsFragment
|
||||||
|
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
|
||||||
import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
|
import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
|
||||||
|
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
|
||||||
|
|
||||||
open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||||
|
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||||
|
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
||||||
|
|
||||||
override fun createNoEuiccPlaceholderFragment(): Fragment =
|
override fun createNoEuiccPlaceholderFragment(): Fragment =
|
||||||
UnprivilegedNoEuiccPlaceholderFragment()
|
UnprivilegedNoEuiccPlaceholderFragment()
|
||||||
|
|
||||||
|
override fun createSettingsFragment(): Fragment =
|
||||||
|
UnprivilegedSettingsFragment()
|
||||||
}
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.text.Html
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -9,6 +10,7 @@ import android.widget.TextView
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -30,15 +32,16 @@ class CompatibilityCheckActivity: AppCompatActivity() {
|
||||||
setupToolbarInsets()
|
setupToolbarInsets()
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
compatibilityCheckList = requireViewById(R.id.recycler_view)
|
compatibilityCheckList = requireViewById<RecyclerView>(R.id.recycler_view).also {
|
||||||
compatibilityCheckList.layoutManager =
|
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||||
compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
it.adapter = adapter
|
||||||
compatibilityCheckList.adapter = adapter
|
}
|
||||||
|
|
||||||
setupRootViewInsets(compatibilityCheckList)
|
setupRootViewInsets(compatibilityCheckList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
@ -62,26 +65,19 @@ class CompatibilityCheckActivity: AppCompatActivity() {
|
||||||
|
|
||||||
fun bindItem(item: CompatibilityCheck) {
|
fun bindItem(item: CompatibilityCheck) {
|
||||||
titleView.text = item.title
|
titleView.text = item.title
|
||||||
descView.text = item.description
|
descView.text = Html.fromHtml(item.description, Html.FROM_HTML_MODE_COMPACT)
|
||||||
|
|
||||||
statusContainer.children.forEach {
|
statusContainer.children.forEach {
|
||||||
it.visibility = View.GONE
|
it.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
when (item.state) {
|
val viewId = when (item.state) {
|
||||||
CompatibilityCheck.State.SUCCESS -> {
|
CompatibilityCheck.State.SUCCESS -> R.id.compatibility_check_checkmark
|
||||||
root.requireViewById<View>(R.id.compatibility_check_checkmark).visibility = View.VISIBLE
|
CompatibilityCheck.State.FAILURE -> R.id.compatibility_check_error
|
||||||
}
|
CompatibilityCheck.State.FAILURE_UNKNOWN -> R.id.compatibility_check_unknown
|
||||||
CompatibilityCheck.State.FAILURE -> {
|
else -> R.id.compatibility_check_progress_bar
|
||||||
root.requireViewById<View>(R.id.compatibility_check_error).visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
CompatibilityCheck.State.FAILURE_UNKNOWN -> {
|
|
||||||
root.requireViewById<View>(R.id.compatibility_check_unknown).visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
root.requireViewById<View>(R.id.compatibility_check_progress_bar).visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
root.requireViewById<View>(viewId).isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.util.SIMToolkit
|
||||||
|
import im.angry.openeuicc.util.newInstanceEuicc
|
||||||
|
import im.angry.openeuicc.util.slotId
|
||||||
|
|
||||||
|
|
||||||
|
class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "UnprivilegedEuiccManagementFragment"
|
||||||
|
|
||||||
|
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||||
|
newInstanceEuicc(UnprivilegedEuiccManagementFragment::class.java, slotId, portId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stk by lazy {
|
||||||
|
SIMToolkit(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
inflater.inflate(R.menu.fragment_sim_toolkit, menu)
|
||||||
|
menu.findItem(R.id.open_sim_toolkit).apply {
|
||||||
|
isVisible = stk.isAvailable(slotId)
|
||||||
|
intent = stk.intent(slotId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.util.encodeHex
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class UnprivilegedSettingsFragment : SettingsFragment() {
|
||||||
|
private val firstSigner by lazy {
|
||||||
|
val packageInfo = requireContext().let {
|
||||||
|
it.packageManager.getPackageInfo(
|
||||||
|
it.packageName,
|
||||||
|
PackageManager.GET_SIGNING_CERTIFICATES,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
packageInfo.signingInfo!!.apkContentsSigners.first().let {
|
||||||
|
MessageDigest.getInstance("SHA-1")
|
||||||
|
.apply { update(it.toByteArray()) }
|
||||||
|
.digest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
super.onCreatePreferences(savedInstanceState, rootKey)
|
||||||
|
addPreferencesFromResource(R.xml.pref_unprivileged_settings)
|
||||||
|
mergePreferenceOverlay("pref_info_overlay", "pref_info")
|
||||||
|
|
||||||
|
requirePreference<Preference>("pref_info_ara_m").apply {
|
||||||
|
summary = firstSigner.encodeHex()
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||||
|
.setPrimaryClip(ClipData.newPlainText("ara-m", summary))
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast
|
||||||
|
.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.annotation.ArrayRes
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
|
|
||||||
|
class SIMToolkit(private val context: Context) {
|
||||||
|
private val slotSelection = getComponentNames(R.array.sim_toolkit_slot_selection)
|
||||||
|
|
||||||
|
private val slots = buildMap {
|
||||||
|
put(0, getComponentNames(R.array.sim_toolkit_slot_1))
|
||||||
|
put(1, getComponentNames(R.array.sim_toolkit_slot_2))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val packageNames = buildSet {
|
||||||
|
addAll(slotSelection.map { it.packageName })
|
||||||
|
addAll(slots.values.flatten().map { it.packageName })
|
||||||
|
}
|
||||||
|
|
||||||
|
private val activities = packageNames.flatMap(::getActivities).toSet()
|
||||||
|
|
||||||
|
private val launchIntent by lazy {
|
||||||
|
packageNames.firstNotNullOfOrNull(::getLaunchIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLaunchIntent(packageName: String) = try {
|
||||||
|
val pm = context.packageManager
|
||||||
|
pm.getLaunchIntentForPackage(packageName)
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActivities(packageName: String): List<ComponentName> {
|
||||||
|
return try {
|
||||||
|
val pm = context.packageManager
|
||||||
|
val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
||||||
|
val activities = packageInfo.activities
|
||||||
|
if (activities.isNullOrEmpty()) return emptyList()
|
||||||
|
activities.filter { it.exported }.map { ComponentName(it.packageName, it.name) }
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComponentNames(@ArrayRes id: Int) =
|
||||||
|
context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
|
||||||
|
|
||||||
|
fun isAvailable(slotId: Int) = when (slotId) {
|
||||||
|
-1 -> false
|
||||||
|
EuiccChannelManager.USB_CHANNEL_ID -> false
|
||||||
|
else -> intent(slotId) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun intent(slotId: Int): Intent? {
|
||||||
|
val components = slots.getOrDefault(slotId, emptySet()) + slotSelection
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN, null).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
component = components.find(activities::contains)
|
||||||
|
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
}
|
||||||
|
return if (intent.component != null) intent else launchIntent
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<item
|
<item
|
||||||
android:id="@+id/ok"
|
android:id="@+id/open_sim_toolkit"
|
||||||
android:icon="@drawable/ic_check_black"
|
android:title="@string/open_sim_toolkit"
|
||||||
android:title="@string/slot_select_select"
|
android:visible="false"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
34
app-unpriv/src/main/res/values-ja/strings.xml
Normal file
34
app-unpriv/src/main/res/values-ja/strings.xml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="compatibility_check">互換性のチェック</string>
|
||||||
|
<string name="open_sim_toolkit">SIM ツールキットを開く</string>
|
||||||
|
<!-- Compatibility Check Descriptions -->
|
||||||
|
<string name="compatibility_check_system_features">システムの機能</string>
|
||||||
|
<string name="compatibility_check_system_features_desc">デバイスにリムーバブル eUICC カードの管理に必要なすべての機能が備わっているかどうか。例えば基本的な電話機能や OMAPI のサポートなど。</string>
|
||||||
|
<string name="compatibility_check_system_features_no_telephony">使用しているデバイスには電話機能がありません。</string>
|
||||||
|
<string name="compatibility_check_system_features_no_omapi">使用しているデバイスまたはシステムには OMAPI のサポートを宣言していません。これは、ハードウェアからのサポートが不足していることが原因の可能性があります。または、フラグが不足していることが原因の可能性もあります。OMAPI が実際にサポートされているかどうかを判断するには次の 2 つのチェック項目を参照してください。</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity">OMAPI の接続</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity_desc">使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可していますか?</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity_fail">OMAPI 経由で SIM カードのセキュアエレメントリーダーを検出できません。このデバイスに SIM を挿入していない場合は、SIM を挿入後にこのチェックを再試行してください。</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: <b>SIM%s</b>.</string>
|
||||||
|
<string name="compatibility_check_isdr_channel">ISD-R チャネルアクセス</string>
|
||||||
|
<string name="compatibility_check_isdr_channel_desc">使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしていますか?</string>
|
||||||
|
<string name="compatibility_check_isdr_channel_desc_unknown">OMAPI 経由での ISD-R アクセスがサポートされているかどうかを確認できません。まだ SIM カードが挿入されていない場合は、挿入した状態で再試行してください (どの SIM カードでも構いません)。</string>
|
||||||
|
<string name="compatibility_check_isdr_channel_desc_partial_fail">ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: <b>SIM%s</b>.</string>
|
||||||
|
<string name="compatibility_check_known_broken">既知の破損リストに掲載されていない</string>
|
||||||
|
<string name="compatibility_check_known_broken_desc">取り外し可能な eSIM に関連するバグがデバイスに存在しないかを確認します。</string>
|
||||||
|
<string name="compatibility_check_known_broken_fail">おっと…使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。</string>
|
||||||
|
<string name="compatibility_check_usb">USB カードリーダーのサポート</string>
|
||||||
|
<string name="compatibility_check_usb_desc">使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしていますか?</string>
|
||||||
|
<string name="compatibility_check_usb_ok">このデバイスの標準 USB CCID リーダーを介して eSIM を管理できます (ここで他のチェック項目に失敗した場合でも)。カードリーダーを挿入し、このアプリを開いてこの方法で eSIM を管理できます。</string>
|
||||||
|
<string name="compatibility_check_usb_fail">使用しているデバイスは USB ホストとしての機能をサポートしていません。</string>
|
||||||
|
<string name="compatibility_check_verdict">判定 (USB 以外)</string>
|
||||||
|
<string name="compatibility_check_verdict_desc">これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどのくらいありますか?</string>
|
||||||
|
<string name="compatibility_check_verdict_ok">このデバイスに挿入された取り外し可能な eSIM の使用および管理が使用できる可能性があります。</string>
|
||||||
|
<string name="compatibility_check_verdict_known_broken">挿入された取り外し可能な eSIM にアクセスするとデバイスにバグが発生することが知られています。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_unknown_likely_ok">挿入された取り外し可能な eSIM が使用しているデバイスで管理できるかはわかりません。ただし、このデバイスは OMAPI のサポートを宣言しているため、動作する可能性はわずかに高くなります。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_unknown_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_unknown">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string>
|
||||||
|
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
|
||||||
|
</resources>
|
32
app-unpriv/src/main/res/values-zh-rCN/strings.xml
Normal file
32
app-unpriv/src/main/res/values-zh-rCN/strings.xml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<resources>
|
||||||
|
<string name="compatibility_check">兼容性检查</string>
|
||||||
|
<string name="open_sim_toolkit">打开 SIM 卡应用程序</string>
|
||||||
|
<string name="compatibility_check_system_features">系统功能</string>
|
||||||
|
<string name="compatibility_check_system_features_desc">您的设备是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的电话功能和 OMAPI 支持。</string>
|
||||||
|
<string name="compatibility_check_system_features_no_telephony">您的设备没有电话功能。</string>
|
||||||
|
<string name="compatibility_check_system_features_no_omapi">您的设备/系统未声明支持 OMAPI。这可能是由于缺少硬件支持,或者可能仅仅是由于缺少标志。请参阅以下两项检查以确定 OMAPI 是否确实受支持。</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity">OMAPI 连接</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity_desc">您的设备是否允许通过 OMAPI 访问 SIM 卡上的安全元件?</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity_fail">无法通过 OMAPI 检测到 SIM 卡的 Secure Element。如果您尚未在此设备中插入 SIM 卡,请尝试插入一张 SIM 卡并重试此检查。</string>
|
||||||
|
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">已成功检测到可访问 Secure Element 的卡槽,但仅限于以下 SIM 卡槽:<b>SIM%s</b>。</string>
|
||||||
|
<string name="compatibility_check_isdr_channel">ISD-R 通道访问</string>
|
||||||
|
<string name="compatibility_check_isdr_channel_desc">您的设备是否支持通过 OMAPI 打开 eSIM 的 ISD-R (管理) 通道?</string>
|
||||||
|
<string name="compatibility_check_isdr_channel_desc_unknown">无法确定是否支持通过 OMAPI 进行 ISD-R 访问。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重试。</string>
|
||||||
|
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI 只能在以下 SIM 插槽上访问 ISD-R:<b>SIM%s</b>。</string>
|
||||||
|
<string name="compatibility_check_known_broken">不在已知的 BUG 名单中</string>
|
||||||
|
<string name="compatibility_check_known_broken_desc">确保您的设备不存在与可插拔 eSIM 相关的错误。</string>
|
||||||
|
<string name="compatibility_check_known_broken_fail">糟糕,您的设备在访问可插拔 eSIM 时存在错误。这并不表示完全无法使用,但我们不保证该应用在您设备上的行为。</string>
|
||||||
|
<string name="compatibility_check_usb">USB 读卡器支持</string>
|
||||||
|
<string name="compatibility_check_usb_desc">您的设备是否支持通过 USB 读卡器管理 eSIM?</string>
|
||||||
|
<string name="compatibility_check_usb_ok">您可以通过此设备上的标准 USB CCID 读取器管理 eSIM (即使您在这里有任何其他检查项失败)。请插入读卡器,然后打开此应用程序以这种方式管理 eSIM。</string>
|
||||||
|
<string name="compatibility_check_usb_fail">您的设备不支持 USB 读卡器。</string>
|
||||||
|
<string name="compatibility_check_verdict">结论 (USB 读卡器以外)</string>
|
||||||
|
<string name="compatibility_check_verdict_desc">根据之前的所有检查,您的设备与可插拔 eSIM 卡兼容的可能性有多大?</string>
|
||||||
|
<string name="compatibility_check_verdict_ok">您可以使用和管理插入此设备的可插拔 eSIM 卡。</string>
|
||||||
|
<string name="compatibility_check_verdict_known_broken">已知您的设备在访问可插拔 eSIM 卡时存在问题。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_unknown_likely_ok">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。不过,您的设备确实声明支持 OMAPI,因此它工作的可能性略高。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_unknown_likely_fail">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。由于您的设备未声明支持OMAPI,因此更有可能不支持在此设备上管理可插拔 eSIM。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_unknown">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。\n%s</string>
|
||||||
|
<string name="compatibility_check_verdict_fail_shared">然而,已经加载了eSIM配置文件的可插拔 eSIM 卡仍然可以工作; 即使无法在装置上直接管理可插拔 eSIM 卡中的配置文件,您仍然可以使用 USB 卡读卡器来管理配置文件。</string>
|
||||||
|
<string name="toast_ara_m_copied">ARA-M SHA-1 已拷贝到剪贴板</string>
|
||||||
|
</resources>
|
32
app-unpriv/src/main/res/values/sim_toolkit.xml
Normal file
32
app-unpriv/src/main/res/values/sim_toolkit.xml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="Typos">
|
||||||
|
<string-array name="sim_toolkit_slot_selection">
|
||||||
|
<item>com.android.stk/.StkMain</item>
|
||||||
|
<item>com.android.stk/.StkMainHide</item>
|
||||||
|
<item>com.android.stk/.StkListActivity</item>
|
||||||
|
<item>com.android.stk/.StkLauncherListActivity</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="sim_toolkit_slot_1">
|
||||||
|
<item>com.android.stk/.StkMain1</item>
|
||||||
|
<item>com.android.stk/.PrimaryStkMain</item>
|
||||||
|
<item>com.android.stk/.StkLauncherActivity</item>
|
||||||
|
<item>com.android.stk/.StkLauncherActivity_Chn</item>
|
||||||
|
<item>com.android.stk/.StkLauncherActivityI</item>
|
||||||
|
<item>com.android.stk/.OppoStkLauncherActivity1</item>
|
||||||
|
<item>com.android.stk/.OplusStkLauncherActivity1</item>
|
||||||
|
<item>com.android.stk/.mtk.StkLauncherActivityI</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="sim_toolkit_slot_2">
|
||||||
|
<item>com.android.stk/.StkMain2</item>
|
||||||
|
<item>com.android.stk/.SecondaryStkMain</item>
|
||||||
|
<item>com.android.stk/.StkLauncherActivity2</item>
|
||||||
|
<item>com.android.stk/.StkLauncherActivityII</item>
|
||||||
|
<item>com.android.stk/.OppoStkLauncherActivity2</item>
|
||||||
|
<item>com.android.stk/.OplusStkLauncherActivity2</item>
|
||||||
|
<item>com.android.stk/.mtk.StkLauncherActivityII</item>
|
||||||
|
<item>com.android.stk1/.StkLauncherActivity</item>
|
||||||
|
<item>com.android.stk2/.StkLauncherActivity</item>
|
||||||
|
<item>com.android.stk2/.StkLauncherActivity_Chn</item>
|
||||||
|
<item>com.android.stk2/.StkLauncherActivity2</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
|
@ -1,7 +1,14 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">EasyEUICC</string>
|
<string name="app_name" translatable="false">EasyEUICC</string>
|
||||||
<string name="channel_name_format">SIM %d</string>
|
<string name="channel_name_format_unpriv" translatable="false">SIM %d</string>
|
||||||
<string name="compatibility_check">Compatibility Check</string>
|
<string name="compatibility_check">Compatibility Check</string>
|
||||||
|
<string name="open_sim_toolkit">Open SIM Toolkit</string>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="pref_developer_ara_m" translatable="false">ARA-M SHA-1</string>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
|
||||||
|
|
||||||
<!-- Compatibility Check Descriptions -->
|
<!-- Compatibility Check Descriptions -->
|
||||||
<string name="compatibility_check_system_features">System Features</string>
|
<string name="compatibility_check_system_features">System Features</string>
|
||||||
|
@ -11,11 +18,11 @@
|
||||||
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
|
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
|
||||||
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
|
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
|
||||||
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string>
|
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string>
|
||||||
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: %s.</string>
|
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: <b>SIM%s</b>.</string>
|
||||||
<string name="compatibility_check_isdr_channel">ISD-R Channel Access</string>
|
<string name="compatibility_check_isdr_channel">ISD-R Channel Access</string>
|
||||||
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
|
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
|
||||||
<string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string>
|
<string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string>
|
||||||
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: %s.</string>
|
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: <b>SIM%s</b>.</string>
|
||||||
<string name="compatibility_check_known_broken">Not on the Known Broken List</string>
|
<string name="compatibility_check_known_broken">Not on the Known Broken List</string>
|
||||||
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
|
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
|
||||||
<string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string>
|
<string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string>
|
||||||
|
|
12
app-unpriv/src/main/res/xml/pref_unprivileged_settings.xml
Normal file
12
app-unpriv/src/main/res/xml/pref_unprivileged_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_info_overlay">
|
||||||
|
<Preference
|
||||||
|
app:enableCopying="true"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:key="pref_info_ara_m"
|
||||||
|
app:title="@string/pref_developer_ara_m" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
</PreferenceScreen>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue