Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
736505a88b | |||
7fa45e9848 | |||
c12cc1e24c |
67 changed files with 565 additions and 1534 deletions
|
@ -1,7 +1,7 @@
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '*'
|
- 'master'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-debug:
|
build-debug:
|
||||||
|
|
6
.idea/codeStyles/Project.xml
generated
6
.idea/codeStyles/Project.xml
generated
|
@ -1,8 +1,5 @@
|
||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<JetCodeStyleSettings>
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</JetCodeStyleSettings>
|
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
|
@ -116,8 +113,5 @@
|
||||||
</rules>
|
</rules>
|
||||||
</arrangement>
|
</arrangement>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
</component>
|
</component>
|
1
.idea/codeStyles/codeStyleConfig.xml
generated
1
.idea/codeStyles/codeStyleConfig.xml
generated
|
@ -1,6 +1,5 @@
|
||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<state>
|
<state>
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
|
||||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
</state>
|
</state>
|
||||||
</component>
|
</component>
|
24
.idea/deploymentTargetSelector.xml
generated
24
.idea/deploymentTargetSelector.xml
generated
|
@ -8,30 +8,6 @@
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="app-unpriv.androidTest">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="app-unpriv.main">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="app-unpriv.unitTest">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="app.unitTest">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="app.androidTest">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="app.main">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="workspace.OpenEUICC.app-unpriv">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="workspace.OpenEUICC.app">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.24" />
|
<option name="version" value="1.9.20" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
26
README.md
26
README.md
|
@ -2,22 +2,18 @@
|
||||||
|
|
||||||
A fully free and open-source Local Profile Assistant implementation for Android devices.
|
A fully free and open-source Local Profile Assistant implementation for Android devices.
|
||||||
|
|
||||||
There are two variants of this project, OpenEUICC and EasyEUICC:
|
There are two variants of this project:
|
||||||
|
|
||||||
| | OpenEUICC | EasyEUICC |
|
- OpenEUICC: The full-fledged privileged variant.
|
||||||
|:------------------------------|:-----------------------------------------------:|:-----------------:|
|
- Due to its privilege requirement, OpenEUICC must be placed inside `/system/priv-app` and be signed with the platform certificate.
|
||||||
| Privileged | Must be installed as system app | No |
|
- The preferred way to including OpenEUICC in a system image is to [build it along with AOSP](#building-aosp).
|
||||||
| Internal eSIM | Supported | Unsupported |
|
- __Note__: When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant.
|
||||||
| External (Removable) eSIM | Supported | Supported |
|
- EasyEUICC: Unprivileged version that can run as a user app.
|
||||||
| USB Readers | Supported | Supported |
|
- This version supports two modes of operation:
|
||||||
| Requires allowlisting by eSIM | No | Yes -- except USB |
|
1. Inserted, removable eSIMs: Due to obvious security requirements, EasyEUICC is only able to access eSIM chips whose [ARF/ARA](https://source.android.com/docs/core/connect/uicc#arf) contains the hash of EasyEUICC's signing certificate.
|
||||||
| System Integration | Partial (carrier partner API unimplemented yet) | No |
|
2. USB CCID Card Readers: Only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported. In this mode, EasyEUICC can access any eSIM chip loaded in the card reader regardless of their ARF/ARA, as long as they implement the [SGP.22 standard](https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf).
|
||||||
|
- Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases)
|
||||||
Some side notes:
|
- For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`
|
||||||
1. When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant.
|
|
||||||
2. Both variants support accessing eUICC chips through USB CCID readers, regardless of whether the chip contains the correct ARA-M hash to allow for unprivileged access. However, only `T=0` readers that use the standard [USB CCID protocol](https://en.wikipedia.org/wiki/CCID_(protocol)) are supported.
|
|
||||||
3. Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases). For OpenEUICC, no official release is currently provided and only debug mode APKs can be found in the CI page.
|
|
||||||
4. For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`.
|
|
||||||
|
|
||||||
__This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.__ Any modification and derivative work __MUST__ be released under the SAME license, which means, at the very least, that the source code __MUST__ be available upon request.
|
__This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.__ Any modification and derivative work __MUST__ be released under the SAME license, which means, at the very least, that the source code __MUST__ be available upon request.
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
@ -28,27 +27,10 @@
|
||||||
android:name="im.angry.openeuicc.ui.LogsActivity"
|
android:name="im.angry.openeuicc.ui.LogsActivity"
|
||||||
android:label="@string/pref_advanced_logs" />
|
android:label="@string/pref_advanced_logs" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="im.angry.openeuicc.ui.IsdrAidListActivity"
|
|
||||||
android:label="@string/isdr_aid_list" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
|
||||||
android:label="@string/download_wizard">
|
android:label="@string/download_wizard" />
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<!-- Accepts URIs that begin with "lpa:" -->
|
|
||||||
<!-- for example: "LPA:1$..." -->
|
|
||||||
<!-- refs: https://www.iana.org/assignments/uri-schemes/prov/lpa -->
|
|
||||||
<data android:scheme="lpa"/>
|
|
||||||
<data android:sspPrefix="1$"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
@ -1,40 +1,39 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.hardware.usb.UsbDevice
|
||||||
|
import android.hardware.usb.UsbInterface
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
import android.se.omapi.SEService
|
import android.se.omapi.SEService
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import im.angry.openeuicc.common.R
|
import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.core.usb.UsbApduInterface
|
import im.angry.openeuicc.core.usb.UsbApduInterface
|
||||||
import im.angry.openeuicc.core.usb.UsbCcidContext
|
import im.angry.openeuicc.core.usb.bulkPair
|
||||||
|
import im.angry.openeuicc.core.usb.endpoints
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
|
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
|
||||||
private var seService: SEService? = null
|
private var seService: SEService? = null
|
||||||
|
|
||||||
|
private val usbManager by lazy {
|
||||||
|
context.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun ensureSEService() {
|
private suspend fun ensureSEService() {
|
||||||
if (seService == null || !seService!!.isConnected) {
|
if (seService == null || !seService!!.isConnected) {
|
||||||
seService = connectSEService(context)
|
seService = connectSEService(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun tryOpenEuiccChannel(
|
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
|
||||||
port: UiccPortInfoCompat,
|
|
||||||
isdrAid: ByteArray
|
|
||||||
): EuiccChannel? {
|
|
||||||
if (port.portIndex != 0) {
|
if (port.portIndex != 0) {
|
||||||
Log.w(
|
Log.w(DefaultEuiccChannelManager.TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
|
||||||
DefaultEuiccChannelManager.TAG,
|
|
||||||
"OMAPI channel attempted on non-zero portId, this may or may not work."
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureSEService()
|
ensureSEService()
|
||||||
|
|
||||||
Log.i(
|
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
|
||||||
DefaultEuiccChannelManager.TAG,
|
|
||||||
"Trying OMAPI for physical slot ${port.card.physicalSlotIndex}"
|
|
||||||
)
|
|
||||||
try {
|
try {
|
||||||
return EuiccChannelImpl(
|
return EuiccChannelImpl(
|
||||||
context.getString(R.string.omapi),
|
context.getString(R.string.omapi),
|
||||||
|
@ -45,48 +44,41 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
||||||
port,
|
port,
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
context.preferenceRepository.verboseLoggingFlow
|
||||||
),
|
),
|
||||||
isdrAid,
|
|
||||||
context.preferenceRepository.verboseLoggingFlow,
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
).also {
|
).also {
|
||||||
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
|
||||||
it.lpa.setEs10xMss(60)
|
it.lpa.setEs10xMss(60)
|
||||||
}
|
}
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// Failed
|
// Failed
|
||||||
Log.w(
|
Log.w(
|
||||||
DefaultEuiccChannelManager.TAG,
|
DefaultEuiccChannelManager.TAG,
|
||||||
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
|
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun tryOpenUsbEuiccChannel(
|
override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? {
|
||||||
ccidCtx: UsbCcidContext,
|
val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
|
||||||
isdrAid: ByteArray
|
if (bulkIn == null || bulkOut == null) return null
|
||||||
): EuiccChannel? {
|
val conn = usbManager.openDevice(usbDevice) ?: return null
|
||||||
try {
|
if (!conn.claimInterface(usbInterface, true)) return null
|
||||||
return EuiccChannelImpl(
|
return EuiccChannelImpl(
|
||||||
context.getString(R.string.usb),
|
context.getString(R.string.usb),
|
||||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||||
intrinsicChannelName = ccidCtx.productName,
|
intrinsicChannelName = usbDevice.productName,
|
||||||
UsbApduInterface(
|
UsbApduInterface(
|
||||||
ccidCtx
|
conn,
|
||||||
),
|
bulkIn,
|
||||||
isdrAid,
|
bulkOut,
|
||||||
context.preferenceRepository.verboseLoggingFlow,
|
context.preferenceRepository.verboseLoggingFlow
|
||||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
),
|
||||||
)
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
} catch (_: IllegalArgumentException) {
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
// Failed
|
)
|
||||||
Log.w(
|
|
||||||
DefaultEuiccChannelManager.TAG,
|
|
||||||
"USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.hardware.usb.UsbDevice
|
||||||
import android.hardware.usb.UsbManager
|
import android.hardware.usb.UsbManager
|
||||||
import android.telephony.SubscriptionManager
|
import android.telephony.SubscriptionManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import im.angry.openeuicc.core.usb.UsbCcidContext
|
|
||||||
import im.angry.openeuicc.core.usb.smartCard
|
import im.angry.openeuicc.core.usb.smartCard
|
||||||
import im.angry.openeuicc.core.usb.interfaces
|
import im.angry.openeuicc.core.usb.interfaces
|
||||||
import im.angry.openeuicc.di.AppContainer
|
import im.angry.openeuicc.di.AppContainer
|
||||||
|
@ -13,7 +12,6 @@ import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
|
@ -51,24 +49,6 @@ open class DefaultEuiccChannelManager(
|
||||||
protected open val uiccCards: Collection<UiccCardInfoCompat>
|
protected open val uiccCards: Collection<UiccCardInfoCompat>
|
||||||
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
|
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
|
||||||
|
|
||||||
private suspend inline fun tryOpenChannelFirstValidAid(openFn: (ByteArray) -> EuiccChannel?): EuiccChannel? {
|
|
||||||
val isdrAidList =
|
|
||||||
parseIsdrAidList(appContainer.preferenceRepository.isdrAidListFlow.first())
|
|
||||||
|
|
||||||
return isdrAidList.firstNotNullOfOrNull {
|
|
||||||
Log.i(TAG, "Opening channel, trying ISDR AID ${it.encodeHex()}")
|
|
||||||
|
|
||||||
openFn(it)?.let { channel ->
|
|
||||||
if (channel.valid) {
|
|
||||||
channel
|
|
||||||
} else {
|
|
||||||
channel.close()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
|
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
|
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||||
|
@ -96,10 +76,9 @@ open class DefaultEuiccChannelManager(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val channel =
|
val channel = euiccChannelFactory.tryOpenEuiccChannel(port) ?: return null
|
||||||
tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
|
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel.valid) {
|
||||||
channelCache.add(channel)
|
channelCache.add(channel)
|
||||||
return channel
|
return channel
|
||||||
} else {
|
} else {
|
||||||
|
@ -107,6 +86,7 @@ open class DefaultEuiccChannelManager(
|
||||||
TAG,
|
TAG,
|
||||||
"Was able to open channel for logical slot ${port.logicalSlotIndex}, but the channel is invalid (cannot get eID or profiles without errors). This slot might be broken, aborting."
|
"Was able to open channel for logical slot ${port.logicalSlotIndex}, but the channel is invalid (cannot get eID or profiles without errors). This slot might be broken, aborting."
|
||||||
)
|
)
|
||||||
|
channel.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,10 +212,7 @@ open class DefaultEuiccChannelManager(
|
||||||
check(channel.valid) { "Invalid channel" }
|
check(channel.valid) { "Invalid channel" }
|
||||||
break
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(
|
Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms")
|
||||||
TAG,
|
|
||||||
"Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
@ -272,19 +249,10 @@ open class DefaultEuiccChannelManager(
|
||||||
// If we don't have permission, tell UI code that we found a candidate device, but we
|
// If we don't have permission, tell UI code that we found a candidate device, but we
|
||||||
// need permission to be able to do anything with it
|
// need permission to be able to do anything with it
|
||||||
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
|
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
|
||||||
Log.i(
|
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
|
||||||
TAG,
|
|
||||||
"Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
|
|
||||||
)
|
|
||||||
|
|
||||||
val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val channel = tryOpenChannelFirstValidAid {
|
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
|
||||||
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
|
|
||||||
}
|
|
||||||
if (channel != null && channel.lpa.valid) {
|
if (channel != null && channel.lpa.valid) {
|
||||||
ccidCtx.allowDisconnect = true
|
|
||||||
usbChannel = channel
|
usbChannel = channel
|
||||||
return@withContext Pair(device, true)
|
return@withContext Pair(device, true)
|
||||||
}
|
}
|
||||||
|
@ -292,14 +260,7 @@ open class DefaultEuiccChannelManager(
|
||||||
// Ignored -- skip forward
|
// Ignored -- skip forward
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
|
||||||
ccidCtx.allowDisconnect = true
|
|
||||||
ccidCtx.disconnect()
|
|
||||||
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return@withContext Pair(null, false)
|
return@withContext Pair(null, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,5 @@ interface EuiccChannel {
|
||||||
*/
|
*/
|
||||||
val apduInterface: ApduInterface
|
val apduInterface: ApduInterface
|
||||||
|
|
||||||
/**
|
|
||||||
* The AID of the ISD-R channel currently in use
|
|
||||||
*/
|
|
||||||
val isdrAid: ByteArray
|
|
||||||
|
|
||||||
fun close()
|
fun close()
|
||||||
}
|
}
|
|
@ -1,17 +1,15 @@
|
||||||
package im.angry.openeuicc.core
|
package im.angry.openeuicc.core
|
||||||
|
|
||||||
import im.angry.openeuicc.core.usb.UsbCcidContext
|
import android.hardware.usb.UsbDevice
|
||||||
|
import android.hardware.usb.UsbInterface
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
|
||||||
// This class is here instead of inside DI because it contains a bit more logic than just
|
// This class is here instead of inside DI because it contains a bit more logic than just
|
||||||
// "dumb" dependency injection.
|
// "dumb" dependency injection.
|
||||||
interface EuiccChannelFactory {
|
interface EuiccChannelFactory {
|
||||||
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
|
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel?
|
||||||
|
|
||||||
fun tryOpenUsbEuiccChannel(
|
fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel?
|
||||||
ccidCtx: UsbCcidContext,
|
|
||||||
isdrAid: ByteArray
|
|
||||||
): EuiccChannel?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release all resources used by this EuiccChannelFactory
|
* Release all resources used by this EuiccChannelFactory
|
||||||
|
|
|
@ -13,17 +13,21 @@ class EuiccChannelImpl(
|
||||||
override val port: UiccPortInfoCompat,
|
override val port: UiccPortInfoCompat,
|
||||||
override val intrinsicChannelName: String?,
|
override val intrinsicChannelName: String?,
|
||||||
override val apduInterface: ApduInterface,
|
override val apduInterface: ApduInterface,
|
||||||
override val isdrAid: ByteArray,
|
|
||||||
verboseLoggingFlow: Flow<Boolean>,
|
verboseLoggingFlow: Flow<Boolean>,
|
||||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||||
) : EuiccChannel {
|
) : EuiccChannel {
|
||||||
|
companion object {
|
||||||
|
// TODO: This needs to go somewhere else.
|
||||||
|
val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
override val slotId = port.card.physicalSlotIndex
|
override val slotId = port.card.physicalSlotIndex
|
||||||
override val logicalSlotId = port.logicalSlotIndex
|
override val logicalSlotId = port.logicalSlotIndex
|
||||||
override val portId = port.portIndex
|
override val portId = port.portIndex
|
||||||
|
|
||||||
override val lpa: LocalProfileAssistant =
|
override val lpa: LocalProfileAssistant =
|
||||||
LocalProfileAssistantImpl(
|
LocalProfileAssistantImpl(
|
||||||
isdrAid,
|
ISDR_AID,
|
||||||
apduInterface,
|
apduInterface,
|
||||||
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
|
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,8 +38,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
||||||
get() = channel.apduInterface
|
get() = channel.apduInterface
|
||||||
override val atr: ByteArray?
|
override val atr: ByteArray?
|
||||||
get() = channel.atr
|
get() = channel.atr
|
||||||
override val isdrAid: ByteArray
|
|
||||||
get() = channel.isdrAid
|
|
||||||
|
|
||||||
override fun close() = channel.close()
|
override fun close() = channel.close()
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
package im.angry.openeuicc.core.usb
|
package im.angry.openeuicc.core.usb
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbDeviceConnection
|
||||||
|
import android.hardware.usb.UsbEndpoint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import net.typeblog.lpac_jni.ApduInterface
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
|
|
||||||
class UsbApduInterface(
|
class UsbApduInterface(
|
||||||
private val ccidCtx: UsbCcidContext
|
private val conn: UsbDeviceConnection,
|
||||||
|
private val bulkIn: UsbEndpoint,
|
||||||
|
private val bulkOut: UsbEndpoint,
|
||||||
|
private val verboseLoggingFlow: Flow<Boolean>
|
||||||
) : ApduInterface, ApduInterfaceAtrProvider {
|
) : ApduInterface, ApduInterfaceAtrProvider {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "UsbApduInterface"
|
private const val TAG = "UsbApduInterface"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val atr: ByteArray?
|
private lateinit var ccidDescription: UsbCcidDescription
|
||||||
get() = ccidCtx.atr
|
private lateinit var transceiver: UsbCcidTransceiver
|
||||||
|
|
||||||
|
override var atr: ByteArray? = null
|
||||||
|
|
||||||
override val valid: Boolean
|
override val valid: Boolean
|
||||||
get() = channels.isNotEmpty()
|
get() = channels.isNotEmpty()
|
||||||
|
@ -21,19 +29,29 @@ class UsbApduInterface(
|
||||||
private var channels = mutableSetOf<Int>()
|
private var channels = mutableSetOf<Int>()
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
ccidCtx.connect()
|
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
||||||
|
|
||||||
// Send Terminal Capabilities
|
if (!ccidDescription.hasT0Protocol) {
|
||||||
// Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
|
throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
|
||||||
val terminalCapabilities = buildCmd(
|
}
|
||||||
0x80.toByte(), 0xaa.toByte(), 0x00, 0x00,
|
|
||||||
"A9088100820101830107".decodeHex(),
|
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
|
||||||
le = null,
|
|
||||||
)
|
try {
|
||||||
transmitApduByChannel(terminalCapabilities, 0)
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disconnect() = ccidCtx.disconnect()
|
override fun disconnect() {
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
atr = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||||
// OPEN LOGICAL CHANNEL
|
// OPEN LOGICAL CHANNEL
|
||||||
|
@ -122,7 +140,7 @@ class UsbApduInterface(
|
||||||
// OR the channel mask into the CLA byte
|
// OR the channel mask into the CLA byte
|
||||||
realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
|
realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
|
||||||
|
|
||||||
var resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
|
var resp = transceiver.sendXfrBlock(realTx).data!!
|
||||||
|
|
||||||
if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
|
if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
|
||||||
|
|
||||||
|
@ -133,7 +151,7 @@ class UsbApduInterface(
|
||||||
// 0x6C = wrong le
|
// 0x6C = wrong le
|
||||||
// so we fix the le field here
|
// so we fix the le field here
|
||||||
realTx[realTx.size - 1] = resp[resp.size - 1]
|
realTx[realTx.size - 1] = resp[resp.size - 1]
|
||||||
resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
|
resp = transceiver.sendXfrBlock(realTx).data!!
|
||||||
} else if (sw1 == 0x61) {
|
} else if (sw1 == 0x61) {
|
||||||
// 0x61 = X bytes available
|
// 0x61 = X bytes available
|
||||||
// continue reading by GET RESPONSE
|
// continue reading by GET RESPONSE
|
||||||
|
@ -143,7 +161,7 @@ class UsbApduInterface(
|
||||||
realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
|
realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
|
||||||
)
|
)
|
||||||
|
|
||||||
val tmp = ccidCtx.transceiver.sendXfrBlock(getResponseCmd).data!!
|
val tmp = transceiver.sendXfrBlock(getResponseCmd).data!!
|
||||||
|
|
||||||
resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
|
resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
|
||||||
|
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
package im.angry.openeuicc.core.usb
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.hardware.usb.UsbDevice
|
|
||||||
import android.hardware.usb.UsbDeviceConnection
|
|
||||||
import android.hardware.usb.UsbEndpoint
|
|
||||||
import android.hardware.usb.UsbInterface
|
|
||||||
import android.hardware.usb.UsbManager
|
|
||||||
import im.angry.openeuicc.util.preferenceRepository
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper over an usb device + interface, manages the lifecycle independent
|
|
||||||
* of the APDU interface exposed to lpac-jni.
|
|
||||||
*
|
|
||||||
* This allows us to try multiple AIDs on each interface without opening / closing
|
|
||||||
* the USB connection numerous times.
|
|
||||||
*/
|
|
||||||
class UsbCcidContext private constructor(
|
|
||||||
private val conn: UsbDeviceConnection,
|
|
||||||
private val bulkIn: UsbEndpoint,
|
|
||||||
private val bulkOut: UsbEndpoint,
|
|
||||||
val productName: String,
|
|
||||||
val verboseLoggingFlow: Flow<Boolean>
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun createFromUsbDevice(
|
|
||||||
context: Context,
|
|
||||||
usbDevice: UsbDevice,
|
|
||||||
usbInterface: UsbInterface
|
|
||||||
): UsbCcidContext? = runCatching {
|
|
||||||
val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
|
|
||||||
if (bulkIn == null || bulkOut == null) return@runCatching null
|
|
||||||
val conn = context.getSystemService(UsbManager::class.java).openDevice(usbDevice)
|
|
||||||
?: return@runCatching null
|
|
||||||
if (!conn.claimInterface(usbInterface, true)) return@runCatching null
|
|
||||||
UsbCcidContext(
|
|
||||||
conn,
|
|
||||||
bulkIn,
|
|
||||||
bulkOut,
|
|
||||||
usbDevice.productName ?: "USB",
|
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
|
||||||
)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When set to false (the default), the disconnect() method does nothing.
|
|
||||||
* This allows the separation of device disconnection from lpac-jni's APDU interface.
|
|
||||||
*/
|
|
||||||
var allowDisconnect = false
|
|
||||||
private var initialized = false
|
|
||||||
lateinit var transceiver: UsbCcidTransceiver
|
|
||||||
var atr: ByteArray? = null
|
|
||||||
|
|
||||||
fun connect() {
|
|
||||||
if (initialized) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
|
||||||
|
|
||||||
if (!ccidDescription.hasT0Protocol) {
|
|
||||||
throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disconnect() {
|
|
||||||
if (initialized && allowDisconnect) {
|
|
||||||
conn.close()
|
|
||||||
atr = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
@ -92,12 +91,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
|
val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
|
||||||
|
|
||||||
private val wakeLock: PowerManager.WakeLock by lazy {
|
|
||||||
(getSystemService(POWER_SERVICE) as PowerManager).run {
|
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this::class.simpleName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state of a "foreground" task (named so due to the need to startForeground())
|
* The state of a "foreground" task (named so due to the need to startForeground())
|
||||||
*/
|
*/
|
||||||
|
@ -282,8 +275,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
|
|
||||||
updateForegroundNotification(title, iconRes)
|
updateForegroundNotification(title, iconRes)
|
||||||
|
|
||||||
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete
|
withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete
|
||||||
this@EuiccChannelManagerService.task()
|
this@EuiccChannelManagerService.task()
|
||||||
|
@ -299,7 +290,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
postForegroundTaskFailureNotification(failureTitle)
|
postForegroundTaskFailureNotification(failureTitle)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
wakeLock.release()
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
@ -456,34 +446,30 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
iccid: String,
|
iccid: String,
|
||||||
enable: Boolean, // Enable or disable the profile indicated in iccid
|
enable: Boolean, // Enable or disable the profile indicated in iccid
|
||||||
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
|
||||||
) =
|
): ForegroundTaskSubscriberFlow =
|
||||||
launchForegroundTask(
|
launchForegroundTask(
|
||||||
getString(R.string.task_profile_switch),
|
getString(R.string.task_profile_switch),
|
||||||
getString(R.string.task_profile_switch_failure),
|
getString(R.string.task_profile_switch_failure),
|
||||||
R.drawable.ic_task_switch
|
R.drawable.ic_task_switch
|
||||||
) {
|
) {
|
||||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
||||||
val (response, refreshed) =
|
val (res, refreshed) = euiccChannelManager.withEuiccChannel(
|
||||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
slotId,
|
||||||
val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
|
portId
|
||||||
val response = channel.lpa.switchProfile(iccid, enable, refresh)
|
) { channel ->
|
||||||
if (response || !refresh) {
|
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
|
||||||
Pair(response, refresh)
|
// Sometimes, we *can* enable or disable the profile, but we cannot
|
||||||
} else {
|
// send the refresh command to the modem because the profile somehow
|
||||||
// refresh failed, but refresh was requested
|
// makes the modem "busy". In this case, we can still switch by setting
|
||||||
// Sometimes, we *can* enable or disable the profile, but we cannot
|
// refresh to false, but then the switch cannot take effect until the
|
||||||
// send the refresh command to the modem because the profile somehow
|
// user resets the modem manually by toggling airplane mode or rebooting.
|
||||||
// makes the modem "busy". In this case, we can still switch by setting
|
Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
|
||||||
// refresh to false, but then the switch cannot take effect until the
|
} else {
|
||||||
// user resets the modem manually by toggling airplane mode or rebooting.
|
Pair(true, true)
|
||||||
Pair(
|
|
||||||
channel.lpa.switchProfile(iccid, enable, refresh = false),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response) {
|
if (!res) {
|
||||||
throw RuntimeException("Could not switch profile")
|
throw RuntimeException("Could not switch profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,19 +495,4 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
|
||||||
preferenceRepository.notificationSwitchFlow.first()
|
preferenceRepository.notificationSwitchFlow.first()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
|
|
||||||
launchForegroundTask(
|
|
||||||
getString(R.string.task_euicc_memory_reset),
|
|
||||||
getString(R.string.task_euicc_memory_reset_failure),
|
|
||||||
R.drawable.ic_euicc_memory_reset
|
|
||||||
) {
|
|
||||||
euiccChannelManager.beginTrackedOperation(slotId, portId) {
|
|
||||||
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
|
|
||||||
channel.lpa.euiccMemoryReset()
|
|
||||||
}
|
|
||||||
|
|
||||||
preferenceRepository.notificationDeleteFlow.first()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -23,17 +23,12 @@ import im.angry.openeuicc.common.R
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
import im.angry.openeuicc.core.EuiccChannel
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
|
import im.angry.openeuicc.vendored.getESTKmeInfo
|
||||||
|
import im.angry.openeuicc.vendored.getSIMLinkVersion
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
|
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
|
||||||
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
|
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
|
||||||
|
|
||||||
// https://euicc-manual.osmocom.org/docs/pki/eum/accredited.json
|
|
||||||
// ref: <https://regex101.com/r/5FFz8u>
|
|
||||||
private val RE_SAS = Regex(
|
|
||||||
"""^[A-Z]{2}-[A-Z]{2}(?:-UP)?-\d{4}T?(?:-\d+)?T?$""",
|
|
||||||
setOf(RegexOption.IGNORE_CASE),
|
|
||||||
)
|
|
||||||
|
|
||||||
class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
companion object {
|
companion object {
|
||||||
private val YES_NO = Pair(R.string.yes, R.string.no)
|
private val YES_NO = Pair(R.string.yes, R.string.no)
|
||||||
|
@ -109,21 +104,22 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
add(Item(R.string.euicc_info_access_mode, channel.type))
|
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_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO)))
|
||||||
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
|
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
|
||||||
add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex()))
|
getESTKmeInfo(channel.apduInterface)?.let {
|
||||||
channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
|
add(Item(R.string.euicc_info_sku, it.skuName))
|
||||||
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
|
add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied))
|
||||||
vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) }
|
add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion))
|
||||||
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
|
add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion))
|
||||||
vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
|
|
||||||
}
|
}
|
||||||
channel.lpa.euiccInfo2?.let { info ->
|
getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let {
|
||||||
add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
|
add(Item(R.string.euicc_info_sku, "9eSIM $it"))
|
||||||
add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString()))
|
}
|
||||||
add(Item(R.string.euicc_info_globalplatform_version, info.globalPlatformVersion.toString()))
|
channel.lpa.euiccInfo2.let { info ->
|
||||||
add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString()))
|
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString()))
|
||||||
info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches)
|
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString()))
|
||||||
?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) }
|
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString()))
|
||||||
add(Item(R.string.euicc_info_free_nvram, info.freeNvram.let(::formatFreeSpace)))
|
add(Item(R.string.euicc_info_pp_version, info?.ppVersion.toString()))
|
||||||
|
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 ->
|
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
|
||||||
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
|
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
|
||||||
|
@ -138,13 +134,18 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
}
|
}
|
||||||
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
||||||
}
|
}
|
||||||
val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
|
val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
|
||||||
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
|
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
|
||||||
private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
|
private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
|
||||||
getString(if (b) res.first else res.second)
|
getString(
|
||||||
|
if (b) {
|
||||||
|
res.first
|
||||||
|
} else {
|
||||||
|
res.second
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
|
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
|
||||||
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
|
private val title: TextView = root.requireViewById(R.id.euicc_info_title)
|
||||||
|
|
|
@ -38,10 +38,8 @@ import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
|
@ -57,7 +55,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
private lateinit var fab: FloatingActionButton
|
private lateinit var fab: FloatingActionButton
|
||||||
private lateinit var profileList: RecyclerView
|
private lateinit var profileList: RecyclerView
|
||||||
private var logicalSlotId: Int = -1
|
private var logicalSlotId: Int = -1
|
||||||
private lateinit var eid: String
|
|
||||||
|
|
||||||
private val adapter = EuiccProfileAdapter()
|
private val adapter = EuiccProfileAdapter()
|
||||||
|
|
||||||
|
@ -134,42 +131,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
inflater.inflate(R.menu.fragment_euicc, menu)
|
inflater.inflate(R.menu.fragment_euicc, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||||
super.onPrepareOptionsMenu(menu)
|
when (item.itemId) {
|
||||||
menu.findItem(R.id.show_notifications).isVisible =
|
R.id.show_notifications -> {
|
||||||
logicalSlotId != -1
|
if (logicalSlotId != -1) {
|
||||||
menu.findItem(R.id.euicc_info).isVisible =
|
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||||
logicalSlotId != -1
|
putExtra("logicalSlotId", logicalSlotId)
|
||||||
menu.findItem(R.id.euicc_memory_reset).isVisible =
|
startActivity(this)
|
||||||
runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
|
}
|
||||||
}
|
}
|
||||||
|
true
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.show_notifications -> {
|
|
||||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
|
||||||
putExtra("logicalSlotId", logicalSlotId)
|
|
||||||
startActivity(this)
|
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.euicc_info -> {
|
R.id.euicc_info -> {
|
||||||
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
if (logicalSlotId != -1) {
|
||||||
putExtra("logicalSlotId", logicalSlotId)
|
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
|
||||||
startActivity(this)
|
putExtra("logicalSlotId", logicalSlotId)
|
||||||
|
startActivity(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.euicc_memory_reset -> {
|
else -> super.onOptionsItemSelected(item)
|
||||||
EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
|
|
||||||
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open suspend fun onCreateFooterViews(
|
protected open suspend fun onCreateFooterViews(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
profiles: List<LocalProfileInfo>
|
profiles: List<LocalProfileInfo>
|
||||||
|
@ -206,7 +192,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||||
|
|
||||||
val profiles = withEuiccChannel { channel ->
|
val profiles = withEuiccChannel { channel ->
|
||||||
logicalSlotId = channel.logicalSlotId
|
logicalSlotId = channel.logicalSlotId
|
||||||
eid = channel.lpa.eID
|
|
||||||
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
|
||||||
if (unfilteredProfileListFlow.value)
|
if (unfilteredProfileListFlow.value)
|
||||||
channel.lpa.profiles
|
channel.lpa.profiles
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
package im.angry.openeuicc.ui
|
|
||||||
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import im.angry.openeuicc.common.R
|
|
||||||
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
|
|
||||||
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
|
|
||||||
import im.angry.openeuicc.util.EuiccProfilesChangedListener
|
|
||||||
import im.angry.openeuicc.util.ensureEuiccChannelManager
|
|
||||||
import im.angry.openeuicc.util.euiccChannelManagerService
|
|
||||||
import im.angry.openeuicc.util.newInstanceEuicc
|
|
||||||
import im.angry.openeuicc.util.notifyEuiccProfilesChanged
|
|
||||||
import im.angry.openeuicc.util.portId
|
|
||||||
import im.angry.openeuicc.util.slotId
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "EuiccMemoryResetFragment"
|
|
||||||
|
|
||||||
private const val FIELD_EID = "eid"
|
|
||||||
|
|
||||||
fun newInstance(slotId: Int, portId: Int, eid: String) =
|
|
||||||
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
|
|
||||||
putString(FIELD_EID, eid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! }
|
|
||||||
|
|
||||||
private val confirmText: String by lazy {
|
|
||||||
getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8))
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline val isMatched: Boolean
|
|
||||||
get() = editText.text.toString() == confirmText
|
|
||||||
|
|
||||||
private var confirmed = false
|
|
||||||
|
|
||||||
private var toast: Toast? = null
|
|
||||||
set(value) {
|
|
||||||
toast?.cancel()
|
|
||||||
field = value
|
|
||||||
value?.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val editText by lazy {
|
|
||||||
EditText(requireContext()).apply {
|
|
||||||
isLongClickable = false
|
|
||||||
typeface = Typeface.MONOSPACE
|
|
||||||
hint = Editable.Factory.getInstance()
|
|
||||||
.newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline val alertDialog: AlertDialog
|
|
||||||
get() = requireDialog() as AlertDialog
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?) =
|
|
||||||
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
|
|
||||||
.setTitle(R.string.euicc_memory_reset_title)
|
|
||||||
.setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText))
|
|
||||||
.setView(editText)
|
|
||||||
// Set listener to null to prevent auto closing
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(R.string.euicc_memory_reset_invoke_button, null)
|
|
||||||
.create()
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
alertDialog.setCanceledOnTouchOutside(false)
|
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
|
||||||
.setOnClickListener { if (!confirmed) confirmation() }
|
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
|
||||||
.setOnClickListener { if (!confirmed) dismiss() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun confirmation() {
|
|
||||||
toast?.cancel()
|
|
||||||
if (!isMatched) {
|
|
||||||
Log.d(TAG, buildString {
|
|
||||||
appendLine("User input is mismatch:")
|
|
||||||
appendLine(editText.text)
|
|
||||||
appendLine(confirmText)
|
|
||||||
})
|
|
||||||
val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched
|
|
||||||
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
confirmed = true
|
|
||||||
preventUserAction()
|
|
||||||
|
|
||||||
requireParentFragment().lifecycleScope.launch {
|
|
||||||
ensureEuiccChannelManager()
|
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
|
||||||
|
|
||||||
euiccChannelManagerService.launchMemoryReset(slotId, portId)
|
|
||||||
.onStart {
|
|
||||||
parentFragment?.notifyEuiccProfilesChanged()
|
|
||||||
|
|
||||||
val resId = R.string.toast_euicc_memory_reset_finitshed
|
|
||||||
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
|
|
||||||
|
|
||||||
runCatching(::dismiss)
|
|
||||||
}
|
|
||||||
.waitDone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun preventUserAction() {
|
|
||||||
editText.isEnabled = false
|
|
||||||
alertDialog.setCancelable(false)
|
|
||||||
alertDialog.setCanceledOnTouchOutside(false)
|
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
|
||||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
package im.angry.openeuicc.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import im.angry.openeuicc.common.R
|
|
||||||
import im.angry.openeuicc.util.preferenceRepository
|
|
||||||
import im.angry.openeuicc.util.setupToolbarInsets
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class IsdrAidListActivity : AppCompatActivity() {
|
|
||||||
private lateinit var isdrAidListEditor: EditText
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
enableEdgeToEdge()
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_isdr_aid_list)
|
|
||||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
|
||||||
setupToolbarInsets()
|
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
isdrAidListEditor = requireViewById(R.id.isdr_aid_list_editor)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
preferenceRepository.isdrAidListFlow.onEach {
|
|
||||||
isdrAidListEditor.text = Editable.Factory.getInstance().newEditable(it)
|
|
||||||
}.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.activity_isdr_aid_list, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.save -> {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
preferenceRepository.isdrAidListFlow.updatePreference(isdrAidListEditor.text.toString())
|
|
||||||
Toast.makeText(
|
|
||||||
this@IsdrAidListActivity,
|
|
||||||
R.string.isdr_aid_list_saved,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.reset -> {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
preferenceRepository.isdrAidListFlow.removePreference()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
android.R.id.home -> {
|
|
||||||
finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,10 +20,13 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||||
private const val FIELD_ICCID = "iccid"
|
private const val FIELD_ICCID = "iccid"
|
||||||
private const val FIELD_NAME = "name"
|
private const val FIELD_NAME = "name"
|
||||||
|
|
||||||
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
|
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
|
||||||
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
|
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
|
||||||
|
instance.requireArguments().apply {
|
||||||
putString(FIELD_ICCID, iccid)
|
putString(FIELD_ICCID, iccid)
|
||||||
putString(FIELD_NAME, name)
|
putString(FIELD_NAME, name)
|
||||||
|
}
|
||||||
|
return instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,12 +91,19 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||||
requireParentFragment().lifecycleScope.launch {
|
requireParentFragment().lifecycleScope.launch {
|
||||||
ensureEuiccChannelManager()
|
ensureEuiccChannelManager()
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
|
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
|
||||||
.onStart {
|
if (parentFragment is EuiccProfilesChangedListener) {
|
||||||
parentFragment?.notifyEuiccProfilesChanged()
|
// Trigger a refresh in the parent fragment -- it should wait until
|
||||||
runCatching(::dismiss)
|
// any foreground task is completed before actually doing a refresh
|
||||||
|
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
||||||
}
|
}
|
||||||
.waitDone()
|
|
||||||
|
try {
|
||||||
|
dismiss()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
}.waitDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,7 +7,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
@ -19,16 +18,16 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
||||||
companion object {
|
companion object {
|
||||||
private const val FIELD_ICCID = "iccid"
|
|
||||||
private const val FIELD_CURRENT_NAME = "currentName"
|
|
||||||
|
|
||||||
const val TAG = "ProfileRenameFragment"
|
const val TAG = "ProfileRenameFragment"
|
||||||
|
|
||||||
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) =
|
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment {
|
||||||
newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) {
|
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId)
|
||||||
putString(FIELD_ICCID, iccid)
|
instance.requireArguments().apply {
|
||||||
putString(FIELD_CURRENT_NAME, currentName)
|
putString("iccid", iccid)
|
||||||
|
putString("currentName", currentName)
|
||||||
}
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var toolbar: Toolbar
|
private lateinit var toolbar: Toolbar
|
||||||
|
@ -37,14 +36,6 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
|
|
||||||
private var renaming = false
|
private var renaming = false
|
||||||
|
|
||||||
private val iccid: String by lazy {
|
|
||||||
requireArguments().getString(FIELD_ICCID)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private val currentName: String by lazy {
|
|
||||||
requireArguments().getString(FIELD_CURRENT_NAME)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -63,7 +54,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
profileRenameNewName.editText!!.setText(currentName)
|
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
|
||||||
toolbar.apply {
|
toolbar.apply {
|
||||||
setTitle(R.string.rename)
|
setTitle(R.string.rename)
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener {
|
||||||
|
@ -87,8 +78,12 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showErrorAndCancel(@StringRes resId: Int) {
|
private fun showErrorAndCancel(errorStrRes: Int) {
|
||||||
Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
errorStrRes,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
renaming = false
|
renaming = false
|
||||||
progress.visibility = View.GONE
|
progress.visibility = View.GONE
|
||||||
|
@ -99,15 +94,17 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
progress.isIndeterminate = true
|
progress.isIndeterminate = true
|
||||||
progress.visibility = View.VISIBLE
|
progress.visibility = View.VISIBLE
|
||||||
|
|
||||||
val newName = profileRenameNewName.editText!!.text.toString().trim()
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
ensureEuiccChannelManager()
|
ensureEuiccChannelManager()
|
||||||
euiccChannelManagerService.waitForForegroundTask()
|
euiccChannelManagerService.waitForForegroundTask()
|
||||||
val response = euiccChannelManagerService
|
val res = euiccChannelManagerService.launchProfileRenameTask(
|
||||||
.launchProfileRenameTask(slotId, portId, iccid, newName).waitDone()
|
slotId,
|
||||||
|
portId,
|
||||||
|
requireArguments().getString("iccid")!!,
|
||||||
|
profileRenameNewName.editText!!.text.toString().trim()
|
||||||
|
).waitDone()
|
||||||
|
|
||||||
when (response) {
|
when (res) {
|
||||||
is LocalProfileAssistant.ProfileNameTooLongException -> {
|
is LocalProfileAssistant.ProfileNameTooLongException -> {
|
||||||
showErrorAndCancel(R.string.profile_rename_too_long)
|
showErrorAndCancel(R.string.profile_rename_too_long)
|
||||||
}
|
}
|
||||||
|
@ -121,9 +118,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
parentFragment?.notifyEuiccProfilesChanged()
|
if (parentFragment is EuiccProfilesChangedListener) {
|
||||||
|
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
||||||
|
}
|
||||||
|
|
||||||
runCatching(::dismiss)
|
try {
|
||||||
|
dismiss()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,16 +77,6 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
|
|
||||||
requirePreference<CheckBoxPreference>("pref_developer_ignore_tls_certificate")
|
requirePreference<CheckBoxPreference>("pref_developer_ignore_tls_certificate")
|
||||||
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
|
||||||
|
|
||||||
requirePreference<CheckBoxPreference>("pref_developer_refresh_after_switch")
|
|
||||||
.bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow)
|
|
||||||
|
|
||||||
requirePreference<CheckBoxPreference>("pref_developer_euicc_memory_reset")
|
|
||||||
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
|
|
||||||
|
|
||||||
requirePreference<Preference>("pref_developer_isdr_aid_list").apply {
|
|
||||||
intent = Intent(requireContext(), IsdrAidListActivity::class.java)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
protected fun <T : Preference> requirePreference(key: CharSequence) =
|
||||||
|
@ -132,7 +122,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
flow.collect { isChecked = it }
|
flow.collect { isChecked = it }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
package im.angry.openeuicc.ui.wizard
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
import android.app.assist.AssistContent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
@ -22,6 +19,7 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
|
@ -35,8 +33,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
var downloadStarted: Boolean,
|
var downloadStarted: Boolean,
|
||||||
var downloadTaskID: Long,
|
var downloadTaskID: Long,
|
||||||
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
|
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
|
||||||
var skipMethodSelect: Boolean,
|
|
||||||
var confirmationCodeRequired: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private lateinit var state: DownloadWizardState
|
private lateinit var state: DownloadWizardState
|
||||||
|
@ -65,21 +61,17 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
})
|
})
|
||||||
|
|
||||||
state = DownloadWizardState(
|
state = DownloadWizardState(
|
||||||
currentStepFragmentClassName = null,
|
null,
|
||||||
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
|
intent.getIntExtra("selectedLogicalSlot", 0),
|
||||||
smdp = "",
|
"",
|
||||||
matchingId = null,
|
null,
|
||||||
confirmationCode = null,
|
null,
|
||||||
imei = null,
|
null,
|
||||||
downloadStarted = false,
|
false,
|
||||||
downloadTaskID = -1,
|
-1,
|
||||||
downloadError = null,
|
null
|
||||||
skipMethodSelect = false,
|
|
||||||
confirmationCodeRequired = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
handleDeepLink()
|
|
||||||
|
|
||||||
progressBar = requireViewById(R.id.progress)
|
progressBar = requireViewById(R.id.progress)
|
||||||
nextButton = requireViewById(R.id.download_wizard_next)
|
nextButton = requireViewById(R.id.download_wizard_next)
|
||||||
prevButton = requireViewById(R.id.download_wizard_back)
|
prevButton = requireViewById(R.id.download_wizard_back)
|
||||||
|
@ -119,35 +111,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeepLink() {
|
|
||||||
// If we get an LPA string from deep-link intents, extract from there.
|
|
||||||
// Note that `onRestoreInstanceState` could override this with user input,
|
|
||||||
// but that _is_ the desired behavior.
|
|
||||||
val uri = intent.data
|
|
||||||
if (uri?.scheme == "lpa") {
|
|
||||||
val parsed = LPAString.parse(uri.schemeSpecificPart)
|
|
||||||
state.smdp = parsed.address
|
|
||||||
state.matchingId = parsed.matchingId
|
|
||||||
state.confirmationCodeRequired = parsed.confirmationCodeRequired
|
|
||||||
state.skipMethodSelect = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
|
||||||
super.onProvideAssistContent(outContent)
|
|
||||||
outContent?.webUri = try {
|
|
||||||
val activationCode = LPAString(
|
|
||||||
state.smdp,
|
|
||||||
state.matchingId,
|
|
||||||
null,
|
|
||||||
state.confirmationCode != null,
|
|
||||||
)
|
|
||||||
"LPA:$activationCode".toUri()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
|
||||||
|
@ -158,7 +121,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
outState.putString("imei", state.imei)
|
outState.putString("imei", state.imei)
|
||||||
outState.putBoolean("downloadStarted", state.downloadStarted)
|
outState.putBoolean("downloadStarted", state.downloadStarted)
|
||||||
outState.putLong("downloadTaskID", state.downloadTaskID)
|
outState.putLong("downloadTaskID", state.downloadTaskID)
|
||||||
outState.putBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
@ -175,8 +137,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
state.downloadStarted =
|
state.downloadStarted =
|
||||||
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
|
||||||
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
|
||||||
state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
|
|
||||||
state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPrevPressed() {
|
private fun onPrevPressed() {
|
||||||
|
@ -252,14 +212,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim)
|
supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim)
|
||||||
.replace(R.id.step_fragment_container, nextFrag)
|
.replace(R.id.step_fragment_container, nextFrag)
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
// Sync screen on state
|
|
||||||
if (nextFrag.keepScreenOn) {
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
} else {
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshButtons()
|
refreshButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,8 +241,6 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
|
||||||
protected val state: DownloadWizardState
|
protected val state: DownloadWizardState
|
||||||
get() = (requireActivity() as DownloadWizardActivity).state
|
get() = (requireActivity() as DownloadWizardActivity).state
|
||||||
|
|
||||||
open val keepScreenOn = false
|
|
||||||
|
|
||||||
abstract val hasNext: Boolean
|
abstract val hasNext: Boolean
|
||||||
abstract val hasPrev: Boolean
|
abstract val hasPrev: Boolean
|
||||||
abstract fun createNextFragment(): DownloadWizardStepFragment?
|
abstract fun createNextFragment(): DownloadWizardStepFragment?
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.ui.wizard
|
package im.angry.openeuicc.ui.wizard
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Patterns
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -35,11 +36,7 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
DownloadWizardProgressFragment()
|
DownloadWizardProgressFragment()
|
||||||
|
|
||||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||||
if (state.skipMethodSelect) {
|
DownloadWizardMethodSelectFragment()
|
||||||
DownloadWizardSlotSelectFragment()
|
|
||||||
} else {
|
|
||||||
DownloadWizardMethodSelectFragment()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -54,9 +51,6 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
smdp.editText!!.addTextChangedListener {
|
smdp.editText!!.addTextChangedListener {
|
||||||
updateInputCompleteness()
|
updateInputCompleteness()
|
||||||
}
|
}
|
||||||
confirmationCode.editText!!.addTextChangedListener {
|
|
||||||
updateInputCompleteness()
|
|
||||||
}
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,15 +61,6 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
confirmationCode.editText!!.setText(state.confirmationCode)
|
confirmationCode.editText!!.setText(state.confirmationCode)
|
||||||
imei.editText!!.setText(state.imei)
|
imei.editText!!.setText(state.imei)
|
||||||
updateInputCompleteness()
|
updateInputCompleteness()
|
||||||
|
|
||||||
if (state.confirmationCodeRequired) {
|
|
||||||
confirmationCode.editText!!.requestFocus()
|
|
||||||
confirmationCode.editText!!.hint =
|
|
||||||
getString(R.string.profile_download_confirmation_code_required)
|
|
||||||
} else {
|
|
||||||
confirmationCode.editText!!.hint =
|
|
||||||
getString(R.string.profile_download_confirmation_code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -84,34 +69,7 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateInputCompleteness() {
|
private fun updateInputCompleteness() {
|
||||||
inputComplete = isValidAddress(smdp.editText!!.text)
|
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
|
||||||
if (state.confirmationCodeRequired) {
|
|
||||||
inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
|
|
||||||
}
|
|
||||||
refreshButtons()
|
refreshButtons()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidAddress(input: CharSequence): Boolean {
|
|
||||||
if (!input.contains('.')) return false
|
|
||||||
var fqdn = input
|
|
||||||
var port = 443
|
|
||||||
if (input.contains(':')) {
|
|
||||||
val portIndex = input.lastIndexOf(':')
|
|
||||||
fqdn = input.substring(0, portIndex)
|
|
||||||
port = input.substring(portIndex + 1, input.length).toIntOrNull(10) ?: 0
|
|
||||||
}
|
|
||||||
// see https://en.wikipedia.org/wiki/Port_(computer_networking)
|
|
||||||
if (port < 1 || port > 0xffff) return false
|
|
||||||
// see https://en.wikipedia.org/wiki/Fully_qualified_domain_name
|
|
||||||
if (fqdn.isEmpty() || fqdn.length > 255) return false
|
|
||||||
for (part in fqdn.split('.')) {
|
|
||||||
if (part.isEmpty() || part.length > 64) return false
|
|
||||||
if (part.first() == '-' || part.last() == '-') return false
|
|
||||||
for (c in part) {
|
|
||||||
if (c.isLetterOrDigit() || c == '-') continue
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
|
@ -42,16 +42,21 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
|
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
|
||||||
if (result == null) return@registerForActivityResult
|
if (result == null) return@registerForActivityResult
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val decoded = withContext(Dispatchers.IO) {
|
runCatching {
|
||||||
runCatching {
|
requireContext().contentResolver.openInputStream(result)?.let { input ->
|
||||||
requireContext().contentResolver.openInputStream(result)?.use { input ->
|
val bmp = BitmapFactory.decodeStream(input)
|
||||||
BitmapFactory.decodeStream(input).use(::decodeQrFromBitmap)
|
input.close()
|
||||||
|
|
||||||
|
decodeQrFromBitmap(bmp)?.let {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
processLpaString(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bmp.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded.getOrNull()?.let { processLpaString(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,10 +126,18 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
|
|
||||||
private fun processLpaString(input: String) {
|
private fun processLpaString(input: String) {
|
||||||
try {
|
try {
|
||||||
val parsed = LPAString.parse(input)
|
val parsed = ActivationCode.fromString(input)
|
||||||
state.smdp = parsed.address
|
state.smdp = parsed.address
|
||||||
state.matchingId = parsed.matchingId
|
state.matchingId = parsed.matchingId
|
||||||
state.confirmationCodeRequired = parsed.confirmationCodeRequired
|
if (parsed.confirmationCodeRequired) {
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
setTitle(R.string.profile_download_required_confirmation_code)
|
||||||
|
setMessage(R.string.profile_download_required_confirmation_code_message)
|
||||||
|
setCancelable(true)
|
||||||
|
setPositiveButton(android.R.string.ok, null)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
gotoNextFragment(DownloadWizardDetailsFragment())
|
gotoNextFragment(DownloadWizardDetailsFragment())
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
@ -137,19 +150,14 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
|
private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
|
||||||
private val icon = root.requireViewById<ImageView>(R.id.download_method_icon)
|
private val icon = root.requireViewById<ImageView>(R.id.download_method_icon)
|
||||||
private val title = root.requireViewById<TextView>(R.id.download_method_title)
|
private val title = root.requireViewById<TextView>(R.id.download_method_title)
|
||||||
|
|
||||||
fun bind(item: DownloadMethod) {
|
fun bind(item: DownloadMethod) {
|
||||||
icon.setImageResource(item.iconRes)
|
icon.setImageResource(item.iconRes)
|
||||||
title.setText(item.titleRes)
|
title.setText(item.titleRes)
|
||||||
root.setOnClickListener {
|
root.setOnClickListener { item.onClick() }
|
||||||
// If the user elected to use another download method, reset the confirmation code flag
|
|
||||||
// too
|
|
||||||
state.confirmationCodeRequired = false
|
|
||||||
item.onClick()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,9 +59,6 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
|
||||||
|
|
||||||
private val adapter = ProgressItemAdapter()
|
private val adapter = ProgressItemAdapter()
|
||||||
|
|
||||||
// We don't want to turn off the screen during a download
|
|
||||||
override val keepScreenOn = true
|
|
||||||
|
|
||||||
private var isDone = false
|
private var isDone = false
|
||||||
|
|
||||||
override val hasNext: Boolean
|
override val hasNext: Boolean
|
||||||
|
|
|
@ -49,11 +49,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
|
||||||
get() = true
|
get() = true
|
||||||
|
|
||||||
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
|
||||||
if (state.skipMethodSelect) {
|
DownloadWizardMethodSelectFragment()
|
||||||
DownloadWizardDetailsFragment()
|
|
||||||
} else {
|
|
||||||
DownloadWizardMethodSelectFragment()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
data class ActivationCode(
|
||||||
|
val address: String,
|
||||||
|
val matchingId: String? = null,
|
||||||
|
val oid: String? = null,
|
||||||
|
val confirmationCodeRequired: Boolean = false,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromString(input: String): ActivationCode {
|
||||||
|
val components = input.removePrefix("LPA:").split('$')
|
||||||
|
if (components.size < 2 || components[0] != "1") {
|
||||||
|
throw IllegalArgumentException("Invalid activation code format")
|
||||||
|
}
|
||||||
|
return ActivationCode(
|
||||||
|
address = components[1].trim(),
|
||||||
|
matchingId = components.getOrNull(2)?.trim()?.ifBlank { null },
|
||||||
|
oid = components.getOrNull(3)?.trim()?.ifBlank { null },
|
||||||
|
confirmationCodeRequired = components.getOrNull(4)?.trim() == "1"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,65 +7,43 @@ import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||||
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
||||||
|
|
||||||
private const val FIELD_SLOT_ID = "slotId"
|
interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
|
||||||
private const val FIELD_PORT_ID = "portId"
|
|
||||||
|
|
||||||
interface EuiccChannelFragmentMarker : OpenEuiccContextMarker
|
|
||||||
|
|
||||||
private typealias BundleSetter = Bundle.() -> Unit
|
|
||||||
|
|
||||||
// We must use extension functions because there is no way to add bounds to the type of "self"
|
// We must use extension functions because there is no way to add bounds to the type of "self"
|
||||||
// in the definition of an interface, so the only way is to limit where the extension functions
|
// in the definition of an interface, so the only way is to limit where the extension functions
|
||||||
// can be applied.
|
// can be applied.
|
||||||
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
|
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccChannelFragmentMarker {
|
||||||
where T : Fragment, T : EuiccChannelFragmentMarker =
|
val instance = clazz.newInstance()
|
||||||
clazz.getDeclaredConstructor().newInstance().apply {
|
instance.arguments = Bundle().apply {
|
||||||
arguments = Bundle()
|
putInt("slotId", slotId)
|
||||||
arguments!!.putInt(FIELD_SLOT_ID, slotId)
|
putInt("portId", portId)
|
||||||
arguments!!.putInt(FIELD_PORT_ID, portId)
|
addArguments()
|
||||||
arguments!!.addArguments()
|
|
||||||
}
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
// Convenient methods to avoid using `channel` for these
|
// Convenient methods to avoid using `channel` for these
|
||||||
// `channel` requires that the channel actually exists in EuiccChannelManager, which is
|
// `channel` requires that the channel actually exists in EuiccChannelManager, which is
|
||||||
// not always the case during operations such as switching
|
// not always the case during operations such as switching
|
||||||
val <T> T.slotId: Int
|
val <T> T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker
|
||||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
get() = requireArguments().getInt("slotId")
|
||||||
get() = requireArguments().getInt(FIELD_SLOT_ID)
|
val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
|
||||||
val <T> T.portId: Int
|
get() = requireArguments().getInt("portId")
|
||||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
|
||||||
get() = requireArguments().getInt(FIELD_PORT_ID)
|
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
|
||||||
val <T> T.isUsb: Boolean
|
|
||||||
where T : Fragment, T : EuiccChannelFragmentMarker
|
|
||||||
get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
|
|
||||||
|
|
||||||
private fun <T> T.requireEuiccActivity(): BaseEuiccAccessActivity
|
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
|
||||||
where T : Fragment, T : OpenEuiccContextMarker =
|
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
|
||||||
requireActivity() as BaseEuiccAccessActivity
|
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
|
||||||
|
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
|
||||||
|
|
||||||
val <T> T.euiccChannelManager: EuiccChannelManager
|
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
|
||||||
where T : Fragment, T : OpenEuiccContextMarker
|
|
||||||
get() = requireEuiccActivity().euiccChannelManager
|
|
||||||
|
|
||||||
val <T> T.euiccChannelManagerService: EuiccChannelManagerService
|
|
||||||
where T : Fragment, T : OpenEuiccContextMarker
|
|
||||||
get() = requireEuiccActivity().euiccChannelManagerService
|
|
||||||
|
|
||||||
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R
|
|
||||||
where T : Fragment, T : EuiccChannelFragmentMarker {
|
|
||||||
ensureEuiccChannelManager()
|
ensureEuiccChannelManager()
|
||||||
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
|
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
|
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
|
||||||
requireEuiccActivity().euiccChannelManagerLoaded.await()
|
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
|
||||||
|
|
||||||
fun <T> T.notifyEuiccProfilesChanged() where T : Fragment {
|
|
||||||
if (this !is EuiccProfilesChangedListener) return
|
|
||||||
// Trigger a refresh in the parent fragment -- it should wait until
|
|
||||||
// any foreground task is completed before actually doing a refresh
|
|
||||||
this.onEuiccProfilesChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EuiccProfilesChangedListener {
|
interface EuiccProfilesChangedListener {
|
||||||
fun onEuiccProfilesChanged()
|
fun onEuiccProfilesChanged()
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
package im.angry.openeuicc.util
|
|
||||||
|
|
||||||
data class LPAString(
|
|
||||||
val address: String,
|
|
||||||
val matchingId: String?,
|
|
||||||
val oid: String?,
|
|
||||||
val confirmationCodeRequired: Boolean,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun parse(input: String): LPAString {
|
|
||||||
var token = input
|
|
||||||
if (token.startsWith("LPA:", ignoreCase = true)) token = token.drop(4)
|
|
||||||
val components = token.split('$').map { it.trim().ifBlank { null } }
|
|
||||||
require(components.getOrNull(0) == "1") { "Invalid AC_Format" }
|
|
||||||
return LPAString(
|
|
||||||
requireNotNull(components.getOrNull(1)) { "SM-DP+ is required" },
|
|
||||||
components.getOrNull(2),
|
|
||||||
components.getOrNull(3),
|
|
||||||
components.getOrNull(4) == "1"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
val parts = arrayOf(
|
|
||||||
"1",
|
|
||||||
address,
|
|
||||||
matchingId ?: "",
|
|
||||||
oid ?: "",
|
|
||||||
if (confirmationCodeRequired) "1" else ""
|
|
||||||
)
|
|
||||||
return parts.joinToString("$").trimEnd('$')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,13 +5,11 @@ import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs")
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs")
|
||||||
|
|
||||||
|
@ -33,38 +31,11 @@ internal object PreferenceKeys {
|
||||||
|
|
||||||
// ---- Developer Options ----
|
// ---- Developer Options ----
|
||||||
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
val DEVELOPER_OPTIONS_ENABLED = booleanPreferencesKey("developer_options_enabled")
|
||||||
val REFRESH_AFTER_SWITCH = booleanPreferencesKey("refresh_after_switch")
|
|
||||||
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
val UNFILTERED_PROFILE_LIST = booleanPreferencesKey("unfiltered_profile_list")
|
||||||
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
val IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
|
||||||
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
|
|
||||||
val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100"
|
class PreferenceRepository(private val context: Context) {
|
||||||
|
|
||||||
internal object PreferenceConstants {
|
|
||||||
val DEFAULT_AID_LIST = """
|
|
||||||
# One AID per line. Comment lines start with #.
|
|
||||||
# Refs: <https://euicc-manual.osmocom.org/docs/lpa/applet-id-oem/>
|
|
||||||
|
|
||||||
# eUICC standard
|
|
||||||
$EUICC_DEFAULT_ISDR_AID
|
|
||||||
|
|
||||||
# eSTK.me
|
|
||||||
A06573746B6D65FFFFFFFF4953442D52
|
|
||||||
|
|
||||||
# eSIM.me
|
|
||||||
A0000005591010000000008900000300
|
|
||||||
|
|
||||||
# 5ber.eSIM
|
|
||||||
A0000005591010FFFFFFFF8900050500
|
|
||||||
|
|
||||||
# Xesim
|
|
||||||
A0000005591010FFFFFFFF8900000177
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
open class PreferenceRepository(private val context: Context) {
|
|
||||||
// Expose flows so that we can also handle default values
|
// Expose flows so that we can also handle default values
|
||||||
// ---- Profile Notifications ----
|
// ---- Profile Notifications ----
|
||||||
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
|
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
|
||||||
|
@ -76,50 +47,26 @@ open class PreferenceRepository(private val context: Context) {
|
||||||
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
|
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
|
||||||
|
|
||||||
// ---- Developer Options ----
|
// ---- Developer Options ----
|
||||||
val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)
|
|
||||||
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
|
||||||
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
|
||||||
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
|
||||||
val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
|
|
||||||
val isdrAidListFlow = bindFlow(
|
|
||||||
PreferenceKeys.ISDR_AID_LIST,
|
|
||||||
PreferenceConstants.DEFAULT_AID_LIST,
|
|
||||||
{ Base64.getEncoder().encodeToString(it.encodeToByteArray()) },
|
|
||||||
{ Base64.getDecoder().decode(it).decodeToString() })
|
|
||||||
|
|
||||||
protected fun <T> bindFlow(
|
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
|
||||||
key: Preferences.Key<T>,
|
PreferenceFlowWrapper(context, key, defaultValue)
|
||||||
defaultValue: T,
|
|
||||||
encoder: (T) -> T = { it },
|
|
||||||
decoder: (T) -> T = { it }
|
|
||||||
): PreferenceFlowWrapper<T> =
|
|
||||||
PreferenceFlowWrapper(context, key, defaultValue, encoder, decoder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreferenceFlowWrapper<T> private constructor(
|
class PreferenceFlowWrapper<T> private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val key: Preferences.Key<T>,
|
private val key: Preferences.Key<T>,
|
||||||
inner: Flow<T>,
|
inner: Flow<T>
|
||||||
private val encoder: (T) -> T,
|
|
||||||
) : Flow<T> by inner {
|
) : Flow<T> by inner {
|
||||||
internal constructor(
|
internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) : this(
|
||||||
context: Context,
|
|
||||||
key: Preferences.Key<T>,
|
|
||||||
defaultValue: T,
|
|
||||||
encoder: (T) -> T,
|
|
||||||
decoder: (T) -> T
|
|
||||||
) : this(
|
|
||||||
context,
|
context,
|
||||||
key,
|
key,
|
||||||
context.dataStore.data.map { it[key]?.let(decoder) ?: defaultValue },
|
context.dataStore.data.map { it[key] ?: defaultValue }
|
||||||
encoder
|
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun updatePreference(value: T) {
|
suspend fun updatePreference(value: T) {
|
||||||
context.dataStore.edit { it[key] = encoder(value) }
|
context.dataStore.edit { it[key] = value }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
suspend fun removePreference() {
|
|
||||||
context.dataStore.edit { it.remove(key) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
fun String.decodeHex(): ByteArray {
|
fun String.decodeHex(): ByteArray {
|
||||||
require(length % 2 == 0) { "Must have an even length" }
|
check(length % 2 == 0) { "Must have an even length" }
|
||||||
|
|
||||||
val decodedLength = length / 2
|
val decodedLength = length / 2
|
||||||
val out = ByteArray(decodedLength)
|
val out = ByteArray(decodedLength)
|
||||||
|
@ -29,19 +29,6 @@ fun formatFreeSpace(size: Int): String =
|
||||||
"$size B"
|
"$size B"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode a list of potential ISDR AIDs, one per line. Lines starting with '#' are ignored.
|
|
||||||
* If none is found, at least EUICC_DEFAULT_ISDR_AID is returned
|
|
||||||
*/
|
|
||||||
fun parseIsdrAidList(s: String): List<ByteArray> =
|
|
||||||
s.split('\n')
|
|
||||||
.map(String::trim)
|
|
||||||
.filter { !it.startsWith('#') }
|
|
||||||
.map(String::trim)
|
|
||||||
.filter(String::isNotEmpty)
|
|
||||||
.mapNotNull { runCatching(it::decodeHex).getOrNull() }
|
|
||||||
.ifEmpty { listOf(EUICC_DEFAULT_ISDR_AID.decodeHex()) }
|
|
||||||
|
|
||||||
fun String.prettyPrintJson(): String {
|
fun String.prettyPrintJson(): String {
|
||||||
val ret = StringBuilder()
|
val ret = StringBuilder()
|
||||||
var inQuotes = false
|
var inQuotes = false
|
||||||
|
|
|
@ -54,9 +54,6 @@ interface OpenEuiccContextMarker {
|
||||||
val appContainer: AppContainer
|
val appContainer: AppContainer
|
||||||
get() = openEuiccApplication.appContainer
|
get() = openEuiccApplication.appContainer
|
||||||
|
|
||||||
val preferenceRepository: PreferenceRepository
|
|
||||||
get() = appContainer.preferenceRepository
|
|
||||||
|
|
||||||
val telephonyManager: TelephonyManager
|
val telephonyManager: TelephonyManager
|
||||||
get() = appContainer.telephonyManager
|
get() = appContainer.telephonyManager
|
||||||
}
|
}
|
||||||
|
@ -89,13 +86,6 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> Bitmap.use(f: (Bitmap) -> T): T =
|
|
||||||
try {
|
|
||||||
f(this)
|
|
||||||
} finally {
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeQrFromBitmap(bmp: Bitmap): String? =
|
fun decodeQrFromBitmap(bmp: Bitmap): String? =
|
||||||
runCatching {
|
runCatching {
|
||||||
val pixels = IntArray(bmp.width * bmp.height)
|
val pixels = IntArray(bmp.width * bmp.height)
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
package im.angry.openeuicc.util
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
|
||||||
import im.angry.openeuicc.core.EuiccChannel
|
|
||||||
import net.typeblog.lpac_jni.Version
|
|
||||||
|
|
||||||
data class EuiccVendorInfo(
|
|
||||||
val skuName: String?,
|
|
||||||
val serialNumber: String?,
|
|
||||||
val bootloaderVersion: String?,
|
|
||||||
val firmwareVersion: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val EUICC_VENDORS: Array<EuiccVendor> = arrayOf(EstkMe(), SimLink())
|
|
||||||
|
|
||||||
fun EuiccChannel.tryParseEuiccVendorInfo(): EuiccVendorInfo? {
|
|
||||||
EUICC_VENDORS.forEach { vendor ->
|
|
||||||
vendor.tryParseEuiccVendorInfo(this@tryParseEuiccVendorInfo)?.let {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EuiccVendor {
|
|
||||||
fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo?
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EstkMe : EuiccVendor {
|
|
||||||
companion object {
|
|
||||||
private val PRODUCT_AID = "A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()
|
|
||||||
private val PRODUCT_ATR_FPR = "estk.me".encodeToByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkAtr(channel: EuiccChannel): Boolean {
|
|
||||||
val iface = channel.apduInterface
|
|
||||||
if (iface !is ApduInterfaceAtrProvider) return false
|
|
||||||
val atr = iface.atr ?: return false
|
|
||||||
for (index in atr.indices) {
|
|
||||||
if (atr.size - index < PRODUCT_ATR_FPR.size) break
|
|
||||||
if (atr.sliceArray(index until index + PRODUCT_ATR_FPR.size)
|
|
||||||
.contentEquals(PRODUCT_ATR_FPR)
|
|
||||||
) return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeAsn1String(b: ByteArray): String? {
|
|
||||||
if (b.size < 2) return null
|
|
||||||
if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null
|
|
||||||
return b.sliceArray(0 until b.size - 2).decodeToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? {
|
|
||||||
if (!checkAtr(channel)) return null
|
|
||||||
|
|
||||||
val iface = channel.apduInterface
|
|
||||||
return try {
|
|
||||||
iface.withLogicalChannel(PRODUCT_AID) { transmit ->
|
|
||||||
fun invoke(p1: Byte) =
|
|
||||||
decodeAsn1String(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)))
|
|
||||||
EuiccVendorInfo(
|
|
||||||
skuName = invoke(0x03),
|
|
||||||
serialNumber = invoke(0x00),
|
|
||||||
bootloaderVersion = invoke(0x01),
|
|
||||||
firmwareVersion = invoke(0x02),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(TAG, "Failed to get ESTKmeInfo", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SimLink : EuiccVendor {
|
|
||||||
companion object {
|
|
||||||
private val EID_PATTERN = Regex("^89044045(84|21)67274948")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun tryParseEuiccVendorInfo(channel: EuiccChannel): EuiccVendorInfo? {
|
|
||||||
val eid = channel.lpa.eID
|
|
||||||
val version = channel.lpa.euiccInfo2?.euiccFirmwareVersion
|
|
||||||
if (version == null || EID_PATTERN.find(eid, 0) == null) return null
|
|
||||||
val versionName = when {
|
|
||||||
// @formatter:off
|
|
||||||
version >= Version(37, 1, 41) -> "v3.1 (beta 1)"
|
|
||||||
version >= Version(36, 18, 5) -> "v3 (final)"
|
|
||||||
version >= Version(36, 17, 39) -> "v3 (beta)"
|
|
||||||
version >= Version(36, 17, 4) -> "v2s"
|
|
||||||
version >= Version(36, 9, 3) -> "v2.1"
|
|
||||||
version >= Version(36, 7, 2) -> "v2"
|
|
||||||
// @formatter:on
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val skuName = if (versionName == null) {
|
|
||||||
"9eSIM"
|
|
||||||
} else {
|
|
||||||
"9eSIM $versionName"
|
|
||||||
}
|
|
||||||
|
|
||||||
return EuiccVendorInfo(
|
|
||||||
skuName = skuName,
|
|
||||||
serialNumber = null,
|
|
||||||
bootloaderVersion = null,
|
|
||||||
firmwareVersion = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package im.angry.openeuicc.vendored
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
|
||||||
|
import im.angry.openeuicc.util.TAG
|
||||||
|
import im.angry.openeuicc.util.decodeHex
|
||||||
|
import net.typeblog.lpac_jni.ApduInterface
|
||||||
|
|
||||||
|
data class ESTKmeInfo(
|
||||||
|
val serialNumber: String?,
|
||||||
|
val bootloaderVersion: String?,
|
||||||
|
val firmwareVersion: String?,
|
||||||
|
val skuName: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun isESTKmeATR(iface: ApduInterface): Boolean {
|
||||||
|
if (iface !is ApduInterfaceAtrProvider) return false
|
||||||
|
val atr = iface.atr ?: return false
|
||||||
|
val fpr = "estk.me".encodeToByteArray()
|
||||||
|
for (index in atr.indices) {
|
||||||
|
if (atr.size - index < fpr.size) break
|
||||||
|
if (atr.sliceArray(index until index + fpr.size).contentEquals(fpr)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? {
|
||||||
|
if (!isESTKmeATR(iface)) return null
|
||||||
|
fun decode(b: ByteArray): String? {
|
||||||
|
if (b.size < 2) return null
|
||||||
|
if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null
|
||||||
|
return b.sliceArray(0 until b.size - 2).decodeToString()
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
iface.withLogicalChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) { transmit ->
|
||||||
|
fun invoke(p1: Byte) = decode(transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)))
|
||||||
|
ESTKmeInfo(
|
||||||
|
invoke(0x00), // serial number
|
||||||
|
invoke(0x01), // bootloader version
|
||||||
|
invoke(0x02), // firmware version
|
||||||
|
invoke(0x03), // sku name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "Failed to get ESTKmeInfo", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package im.angry.openeuicc.vendored
|
||||||
|
|
||||||
|
import net.typeblog.lpac_jni.Version
|
||||||
|
|
||||||
|
private val prefix = Regex("^89044045(84|21)67274948") // SIMLink EID prefix
|
||||||
|
|
||||||
|
fun getSIMLinkVersion(eid: String, version: Version?): String? {
|
||||||
|
if (version == null || prefix.find(eid, 0) == null) return null
|
||||||
|
return when {
|
||||||
|
// @formatter:off
|
||||||
|
version >= Version(37, 1, 41) -> "v3.1 (beta 1)"
|
||||||
|
version >= Version(36, 18, 5) -> "v3 (final)"
|
||||||
|
version >= Version(36, 17, 39) -> "v3 (beta)"
|
||||||
|
version >= Version(36, 17, 4) -> "v2s"
|
||||||
|
version >= Version(36, 9, 3) -> "v2.1"
|
||||||
|
version >= Version(36, 7, 2) -> "v2"
|
||||||
|
// @formatter:on
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="21"
|
|
||||||
android:viewportHeight="21">
|
|
||||||
<path
|
|
||||||
android:pathData="m3.578,6.487c1.385,-2.384 3.966,-3.987 6.922,-3.987 4.418,0 8,3.582 8,8s-3.582,8 -8,8 -8,-3.582 -8,-8"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="@android:color/white"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
<path
|
|
||||||
android:pathData="m7.5,6.5l-4,0l-0,-4"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="@android:color/white"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
</vector>
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<include layout="@layout/toolbar_activity" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/isdr_aid_list_editor"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:fontFamily="monospace"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:gravity="top|start"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
tools:ignore="LabelFor" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,15 +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/save"
|
|
||||||
android:icon="@drawable/ic_save_as_black"
|
|
||||||
android:title="@string/logs_save"
|
|
||||||
app:showAsAction="always" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/reset"
|
|
||||||
android:title="@string/reset"
|
|
||||||
android:icon="@drawable/ic_refresh_black"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
</menu>
|
|
|
@ -10,9 +10,4 @@
|
||||||
android:id="@+id/euicc_info"
|
android:id="@+id/euicc_info"
|
||||||
android:title="@string/euicc_info"
|
android:title="@string/euicc_info"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/euicc_memory_reset"
|
|
||||||
android:title="@string/euicc_memory_reset"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
</menu>
|
|
@ -42,11 +42,12 @@
|
||||||
<string name="profile_download_server">サーバー (RSP / SM-DP+)</string>
|
<string name="profile_download_server">サーバー (RSP / SM-DP+)</string>
|
||||||
<string name="profile_download_code">アクティベーションコード</string>
|
<string name="profile_download_code">アクティベーションコード</string>
|
||||||
<string name="profile_download_confirmation_code">確認コード (オプション)</string>
|
<string name="profile_download_confirmation_code">確認コード (オプション)</string>
|
||||||
<string name="profile_download_confirmation_code_required">確認コード (必須)</string>
|
|
||||||
<string name="profile_download_imei">IMEI (オプション)</string>
|
<string name="profile_download_imei">IMEI (オプション)</string>
|
||||||
<string name="profile_download_low_nvram_title">残り容量が少ない</string>
|
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
|
||||||
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
|
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
|
||||||
<string name="profile_download_no_lpa_string">クリップボードに LPA コードがありません</string>
|
<string name="profile_download_no_lpa_string">クリップボードに LPA コードがありません</string>
|
||||||
|
<string name="profile_download_required_confirmation_code">確認コードが必要です</string>
|
||||||
|
<string name="profile_download_required_confirmation_code_message">クリップボードからスキャンした QR コードまたは LPA コードに必要な確認コードを入力してください。</string>
|
||||||
<string name="profile_download_incorrect_lpa_string">解析できません</string>
|
<string name="profile_download_incorrect_lpa_string">解析できません</string>
|
||||||
<string name="profile_download_incorrect_lpa_string_message">QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。</string>
|
<string name="profile_download_incorrect_lpa_string_message">QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。</string>
|
||||||
<string name="download_wizard">ダウンロードウィザード</string>
|
<string name="download_wizard">ダウンロードウィザード</string>
|
||||||
|
@ -124,7 +125,6 @@
|
||||||
<string name="logs_filename_template">%s のログ</string>
|
<string name="logs_filename_template">%s のログ</string>
|
||||||
<string name="developer_options_steps">開発者になるまであと %d ステップです。</string>
|
<string name="developer_options_steps">開発者になるまであと %d ステップです。</string>
|
||||||
<string name="developer_options_enabled">あなたは開発者になりました!</string>
|
<string name="developer_options_enabled">あなたは開発者になりました!</string>
|
||||||
<string name="isdr_aid_list_saved">カスタム ISD-R AID リストが保存されました</string>
|
|
||||||
<string name="pref_settings">設定</string>
|
<string name="pref_settings">設定</string>
|
||||||
<string name="pref_notifications">通知</string>
|
<string name="pref_notifications">通知</string>
|
||||||
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。</string>
|
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。</string>
|
||||||
|
@ -144,29 +144,11 @@
|
||||||
<string name="pref_advanced_logs">ログ</string>
|
<string name="pref_advanced_logs">ログ</string>
|
||||||
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string>
|
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</string>
|
||||||
<string name="pref_developer">開発者オプション</string>
|
<string name="pref_developer">開発者オプション</string>
|
||||||
<string name="pref_developer_refresh_after_switch_desc">プロファイルを切り替えた後にモデムに更新コマンドを送信するかどうか。クラッシュが発生する場合は、これを無効にしてみてください。</string>
|
|
||||||
<string name="pref_developer_unfiltered_profile_list">フィルタリングされていないプロファイル一覧を表示</string>
|
<string name="pref_developer_unfiltered_profile_list">フィルタリングされていないプロファイル一覧を表示</string>
|
||||||
<string name="pref_developer_unfiltered_profile_list_desc">非運用のプロファイルも含めます</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">SM-DP+ TLS 証明書を無視する</string>
|
||||||
<string name="pref_developer_ignore_tls_certificate_desc">RSP サーバーで使用される TLS 証明書を受け入れます</string>
|
<string name="pref_developer_ignore_tls_certificate_desc">RSP サーバーで使用される TLS 証明書を受け入れます</string>
|
||||||
<string name="pref_developer_isdr_aid_list_desc">一部のブランドの取り外し可能な eUICC では、独自の非標準 ISD-R AID が使用されている場合があり、サードパーティ アプリからアクセスできなくなります。アプリはこのリストに追加された非標準の AID の使用を試みる可能性がありますが、動作することは保証されません。</string>
|
|
||||||
<string name="pref_info">情報</string>
|
<string name="pref_info">情報</string>
|
||||||
<string name="pref_info_app_version">アプリバージョン</string>
|
<string name="pref_info_app_version">アプリバージョン</string>
|
||||||
<string name="pref_info_source_code">ソースコード</string>
|
<string name="pref_info_source_code">ソースコード</string>
|
||||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">確認文字列が一致しません</string>
|
|
||||||
<string name="toast_euicc_memory_reset_finitshed">このチップは消去されました</string>
|
|
||||||
<string name="task_euicc_memory_reset">eSIM チップを消去しています</string>
|
|
||||||
<string name="task_euicc_memory_reset_failure">eSIM チップの消去は失敗しました</string>
|
|
||||||
<string name="euicc_memory_reset">eSIM を消去する</string>
|
|
||||||
<string name="euicc_memory_reset_title">eSIM を消去する</string>
|
|
||||||
<string name="euicc_memory_reset_message">このチップ内のすべてのプロファイルを削除することをご確認してください。この操作は元に戻せないことをご理解してください。\n\nEID: %1$s\n\n%2$s</string>
|
|
||||||
<string name="euicc_memory_reset_hint_text">確認のため、ここに「%s」を入力してください</string>
|
|
||||||
<string name="euicc_memory_reset_confirm_text">EID が %s で終わるチップを消去することに同意します。これは元に戻せないことを理解しています。</string>
|
|
||||||
<string name="euicc_memory_reset_invoke_button">消去する</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset">eUICC の消去を可能にする</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset_desc">この操作は、デフォルトでは非表示になっている危険な操作です。代わりに、すべての構成ファイルを手動で削除することもできます。</string>
|
|
||||||
<string name="pref_developer_refresh_after_switch">モデムに更新コマンドを送信</string>
|
|
||||||
<string name="pref_developer_isdr_aid_list">ISD-R AID リストのカスタマイズ</string>
|
|
||||||
<string name="reset">リセット</string>
|
|
||||||
<string name="isdr_aid_list">ISD-R AID リスト</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -37,14 +37,8 @@
|
||||||
<string name="profile_download_server">服务器 (RSP / SM-DP+)</string>
|
<string name="profile_download_server">服务器 (RSP / SM-DP+)</string>
|
||||||
<string name="profile_download_code">激活码</string>
|
<string name="profile_download_code">激活码</string>
|
||||||
<string name="profile_download_confirmation_code">确认码 (可选)</string>
|
<string name="profile_download_confirmation_code">确认码 (可选)</string>
|
||||||
<string name="toast_sn_copied">已复制序列号到剪贴板</string>
|
|
||||||
<string name="euicc_info_sku">产品名称</string>
|
|
||||||
<string name="euicc_info_sn">产品序列号</string>
|
|
||||||
<string name="euicc_info_bl_ver">产品 Bootloader 版本</string>
|
|
||||||
<string name="euicc_info_fw_ver">产品固件版本</string>
|
|
||||||
<string name="profile_download_confirmation_code_required">确认码 (必需)</string>
|
|
||||||
<string name="profile_download_imei">IMEI (可选)</string>
|
<string name="profile_download_imei">IMEI (可选)</string>
|
||||||
<string name="profile_download_low_nvram_title">剩余空间不足</string>
|
<string name="profile_download_low_nvram_title">本次下载可能会失败</string>
|
||||||
<string name="profile_download_low_nvram_message">当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?</string>
|
<string name="profile_download_low_nvram_message">当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载?</string>
|
||||||
<string name="logs_saved_message">日志已保存到指定路径。需要通过其他 App 分享吗?</string>
|
<string name="logs_saved_message">日志已保存到指定路径。需要通过其他 App 分享吗?</string>
|
||||||
<string name="profile_rename_new_name">新昵称</string>
|
<string name="profile_rename_new_name">新昵称</string>
|
||||||
|
@ -65,7 +59,6 @@
|
||||||
<string name="profile_notification_delete">删除</string>
|
<string name="profile_notification_delete">删除</string>
|
||||||
<string name="logs_save">保存日志</string>
|
<string name="logs_save">保存日志</string>
|
||||||
<string name="logs_filename_template">%s 的日志</string>
|
<string name="logs_filename_template">%s 的日志</string>
|
||||||
<string name="isdr_aid_list_saved">自定义 ISD-R AID 列表已保存</string>
|
|
||||||
<string name="pref_settings">设置</string>
|
<string name="pref_settings">设置</string>
|
||||||
<string name="pref_notifications">通知</string>
|
<string name="pref_notifications">通知</string>
|
||||||
<string name="pref_notifications_desc">操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。</string>
|
<string name="pref_notifications_desc">操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。</string>
|
||||||
|
@ -82,7 +75,6 @@
|
||||||
<string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
|
<string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
|
||||||
<string name="pref_advanced_logs">日志</string>
|
<string name="pref_advanced_logs">日志</string>
|
||||||
<string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
|
<string name="pref_advanced_logs_desc">查看应用程序的最新调试日志</string>
|
||||||
<string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eUICC 可能会使用自己的非标准 ISD-R AID,导致第三方应用无法访问。此 App 可以尝试使用此列表中添加的非标准 AID,但不能保证它们一定有效。</string>
|
|
||||||
<string name="pref_info">信息</string>
|
<string name="pref_info">信息</string>
|
||||||
<string name="pref_info_app_version">App 版本</string>
|
<string name="pref_info_app_version">App 版本</string>
|
||||||
<string name="pref_info_source_code">源码</string>
|
<string name="pref_info_source_code">源码</string>
|
||||||
|
@ -147,26 +139,11 @@
|
||||||
<string name="pref_advanced_language">语言</string>
|
<string name="pref_advanced_language">语言</string>
|
||||||
<string name="pref_advanced_language_desc">选择 App 语言</string>
|
<string name="pref_advanced_language_desc">选择 App 语言</string>
|
||||||
<string name="pref_developer">开发者选项</string>
|
<string name="pref_developer">开发者选项</string>
|
||||||
<string name="pref_developer_refresh_after_switch_desc">切换配置文件后是否向基带发送刷新命令。如果发现崩溃,请尝试禁用此功能。</string>
|
|
||||||
<string name="pref_developer_unfiltered_profile_list">显示未经过滤的配置文件列表</string>
|
<string name="pref_developer_unfiltered_profile_list">显示未经过滤的配置文件列表</string>
|
||||||
<string name="pref_developer_unfiltered_profile_list_desc">在配置文件列表中包括非生产环境的配置文件</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">无视 SM-DP+ 的 TLS 证书</string>
|
||||||
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
|
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
|
||||||
<string name="information_unavailable">无信息</string>
|
<string name="information_unavailable">无信息</string>
|
||||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">输入的确认文本不匹配</string>
|
<string name="profile_download_required_confirmation_code">需要确认码</string>
|
||||||
<string name="toast_euicc_memory_reset_finitshed">此芯片已被擦除</string>
|
<string name="profile_download_required_confirmation_code_message">您扫描的二维码或粘贴的 LPA 码需要一个额外的确认码</string>
|
||||||
<string name="task_euicc_memory_reset">正在擦除 eSIM 芯片</string>
|
|
||||||
<string name="task_euicc_memory_reset_failure">eSIM 芯片擦除失败</string>
|
|
||||||
<string name="euicc_memory_reset">擦除 eSIM 芯片</string>
|
|
||||||
<string name="euicc_memory_reset_title">擦除 eSIM 芯片</string>
|
|
||||||
<string name="euicc_memory_reset_message">请确认删除此芯片上的所有配置文件,并了解此操作不可逆。\n\nEID: %1$s\n\n%2$s</string>
|
|
||||||
<string name="euicc_memory_reset_hint_text">请在此处输入「%s」以确认</string>
|
|
||||||
<string name="euicc_memory_reset_confirm_text">我确认擦除 EID 以 %s 结尾的芯片,并了解此操作不可逆</string>
|
|
||||||
<string name="euicc_memory_reset_invoke_button">擦除</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset">允许擦除 eUICC</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset_desc">此操作是默认隐藏的危险操作。作为替代方案,您可以手动删除所有配置文件。</string>
|
|
||||||
<string name="pref_developer_refresh_after_switch">向基带发送刷新命令</string>
|
|
||||||
<string name="pref_developer_isdr_aid_list">自定义 ISD-R AID 列表</string>
|
|
||||||
<string name="reset">重置</string>
|
|
||||||
<string name="isdr_aid_list">ISD-R AID 列表</string>
|
|
||||||
</resources>
|
</resources>
|
|
@ -37,14 +37,8 @@
|
||||||
<string name="profile_download_server">伺服器 (RSP / SM-DP+)</string>
|
<string name="profile_download_server">伺服器 (RSP / SM-DP+)</string>
|
||||||
<string name="profile_download_code">啟用碼</string>
|
<string name="profile_download_code">啟用碼</string>
|
||||||
<string name="profile_download_confirmation_code">確認碼 (可選)</string>
|
<string name="profile_download_confirmation_code">確認碼 (可選)</string>
|
||||||
<string name="toast_sn_copied">已複製序號到剪貼簿</string>
|
|
||||||
<string name="euicc_info_sku">產品名稱</string>
|
|
||||||
<string name="euicc_info_sn">產品序號</string>
|
|
||||||
<string name="euicc_info_bl_ver">產品引導程式版本</string>
|
|
||||||
<string name="euicc_info_fw_ver">產品韌體版本</string>
|
|
||||||
<string name="profile_download_confirmation_code_required">確認碼 (必需)</string>
|
|
||||||
<string name="profile_download_imei">IMEI (可選)</string>
|
<string name="profile_download_imei">IMEI (可選)</string>
|
||||||
<string name="profile_download_low_nvram_title">剩餘空間不足</string>
|
<string name="profile_download_low_nvram_title">本次下載可能會失敗</string>
|
||||||
<string name="profile_download_low_nvram_message">目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?</string>
|
<string name="profile_download_low_nvram_message">目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載?</string>
|
||||||
<string name="logs_saved_message">日誌已儲存到指定路徑。需要透過其他 App 分享嗎?</string>
|
<string name="logs_saved_message">日誌已儲存到指定路徑。需要透過其他 App 分享嗎?</string>
|
||||||
<string name="profile_rename_new_name">新名稱</string>
|
<string name="profile_rename_new_name">新名稱</string>
|
||||||
|
@ -65,7 +59,6 @@
|
||||||
<string name="profile_notification_delete">刪除</string>
|
<string name="profile_notification_delete">刪除</string>
|
||||||
<string name="logs_save">儲存日誌</string>
|
<string name="logs_save">儲存日誌</string>
|
||||||
<string name="logs_filename_template">%s 的日誌</string>
|
<string name="logs_filename_template">%s 的日誌</string>
|
||||||
<string name="isdr_aid_list_saved">自訂 ISD-R AID 列表已儲存</string>
|
|
||||||
<string name="pref_settings">設定</string>
|
<string name="pref_settings">設定</string>
|
||||||
<string name="pref_notifications">通知</string>
|
<string name="pref_notifications">通知</string>
|
||||||
<string name="pref_notifications_desc">變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。</string>
|
<string name="pref_notifications_desc">變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。</string>
|
||||||
|
@ -82,7 +75,6 @@
|
||||||
<string name="pref_advanced">進階</string>
|
<string name="pref_advanced">進階</string>
|
||||||
<string name="pref_advanced_disable_safeguard_removable_esim">允許 停用/刪除 已啟用的設定檔</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_disable_safeguard_removable_esim_desc">預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 <i>有時</i> 會導致無法存取。\n勾選此框以 <i>移除</i> 此保護措施。</string>
|
||||||
<string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID,導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID,但不能保證它們一定有效。</string>
|
|
||||||
<string name="pref_info">資訊</string>
|
<string name="pref_info">資訊</string>
|
||||||
<string name="pref_info_app_version">App 版本</string>
|
<string name="pref_info_app_version">App 版本</string>
|
||||||
<string name="pref_info_source_code">原始碼</string>
|
<string name="pref_info_source_code">原始碼</string>
|
||||||
|
@ -147,26 +139,9 @@
|
||||||
<string name="pref_advanced_language">語言</string>
|
<string name="pref_advanced_language">語言</string>
|
||||||
<string name="pref_advanced_language_desc">選擇 App 語言</string>
|
<string name="pref_advanced_language_desc">選擇 App 語言</string>
|
||||||
<string name="pref_developer">開發人員選項</string>
|
<string name="pref_developer">開發人員選項</string>
|
||||||
<string name="pref_developer_refresh_after_switch_desc">切換設定檔後是否向基帶發送刷新命令。如果發現崩潰,請嘗試停用此功能。</string>
|
|
||||||
<string name="pref_developer_unfiltered_profile_list">顯示未經過濾的設定檔列表</string>
|
<string name="pref_developer_unfiltered_profile_list">顯示未經過濾的設定檔列表</string>
|
||||||
<string name="pref_developer_unfiltered_profile_list_desc">在設定檔列表中包括非生產環境的設定檔</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">忽略 SM-DP+ 的 TLS 證書</string>
|
||||||
<string name="pref_developer_ignore_tls_certificate_desc">允許 RSP 伺服器使用任意證書</string>
|
<string name="pref_developer_ignore_tls_certificate_desc">允許 RSP 伺服器使用任意證書</string>
|
||||||
<string name="information_unavailable">無資訊</string>
|
<string name="information_unavailable">無資訊</string>
|
||||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">輸入的確認文字不匹配</string>
|
|
||||||
<string name="toast_euicc_memory_reset_finitshed">此晶片已被擦除</string>
|
|
||||||
<string name="task_euicc_memory_reset">正在擦除 eSIM 晶片</string>
|
|
||||||
<string name="task_euicc_memory_reset_failure">eSIM 晶片擦除失敗</string>
|
|
||||||
<string name="euicc_memory_reset">擦除 eSIM 晶片</string>
|
|
||||||
<string name="euicc_memory_reset_title">擦除 eSIM 晶片</string>
|
|
||||||
<string name="euicc_memory_reset_message">請確認刪除此晶片上的所有配置文件,並了解此操作不可逆。\n\nEID: %1$s\n\n%2$s</string>
|
|
||||||
<string name="euicc_memory_reset_hint_text">請在此輸入「%s」以確認</string>
|
|
||||||
<string name="euicc_memory_reset_confirm_text">我確認擦除 EID 以 %s 結尾的晶片,並了解此操作不可逆</string>
|
|
||||||
<string name="euicc_memory_reset_invoke_button">擦除</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset">允許擦除 eUICC</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset_desc">此操作是預設隱藏的危險操作。作為替代方案,您可以手動刪除所有設定檔。</string>
|
|
||||||
<string name="pref_developer_refresh_after_switch">向基帶發送刷新命令</string>
|
|
||||||
<string name="pref_developer_isdr_aid_list">自訂 ISD-R AID 列表</string>
|
|
||||||
<string name="reset">重置</string>
|
|
||||||
<string name="isdr_aid_list">ISD-R AID 列表</string>
|
|
||||||
</resources>
|
</resources>
|
|
@ -30,10 +30,8 @@
|
||||||
|
|
||||||
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
||||||
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||||
<string name="toast_euicc_memory_reset_confirm_text_mismatched">Confirmation string mismatch</string>
|
|
||||||
<string name="toast_euicc_memory_reset_finitshed">This chip has been erased</string>
|
|
||||||
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
||||||
<string name="toast_sn_copied">Serial number copied to clipboard</string>
|
<string name="toast_sn_copied">Serial Number copied to clipboard</string>
|
||||||
<string name="toast_eid_copied">EID copied to clipboard</string>
|
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||||
<string name="toast_atr_copied">ATR copied to clipboard</string>
|
<string name="toast_atr_copied">ATR copied to clipboard</string>
|
||||||
|
|
||||||
|
@ -50,19 +48,18 @@
|
||||||
<string name="task_profile_delete_failure">Failed to delete eSIM profile</string>
|
<string name="task_profile_delete_failure">Failed to delete eSIM profile</string>
|
||||||
<string name="task_profile_switch">Switching eSIM profile</string>
|
<string name="task_profile_switch">Switching eSIM profile</string>
|
||||||
<string name="task_profile_switch_failure">Failed to switch eSIM profile</string>
|
<string name="task_profile_switch_failure">Failed to switch eSIM profile</string>
|
||||||
<string name="task_euicc_memory_reset">Erasing eSIM chip</string>
|
|
||||||
<string name="task_euicc_memory_reset_failure">Failed to erase eSIM chip</string>
|
|
||||||
|
|
||||||
<string name="profile_download">New eSIM</string>
|
<string name="profile_download">New eSIM</string>
|
||||||
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
|
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
|
||||||
<string name="profile_download_code">Activation Code</string>
|
<string name="profile_download_code">Activation Code</string>
|
||||||
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
|
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
|
||||||
<string name="profile_download_confirmation_code_required">Confirmation Code (Required)</string>
|
|
||||||
<string name="profile_download_imei">IMEI (Optional)</string>
|
<string name="profile_download_imei">IMEI (Optional)</string>
|
||||||
|
|
||||||
<string name="profile_download_low_nvram_title">Low remaining capacity</string>
|
<string name="profile_download_low_nvram_title">This download may fail</string>
|
||||||
<string name="profile_download_low_nvram_message">This profile may fail to download due to low remaining capacity.</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_no_lpa_string">No LPA code found in clipboard</string>
|
||||||
|
<string name="profile_download_required_confirmation_code">Confirmation Code Required</string>
|
||||||
|
<string name="profile_download_required_confirmation_code_message">Please provide a confirmation code as required by the scanned QR code or LPA code from clipboard.</string>
|
||||||
<string name="profile_download_incorrect_lpa_string">Unable to parse</string>
|
<string name="profile_download_incorrect_lpa_string">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="profile_download_incorrect_lpa_string_message">Could not parse QR code or clipboard content as a LPA code.</string>
|
||||||
|
|
||||||
|
@ -134,7 +131,6 @@
|
||||||
<string name="euicc_info_bl_ver">Product Bootloader Version</string>
|
<string name="euicc_info_bl_ver">Product Bootloader Version</string>
|
||||||
<string name="euicc_info_fw_ver">Product Firmware Version</string>
|
<string name="euicc_info_fw_ver">Product Firmware Version</string>
|
||||||
<string name="euicc_info_eid" translatable="false">EID</string>
|
<string name="euicc_info_eid" translatable="false">EID</string>
|
||||||
<string name="euicc_info_isdr_aid" translatable="false">ISD-R AID</string>
|
|
||||||
<string name="euicc_info_sgp22_version">SGP.22 Version</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_firmware_version">eUICC OS Version</string>
|
||||||
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>
|
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>
|
||||||
|
@ -147,13 +143,6 @@
|
||||||
<string name="euicc_info_ci_unknown">Unknown eSIM 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="euicc_info_atr" translatable="false">Answer To Reset (ATR)</string>
|
||||||
|
|
||||||
<string name="euicc_memory_reset">Erase eUICC</string>
|
|
||||||
<string name="euicc_memory_reset_title">Erase eUICC</string>
|
|
||||||
<string name="euicc_memory_reset_message">Please confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %1$s\n\n%2$s</string>
|
|
||||||
<string name="euicc_memory_reset_hint_text">Type \'%s\' here to confirm</string>
|
|
||||||
<string name="euicc_memory_reset_confirm_text">I CONFIRM TO ERASE THE CHIP WHOSE EID ENDS WITH %s AND UNDERSTAND THAT THIS IS IRREVERSIBLE</string>
|
|
||||||
<string name="euicc_memory_reset_invoke_button">Erase</string>
|
|
||||||
|
|
||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
|
|
||||||
|
@ -163,11 +152,6 @@
|
||||||
<string name="developer_options_steps">You are %d steps away from being a developer.</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="developer_options_enabled">You are now a developer!</string>
|
||||||
|
|
||||||
<string name="reset">Reset</string>
|
|
||||||
|
|
||||||
<string name="isdr_aid_list">ISD-R AID List</string>
|
|
||||||
<string name="isdr_aid_list_saved">Saved custom ISD-R AID list.</string>
|
|
||||||
|
|
||||||
<string name="pref_settings">Settings</string>
|
<string name="pref_settings">Settings</string>
|
||||||
<string name="pref_notifications">Notifications</string>
|
<string name="pref_notifications">Notifications</string>
|
||||||
<string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
|
<string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
|
||||||
|
@ -187,16 +171,10 @@
|
||||||
<string name="pref_advanced_logs">Logs</string>
|
<string name="pref_advanced_logs">Logs</string>
|
||||||
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
||||||
<string name="pref_developer">Developer Options</string>
|
<string name="pref_developer">Developer Options</string>
|
||||||
<string name="pref_developer_refresh_after_switch">Send refresh command to modem</string>
|
|
||||||
<string name="pref_developer_refresh_after_switch_desc">Whether to send a refresh command to the modem after switching profiles. Try disabling this if you see crashes.</string>
|
|
||||||
<string name="pref_developer_unfiltered_profile_list">Show unfiltered profile list</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_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">Ignore SM-DP+ TLS certificate</string>
|
||||||
<string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
|
<string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
|
||||||
<string name="pref_developer_euicc_memory_reset">Allow erasing eUICC</string>
|
|
||||||
<string name="pref_developer_euicc_memory_reset_desc">This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.</string>
|
|
||||||
<string name="pref_developer_isdr_aid_list">Customize ISD-R AID list</string>
|
|
||||||
<string name="pref_developer_isdr_aid_list_desc">Some brands of removable eUICCs may use their own non-standard ISD-R AID, rendering them inaccessible to third-party apps. We can attempt to use non-standard AIDs added in this list, but there is no guarantee that they will work.</string>
|
|
||||||
<string name="pref_info">Info</string>
|
<string name="pref_info">Info</string>
|
||||||
<string name="pref_info_app_version">App Version</string>
|
<string name="pref_info_app_version">App Version</string>
|
||||||
<string name="pref_info_source_code">Source Code</string>
|
<string name="pref_info_source_code">Source Code</string>
|
||||||
|
|
|
@ -52,17 +52,11 @@
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
|
<PreferenceCategory
|
||||||
app:key="pref_developer"
|
app:key="pref_developer"
|
||||||
app:title="@string/pref_developer"
|
app:title="@string/pref_developer"
|
||||||
app:iconSpaceReserved="false">
|
app:iconSpaceReserved="false">
|
||||||
|
|
||||||
<CheckBoxPreference
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:key="pref_developer_refresh_after_switch"
|
|
||||||
app:summary="@string/pref_developer_refresh_after_switch_desc"
|
|
||||||
app:title="@string/pref_developer_refresh_after_switch" />
|
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:key="pref_developer_unfiltered_profile_list"
|
app:key="pref_developer_unfiltered_profile_list"
|
||||||
|
@ -75,19 +69,7 @@
|
||||||
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
|
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
|
||||||
app:title="@string/pref_developer_ignore_tls_certificate" />
|
app:title="@string/pref_developer_ignore_tls_certificate" />
|
||||||
|
|
||||||
<CheckBoxPreference
|
</PreferenceCategory>
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:key="pref_developer_euicc_memory_reset"
|
|
||||||
app:summary="@string/pref_developer_euicc_memory_reset_desc"
|
|
||||||
app:title="@string/pref_developer_euicc_memory_reset" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:key="pref_developer_isdr_aid_list"
|
|
||||||
app:title="@string/pref_developer_isdr_aid_list"
|
|
||||||
app:summary="@string/pref_developer_isdr_aid_list_desc" />
|
|
||||||
|
|
||||||
</im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
app:key="pref_info"
|
app:key="pref_info"
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.content.pm.PackageManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import im.angry.easyeuicc.R
|
import im.angry.easyeuicc.R
|
||||||
import im.angry.openeuicc.util.SIMToolkit
|
import im.angry.openeuicc.util.SIMToolkit
|
||||||
|
@ -27,27 +26,21 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
inflater.inflate(R.menu.fragment_sim_toolkit, menu)
|
inflater.inflate(R.menu.fragment_sim_toolkit, menu)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
|
||||||
super.onPrepareOptionsMenu(menu)
|
|
||||||
menu.findItem(R.id.open_sim_toolkit).apply {
|
menu.findItem(R.id.open_sim_toolkit).apply {
|
||||||
intent = stk[slotId]?.intent
|
val slot = stk[slotId] ?: return@apply
|
||||||
isVisible = intent != null
|
isVisible = slot.intent != null
|
||||||
}
|
setOnMenuItemClickListener {
|
||||||
}
|
val intent = slot.intent ?: return@setOnMenuItemClickListener false
|
||||||
|
if (intent.action == Settings.ACTION_APPLICATION_DETAILS_SETTINGS) {
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
val packageName = intent.data!!.schemeSpecificPart
|
||||||
R.id.open_sim_toolkit -> {
|
val label = requireContext().packageManager.getApplicationLabel(packageName)
|
||||||
SIMToolkit.getDisabledPackageName(item.intent)?.also { packageName ->
|
val message = requireContext().getString(R.string.toast_prompt_to_enable_sim_toolkit, label)
|
||||||
val label = requireContext().packageManager.getApplicationLabel(packageName)
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
val message = getString(R.string.toast_prompt_to_enable_sim_toolkit, label)
|
}
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
startActivity(intent)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
super.onOptionsItemSelected(item) // handling intent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,10 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class IsdrChannelAccessCheck(private val context: Context): CompatibilityCheck(context) {
|
internal class IsdrChannelAccessCheck(private val context: Context): CompatibilityCheck(context) {
|
||||||
|
companion object {
|
||||||
|
val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
override val title: String
|
override val title: String
|
||||||
get() = context.getString(R.string.compatibility_check_isdr_channel)
|
get() = context.getString(R.string.compatibility_check_isdr_channel)
|
||||||
override val defaultDescription: String
|
override val defaultDescription: String
|
||||||
|
@ -143,10 +147,7 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili
|
||||||
|
|
||||||
val (validSlotIds, result) = readers.map {
|
val (validSlotIds, result) = readers.map {
|
||||||
try {
|
try {
|
||||||
// Note: we ONLY check the default ISD-R AID, because this test is for the _device_,
|
it.openSession().openLogicalChannel(ISDR_AID)?.close()
|
||||||
// NOT the eUICC. We don't care what AID a potential eUICC might use, all we need to
|
|
||||||
// check is we can open _some_ AID.
|
|
||||||
it.openSession().openLogicalChannel(EUICC_DEFAULT_ISDR_AID.decodeHex())?.close()
|
|
||||||
Pair(it.slotIndex, State.SUCCESS)
|
Pair(it.slotIndex, State.SUCCESS)
|
||||||
} catch (_: SecurityException) {
|
} catch (_: SecurityException) {
|
||||||
// Ignore; this is expected when everything works
|
// Ignore; this is expected when everything works
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.ArrayRes
|
import androidx.annotation.ArrayRes
|
||||||
import im.angry.easyeuicc.R
|
import im.angry.easyeuicc.R
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
|
@ -31,10 +32,9 @@ class SIMToolkit(private val context: Context) {
|
||||||
data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
|
data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
|
||||||
private val packageNames: Iterable<String>
|
private val packageNames: Iterable<String>
|
||||||
get() = components.map(ComponentName::getPackageName).toSet()
|
get() = components.map(ComponentName::getPackageName).toSet()
|
||||||
.filter(packageManager::isInstalledApp)
|
|
||||||
|
|
||||||
private val launchIntent: Intent?
|
private val launchIntent: Intent?
|
||||||
get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
|
get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntent)
|
||||||
|
|
||||||
private val activities: Iterable<ComponentName>
|
private val activities: Iterable<ComponentName>
|
||||||
get() = packageNames.flatMap(packageManager::getActivities)
|
get() = packageNames.flatMap(packageManager::getActivities)
|
||||||
|
@ -50,23 +50,23 @@ class SIMToolkit(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisabledPackageIntent(): Intent? {
|
private fun getDisabledPackageIntent(): Intent? {
|
||||||
val disabledPackageName = packageNames
|
val disabledPackageName = packageNames.find {
|
||||||
.find { isDisabledState(packageManager.getApplicationEnabledSetting(it)) }
|
try {
|
||||||
?: return null
|
isDisabledState(packageManager.getApplicationEnabledSetting(it))
|
||||||
val uri = Uri.fromParts("package", disabledPackageName, null)
|
} catch (_: IllegalArgumentException) {
|
||||||
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (disabledPackageName == null) return null
|
||||||
|
return Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", disabledPackageName, null)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent: Intent?
|
val intent: Intent?
|
||||||
get() = getActivityIntent() ?: getDisabledPackageIntent()
|
get() = getActivityIntent() ?: getDisabledPackageIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getDisabledPackageName(intent: Intent?): String? {
|
|
||||||
if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null
|
|
||||||
return intent.data?.schemeSpecificPart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDisabledState(state: Int) = when (state) {
|
private fun isDisabledState(state: Int) = when (state) {
|
||||||
|
@ -75,12 +75,15 @@ private fun isDisabledState(state: Int) = when (state) {
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PackageManager.isInstalledApp(packageName: String) = try {
|
private fun PackageManager.getLaunchIntent(packageName: String) = try {
|
||||||
getPackageInfo(packageName, 0)
|
getLaunchIntentForPackage(packageName)
|
||||||
true
|
|
||||||
} catch (_: PackageManager.NameNotFoundException) {
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
false
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PackageManager.getActivities(packageName: String) =
|
private fun PackageManager.getActivities(packageName: String) = try {
|
||||||
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList()
|
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
||||||
|
.activities?.toList() ?: emptyList()
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
|
@ -5,14 +5,12 @@
|
||||||
<item>com.android.stk/.StkMainHide</item>
|
<item>com.android.stk/.StkMainHide</item>
|
||||||
<item>com.android.stk/.StkListActivity</item>
|
<item>com.android.stk/.StkListActivity</item>
|
||||||
<item>com.android.stk/.StkLauncherListActivity</item>
|
<item>com.android.stk/.StkLauncherListActivity</item>
|
||||||
<item>com.android.stk/.StkSelectionActivity</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="sim_toolkit_slot_1">
|
<string-array name="sim_toolkit_slot_1">
|
||||||
<item>com.android.stk/.StkMain1</item>
|
<item>com.android.stk/.StkMain1</item>
|
||||||
<item>com.android.stk/.PrimaryStkMain</item>
|
<item>com.android.stk/.PrimaryStkMain</item>
|
||||||
<item>com.android.stk/.StkLauncherActivity</item>
|
<item>com.android.stk/.StkLauncherActivity</item>
|
||||||
<item>com.android.stk/.StkLauncherActivity_Chn</item>
|
<item>com.android.stk/.StkLauncherActivity_Chn</item>
|
||||||
<item>com.android.stk/.StkLauncherActivity1</item>
|
|
||||||
<item>com.android.stk/.StkLauncherActivityI</item>
|
<item>com.android.stk/.StkLauncherActivityI</item>
|
||||||
<item>com.android.stk/.OppoStkLauncherActivity1</item>
|
<item>com.android.stk/.OppoStkLauncherActivity1</item>
|
||||||
<item>com.android.stk/.OplusStkLauncherActivity1</item>
|
<item>com.android.stk/.OplusStkLauncherActivity1</item>
|
||||||
|
|
|
@ -5,27 +5,23 @@ import android.util.Log
|
||||||
import im.angry.openeuicc.OpenEuiccApplication
|
import im.angry.openeuicc.OpenEuiccApplication
|
||||||
import im.angry.openeuicc.R
|
import im.angry.openeuicc.R
|
||||||
import im.angry.openeuicc.util.*
|
import im.angry.openeuicc.util.*
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context),
|
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
|
||||||
PrivilegedEuiccContextMarker {
|
private val tm by lazy {
|
||||||
override val openEuiccMarkerContext: Context
|
(context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager
|
||||||
get() = context
|
}
|
||||||
|
|
||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
override suspend fun tryOpenEuiccChannel(
|
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
|
||||||
port: UiccPortInfoCompat,
|
|
||||||
isdrAid: ByteArray
|
|
||||||
): EuiccChannel? {
|
|
||||||
val port = port as RealUiccPortInfoCompat
|
val port = port as RealUiccPortInfoCompat
|
||||||
if (port.card.isRemovable) {
|
if (port.card.isRemovable) {
|
||||||
// Attempt unprivileged (OMAPI) before TelephonyManager
|
// Attempt unprivileged (OMAPI) before TelephonyManager
|
||||||
// but still try TelephonyManager in case OMAPI is broken
|
// but still try TelephonyManager in case OMAPI is broken
|
||||||
super.tryOpenEuiccChannel(port, isdrAid)?.let { return it }
|
super.tryOpenEuiccChannel(port)?.let { return it }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) {
|
if (port.card.isEuicc) {
|
||||||
Log.i(
|
Log.i(
|
||||||
DefaultEuiccChannelManager.TAG,
|
DefaultEuiccChannelManager.TAG,
|
||||||
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
|
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
|
||||||
|
@ -37,22 +33,21 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
||||||
intrinsicChannelName = null,
|
intrinsicChannelName = null,
|
||||||
TelephonyManagerApduInterface(
|
TelephonyManagerApduInterface(
|
||||||
port,
|
port,
|
||||||
telephonyManager,
|
tm,
|
||||||
context.preferenceRepository.verboseLoggingFlow
|
context.preferenceRepository.verboseLoggingFlow
|
||||||
),
|
),
|
||||||
isdrAid,
|
|
||||||
context.preferenceRepository.verboseLoggingFlow,
|
context.preferenceRepository.verboseLoggingFlow,
|
||||||
context.preferenceRepository.ignoreTLSCertificateFlow,
|
context.preferenceRepository.ignoreTLSCertificateFlow,
|
||||||
)
|
)
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
// Failed
|
// Failed
|
||||||
Log.w(
|
Log.w(
|
||||||
DefaultEuiccChannelManager.TAG,
|
DefaultEuiccChannelManager.TAG,
|
||||||
"TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
|
"TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.tryOpenEuiccChannel(port, isdrAid)
|
return super.tryOpenEuiccChannel(port)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,6 @@ import im.angry.openeuicc.core.EuiccChannelManagerFactory
|
||||||
import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
|
import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
|
||||||
import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
|
import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
|
||||||
import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory
|
import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory
|
||||||
import im.angry.openeuicc.util.*
|
|
||||||
|
|
||||||
class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
||||||
override val euiccChannelManager: EuiccChannelManager by lazy {
|
override val euiccChannelManager: EuiccChannelManager by lazy {
|
||||||
|
@ -28,8 +27,4 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
||||||
override val customizableTextProvider by lazy {
|
override val customizableTextProvider by lazy {
|
||||||
PrivilegedCustomizableTextProvider(context)
|
PrivilegedCustomizableTextProvider(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val preferenceRepository by lazy {
|
|
||||||
PrivilegedPreferenceRepository(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,17 +1,11 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.CheckBoxPreference
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import im.angry.openeuicc.R
|
|
||||||
import im.angry.openeuicc.util.*
|
|
||||||
|
|
||||||
class PrivilegedSettingsFragment : SettingsFragment(), PrivilegedEuiccContextMarker {
|
class PrivilegedSettingsFragment : SettingsFragment() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
super.onCreatePreferences(savedInstanceState, rootKey)
|
super.onCreatePreferences(savedInstanceState, rootKey)
|
||||||
addPreferencesFromResource(R.xml.pref_privileged_settings)
|
|
||||||
mergePreferenceOverlay("pref_developer_overlay", "pref_developer")
|
|
||||||
|
|
||||||
// It's stupid to _disable_ things for privileged, but for now, the per-app locale picker
|
// It's stupid to _disable_ things for privileged, but for now, the per-app locale picker
|
||||||
// is not usable for apps signed with the platform key.
|
// is not usable for apps signed with the platform key.
|
||||||
// ref: <https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-15.0.0_r6/src/com/android/settings/applications/AppLocaleUtil.java#60>
|
// ref: <https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-15.0.0_r6/src/com/android/settings/applications/AppLocaleUtil.java#60>
|
||||||
|
@ -19,9 +13,5 @@ class PrivilegedSettingsFragment : SettingsFragment(), PrivilegedEuiccContextMar
|
||||||
// eventually work for platform-signed apps. Or, at some point we might introduce our own
|
// eventually work for platform-signed apps. Or, at some point we might introduce our own
|
||||||
// locale picker, which hopefully works whether privileged or not.
|
// locale picker, which hopefully works whether privileged or not.
|
||||||
requirePreference<Preference>("pref_advanced_language").isVisible = false
|
requirePreference<Preference>("pref_advanced_language").isVisible = false
|
||||||
|
|
||||||
// Force use TelephonyManager API
|
|
||||||
requirePreference<CheckBoxPreference>("pref_developer_removable_telephony_manager")
|
|
||||||
.bindBooleanFlow(preferenceRepository.removableTelephonyManagerFlow)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
package im.angry.openeuicc.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
|
||||||
|
|
||||||
internal object PrivilegedPreferenceKeys {
|
|
||||||
// ---- Developer Options ----
|
|
||||||
val REMOVABLE_TELEPHONY_MANAGER = booleanPreferencesKey("removable_telephony_manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
class PrivilegedPreferenceRepository(context: Context) : PreferenceRepository(context) {
|
|
||||||
// ---- Developer Options ----
|
|
||||||
val removableTelephonyManagerFlow =
|
|
||||||
bindFlow(PrivilegedPreferenceKeys.REMOVABLE_TELEPHONY_MANAGER, false)
|
|
||||||
}
|
|
|
@ -5,16 +5,10 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
interface PrivilegedEuiccContextMarker : OpenEuiccContextMarker {
|
|
||||||
override val preferenceRepository: PrivilegedPreferenceRepository
|
|
||||||
get() = appContainer.preferenceRepository as PrivilegedPreferenceRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun Context.bindServiceSuspended(intent: Intent, flags: Int): Pair<IBinder?, () -> Unit> =
|
suspend fun Context.bindServiceSuspended(intent: Intent, flags: Int): Pair<IBinder?, () -> Unit> =
|
||||||
suspendCoroutine { cont ->
|
suspendCoroutine { cont ->
|
||||||
var binder: IBinder?
|
var binder: IBinder?
|
||||||
|
|
|
@ -17,6 +17,4 @@
|
||||||
<string name="lui_desc">使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。</string>
|
<string name="lui_desc">使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。</string>
|
||||||
<string name="lui_skip">スキップ</string>
|
<string name="lui_skip">スキップ</string>
|
||||||
<string name="lui_download">eSIM をダウンロード</string>
|
<string name="lui_download">eSIM をダウンロード</string>
|
||||||
<string name="pref_developer_telephony_manager_removable">TelephonyManagerをどこでも使用</string>
|
|
||||||
<string name="pref_developer_telephony_manager_removable_desc">デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -17,6 +17,4 @@
|
||||||
<string name="lui_skip">跳过</string>
|
<string name="lui_skip">跳过</string>
|
||||||
<string name="lui_download">下载 eSIM</string>
|
<string name="lui_download">下载 eSIM</string>
|
||||||
<string name="telephony_manager">TelephonyManager (特权)</string>
|
<string name="telephony_manager">TelephonyManager (特权)</string>
|
||||||
<string name="pref_developer_telephony_manager_removable">全局使用 TelephonyManager</string>
|
|
||||||
<string name="pref_developer_telephony_manager_removable_desc">在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。</string>
|
|
||||||
</resources>
|
</resources>
|
|
@ -17,6 +17,4 @@
|
||||||
<string name="lui_skip">跳過</string>
|
<string name="lui_skip">跳過</string>
|
||||||
<string name="lui_download">下載 eSIM</string>
|
<string name="lui_download">下載 eSIM</string>
|
||||||
<string name="telephony_manager">TelephonyManager (特權)</string>
|
<string name="telephony_manager">TelephonyManager (特權)</string>
|
||||||
<string name="pref_developer_telephony_manager_removable">全域使用 TelephonyManager</string>
|
|
||||||
<string name="pref_developer_telephony_manager_removable_desc">在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。</string>
|
|
||||||
</resources>
|
</resources>
|
|
@ -22,8 +22,4 @@
|
||||||
<string name="lui_desc">Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.</string>
|
<string name="lui_desc">Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.</string>
|
||||||
<string name="lui_skip">Skip</string>
|
<string name="lui_skip">Skip</string>
|
||||||
<string name="lui_download">Download eSIM</string>
|
<string name="lui_download">Download eSIM</string>
|
||||||
|
|
||||||
<!-- Preference -->
|
|
||||||
<string name="pref_developer_telephony_manager_removable">Use TelephonyManager everywhere</string>
|
|
||||||
<string name="pref_developer_telephony_manager_removable_desc">By default, only OMAPI is attempted for removable eUICCs to match what is done in unprivileged mode (i.e. EasyEUICC). This may not work well on some devices. Select this option to force the use of TelephonyManager even for removable eUICCs.</string>
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<PreferenceCategory
|
|
||||||
app:isPreferenceVisible="false"
|
|
||||||
app:key="pref_developer_overlay">
|
|
||||||
<CheckBoxPreference
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:key="pref_developer_removable_telephony_manager"
|
|
||||||
app:summary="@string/pref_developer_telephony_manager_removable_desc"
|
|
||||||
app:title="@string/pref_developer_telephony_manager_removable" />
|
|
||||||
</PreferenceCategory>
|
|
||||||
</PreferenceScreen>
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,7 +1,6 @@
|
||||||
|
#Wed Jun 08 13:28:20 EDT 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
285
gradlew
vendored
285
gradlew
vendored
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright 2015 the original author or authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -15,104 +15,69 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
##
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
## Gradle start up script for UN*X
|
||||||
#
|
##
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
app_path=$0
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
# Need this for daisy-chained symlinks.
|
while [ -h "$PRG" ] ; do
|
||||||
while
|
ls=`ls -ld "$PRG"`
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
[ -h "$app_path" ]
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
do
|
PRG="$link"
|
||||||
ls=$( ls -ld "$app_path" )
|
else
|
||||||
link=${ls#*' -> '}
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
case $link in #(
|
fi
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
# This is normally unused
|
APP_NAME="Gradle"
|
||||||
# shellcheck disable=SC2034
|
APP_BASE_NAME=`basename "$0"`
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD="maximum"
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "$( uname )" in #(
|
case "`uname`" in
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
CYGWIN* )
|
||||||
Darwin* ) darwin=true ;; #(
|
cygwin=true
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
;;
|
||||||
NONSTOP* ) nonstop=true ;;
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
@ -122,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
else
|
else
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
@ -133,120 +98,88 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD="java"
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
case $MAX_FD in #(
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
max*)
|
if [ $? -eq 0 ] ; then
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
# shellcheck disable=SC2039,SC3045
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
fi
|
||||||
warn "Could not query maximum file descriptor limit"
|
ulimit -n $MAX_FD
|
||||||
esac
|
if [ $? -ne 0 ] ; then
|
||||||
case $MAX_FD in #(
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
'' | soft) :;; #(
|
fi
|
||||||
*)
|
else
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
# shellcheck disable=SC2039,SC3045
|
fi
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
# * args from the command line
|
if $darwin; then
|
||||||
# * the main class name
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
# * -classpath
|
fi
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
for arg do
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
if
|
SEP=""
|
||||||
case $arg in #(
|
for dir in $ROOTDIRSRAW ; do
|
||||||
-*) false ;; # don't mess with options #(
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
SEP="|"
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|
37
gradlew.bat
vendored
37
gradlew.bat
vendored
|
@ -13,10 +13,8 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
|
@ -27,8 +25,7 @@
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@ -43,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -59,11 +56,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -78,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
exit /b 1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
|
@ -69,13 +69,11 @@ fun TelephonyManager.iccOpenLogicalChannelByPort(
|
||||||
): IccOpenLogicalChannelResponse =
|
): IccOpenLogicalChannelResponse =
|
||||||
iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse
|
iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse
|
||||||
|
|
||||||
fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int) {
|
fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean =
|
||||||
iccCloseLogicalChannelBySlot.invoke(this, slotId, channel)
|
iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as Boolean
|
||||||
}
|
|
||||||
|
|
||||||
fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int) {
|
fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int): Boolean =
|
||||||
iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel)
|
iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) as Boolean
|
||||||
}
|
|
||||||
|
|
||||||
fun TelephonyManager.iccTransmitApduLogicalChannelBySlot(
|
fun TelephonyManager.iccTransmitApduLogicalChannelBySlot(
|
||||||
slotId: Int, channel: Int, cla: Int, instruction: Int,
|
slotId: Int, channel: Int, cla: Int, instruction: Int,
|
||||||
|
|
|
@ -80,7 +80,7 @@ apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, c
|
||||||
LPAC_JNI_EXCEPTION_RETURN;
|
LPAC_JNI_EXCEPTION_RETURN;
|
||||||
*rx_len = (*env)->GetArrayLength(env, ret);
|
*rx_len = (*env)->GetArrayLength(env, ret);
|
||||||
*rx = calloc(*rx_len, sizeof(uint8_t));
|
*rx = calloc(*rx_len, sizeof(uint8_t));
|
||||||
(*env)->GetByteArrayRegion(env, ret, 0, *rx_len, (jbyte *) *rx);
|
(*env)->GetByteArrayRegion(env, ret, 0, *rx_len, *rx);
|
||||||
(*env)->DeleteLocalRef(env, txArr);
|
(*env)->DeleteLocalRef(env, txArr);
|
||||||
(*env)->DeleteLocalRef(env, ret);
|
(*env)->DeleteLocalRef(env, ret);
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -113,7 +113,7 @@ http_interface_transmit(struct euicc_ctx *ctx, const char *url, uint32_t *rcode,
|
||||||
jbyteArray rxArr = (jbyteArray) (*env)->GetObjectField(env, ret, field_resp_data);
|
jbyteArray rxArr = (jbyteArray) (*env)->GetObjectField(env, ret, field_resp_data);
|
||||||
*rx_len = (*env)->GetArrayLength(env, rxArr);
|
*rx_len = (*env)->GetArrayLength(env, rxArr);
|
||||||
*rx = calloc(*rx_len, sizeof(uint8_t));
|
*rx = calloc(*rx_len, sizeof(uint8_t));
|
||||||
(*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, (jbyte *) *rx);
|
(*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, *rx);
|
||||||
(*env)->DeleteLocalRef(env, txArr);
|
(*env)->DeleteLocalRef(env, txArr);
|
||||||
(*env)->DeleteLocalRef(env, rxArr);
|
(*env)->DeleteLocalRef(env, rxArr);
|
||||||
(*env)->DeleteLocalRef(env, headersArr);
|
(*env)->DeleteLocalRef(env, headersArr);
|
||||||
|
|
|
@ -126,11 +126,8 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
|
||||||
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
|
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
ret = - (int) es10b_load_bound_profile_package_result.errorReason;
|
ret = - (int) es10b_load_bound_profile_package_result.errorReason;
|
||||||
goto out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
euicc_http_cleanup(ctx);
|
|
||||||
|
|
||||||
out:
|
out:
|
||||||
// We expect Java side to call cancelSessions after any error -- thus, `euicc_http_cleanup` is done there
|
// We expect Java side to call cancelSessions after any error -- thus, `euicc_http_cleanup` is done there
|
||||||
// This is so that Java side can access the last HTTP and/or APDU errors when we return.
|
// This is so that Java side can access the last HTTP and/or APDU errors when we return.
|
||||||
|
|
Loading…
Add table
Reference in a new issue