forked from PeterCxy/OpenEUICC
Compare commits
193 commits
jmp-v1.1.2
...
jmp
Author | SHA1 | Date | |
---|---|---|---|
c60ba8e103 | |||
cb7d6a5fc2 | |||
b4ea193de2 | |||
654932a9f0 | |||
a46849de40 | |||
0818abf71b | |||
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 |
142 changed files with 4624 additions and 1434 deletions
|
@ -38,11 +38,12 @@ jobs:
|
|||
- name: Build Debug Bundle
|
||||
run: ./gradlew --no-daemon :app-unpriv:bundleJmpDebug
|
||||
|
||||
- name: Copy Artifacts
|
||||
run: find . -name 'app*-debug.apk' -exec cp {} . \;
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: https://gitea.angry.im/actions/upload-artifact@v3
|
||||
with:
|
||||
name: Debug APKs
|
||||
compression-level: 0
|
||||
path: |
|
||||
app-unpriv/build/outputs/apk/jmp/debug/app-unpriv-jmp-debug.apk
|
||||
app-unpriv/build/outputs/bundle/jmpDebug/app-unpriv-jmp-debug.aab
|
||||
path: app*-debug.apk
|
||||
|
|
29
.gitignore
vendored
29
.gitignore
vendored
|
@ -1,20 +1,11 @@
|
|||
*.iml
|
||||
.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
|
||||
/.gradle
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/libs/**/build
|
||||
/buildSrc/build
|
||||
/app-deps/libs
|
||||
|
||||
# Configuration files
|
||||
|
||||
/keystore.properties
|
||||
/local.properties
|
||||
|
||||
# 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
|
||||
|
||||
**/*.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"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<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>
|
||||
<bytecodeTargetLevel target="1.7" />
|
||||
</component>
|
||||
</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>
|
|
@ -1 +1 @@
|
|||
287
|
||||
294
|
||||
|
|
|
@ -20,14 +20,23 @@
|
|||
android:label="@string/profile_notifications" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
||||
android:label="@string/profile_download"
|
||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
||||
android:name="im.angry.openeuicc.ui.EuiccInfoActivity"
|
||||
android:label="@string/euicc_info" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.LogsActivity"
|
||||
android:label="@string/pref_advanced_logs" />
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||
android:label="@string/download_wizard" />
|
||||
|
||||
<activity-alias
|
||||
android:exported="true"
|
||||
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
||||
android:targetActivity="im.angry.openeuicc.ui.wizard.DownloadWizardActivity" />
|
||||
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
|
|
|
@ -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.se.omapi.SEService
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.usb.UsbApduInterface
|
||||
import im.angry.openeuicc.core.usb.getIoEndpoints
|
||||
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}")
|
||||
try {
|
||||
return EuiccChannel(
|
||||
return EuiccChannelImpl(
|
||||
context.getString(R.string.omapi),
|
||||
port,
|
||||
intrinsicChannelName = null,
|
||||
OmapiApduInterface(
|
||||
seService!!,
|
||||
port,
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
context.preferenceRepository.verboseLoggingFlow,
|
||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||
).also {
|
||||
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
||||
it.lpa.setEs10xMss(60)
|
||||
|
@ -61,15 +65,18 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
if (bulkIn == null || bulkOut == null) return null
|
||||
val conn = usbManager.openDevice(usbDevice) ?: return null
|
||||
if (!conn.claimInterface(usbInterface, true)) return null
|
||||
return EuiccChannel(
|
||||
return EuiccChannelImpl(
|
||||
context.getString(R.string.usb),
|
||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||
intrinsicChannelName = usbDevice.productName,
|
||||
UsbApduInterface(
|
||||
conn,
|
||||
bulkIn,
|
||||
bulkOut,
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
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.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -88,44 +91,24 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
}
|
||||
|
||||
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannel
|
||||
}
|
||||
protected suspend fun findEuiccChannelByLogicalSlot(logicalSlotId: Int): EuiccChannel? =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannel
|
||||
}
|
||||
|
||||
for (card in uiccCards) {
|
||||
for (port in card.ports) {
|
||||
if (port.logicalSlotIndex == logicalSlotId) {
|
||||
return@withContext tryOpenEuiccChannel(port)
|
||||
}
|
||||
for (card in uiccCards) {
|
||||
for (port in card.ports) {
|
||||
if (port.logicalSlotIndex == logicalSlotId) {
|
||||
return@withContext tryOpenEuiccChannel(port)
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): 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>? {
|
||||
private suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return usbChannel?.let { listOf(it) }
|
||||
}
|
||||
|
@ -138,12 +121,7 @@ open class DefaultEuiccChannelManager(
|
|||
return null
|
||||
}
|
||||
|
||||
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
|
||||
runBlocking {
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)
|
||||
}
|
||||
|
||||
override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||
private suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext usbChannel
|
||||
|
@ -154,26 +132,82 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
}
|
||||
|
||||
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
|
||||
runBlocking {
|
||||
findEuiccChannelByPort(physicalSlotId, portId)
|
||||
override suspend fun findFirstAvailablePort(physicalSlotId: Int): Int =
|
||||
withContext(Dispatchers.IO) {
|
||||
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) {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return
|
||||
override suspend fun findAvailablePorts(physicalSlotId: Int): List<Int> =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
return@withContext listOf(0)
|
||||
}
|
||||
|
||||
// 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()
|
||||
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)?.map { it.portId } ?: listOf()
|
||||
}
|
||||
|
||||
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) {
|
||||
while (true) {
|
||||
try {
|
||||
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
||||
// and recreate when needed
|
||||
val channel = findEuiccChannelByPort(physicalSlotId, portId)!!
|
||||
val channel = if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
// tryOpenUsbEuiccChannel() will always try to reopen the channel, even if
|
||||
// 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" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
|
@ -184,34 +218,42 @@ open class DefaultEuiccChannelManager(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun enumerateEuiccChannels(): List<EuiccChannel> =
|
||||
withContext(Dispatchers.IO) {
|
||||
uiccCards.flatMap { info ->
|
||||
info.ports.mapNotNull { port ->
|
||||
tryOpenEuiccChannel(port)?.also {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}"
|
||||
)
|
||||
}
|
||||
override fun flowInternalEuiccPorts(): Flow<Pair<Int, Int>> = flow {
|
||||
uiccCards.forEach { info ->
|
||||
info.ports.forEach { port ->
|
||||
tryOpenEuiccChannel(port)?.also {
|
||||
Log.d(
|
||||
TAG,
|
||||
"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) {
|
||||
usbManager.deviceList.values.forEach { device ->
|
||||
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
|
||||
val iface = device.getSmartCardInterface() ?: return@forEach
|
||||
// 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
|
||||
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")
|
||||
try {
|
||||
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
|
||||
if (channel != null && channel.lpa.valid) {
|
||||
usbChannel = channel
|
||||
return@withContext Pair(device, channel)
|
||||
return@withContext Pair(device, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 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}")
|
||||
}
|
||||
return@withContext Pair(null, null)
|
||||
return@withContext Pair(null, false)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
|
|
|
@ -1,26 +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 EuiccChannel(
|
||||
val port: UiccPortInfoCompat,
|
||||
apduInterface: ApduInterface,
|
||||
verboseLoggingFlow: Flow<Boolean>
|
||||
) {
|
||||
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
|
||||
val logicalSlotId = port.logicalSlotIndex
|
||||
val portId = port.portIndex
|
||||
interface EuiccChannel {
|
||||
val type: String
|
||||
|
||||
val lpa: LocalProfileAssistant =
|
||||
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow))
|
||||
val port: UiccPortInfoCompat
|
||||
|
||||
val slotId: Int // PHYSICAL slot
|
||||
val logicalSlotId: Int
|
||||
val portId: Int
|
||||
|
||||
val lpa: LocalProfileAssistant
|
||||
|
||||
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
|
||||
|
||||
import android.hardware.usb.UsbDevice
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* 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
|
||||
* scanned channels cached; these channels will remain open for the entire lifetime of
|
||||
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d
|
||||
* Scan all possible _device internal_ sources for EuiccChannels, as a flow, return their physical
|
||||
* (slotId, portId) and have all scanned channels cached; these channels will remain open
|
||||
* 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.
|
||||
* 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
|
||||
* 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)
|
||||
|
@ -40,29 +57,40 @@ interface EuiccChannelManager {
|
|||
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
|
||||
* If the physical slot supports MEP and has multiple ports, it is undefined
|
||||
* which of the two channels will be returned.
|
||||
* Returns all mapped & available port IDs for a physical slot.
|
||||
*/
|
||||
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
|
||||
* Multiple channels are possible in the case of MEP
|
||||
* Find a EuiccChannel by its slot and port, then run a callback with a reference to it.
|
||||
* 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>?
|
||||
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>?
|
||||
suspend fun <R> withEuiccChannel(
|
||||
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?
|
||||
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
|
||||
suspend fun <R> withEuiccChannel(
|
||||
logicalSlotId: Int,
|
||||
fn: suspend (EuiccChannel) -> R
|
||||
): R
|
||||
|
||||
/**
|
||||
* 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
|
||||
* TODO: Remove this from the common interface
|
||||
*/
|
||||
fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||
suspend fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
|
||||
// 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 port: UiccPortInfoCompat,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
): ApduInterface {
|
||||
): ApduInterface, ApduInterfaceAtrProvider {
|
||||
companion object {
|
||||
const val TAG = "OmapiApduInterface"
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ class OmapiApduInterface(
|
|||
override val valid: Boolean
|
||||
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
||||
|
||||
override val atr: ByteArray?
|
||||
get() = session.atr
|
||||
|
||||
override fun connect() {
|
||||
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession()
|
||||
}
|
||||
|
@ -38,8 +41,8 @@ class OmapiApduInterface(
|
|||
check(!this::lastChannel.isInitialized) {
|
||||
"Can only open one channel"
|
||||
}
|
||||
lastChannel = session.openLogicalChannel(aid)!!;
|
||||
return 1;
|
||||
lastChannel = session.openLogicalChannel(aid)!!
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun logicalChannelClose(handle: Int) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package im.angry.openeuicc.core.usb
|
|||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
|
@ -12,7 +13,7 @@ class UsbApduInterface(
|
|||
private val bulkIn: UsbEndpoint,
|
||||
private val bulkOut: UsbEndpoint,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
): ApduInterface {
|
||||
) : ApduInterface, ApduInterfaceAtrProvider {
|
||||
companion object {
|
||||
private const val TAG = "UsbApduInterface"
|
||||
}
|
||||
|
@ -22,6 +23,8 @@ class UsbApduInterface(
|
|||
|
||||
private var channelId = -1
|
||||
|
||||
override var atr: ByteArray? = null
|
||||
|
||||
override fun connect() {
|
||||
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
||||
|
||||
|
@ -32,7 +35,9 @@ class UsbApduInterface(
|
|||
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
|
||||
|
||||
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) {
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
|
|
|
@ -15,4 +15,5 @@ interface AppContainer {
|
|||
val preferenceRepository: PreferenceRepository
|
||||
val uiComponentFactory: UiComponentFactory
|
||||
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 {
|
||||
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
|
||||
|
||||
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.NoEuiccPlaceholderFragment
|
||||
import im.angry.openeuicc.ui.SettingsFragment
|
||||
|
||||
open class DefaultUiComponentFactory : UiComponentFactory {
|
||||
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(slotId, portId)
|
||||
|
||||
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
|
||||
|
||||
override fun createSettingsFragment(): Fragment = SettingsFragment()
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||
|
||||
interface UiComponentFactory {
|
||||
fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment
|
||||
fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment
|
||||
fun createNoEuiccPlaceholderFragment(): Fragment
|
||||
fun createSettingsFragment(): Fragment
|
||||
}
|
|
@ -15,14 +15,19 @@ import im.angry.openeuicc.core.EuiccChannelManager
|
|||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.isActive
|
||||
|
@ -55,7 +60,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
private const val TAG = "EuiccChannelManagerService"
|
||||
private const val CHANNEL_ID = "tasks"
|
||||
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() {
|
||||
|
@ -89,6 +113,25 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
private val foregroundTaskState: MutableStateFlow<ForegroundTaskState> =
|
||||
MutableStateFlow(ForegroundTaskState.Idle)
|
||||
|
||||
/**
|
||||
* A simple wrapper over a flow with taskId added.
|
||||
*
|
||||
* taskID is the exact millisecond-precision timestamp when the task is launched.
|
||||
*/
|
||||
class ForegroundTaskSubscriberFlow(val taskId: Long, inner: Flow<ForegroundTaskState>) :
|
||||
Flow<ForegroundTaskState> by inner
|
||||
|
||||
/**
|
||||
* A cache of subscribers to 5 recently-launched foreground tasks, identified by ID
|
||||
*
|
||||
* Only one can be run at the same time, but those that are done will be kept in this
|
||||
* map for a little while -- because UI components may be stopped and recreated while
|
||||
* tasks are running. Having this buffer allows the components to re-subscribe even if
|
||||
* the task completes while they are being recreated.
|
||||
*/
|
||||
private val foregroundTaskSubscribers: MutableMap<Long, SharedFlow<ForegroundTaskState>> =
|
||||
mutableMapOf()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return LocalBinder()
|
||||
|
@ -166,12 +209,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the subscriber to a foreground task that is recently launched.
|
||||
*
|
||||
* null if the task doesn't exist, or was launched too long ago.
|
||||
*/
|
||||
fun recoverForegroundTaskSubscriber(taskId: Long): ForegroundTaskSubscriberFlow? =
|
||||
foregroundTaskSubscribers[taskId]?.let {
|
||||
ForegroundTaskSubscriberFlow(taskId, it.applyCompletionTransform())
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a potentially blocking foreground task in this service's lifecycle context.
|
||||
* This function does not block, but returns a Flow that emits ForegroundTaskState
|
||||
* updates associated with this task. The last update the returned flow will emit is
|
||||
* always ForegroundTaskState.Done. The returned flow MUST be started in order for the
|
||||
* foreground task to run.
|
||||
* always ForegroundTaskState.Done.
|
||||
*
|
||||
* The returned flow can only be subscribed to once even though the underlying implementation
|
||||
* is a SharedFlow. This is due to the need to apply transformations so that the stream
|
||||
* actually completes. In order to subscribe multiple times, use `recoverForegroundTaskSubscriber`
|
||||
* to acquire another instance.
|
||||
*
|
||||
* The task closure is expected to update foregroundTaskState whenever appropriate.
|
||||
* If a foreground task is already running, this function returns null.
|
||||
|
@ -185,7 +242,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
failureTitle: String,
|
||||
iconRes: Int,
|
||||
task: suspend EuiccChannelManagerService.() -> Unit
|
||||
): Flow<ForegroundTaskState>? {
|
||||
): ForegroundTaskSubscriberFlow {
|
||||
val taskID = System.currentTimeMillis()
|
||||
|
||||
// Atomically set the state to InProgress. If this returns true, we are
|
||||
// the only task currently in progress.
|
||||
if (!foregroundTaskState.compareAndSet(
|
||||
|
@ -193,7 +252,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
ForegroundTaskState.InProgress(0)
|
||||
)
|
||||
) {
|
||||
return null
|
||||
return ForegroundTaskSubscriberFlow(
|
||||
taskID,
|
||||
flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) })
|
||||
}
|
||||
|
||||
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
|
||||
// until we encounter ForegroundTaskState.Done.
|
||||
// Then, we complete the returned flow, but we also set the state back to Idle.
|
||||
// The state update back to Idle won't show up in the returned stream, because
|
||||
// it has been completed by that point.
|
||||
return foregroundTaskState.transformWhile {
|
||||
// Also update our notification when we see an update
|
||||
// But ignore the first progress = 0 update -- that is the current value.
|
||||
// we need that to be handled by the main coroutine after it finishes.
|
||||
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateForegroundNotification(title, iconRes)
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
foregroundTaskState
|
||||
.applyCompletionTransform()
|
||||
.onEach {
|
||||
// Also update our notification when we see an update
|
||||
// But ignore the first progress = 0 update -- that is the current value.
|
||||
// we need that to be handled by the main coroutine after it finishes.
|
||||
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
|
||||
updateForegroundNotification(title, iconRes)
|
||||
}
|
||||
|
||||
val isForegroundTaskRunning: Boolean
|
||||
get() = foregroundTaskState.value != ForegroundTaskState.Idle
|
||||
subscriberFlow.emit(it)
|
||||
}
|
||||
.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() {
|
||||
foregroundTaskState.takeWhile { it != ForegroundTaskState.Idle }
|
||||
|
@ -280,30 +374,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
matchingId: String?,
|
||||
confirmationCode: String?,
|
||||
imei: String?
|
||||
): Flow<ForegroundTaskState>? =
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_download),
|
||||
getString(R.string.task_profile_download_failure),
|
||||
R.drawable.ic_task_sim_card_download
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)
|
||||
val res = channel!!.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
imei,
|
||||
confirmationCode,
|
||||
object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
if (state.progress == 0) return
|
||||
foregroundTaskState.value =
|
||||
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.")
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.downloadProfile(
|
||||
smdp,
|
||||
matchingId,
|
||||
imei,
|
||||
confirmationCode,
|
||||
object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
if (state.progress == 0) return
|
||||
foregroundTaskState.value =
|
||||
ForegroundTaskState.InProgress(state.progress)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
preferenceRepository.notificationDownloadFlow.first()
|
||||
|
@ -315,19 +405,17 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
portId: Int,
|
||||
iccid: String,
|
||||
name: String
|
||||
): Flow<ForegroundTaskState>? =
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_rename),
|
||||
getString(R.string.task_profile_rename_failure),
|
||||
R.drawable.ic_task_rename
|
||||
) {
|
||||
val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname(
|
||||
iccid,
|
||||
name
|
||||
)
|
||||
|
||||
if (!res) {
|
||||
throw RuntimeException("Profile not renamed")
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.setNickname(
|
||||
iccid,
|
||||
name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,17 +423,16 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
slotId: Int,
|
||||
portId: Int,
|
||||
iccid: String
|
||||
): Flow<ForegroundTaskState>? =
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_delete),
|
||||
getString(R.string.task_profile_delete_failure),
|
||||
R.drawable.ic_task_delete
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
euiccChannelManager.findEuiccChannelByPort(
|
||||
slotId,
|
||||
portId
|
||||
)!!.lpa.deleteProfile(iccid)
|
||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.deleteProfile(iccid)
|
||||
}
|
||||
|
||||
preferenceRepository.notificationDeleteFlow.first()
|
||||
}
|
||||
|
@ -358,16 +445,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
portId: Int,
|
||||
iccid: String,
|
||||
enable: Boolean, // Enable or disable the profile indicated in iccid
|
||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers
|
||||
): Flow<ForegroundTaskState>? =
|
||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
||||
): ForegroundTaskSubscriberFlow =
|
||||
launchForegroundTask(
|
||||
getString(R.string.task_profile_switch),
|
||||
getString(R.string.task_profile_switch_failure),
|
||||
R.drawable.ic_task_switch
|
||||
) {
|
||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!
|
||||
val (res, refreshed) =
|
||||
val (res, refreshed) = euiccChannelManager.withEuiccChannel(
|
||||
slotId,
|
||||
portId
|
||||
) { channel ->
|
||||
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
|
||||
// Sometimes, we *can* enable or disable the profile, but we cannot
|
||||
// send the refresh command to the modem because the profile somehow
|
||||
|
@ -378,13 +467,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
|||
} else {
|
||||
Pair(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
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
|
||||
// but only if we are talking to a modem and not a USB reader
|
||||
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.ClipboardManager
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
|
@ -21,6 +21,7 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -31,11 +32,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import im.angry.openeuicc.common.R
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -52,6 +54,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var profileList: RecyclerView
|
||||
private var logicalSlotId: Int = -1
|
||||
|
||||
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
|
||||
private lateinit var disableSafeguardFlow: StateFlow<Boolean>
|
||||
|
||||
private lateinit var unfilteredProfileListFlow: StateFlow<Boolean>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
@ -105,8 +110,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
fab.setOnClickListener {
|
||||
ProfileDownloadFragment.newInstance(slotId, portId)
|
||||
.show(childFragmentManager, ProfileDownloadFragment.TAG)
|
||||
Intent(requireContext(), DownloadWizardActivity::class.java).apply {
|
||||
putExtra("selectedLogicalSlot", logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,9 +134,21 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.show_notifications -> {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", channel.logicalSlotId)
|
||||
startActivity(this)
|
||||
if (logicalSlotId != -1) {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
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
|
||||
}
|
||||
|
@ -148,31 +167,43 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
listOf()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun refresh() {
|
||||
if (invalid) return
|
||||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
doRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) {
|
||||
disableSafeguardFlow =
|
||||
preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope)
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
protected open suspend fun doRefresh() {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val profiles = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||
if (!::disableSafeguardFlow.isInitialized) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.profiles = profiles
|
||||
adapter.footerViews = onCreateFooterViews(profileList, profiles)
|
||||
adapter.notifyDataSetChanged()
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.profiles = profiles
|
||||
adapter.footerViews = onCreateFooterViews(profileList, profiles)
|
||||
adapter.notifyDataSetChanged()
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,24 +223,15 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val res = euiccChannelManagerService.launchProfileSwitchTask(
|
||||
val err = euiccChannelManagerService.launchProfileSwitchTask(
|
||||
slotId,
|
||||
portId,
|
||||
iccid,
|
||||
enable,
|
||||
reconnectTimeoutMillis = if (isUsb) {
|
||||
0
|
||||
} else {
|
||||
30 * 1000
|
||||
}
|
||||
)?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done
|
||||
reconnectTimeoutMillis = 30 * 1000
|
||||
).waitDone()
|
||||
|
||||
if (res == null) {
|
||||
showSwitchFailureText()
|
||||
return@launch
|
||||
}
|
||||
|
||||
when (res.error) {
|
||||
when (err) {
|
||||
null -> {}
|
||||
is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
|
||||
// This is only really fatal for internal eSIMs
|
||||
|
@ -236,7 +258,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
invalid = true
|
||||
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setMessage(R.string.enable_disable_timeout)
|
||||
setMessage(appContainer.customizableTextProvider.profileSwitchingTimeoutMessage)
|
||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
|
@ -279,7 +301,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
companion object {
|
||||
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 state: TextView = root.requireViewById(R.id.state)
|
||||
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)
|
||||
|
||||
init {
|
||||
|
@ -321,7 +345,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
iccid.setOnLongClickListener {
|
||||
requireContext().getSystemService(ClipboardManager::class.java)!!
|
||||
.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()
|
||||
true
|
||||
}
|
||||
|
@ -343,6 +368,15 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
}
|
||||
)
|
||||
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.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
|
@ -8,7 +9,6 @@ import android.view.View
|
|||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
@ -17,7 +17,6 @@ import im.angry.openeuicc.util.*
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Date
|
||||
|
||||
class LogsActivity : AppCompatActivity() {
|
||||
|
@ -27,15 +26,25 @@ class LogsActivity : AppCompatActivity() {
|
|||
private lateinit var logStr: String
|
||||
|
||||
private val saveLogs =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
if (uri == null) return@registerForActivityResult
|
||||
if (!this::logStr.isInitialized) return@registerForActivityResult
|
||||
contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { os ->
|
||||
os.write(logStr.encodeToByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
setupLogSaving(
|
||||
getLogFileName = {
|
||||
getString(
|
||||
R.string.logs_filename_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
)
|
||||
},
|
||||
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?) {
|
||||
enableEdgeToEdge()
|
||||
|
@ -76,9 +85,7 @@ class LogsActivity : AppCompatActivity() {
|
|||
true
|
||||
}
|
||||
R.id.save -> {
|
||||
saveLogs.launch(getString(R.string.logs_filename_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
))
|
||||
saveLogs()
|
||||
true
|
||||
}
|
||||
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.TabLayoutMediator
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -44,6 +47,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private var refreshing = false
|
||||
|
||||
private data class Page(
|
||||
val logicalSlotId: Int,
|
||||
val title: String,
|
||||
val createFragment: () -> Fragment
|
||||
)
|
||||
|
@ -105,7 +109,7 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java));
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
true
|
||||
}
|
||||
R.id.reload -> {
|
||||
|
@ -122,7 +126,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
|
||||
private fun ensureNotificationPermissions() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
val needsNotificationPerms = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
|
||||
val notificationPermsGranted =
|
||||
needsNotificationPerms && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||
if (needsNotificationPerms && !notificationPermsGranted) {
|
||||
requestPermissions(
|
||||
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
|
||||
PERMISSION_REQUEST_CODE
|
||||
|
@ -138,65 +145,75 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
// Prevent concurrent access with any running foreground task
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val knownChannels = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.enumerateEuiccChannels().onEach {
|
||||
Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
|
||||
val (usbDevice, _) = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.tryOpenUsbEuiccChannel()
|
||||
}
|
||||
|
||||
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()) {
|
||||
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
|
||||
// Note that this is currently supposed to be no-op when unprivileged,
|
||||
// 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) {
|
||||
euiccChannelManager.enumerateUsbEuiccChannel()
|
||||
newPages.sortBy { it.logicalSlotId }
|
||||
|
||||
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) {
|
||||
loadingProgress.visibility = View.GONE
|
||||
|
||||
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
|
||||
if (pages.size > 0) {
|
||||
ensureNotificationPermissions()
|
||||
}
|
||||
|
||||
refreshing = false
|
||||
}
|
||||
|
||||
private fun refresh(fromUsbEvent: Boolean = false) {
|
||||
|
|
|
@ -4,15 +4,20 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class NoEuiccPlaceholderFragment : Fragment() {
|
||||
class NoEuiccPlaceholderFragment : Fragment(), OpenEuiccContextMarker {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): 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.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.Dispatchers
|
||||
|
@ -33,7 +32,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private lateinit var notificationList: RecyclerView
|
||||
private val notificationAdapter = NotificationAdapter()
|
||||
|
||||
private lateinit var euiccChannel: EuiccChannel
|
||||
private var logicalSlotId = -1
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
@ -56,14 +55,14 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
notificationList.adapter = notificationAdapter
|
||||
registerForContextMenu(notificationList)
|
||||
|
||||
val logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||
logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||
|
||||
// This is slightly different from the MainActivity logic
|
||||
// due to the length (we don't want to display the full USB product name)
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.usb)
|
||||
} else {
|
||||
getString(R.string.channel_name_format, logicalSlotId)
|
||||
appContainer.customizableTextProvider.formatInternalChannelName(logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
||||
|
@ -104,16 +103,8 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
if (!this@NotificationsActivity::euiccChannel.isInitialized) {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannelManagerLoaded.await()
|
||||
euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking(
|
||||
intent.getIntExtra(
|
||||
"logicalSlotId",
|
||||
0
|
||||
)
|
||||
)!!
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannelManagerLoaded.await()
|
||||
}
|
||||
|
||||
task()
|
||||
|
@ -124,15 +115,16 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
|
||||
private fun refresh() {
|
||||
launchTask {
|
||||
val profiles = withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.profiles
|
||||
}
|
||||
|
||||
notificationAdapter.notifications =
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.notifications.map {
|
||||
val profile = profiles.find { p -> p.iccid == it.iccid }
|
||||
LocalProfileNotificationWrapper(it, profile?.displayName ?: "???")
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||
val nameMap = buildMap {
|
||||
for (profile in channel.lpa.profiles) {
|
||||
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):
|
||||
RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
|
||||
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 lateinit var notification: LocalProfileNotificationWrapper
|
||||
|
@ -168,6 +162,7 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun operationToLocalizedText(operation: LocalProfileNotification.Operation) =
|
||||
root.context.getText(
|
||||
when (operation) {
|
||||
|
@ -181,6 +176,10 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
notification = value
|
||||
|
||||
address.text = value.inner.notificationAddress
|
||||
sequenceNumber.text = root.context.getString(
|
||||
R.string.profile_notification_sequence_number_format,
|
||||
value.inner.seqNumber
|
||||
)
|
||||
profileName.text = Html.fromHtml(
|
||||
root.context.getString(R.string.profile_notification_name_format,
|
||||
operationToLocalizedText(value.inner.profileManagementOperation),
|
||||
|
@ -205,7 +204,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
R.id.notification_process -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.handleNotification(notification.inner.seqNumber)
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||
channel.lpa.handleNotification(notification.inner.seqNumber)
|
||||
}
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
@ -215,7 +216,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
R.id.notification_delete -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.deleteNotification(notification.inner.seqNumber)
|
||||
euiccChannelManager.withEuiccChannel(logicalSlotId) { channel ->
|
||||
channel.lpa.deleteNotification(notification.inner.seqNumber)
|
||||
}
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
|
|
@ -4,54 +4,69 @@ import android.app.Dialog
|
|||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||
companion object {
|
||||
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 {
|
||||
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
|
||||
instance.requireArguments().apply {
|
||||
putString("iccid", iccid)
|
||||
putString("name", name)
|
||||
putString(FIELD_ICCID, iccid)
|
||||
putString(FIELD_NAME, name)
|
||||
}
|
||||
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 {
|
||||
EditText(requireContext()).apply {
|
||||
hint = Editable.Factory.getInstance().newEditable(
|
||||
getString(R.string.profile_delete_confirm_input, requireArguments().getString("name")!!)
|
||||
)
|
||||
hint = Editable.Factory.getInstance()
|
||||
.newEditable(getString(R.string.profile_delete_confirm_input, name))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply {
|
||||
setMessage(getString(R.string.profile_delete_confirm, requireArguments().getString("name")))
|
||||
private val alertDialog: AlertDialog
|
||||
get() = requireDialog() as AlertDialog
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply {
|
||||
setMessage(getString(R.string.profile_delete_confirm, name))
|
||||
setView(editText)
|
||||
setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.create()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val alertDialog = dialog!! as AlertDialog
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
if (!deleting && inputMatchesName) delete()
|
||||
if (!deleting) delete()
|
||||
}
|
||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||
if (!deleting) dismiss()
|
||||
|
@ -59,8 +74,15 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
}
|
||||
|
||||
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
|
||||
val alertDialog = dialog!! as AlertDialog
|
||||
alertDialog.setCanceledOnTouchOutside(false)
|
||||
alertDialog.setCancelable(false)
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
|
@ -69,12 +91,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
requireParentFragment().lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
euiccChannelManagerService.launchProfileDeleteTask(
|
||||
slotId,
|
||||
portId,
|
||||
requireArguments().getString("iccid")!!
|
||||
)!!.onStart {
|
||||
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
|
||||
if (parentFragment is EuiccProfilesChangedListener) {
|
||||
// Trigger a refresh in the parent fragment -- it should wait until
|
||||
// any foreground task is completed before actually doing a refresh
|
||||
|
@ -86,7 +103,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
} catch (e: IllegalStateException) {
|
||||
// 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 com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
||||
companion object {
|
||||
|
@ -53,6 +54,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
|
||||
toolbar.apply {
|
||||
setTitle(R.string.rename)
|
||||
setNavigationOnClickListener {
|
||||
|
@ -65,11 +67,6 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setWidthPercent(95)
|
||||
|
@ -81,13 +78,18 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
}
|
||||
}
|
||||
|
||||
private fun rename() {
|
||||
val name = profileRenameNewName.editText!!.text.toString().trim()
|
||||
if (name.length >= 64) {
|
||||
Toast.makeText(context, R.string.toast_profile_name_too_long, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
private fun showErrorAndCancel(errorStrRes: Int) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
errorStrRes,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
renaming = false
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun rename() {
|
||||
renaming = true
|
||||
progress.isIndeterminate = true
|
||||
progress.visibility = View.VISIBLE
|
||||
|
@ -95,21 +97,37 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
euiccChannelManagerService.launchProfileRenameTask(
|
||||
val res = euiccChannelManagerService.launchProfileRenameTask(
|
||||
slotId,
|
||||
portId,
|
||||
requireArguments().getString("iccid")!!,
|
||||
name
|
||||
)?.collect()
|
||||
profileRenameNewName.editText!!.text.toString().trim()
|
||||
).waitDone()
|
||||
|
||||
if (parentFragment is EuiccProfilesChangedListener) {
|
||||
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
||||
}
|
||||
when (res) {
|
||||
is LocalProfileAssistant.ProfileNameTooLongException -> {
|
||||
showErrorAndCancel(R.string.profile_rename_too_long)
|
||||
}
|
||||
|
||||
try {
|
||||
dismiss()
|
||||
} catch (e: IllegalStateException) {
|
||||
// Ignored
|
||||
is LocalProfileAssistant.ProfileNameIsInvalidUTF8Exception -> {
|
||||
showErrorAndCancel(R.string.profile_rename_encoding_error)
|
||||
}
|
||||
|
||||
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 androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import im.angry.openeuicc.OpenEuiccApplication
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
private val appContainer
|
||||
get() = (application as OpenEuiccApplication).appContainer
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -15,8 +19,9 @@ class SettingsActivity: AppCompatActivity() {
|
|||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
val settingsFragment = appContainer.uiComponentFactory.createSettingsFragment()
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings_container, SettingsFragment())
|
||||
.replace(R.id.settings_container, settingsFragment)
|
||||
.commit()
|
||||
}
|
||||
|
||||
|
|
|
@ -2,71 +2,154 @@ package im.angry.openeuicc.ui
|
|||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.common.R
|
||||
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.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?) {
|
||||
setPreferencesFromResource(R.xml.pref_settings, rootKey)
|
||||
|
||||
findPreference<Preference>("pref_info_app_version")
|
||||
?.summary = requireContext().selfAppVersion
|
||||
developerPref = requirePreference("pref_developer")
|
||||
|
||||
findPreference<Preference>("pref_info_source_code")
|
||||
?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.summary.toString())))
|
||||
true
|
||||
// Show / hide developer preference based on whether it is enabled
|
||||
lifecycleScope.launch {
|
||||
preferenceRepository.developerOptionsEnabledFlow
|
||||
.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")
|
||||
?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(requireContext(), LogsActivity::class.java))
|
||||
true
|
||||
}
|
||||
requirePreference<Preference>("pref_advanced_logs").apply {
|
||||
intent = Intent(requireContext(), LogsActivity::class.java)
|
||||
}
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_download")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
|
||||
requirePreference<CheckBoxPreference>("pref_notifications_download")
|
||||
.bindBooleanFlow(preferenceRepository.notificationDownloadFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_delete")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
|
||||
requirePreference<CheckBoxPreference>("pref_notifications_delete")
|
||||
.bindBooleanFlow(preferenceRepository.notificationDeleteFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_switch")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationSwitchFlow, PreferenceKeys.NOTIFICATION_SWITCH)
|
||||
requirePreference<CheckBoxPreference>("pref_notifications_switch")
|
||||
.bindBooleanFlow(preferenceRepository.notificationSwitchFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
||||
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM)
|
||||
requirePreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
|
||||
.bindBooleanFlow(preferenceRepository.disableSafeguardFlow)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
|
||||
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
|
||||
requirePreference<CheckBoxPreference>("pref_advanced_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() {
|
||||
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 {
|
||||
flow.collect { isChecked = it }
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
runBlocking {
|
||||
preferenceRepository.updatePreference(key, newValue as Boolean)
|
||||
flow.updatePreference(newValue as Boolean)
|
||||
}
|
||||
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.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -73,7 +72,6 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
private lateinit var loadingProgress: ProgressBar
|
||||
|
||||
private var usbDevice: UsbDevice? = null
|
||||
private var usbChannel: EuiccChannel? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -122,7 +120,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
try {
|
||||
requireContext().unregisterReceiver(usbPermissionReceiver)
|
||||
} catch (_: Exception) {
|
||||
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,7 +129,7 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
try {
|
||||
requireContext().unregisterReceiver(usbPermissionReceiver)
|
||||
} catch (_: Exception) {
|
||||
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,24 +138,26 @@ class UsbCcidReaderFragment : Fragment(), OpenEuiccContextMarker {
|
|||
permissionButton.visibility = View.GONE
|
||||
loadingProgress.visibility = View.VISIBLE
|
||||
|
||||
val (device, channel) = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.enumerateUsbEuiccChannel()
|
||||
val (device, canOpen) = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.tryOpenUsbEuiccChannel()
|
||||
}
|
||||
|
||||
loadingProgress.visibility = View.GONE
|
||||
|
||||
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.visibility = View.VISIBLE
|
||||
permissionButton.visibility = View.VISIBLE
|
||||
} else if (device != null && channel != null) {
|
||||
} else if (device != null && canOpen) {
|
||||
childFragmentManager.commit {
|
||||
replace(
|
||||
R.id.child_container,
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(channel)
|
||||
appContainer.uiComponentFactory.createEuiccManagementFragment(
|
||||
slotId = EuiccChannelManager.USB_CHANNEL_ID,
|
||||
portId = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
} 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
|
||||
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
|
||||
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
|
||||
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()
|
||||
|
||||
interface EuiccProfilesChangedListener {
|
||||
|
|
|
@ -3,9 +3,6 @@ package im.angry.openeuicc.util
|
|||
import android.util.Log
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
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.LocalProfileInfo
|
||||
|
||||
|
@ -19,9 +16,10 @@ val LocalProfileInfo.isEnabled: Boolean
|
|||
get() = state == LocalProfileInfo.State.Enabled
|
||||
|
||||
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
|
||||
get() = filter {
|
||||
it.profileClass == LocalProfileInfo.Clazz.Operational
|
||||
}
|
||||
get() = filter { it.profileClass == LocalProfileInfo.Clazz.Operational }
|
||||
|
||||
val List<LocalProfileInfo>.enabled: LocalProfileInfo?
|
||||
get() = find { it.isEnabled }
|
||||
|
||||
val List<EuiccChannel>.hasMultipleChips: Boolean
|
||||
get() = distinctBy { it.slotId }.size > 1
|
||||
|
@ -42,22 +40,27 @@ fun LocalProfileAssistant.switchProfile(
|
|||
* See EuiccManager.waitForReconnect()
|
||||
*/
|
||||
fun LocalProfileAssistant.disableActiveProfile(refresh: Boolean): Boolean =
|
||||
profiles.find { it.isEnabled }?.let {
|
||||
profiles.enabled?.let {
|
||||
Log.i(TAG, "Disabling active profile ${it.iccid}")
|
||||
disableProfile(it.iccid, refresh)
|
||||
} ?: true
|
||||
|
||||
/**
|
||||
* Disable the active profile, return a lambda that reverts this action when called.
|
||||
* 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.
|
||||
* Disable the current active profile if any. If refresh is true, also cause a refresh command.
|
||||
* 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 =
|
||||
profiles.find { it.isEnabled }?.let {
|
||||
disableProfile(it.iccid, refreshOnDisable)
|
||||
return { enableProfile(it.iccid) }
|
||||
} ?: { }
|
||||
fun LocalProfileAssistant.disableActiveProfileKeepIccId(refresh: Boolean): String? =
|
||||
profiles.enabled?.let {
|
||||
Log.i(TAG, "Disabling active profile ${it.iccid}")
|
||||
if (disableProfile(it.iccid, refresh)) {
|
||||
it.iccid
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a "tracked" operation where notifications may be generated by the eSIM
|
||||
|
@ -78,60 +81,21 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
|
|||
portId: Int,
|
||||
op: () -> Boolean
|
||||
) {
|
||||
val latestSeq =
|
||||
findEuiccChannelByPort(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber
|
||||
val latestSeq = withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.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"
|
||||
findEuiccChannelByPort(
|
||||
slotId,
|
||||
portId
|
||||
)?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach {
|
||||
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)
|
||||
// this is why we need to use two distinct calls to withEuiccChannel()
|
||||
withEuiccChannel(slotId, portId) { channel ->
|
||||
channel.lpa.notifications.filter { it.seqNumber > latestSeq }.forEach {
|
||||
Log.d(TAG, "Handling notification $it")
|
||||
channel.lpa.handleNotification(it.seqNumber)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore any error during notification handling
|
||||
|
|
|
@ -19,38 +19,54 @@ val Context.preferenceRepository: PreferenceRepository
|
|||
val Fragment.preferenceRepository: PreferenceRepository
|
||||
get() = requireContext().preferenceRepository
|
||||
|
||||
object PreferenceKeys {
|
||||
internal object PreferenceKeys {
|
||||
// ---- Profile Notifications ----
|
||||
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
||||
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
||||
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 ----
|
||||
val disableSafeguardFlow: Flow<Boolean> =
|
||||
dataStore.data.map { it[PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM] ?: false }
|
||||
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
|
||||
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
|
||||
|
||||
val verboseLoggingFlow: Flow<Boolean> =
|
||||
dataStore.data.map { it[PreferenceKeys.VERBOSE_LOGGING] ?: false }
|
||||
// ---- Developer Options ----
|
||||
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) {
|
||||
dataStore.edit {
|
||||
it[key] = value
|
||||
}
|
||||
class PreferenceRepository(private val context: Context) {
|
||||
// Expose flows so that we can also handle default values
|
||||
// ---- Profile Notifications ----
|
||||
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
|
||||
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 }
|
||||
}
|
||||
}
|
|
@ -27,4 +27,74 @@ fun formatFreeSpace(size: Int): String =
|
|||
"%.2f KiB".format(size.toDouble() / 1024)
|
||||
} else {
|
||||
"$size B"
|
||||
}
|
||||
}
|
||||
|
||||
fun String.prettyPrintJson(): String {
|
||||
val ret = StringBuilder()
|
||||
var inQuotes = false
|
||||
var escaped = false
|
||||
val indentSymbolStack = ArrayDeque<Char>()
|
||||
|
||||
val addNewLine = {
|
||||
ret.append('\n')
|
||||
repeat(indentSymbolStack.size) {
|
||||
ret.append('\t')
|
||||
}
|
||||
}
|
||||
|
||||
var lastChar = ' '
|
||||
|
||||
for (c in this) {
|
||||
when {
|
||||
!inQuotes && (c == '{' || c == '[') -> {
|
||||
ret.append(c)
|
||||
indentSymbolStack.addLast(c)
|
||||
addNewLine()
|
||||
}
|
||||
|
||||
!inQuotes && (c == '}' || c == ']') -> {
|
||||
indentSymbolStack.removeLast()
|
||||
if (lastChar != ',') {
|
||||
addNewLine()
|
||||
}
|
||||
ret.append(c)
|
||||
}
|
||||
|
||||
!inQuotes && c == ',' -> {
|
||||
ret.append(c)
|
||||
addNewLine()
|
||||
}
|
||||
|
||||
!inQuotes && c == ':' -> {
|
||||
ret.append(c)
|
||||
ret.append(' ')
|
||||
}
|
||||
|
||||
inQuotes && c == '\\' -> {
|
||||
ret.append(c)
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
!escaped && c == '"' -> {
|
||||
ret.append(c)
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
|
||||
!inQuotes && c == ' ' -> {
|
||||
// Do nothing -- we ignore spaces outside of quotes by default
|
||||
// This is to ensure predictable formatting
|
||||
}
|
||||
|
||||
else -> ret.append(c)
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
escaped = false
|
||||
}
|
||||
|
||||
lastChar = c
|
||||
}
|
||||
|
||||
return ret.toString()
|
||||
}
|
|
@ -45,6 +45,8 @@ fun SEService.getUiccReaderCompat(slotNumber: Int): Reader {
|
|||
interface UiccCardInfoCompat {
|
||||
val physicalSlotIndex: Int
|
||||
val ports: Collection<UiccPortInfoCompat>
|
||||
val isRemovable: Boolean
|
||||
get() = true // This defaults to removable unless overridden
|
||||
}
|
||||
|
||||
interface UiccPortInfoCompat {
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
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.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.common.R
|
||||
import java.io.FileOutputStream
|
||||
|
||||
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
||||
/**
|
||||
|
@ -69,4 +76,49 @@ fun setupRootViewInsets(view: ViewGroup) {
|
|||
|
||||
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_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/state"
|
||||
app:layout_constraintHorizontal_bias="0" />
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constrainedWidth="true" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/profile_menu"
|
||||
|
@ -62,18 +63,45 @@
|
|||
android:singleLine="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/state"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iccid_label"/>
|
||||
app:layout_constraintBottom_toTopOf="@+id/profile_class_label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/provider"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:layout_marginStart="7dp"
|
||||
android:textSize="14sp"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintLeft_toRightOf="@id/provider_label"
|
||||
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"/>
|
||||
|
||||
<TextView
|
||||
|
@ -86,7 +114,7 @@
|
|||
android:textStyle="bold"
|
||||
android:singleLine="true"
|
||||
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"/>
|
||||
|
||||
<TextView
|
||||
|
@ -94,11 +122,11 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:layout_marginStart="7dp"
|
||||
android:textSize="14sp"
|
||||
android:singleLine="true"
|
||||
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"/>
|
||||
|
||||
</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_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
|
||||
android:id="@+id/notification_profile_name"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -5,4 +5,9 @@
|
|||
android:id="@+id/show_notifications"
|
||||
android:title="@string/profile_notifications_show"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/euicc_info"
|
||||
android:title="@string/euicc_info"
|
||||
app:showAsAction="never" />
|
||||
</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_profile">No profiles (yet) on this eSIM.</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="information_unavailable">Information Unavailable</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="reload">Reload Slots</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="disabled">Disabled</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="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="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="slot_select">Select Slot</string>
|
||||
<string name="slot_select_select">Select</string>
|
||||
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||
<string name="toast_atr_copied">ATR copied to clipboard</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>
|
||||
|
@ -48,13 +53,55 @@
|
|||
<string name="profile_download_code">Activation Code</string>
|
||||
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
|
||||
<string name="profile_download_imei">IMEI (Optional)</string>
|
||||
<string name="profile_download_free_space">Space remaining: %s</string>
|
||||
<string name="profile_download_scan">Scan QR Code</string>
|
||||
<string name="profile_download_scan_from_gallery">Scan QR Code from Gallery</string>
|
||||
<string name="profile_download_ok">Download</string>
|
||||
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
|
||||
|
||||
<string name="profile_download_low_nvram_title">This download may fail</string>
|
||||
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
|
||||
<string name="profile_download_no_lpa_string">No LPA code found in clipboard</string>
|
||||
<string name="profile_download_incorrect_lpa_string">Unable to parse</string>
|
||||
<string name="profile_download_incorrect_lpa_string_message">Could not parse QR code or clipboard content as a LPA code.</string>
|
||||
|
||||
<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_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_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_enable">Enabled</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_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_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_notifications">Notifications</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_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_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_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_app_version">App Version</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"?>
|
||||
<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
|
||||
app:title="@string/pref_notifications"
|
||||
app:summary="@string/pref_notifications_desc"
|
||||
|
@ -36,14 +37,42 @@
|
|||
app:title="@string/pref_advanced_verbose_logging"
|
||||
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
|
||||
app:key="pref_advanced_logs"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_advanced_logs"
|
||||
app:summary="@string/pref_advanced_logs_desc" />
|
||||
|
||||
</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:iconSpaceReserved="false">
|
||||
<Preference
|
||||
|
@ -55,6 +84,10 @@
|
|||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_info_source_code"
|
||||
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>
|
||||
</PreferenceScreen>
|
3
app-deps/.gitignore
vendored
3
app-deps/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/build
|
||||
/build
|
||||
/libs
|
|
@ -49,6 +49,10 @@ android {
|
|||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
dependenciesInfo {
|
||||
// Disable dependency metadata -- breaks compatibility with F-Droid
|
||||
includeInApk = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -9,5 +9,6 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_jmp"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locale_config"
|
||||
android:theme="@style/Theme.OpenEUICC" />
|
||||
</manifest>
|
|
@ -6,4 +6,8 @@ class JmpAppContainer(context: Context) : UnprivilegedAppContainer(context) {
|
|||
override val uiComponentFactory by lazy {
|
||||
JmpUiComponentFactory()
|
||||
}
|
||||
|
||||
override val customizableTextProvider by lazy {
|
||||
JmpCustomizableTextProvider(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.content.Context
|
||||
import im.angry.easyeuicc.R
|
||||
|
||||
class JmpCustomizableTextProvider(private val context: Context) :
|
||||
UnprivilegedCustomizableTextProvider(context) {
|
||||
override val noEuiccExplanation: String
|
||||
get() = context.getString(R.string.no_euicc_jmp)
|
||||
override val profileSwitchingTimeoutMessage: String
|
||||
get() = context.getString(R.string.enable_disable_timeout_jmp)
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
android:layout_marginStart="40dp"
|
||||
android:layout_marginEnd="40dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_euicc"
|
||||
android:text="@string/no_euicc_jmp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
|
|
6
app-unpriv/src/jmp/res/values-ja/strings.xml
Normal file
6
app-unpriv/src/jmp/res/values-ja/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="no_euicc_jmp">このデバイスで JMP eSIM Adapter を見つかりません。JMP eSIM Adapter をデバイスに挿入、または USB リーダーに経由し接続してください。</string>
|
||||
<string name="purchase_esim">JMP eSIM Adapter を購入</string>
|
||||
<string name="enable_disable_timeout_jmp">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。 SIM ツールキットの Tools -> Reboot を選択し、eSIM Adapter をリフレッシュしてください。</string>
|
||||
</resources>
|
6
app-unpriv/src/jmp/res/values-zh-rCN/strings.xml
Normal file
6
app-unpriv/src/jmp/res/values-zh-rCN/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="no_euicc_jmp">没有在此设备上发现 JMP eSIM Adapter。请将其插入本设备或 USB 读卡器。</string>
|
||||
<string name="purchase_esim">购入 JMP eSIM Adapter</string>
|
||||
<string name="enable_disable_timeout_jmp">等待 eSIM 芯片切换配置文件超时。请使用 SIM Toolkit 中的 Tools -> Reboot 手动刷新 eSIM Adapter。</string>
|
||||
</resources>
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">JMP SIM Manager</string>
|
||||
<string name="no_euicc">No JMP eSIM Adapter found on this device. Insert one into the device or through a USB card reader.</string>
|
||||
<string name="no_euicc_jmp">No JMP eSIM Adapter found on this device. Insert one into the device or through a USB card reader.</string>
|
||||
<string name="purchase_esim">Buy JMP eSIM Adapter</string>
|
||||
<string name="purchase_sim_url" translatable="false">https://jmp.chat/esim-adapter</string>
|
||||
<string name="pref_info_source_code_url" translatable="false">https://gitea.angry.im/jmp-sim/jmp-sim-manager</string>
|
||||
|
||||
<string name="enable_disable_timeout">Timed out waiting for the eSIM chip to switch profiles. Please manually refresh the eSIM adapter by going to SIM Toolkit, and select Tools -> Reboot.</string>
|
||||
<string name="enable_disable_timeout_jmp">Timed out waiting for the eSIM chip to switch profiles. Please manually refresh the eSIM adapter by going to SIM Toolkit, and select Tools -> Reboot.</string>
|
||||
</resources>
|
|
@ -1,5 +1,6 @@
|
|||
<?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
|
||||
android:name="im.angry.openeuicc.UnprivilegedOpenEuiccApplication"
|
||||
|
@ -8,7 +9,9 @@
|
|||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OpenEUICC">
|
||||
android:localeConfig="@xml/locale_config"
|
||||
android:theme="@style/Theme.OpenEUICC"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
|
||||
|
@ -22,9 +25,22 @@
|
|||
|
||||
<activity
|
||||
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>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.android.stk" />
|
||||
<package android:name="com.android.stk1" />
|
||||
<package android:name="com.android.stk2" />
|
||||
</queries>
|
||||
</manifest>
|