Compare commits

...
Sign in to create a new pull request.

56 commits

Author SHA1 Message Date
21c04ed179 Allow Actions to build from any branch 2025-06-17 07:57:19 -04:00
db4645b17f feat: sas accreditation number format check (#193)
Reviewed-on: PeterCxy/OpenEUICC#193
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-06-16 03:54:32 +02:00
149a19ca1c fix: build warning (#194)
Reviewed-on: PeterCxy/OpenEUICC#194
Co-authored-by: xqdoo00o <xqdoo00o@gmail.com>
Co-committed-by: xqdoo00o <xqdoo00o@gmail.com>
2025-06-16 03:54:02 +02:00
eaef00b88a Fixup README 2025-05-18 11:17:43 -04:00
023f6ded28 Update README.md to add a comparison matrix for Easy/OpenEUICC 2025-05-04 21:10:57 -04:00
a601ab7d72 fix: send euicc memory reset notification (#189)
```console
$ ./lpac profile list | jq
{
  "type": "lpa",
  "payload": {
    "code": 0,
    "message": "success",
    "data": [
      {
        "iccid": "8944476500001320600",
        "isdpAid": "a0000005591010ffffffff8900001100",
        "profileState": "enabled",
        "profileNickname": null,
        "serviceProviderName": "BetterRoaming",
        "profileName": "BetterRoaming",
        "iconType": null,
        "icon": null,
        "profileClass": "operational"
      },
      {
        "iccid": "89861234567891232113",
        "isdpAid": "a0000005591010ffffffff8900001200",
        "profileState": "disabled",
        "profileNickname": null,
        "serviceProviderName": "rspmanager_tester2",
        "profileName": "20230625_yysx",
        "iconType": null,
        "icon": null,
        "profileClass": "operational"
      }
    ]
  }
}
$ ./lpac notification list | jq
{
  "type": "lpa",
  "payload": {
    "code": 0,
    "message": "success",
    "data": [
      {
        "seqNumber": 48,
        "profileManagementOperation": "install",
        "notificationAddress": "smdp.io",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 49,
        "profileManagementOperation": "enable",
        "notificationAddress": "rsp.truphone.com",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 50,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      },
      {
        "seqNumber": 51,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      }
    ]
  }
}
$ ./lpac chip purge yes
{"type":"lpa","payload":{"code":0,"message":"success","data":null}}
$ ./lpac notification list | jq
{
  "type": "lpa",
  "payload": {
    "code": 0,
    "message": "success",
    "data": [
      {
        "seqNumber": 48,
        "profileManagementOperation": "install",
        "notificationAddress": "smdp.io",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 49,
        "profileManagementOperation": "enable",
        "notificationAddress": "rsp.truphone.com",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 50,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      },
      {
        "seqNumber": 51,
        "profileManagementOperation": "install",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      },
      {
        "seqNumber": 52,
        "profileManagementOperation": "delete",
        "notificationAddress": "rsp.truphone.com",
        "iccid": "8944476500001320600"
      },
      {
        "seqNumber": 53,
        "profileManagementOperation": "delete",
        "notificationAddress": "secsmsminiapp.eastcompeace.com",
        "iccid": "89861234567891232113"
      }
    ]
  }
}
```

Reviewed-on: PeterCxy/OpenEUICC#189
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-14 03:36:38 +02:00
756c621d5e fix: stricted sm-dp+ address checking (#190)
Reviewed-on: PeterCxy/OpenEUICC#190
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-14 03:36:14 +02:00
68114fa863 Expose the current ISD-R AID in use 2025-04-05 20:50:10 -04:00
1fda120459 Avoid reconnecting to USB iface repeatedly while trying different AIDs 2025-04-05 20:44:15 -04:00
994324acb6 Fix up back button for IsdrAidListActivity 2025-04-05 17:58:56 -04:00
6c774450ec fix: usb isd-r aid fallback (#188)
Reviewed-on: PeterCxy/OpenEUICC#188
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-01 23:13:57 +02:00
00ddf09287 fix: improve lpa string parsing (#181)
Reviewed-on: PeterCxy/OpenEUICC#181
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-01 03:19:21 +02:00
3662f93760 fix: send terminal capabilities (#187)
fix 9eSIM v1 (G+D) on USB

Reviewed-on: PeterCxy/OpenEUICC#187
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-01 03:18:45 +02:00
05abed117a fix: click sn copy (#186)
Reviewed-on: PeterCxy/OpenEUICC#186
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-01 03:18:22 +02:00
92fbfc5229 chore: add more isd-r aids (#184)
Reviewed-on: PeterCxy/OpenEUICC#184
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-04-01 03:18:02 +02:00
d7bfd84de9 CompatibilityCheck: use the shared default AID constant 2025-03-23 11:00:28 -04:00
c6963feb17 feat: Customizable ISD-R AID list
This is stored base64-encoded in shared preferences (to avoid XML
encoding issues).

By default we have the standard AID plus the 5ber one. We may add more
going forward.
2025-03-23 10:35:07 -04:00
dc6b3a4810 feat: support disabling refresh after switch in settings
peter: Reworked strings and i18 translations. Also removed the ad-hoc
function in favor of a lambda.
2025-03-16 21:04:28 -04:00
e08f8beb45 feat: add iQOO stk launch support (#179)
![image](/attachments/b2aac119-c488-41e6-a39f-eab8559cd63b)

Reviewed-on: PeterCxy/OpenEUICC#179
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-17 00:01:22 +01:00
6b169c505d fix: crash (priv) (#177)
resolves #178

```
1741836445.585 10331 13748 13748 E AndroidRuntime: FATAL EXCEPTION: main
1741836445.585 10331 13748 13748 E AndroidRuntime: Process: im.angry.openeuicc, PID: 13748
1741836445.585 10331 13748 13748 E AndroidRuntime: java.lang.RuntimeException: PrivilegedEuiccContextMarker shall only be used on Fragments or UI types that derive from Context
1741836445.585 10331 13748 13748 E AndroidRuntime: 	at im.angry.openeuicc.util.PrivilegedEuiccContextMarker$DefaultImpls.getPrivilegedEuiccMarkerContext(PrivilegedUtils.kt:18)
```

Reviewed-on: PeterCxy/OpenEUICC#177
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-17 00:01:06 +01:00
33d383a3ce ui: wizard: Keep screen on during the download process 2025-03-16 17:54:54 -04:00
291869207a We don't need a public wake lock 2025-03-16 17:35:46 -04:00
a6286ed097 feat: Acquire partial wake lock for all foreground tasks
All of our foreground tasks require the CPU to be at least awake to make
any progress. We could keep the screen on but we really only need the
partial wake lock to make sure progress is made.
2025-03-16 17:32:09 -04:00
360760b78f chore: Upgrade Android Studio and Gradle 2025-03-15 15:39:04 -04:00
b9849afe18 fix: Address multiple substitutions in string format error (#175)
Reviewed-on: PeterCxy/OpenEUICC#175
Co-authored-by: LuK1337 <priv.luk@gmail.com>
Co-committed-by: LuK1337 <priv.luk@gmail.com>
2025-03-12 01:00:10 +01:00
88eb1ce0e2 feat: update TelephonyManager preference key and implement context marker interface (#167)
Reviewed-on: PeterCxy/OpenEUICC#167
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-09 22:47:02 +01:00
74cc08ce8e chore: remove visibility attribute from euicc memory reset preference (#166)
Reviewed-on: PeterCxy/OpenEUICC#166
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-09 04:22:07 +01:00
f6c50490b8 fix: No USB_HOST feature requirement for eUICC erasure 2025-03-08 20:35:00 -05:00
c2659ddb69 fix: Don't use beginTrackedOperation for erasure. It's wrong. 2025-03-08 20:28:19 -05:00
5dd9eed4fe feat: euicc memory reset
peter: Adjusted strings and i18n translation. Also removed the arbitrary
limit on USB channels -- this is a developer option anyway.
2025-03-08 20:26:30 -05:00
17102be7cb feat: Allow forcing the use of TelephonyManager everywhere
Manual merge of #139, but removed all reference to "TMAPI" because such
a term does not exist.

Also reworked PreferenceRepository to allow extensibility from the
privileged app.
2025-03-08 17:25:26 -05:00
ece231f17b lpac-jni: Run euicc_http_cleanup() on success 2025-03-08 17:05:52 -05:00
db8063cd5f wizard: Reduce nested closures 2025-03-08 16:42:48 -05:00
d3df70501a wizard: Make sure bitmaps are recycled properly
Co-authored-by: septs <github@septs.pw>
2025-03-08 16:27:36 -05:00
53f9459aed i18n: Update missing translations 2025-03-08 16:14:37 -05:00
6557ce45a7 i18n: Update alert for low NVRAM 2025-03-08 16:09:47 -05:00
2b86d719dd ActivationCode -> LPAString
Un-confuse myself.... The term "ActivationCode" is too overloaded even
within OpenEUICC itself.
2025-03-08 16:07:41 -05:00
7edde1ffa4 wizard: Rework handling for confirmation code from LPA strings
There are more than one way to acquire an LPA string here. Let's just
store whether we need confirmation code as a boolean in state and then
use that to decide whether it is actually required in the step for
inputting details.
2025-03-08 16:05:00 -05:00
e5753ec2d9 ActivationCode::fromString -> ActivationCode::parse
Un-confusion :D
2025-03-08 15:45:58 -05:00
c528962f29 wizard: Accept deep-links with the LPA: schema
Co-authored-by: septs <github@septs.pw>
2025-03-08 15:40:30 -05:00
889b08767c Move eUICC vendor handling to an interface in util
...instead of ad-hoc functions
2025-03-08 11:34:26 -05:00
8243914588 feat: recent url sharing (#160)
![Screenshot_20250306-135020.png](/attachments/c2932882-72bd-4fb4-8955-e538d2dcd59c)

see https://developer.android.com/guide/components/activities/recents#url-sharing

Reviewed-on: PeterCxy/OpenEUICC#160
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:54:53 +01:00
2eabf719d0 refactor: EuiccChannelFragmentUtils (#164)
Reviewed-on: PeterCxy/OpenEUICC#164
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:52:19 +01:00
d068261ff9 improve stk menu handling (#162)
Reviewed-on: PeterCxy/OpenEUICC#162
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:48:32 +01:00
6a5d4b9288 Update Japanese (#159)
Crowdinで英語のXMLを同期、Stringsを比較しやすいように再成形済み。

Reviewed-on: PeterCxy/OpenEUICC#159
Co-authored-by: reindex <reindex@noreply.gitea.angry.im>
Co-committed-by: reindex <reindex@noreply.gitea.angry.im>
2025-03-08 16:47:43 +01:00
1313bfd24e fix: iccCloseLogicalChannelByPort method signature (#157)
fixes #154

Reviewed-on: PeterCxy/OpenEUICC#157
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-08 16:46:46 +01:00
99d9200c28 fix: omapi apdu interface (#152)
Reviewed-on: PeterCxy/OpenEUICC#152
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:19:16 +01:00
65c7f8de83 feat: prompt to enable disabled sim toolkit app (#153)
<video src="/attachments/fb9f210c-5960-4889-ba6a-dba4aa085a12" title="screen-20250305-130942.mp4" controls></video>

Reviewed-on: PeterCxy/OpenEUICC#153
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:18:56 +01:00
6c9063a761 chore: add zh-TW to locale-config (#155)
Reviewed-on: PeterCxy/OpenEUICC#155
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:18:03 +01:00
d5aefcaec7 refactor: usb ccid driver (#149)
Reviewed-on: PeterCxy/OpenEUICC#149
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-04 03:14:01 +01:00
ef295c9d12 Add Traditional Chinese (zh-TW) translation (#142)
Converted from Simplified Chinese

Co-authored-by: h0353914 <45159732+h0353914@users.noreply.github.com>
Reviewed-on: PeterCxy/OpenEUICC#142
Co-authored-by: h0353914 <h0353914@gmail.com>
Co-committed-by: h0353914 <h0353914@gmail.com>
2025-03-04 03:13:39 +01:00
1d67fa5cfa feat: detect used product (#147)
Reviewed-on: PeterCxy/OpenEUICC#147
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-04 03:12:40 +01:00
c8ecdee095 feat: supports for multi logical channel (#148)
Reviewed-on: PeterCxy/OpenEUICC#148
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-02-26 14:33:18 +01:00
03bfdf373c lpac-jni: Expose customizable ISDR AIDs from lpac
...so that we could expose an option to the user going forward.
2025-02-17 17:07:11 -05:00
9517f53712 chore: Update lpac dependency 2025-02-15 14:26:45 -05:00
f5074acae2 feat: alert when confirmation code is required by QR code
Closes #136

Co-authored-by: septs <github@septs.pw>
2025-02-04 22:06:58 -05:00
83 changed files with 2095 additions and 718 deletions

View file

@ -1,7 +1,7 @@
on:
push:
branches:
- 'master'
- '*'
jobs:
build-debug:

1
.idea/.gitignore generated vendored
View file

@ -9,5 +9,6 @@
/navEditor.xml
/runConfigurations.xml
/workspace.xml
/AndroidProjectSystem.xml
**/*.iml

View file

@ -1,5 +1,8 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
@ -113,5 +116,8 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -8,6 +8,30 @@
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</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>
</component>
</project>

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.20" />
<option name="version" value="1.9.24" />
</component>
</project>

View file

@ -2,18 +2,22 @@
A fully free and open-source Local Profile Assistant implementation for Android devices.
There are two variants of this project:
There are two variants of this project, OpenEUICC and 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.
- The preferred way to including OpenEUICC in a system image is to [build it along with AOSP](#building-aosp).
- __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.
- EasyEUICC: Unprivileged version that can run as a user app.
- This version supports two modes of operation:
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.
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)
- For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`
| | OpenEUICC | EasyEUICC |
|:------------------------------|:-----------------------------------------------:|:-----------------:|
| Privileged | Must be installed as system app | No |
| Internal eSIM | Supported | Unsupported |
| External (Removable) eSIM | Supported | Supported |
| USB Readers | Supported | Supported |
| Requires allowlisting by eSIM | No | Yes -- except USB |
| System Integration | Partial (carrier partner API unimplemented yet) | No |
Some side notes:
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.

View file

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:enableOnBackInvokedCallback="true"
@ -27,10 +28,27 @@
android:name="im.angry.openeuicc.ui.LogsActivity"
android:label="@string/pref_advanced_logs" />
<activity
android:name="im.angry.openeuicc.ui.IsdrAidListActivity"
android:label="@string/isdr_aid_list" />
<activity
android:exported="true"
android:name="im.angry.openeuicc.ui.wizard.DownloadWizardActivity"
android:label="@string/download_wizard" />
android:label="@string/download_wizard">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "lpa:" -->
<!-- for example: "LPA:1$..." -->
<!-- refs: https://www.iana.org/assignments/uri-schemes/prov/lpa -->
<data android:scheme="lpa"/>
<data android:sspPrefix="1$"/>
</intent-filter>
</activity>
<activity-alias
android:exported="true"

View file

@ -1,38 +1,40 @@
package im.angry.openeuicc.core
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.util.Log
import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.usb.UsbApduInterface
import im.angry.openeuicc.core.usb.getIoEndpoints
import im.angry.openeuicc.core.usb.UsbCcidContext
import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
private var seService: SEService? = null
private val usbManager by lazy {
context.getSystemService(Context.USB_SERVICE) as UsbManager
}
private suspend fun ensureSEService() {
if (seService == null || !seService!!.isConnected) {
seService = connectSEService(context)
}
}
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat,
isdrAid: ByteArray
): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(DefaultEuiccChannelManager.TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
Log.w(
DefaultEuiccChannelManager.TAG,
"OMAPI channel attempted on non-zero portId, this may or may not work."
)
}
ensureSEService()
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
Log.i(
DefaultEuiccChannelManager.TAG,
"Trying OMAPI for physical slot ${port.card.physicalSlotIndex}"
)
try {
return EuiccChannelImpl(
context.getString(R.string.omapi),
@ -43,41 +45,48 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
port,
context.preferenceRepository.verboseLoggingFlow
),
isdrAid,
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow,
).also {
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
it.lpa.setEs10xMss(60)
}
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// Failed
Log.w(
DefaultEuiccChannelManager.TAG,
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}."
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
)
}
return null
}
override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? {
val (bulkIn, bulkOut) = usbInterface.getIoEndpoints()
if (bulkIn == null || bulkOut == null) return null
val conn = usbManager.openDevice(usbDevice) ?: return null
if (!conn.claimInterface(usbInterface, true)) return null
return EuiccChannelImpl(
context.getString(R.string.usb),
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
intrinsicChannelName = usbDevice.productName,
UsbApduInterface(
conn,
bulkIn,
bulkOut,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow,
)
override fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext,
isdrAid: ByteArray
): EuiccChannel? {
try {
return EuiccChannelImpl(
context.getString(R.string.usb),
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
intrinsicChannelName = ccidCtx.productName,
UsbApduInterface(
ccidCtx
),
isdrAid,
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow,
)
} catch (_: IllegalArgumentException) {
// Failed
Log.w(
DefaultEuiccChannelManager.TAG,
"USB APDU interface unavailable for ISD-R AID: ${isdrAid.encodeHex()}."
)
}
return null
}
override fun cleanup() {

View file

@ -5,12 +5,15 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.telephony.SubscriptionManager
import android.util.Log
import im.angry.openeuicc.core.usb.getSmartCardInterface
import im.angry.openeuicc.core.usb.UsbCcidContext
import im.angry.openeuicc.core.usb.smartCard
import im.angry.openeuicc.core.usb.interfaces
import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.merge
@ -48,6 +51,24 @@ open class DefaultEuiccChannelManager(
protected open val uiccCards: Collection<UiccCardInfoCompat>
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? {
lock.withLock {
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
@ -75,9 +96,10 @@ open class DefaultEuiccChannelManager(
return null
}
val channel = euiccChannelFactory.tryOpenEuiccChannel(port) ?: return null
val channel =
tryOpenChannelFirstValidAid { euiccChannelFactory.tryOpenEuiccChannel(port, it) }
if (channel.valid) {
if (channel != null) {
channelCache.add(channel)
return channel
} else {
@ -85,7 +107,6 @@ open class DefaultEuiccChannelManager(
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."
)
channel.close()
return null
}
}
@ -211,7 +232,10 @@ open class DefaultEuiccChannelManager(
check(channel.valid) { "Invalid channel" }
break
} catch (e: Exception) {
Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms")
Log.d(
TAG,
"Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms"
)
}
delay(1000)
}
@ -244,14 +268,23 @@ open class DefaultEuiccChannelManager(
withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device ->
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
val iface = device.getSmartCardInterface() ?: return@forEach
val iface = device.interfaces.smartCard ?: return@forEach
// If we don't have permission, tell UI code that we found a candidate device, but we
// need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, false)
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
Log.i(
TAG,
"Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel"
)
val ccidCtx = UsbCcidContext.createFromUsbDevice(context, device, iface) ?: return@forEach
try {
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
val channel = tryOpenChannelFirstValidAid {
euiccChannelFactory.tryOpenUsbEuiccChannel(ccidCtx, it)
}
if (channel != null && channel.lpa.valid) {
ccidCtx.allowDisconnect = true
usbChannel = channel
return@withContext Pair(device, true)
}
@ -259,7 +292,14 @@ open class DefaultEuiccChannelManager(
// Ignored -- skip forward
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)
}

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
interface EuiccChannel {
@ -28,5 +29,15 @@ interface EuiccChannel {
*/
val intrinsicChannelName: String?
/**
* The underlying APDU interface for this channel
*/
val apduInterface: ApduInterface
/**
* The AID of the ISD-R channel currently in use
*/
val isdrAid: ByteArray
fun close()
}

View file

@ -1,15 +1,17 @@
package im.angry.openeuicc.core
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbInterface
import im.angry.openeuicc.core.usb.UsbCcidContext
import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just
// "dumb" dependency injection.
interface EuiccChannelFactory {
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel?
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat, isdrAid: ByteArray): EuiccChannel?
fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel?
fun tryOpenUsbEuiccChannel(
ccidCtx: UsbCcidContext,
isdrAid: ByteArray
): EuiccChannel?
/**
* Release all resources used by this EuiccChannelFactory

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import im.angry.openeuicc.util.UiccPortInfoCompat
import im.angry.openeuicc.util.decodeHex
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
@ -11,7 +12,8 @@ class EuiccChannelImpl(
override val type: String,
override val port: UiccPortInfoCompat,
override val intrinsicChannelName: String?,
private val apduInterface: ApduInterface,
override val apduInterface: ApduInterface,
override val isdrAid: ByteArray,
verboseLoggingFlow: Flow<Boolean>,
ignoreTLSCertificateFlow: Flow<Boolean>
) : EuiccChannel {
@ -20,7 +22,11 @@ class EuiccChannelImpl(
override val portId = port.portIndex
override val lpa: LocalProfileAssistant =
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow))
LocalProfileAssistantImpl(
isdrAid,
apduInterface,
HttpInterfaceImpl(verboseLoggingFlow, ignoreTLSCertificateFlow)
)
override val atr: ByteArray?
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
@ -33,8 +34,12 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
get() = channel.valid
override val intrinsicChannelName: String?
get() = channel.intrinsicChannelName
override val apduInterface: ApduInterface
get() = channel.apduInterface
override val atr: ByteArray?
get() = channel.atr
override val isdrAid: ByteArray
get() = channel.isdrAid
override fun close() = channel.close()

View file

@ -7,9 +7,9 @@ import android.util.Log
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface
import java.util.concurrent.atomic.AtomicInteger
class OmapiApduInterface(
private val service: SEService,
@ -21,7 +21,8 @@ class OmapiApduInterface(
}
private lateinit var session: Session
private lateinit var lastChannel: Channel
private val index = AtomicInteger(0)
private val channels = mutableMapOf<Int, Channel>()
override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@ -38,24 +39,23 @@ class OmapiApduInterface(
}
override fun logicalChannelOpen(aid: ByteArray): Int {
check(!this::lastChannel.isInitialized) {
"Can only open one channel"
}
lastChannel = session.openLogicalChannel(aid)!!
return 1
val channel = session.openLogicalChannel(aid)
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
val handle = index.incrementAndGet()
synchronized(channels) { channels[handle] = channel }
return handle
}
override fun logicalChannelClose(handle: Int) {
check(handle == 1 && !this::lastChannel.isInitialized) {
"Unknown channel"
}
lastChannel.close()
val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" }
if (channel.isOpen) channel.close()
synchronized(channels) { channels.remove(handle) }
}
override fun transmit(tx: ByteArray): ByteArray {
check(this::lastChannel.isInitialized) {
"Unknown channel"
}
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" }
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
@ -63,7 +63,7 @@ class OmapiApduInterface(
try {
for (i in 0..10) {
val res = lastChannel.transmit(tx)
val res = channel.transmit(tx)
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
}

View file

@ -1,56 +1,41 @@
package im.angry.openeuicc.core.usb
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.util.Log
import im.angry.openeuicc.core.ApduInterfaceAtrProvider
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface
class UsbApduInterface(
private val conn: UsbDeviceConnection,
private val bulkIn: UsbEndpoint,
private val bulkOut: UsbEndpoint,
private val verboseLoggingFlow: Flow<Boolean>
private val ccidCtx: UsbCcidContext
) : ApduInterface, ApduInterfaceAtrProvider {
companion object {
private const val TAG = "UsbApduInterface"
}
private lateinit var ccidDescription: UsbCcidDescription
private lateinit var transceiver: UsbCcidTransceiver
override val atr: ByteArray?
get() = ccidCtx.atr
private var channelId = -1
override val valid: Boolean
get() = channels.isNotEmpty()
override var atr: ByteArray? = null
private var channels = mutableSetOf<Int>()
override fun connect() {
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
ccidCtx.connect()
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
}
// Send Terminal Capabilities
// Specs: ETSI TS 102 221 v15.0.0 - 11.1.19 TERMINAL CAPABILITY
val terminalCapabilities = buildCmd(
0x80.toByte(), 0xaa.toByte(), 0x00, 0x00,
"A9088100820101830107".decodeHex(),
le = null,
)
transmitApduByChannel(terminalCapabilities, 0)
}
override fun disconnect() {
conn.close()
}
override fun disconnect() = ccidCtx.disconnect()
override fun logicalChannelOpen(aid: ByteArray): Int {
check(channelId == -1) { "Logical channel already opened" }
// OPEN LOGICAL CHANNEL
val req = manageChannelCmd(true, 0)
@ -66,7 +51,7 @@ class UsbApduInterface(
return -1
}
channelId = resp[0].toInt()
val channelId = resp[0].toInt()
Log.d(TAG, "channelId = $channelId")
// Then, select AID
@ -78,32 +63,32 @@ class UsbApduInterface(
return -1
}
channels.add(channelId)
return channelId
}
override fun logicalChannelClose(handle: Int) {
check(handle == channelId) { "Logical channel ID mismatch" }
check(channelId != -1) { "Logical channel is not opened" }
check(channels.contains(handle)) {
"Invalid logical channel handle $handle"
}
// CLOSE LOGICAL CHANNEL
val req = manageChannelCmd(false, channelId.toByte())
val resp = transmitApduByChannel(req, channelId.toByte())
val req = manageChannelCmd(false, handle.toByte())
val resp = transmitApduByChannel(req, handle.toByte())
if (!isSuccessResponse(resp)) {
Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
}
channelId = -1
channels.remove(handle)
}
override fun transmit(tx: ByteArray): ByteArray {
check(channelId != -1) { "Logical channel is not opened" }
return transmitApduByChannel(tx, channelId.toByte())
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
check(channels.contains(handle)) {
"Invalid logical channel handle $handle"
}
return transmitApduByChannel(tx, handle.toByte())
}
override val valid: Boolean
get() = channelId != -1
private fun isSuccessResponse(resp: ByteArray): Boolean =
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
@ -137,7 +122,7 @@ class UsbApduInterface(
// OR the channel mask into the CLA byte
realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
var resp = transceiver.sendXfrBlock(realTx).data!!
var resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
if (resp.size < 2) throw RuntimeException("APDU response smaller than 2 (sw1 + sw2)!")
@ -148,7 +133,7 @@ class UsbApduInterface(
// 0x6C = wrong le
// so we fix the le field here
realTx[realTx.size - 1] = resp[resp.size - 1]
resp = transceiver.sendXfrBlock(realTx).data!!
resp = ccidCtx.transceiver.sendXfrBlock(realTx).data!!
} else if (sw1 == 0x61) {
// 0x61 = X bytes available
// continue reading by GET RESPONSE
@ -158,7 +143,7 @@ class UsbApduInterface(
realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
)
val tmp = transceiver.sendXfrBlock(getResponseCmd).data!!
val tmp = ccidCtx.transceiver.sendXfrBlock(getResponseCmd).data!!
resp = resp.sliceArray(0 until (resp.size - 2)) + tmp

View file

@ -0,0 +1,87 @@
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
}
}
}

View file

@ -20,12 +20,12 @@ data class UsbCcidDescription(
private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000
private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000
private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000
private const val FEATURE_EXCHANGE_LEVEL_EXTENDED_APDU = 0x40000
// bVoltageSupport Masks
private const val VOLTAGE_5V: Byte = 1
private const val VOLTAGE_3V: Byte = 2
private const val VOLTAGE_1_8V: Byte = 4
private const val VOLTAGE_5V0: Byte = 1
private const val VOLTAGE_3V0: Byte = 2
private const val VOLTAGE_1V8: Byte = 4
private const val SLOT_OFFSET = 4
private const val FEATURES_OFFSET = 40
@ -71,31 +71,24 @@ data class UsbCcidDescription(
}
enum class Voltage(powerOnValue: Int, mask: Int) {
AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V(
3,
VOLTAGE_1_8V.toInt()
);
// @formatter:off
AUTO(0, 0),
V50(1, VOLTAGE_5V0.toInt()),
V30(2, VOLTAGE_3V0.toInt()),
V18(3, VOLTAGE_1V8.toInt());
// @formatter:on
val mask = powerOnValue.toByte()
val powerOnValue = mask.toByte()
}
private fun hasFeature(feature: Int): Boolean =
(dwFeatures and feature) != 0
private fun hasFeature(feature: Int) = (dwFeatures and feature) != 0
val voltages: Array<Voltage>
get() =
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) {
arrayOf(Voltage.AUTO)
} else {
Voltage.values().mapNotNull {
if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) {
it
} else {
null
}
}.toTypedArray()
}
val voltages: List<Voltage>
get() {
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) return listOf(Voltage.AUTO)
return Voltage.entries.filter { (it.mask.toInt() and bVoltageSupport.toInt()) != 0 }
}
val hasAutomaticPps: Boolean
get() = hasFeature(FEATURE_AUTOMATIC_PPS)

View file

@ -95,6 +95,7 @@ class UsbCcidTransceiver(
data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) :
Exception(msg)
@Suppress("ArrayInDataClass")
data class CcidDataBlock(
val dwLength: Int,
val bSlot: Byte,
@ -183,31 +184,26 @@ class UsbCcidTransceiver(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
Log.d(TAG, "Received $readBytes bytes: ${inputBuffer.encodeHex()}")
}
} while (readBytes <= 0 && attempts-- > 0)
if (readBytes < CCID_HEADER_LENGTH) {
throw UsbTransportException("USB-CCID error - failed to receive CCID header")
}
if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) {
if (expectedSequenceNumber != inputBuffer[6]) {
throw UsbTransportException(
((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " +
MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6]
) + " (expected " +
expectedSequenceNumber + ")"
)
}
throw UsbTransportException(
"USB-CCID error - bad CCID header type " + inputBuffer[0]
)
throw UsbTransportException(buildString {
append("USB-CCID error - bad CCID header")
append(", type ")
append("%d (expected %d)".format(inputBuffer[0], MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK))
if (expectedSequenceNumber != inputBuffer[6]) {
append(", sequence number ")
append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber))
}
})
}
var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer)
if (expectedSequenceNumber != result.bSeq) {
throw UsbTransportException(
("USB-CCID error - expected sequence number " +
expectedSequenceNumber + ", got " + result)
)
throw UsbTransportException("USB-CCID error - expected sequence number $expectedSequenceNumber, got $result")
}
val dataBuffer = ByteArray(result.dwLength)
@ -218,9 +214,7 @@ class UsbCcidTransceiver(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
if (readBytes < 0) {
throw UsbTransportException(
"USB error - failed reading response data! Header: $result"
)
throw UsbTransportException("USB error - failed reading response data! Header: $result")
}
System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes)
bufferedBytes += readBytes
@ -285,7 +279,7 @@ class UsbCcidTransceiver(
}
val ccidDataBlock = receiveDataBlock(sequenceNumber)
val elapsedTime = SystemClock.elapsedRealtime() - startTime
Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms")
Log.d(TAG, "USB XferBlock call took ${elapsedTime}ms")
return ccidDataBlock
}
@ -293,13 +287,13 @@ class UsbCcidTransceiver(
val startTime = SystemClock.elapsedRealtime()
skipAvailableInput()
var response: CcidDataBlock? = null
for (v in usbCcidDescription.voltages) {
Log.v(TAG, "CCID: attempting to power on with voltage $v")
for (voltage in usbCcidDescription.voltages) {
Log.v(TAG, "CCID: attempting to power on with voltage $voltage")
response = try {
iccPowerOnVoltage(v.powerOnValue)
iccPowerOnVoltage(voltage.powerOnValue)
} catch (e: UsbCcidErrorException) {
if (e.errorResponse.bError.toInt() == 7) { // Power select error
Log.v(TAG, "CCID: failed to power on with voltage $v")
Log.v(TAG, "CCID: failed to power on with voltage $voltage")
iccPowerOff()
Log.v(TAG, "CCID: powered off")
continue
@ -314,8 +308,11 @@ class UsbCcidTransceiver(
val elapsedTime = SystemClock.elapsedRealtime() - startTime
Log.d(
TAG,
"Usb transport connected, took " + elapsedTime + "ms, ATR=" +
response.data?.encodeHex()
buildString {
append("Usb transport connected")
append(", took ", elapsedTime, "ms")
append(", ATR=", response.data?.encodeHex())
}
)
return response
}

View file

@ -6,31 +6,22 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
class UsbTransportException(msg: String) : Exception(msg)
class UsbTransportException(message: String) : Exception(message)
fun UsbInterface.getIoEndpoints(): Pair<UsbEndpoint?, UsbEndpoint?> {
var bulkIn: UsbEndpoint? = null
var bulkOut: UsbEndpoint? = null
for (i in 0 until endpointCount) {
val endpoint = getEndpoint(i)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
if (endpoint.direction == UsbConstants.USB_DIR_IN) {
bulkIn = endpoint
} else if (endpoint.direction == UsbConstants.USB_DIR_OUT) {
bulkOut = endpoint
}
}
return Pair(bulkIn, bulkOut)
}
val UsbDevice.interfaces: Iterable<UsbInterface>
get() = (0 until interfaceCount).map(::getInterface)
fun UsbDevice.getSmartCardInterface(): UsbInterface? {
for (i in 0 until interfaceCount) {
val anInterface = getInterface(i)
if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) {
return anInterface
}
val Iterable<UsbInterface>.smartCard: UsbInterface?
get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID }
val UsbInterface.endpoints: Iterable<UsbEndpoint>
get() = (0 until endpointCount).map(::getEndpoint)
val Iterable<UsbEndpoint>.bulkPair: Pair<UsbEndpoint?, UsbEndpoint?>
get() {
val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK }
return Pair(
endpoints.find { it.direction == UsbConstants.USB_DIR_IN },
endpoints.find { it.direction == UsbConstants.USB_DIR_OUT },
)
}
return null
}

View file

@ -4,6 +4,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
@ -91,6 +92,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
}
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())
*/
@ -275,6 +282,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
updateForegroundNotification(title, iconRes)
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
try {
withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete
this@EuiccChannelManagerService.task()
@ -290,6 +299,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
postForegroundTaskFailureNotification(failureTitle)
}
} finally {
wakeLock.release()
if (isActive) {
stopSelf()
}
@ -446,30 +456,34 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
iccid: String,
enable: Boolean, // Enable or disable the profile indicated in iccid
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect
): ForegroundTaskSubscriberFlow =
) =
launchForegroundTask(
getString(R.string.task_profile_switch),
getString(R.string.task_profile_switch_failure),
R.drawable.ic_task_switch
) {
euiccChannelManager.beginTrackedOperation(slotId, portId) {
val (res, refreshed) = euiccChannelManager.withEuiccChannel(
slotId,
portId
) { channel ->
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
// Sometimes, we *can* enable or disable the profile, but we cannot
// send the refresh command to the modem because the profile somehow
// makes the modem "busy". In this case, we can still switch by setting
// refresh to false, but then the switch cannot take effect until the
// user resets the modem manually by toggling airplane mode or rebooting.
Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
} else {
Pair(true, true)
val (response, refreshed) =
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
val refresh = preferenceRepository.refreshAfterSwitchFlow.first()
val response = channel.lpa.switchProfile(iccid, enable, refresh)
if (response || !refresh) {
Pair(response, refresh)
} else {
// refresh failed, but refresh was requested
// Sometimes, we *can* enable or disable the profile, but we cannot
// send the refresh command to the modem because the profile somehow
// makes the modem "busy". In this case, we can still switch by setting
// refresh to false, but then the switch cannot take effect until the
// user resets the modem manually by toggling airplane mode or rebooting.
Pair(
channel.lpa.switchProfile(iccid, enable, refresh = false),
false
)
}
}
}
if (!res) {
if (!response) {
throw RuntimeException("Could not switch profile")
}
@ -495,4 +509,19 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
preferenceRepository.notificationSwitchFlow.first()
}
}
fun launchMemoryReset(slotId: Int, portId: Int): ForegroundTaskSubscriberFlow =
launchForegroundTask(
getString(R.string.task_euicc_memory_reset),
getString(R.string.task_euicc_memory_reset_failure),
R.drawable.ic_euicc_memory_reset
) {
euiccChannelManager.beginTrackedOperation(slotId, portId) {
euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
channel.lpa.euiccMemoryReset()
}
preferenceRepository.notificationDeleteFlow.first()
}
}
}

View file

@ -27,6 +27,13 @@ import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_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 {
companion object {
private val YES_NO = Pair(R.string.yes, R.string.no)
@ -100,26 +107,23 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
add(Item(R.string.euicc_info_access_mode, channel.type))
add(
Item(
R.string.euicc_info_removable,
formatByBoolean(channel.port.card.isRemovable, YES_NO)
)
)
add(
Item(
R.string.euicc_info_eid,
channel.lpa.eID,
copiedToastResId = R.string.toast_eid_copied
)
)
channel.lpa.euiccInfo2.let { info ->
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version))
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
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_isdr_aid, channel.isdrAid.encodeHex()))
channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) }
vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
vendorInfo.bootloaderVersion?.let { add(Item(R.string.euicc_info_bl_ver, it)) }
}
channel.lpa.euiccInfo2?.let { info ->
add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
add(Item(R.string.euicc_info_firmware_version, info.euiccFirmwareVersion.toString()))
add(Item(R.string.euicc_info_globalplatform_version, info.globalPlatformVersion.toString()))
add(Item(R.string.euicc_info_pp_version, info.ppVersion.toString()))
info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches)
?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) }
add(Item(R.string.euicc_info_free_nvram, info.freeNvram.let(::formatFreeSpace)))
}
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
// SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
@ -134,23 +138,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
}
add(Item(R.string.euicc_info_ci_type, getString(resId)))
}
add(
Item(
R.string.euicc_info_atr,
channel.atr?.encodeHex() ?: getString(R.string.information_unavailable),
copiedToastResId = R.string.toast_atr_copied,
)
)
val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
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 =
getString(
if (b) {
res.first
} else {
res.second
}
)
getString(if (b) res.first else res.second)
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val title: TextView = root.requireViewById(R.id.euicc_info_title)

View file

@ -38,8 +38,10 @@ import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
@ -55,6 +57,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private lateinit var fab: FloatingActionButton
private lateinit var profileList: RecyclerView
private var logicalSlotId: Int = -1
private lateinit var eid: String
private val adapter = EuiccProfileAdapter()
@ -131,31 +134,42 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
inflater.inflate(R.menu.fragment_euicc, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.show_notifications -> {
if (logicalSlotId != -1) {
Intent(requireContext(), NotificationsActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
startActivity(this)
}
}
true
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.show_notifications).isVisible =
logicalSlotId != -1
menu.findItem(R.id.euicc_info).isVisible =
logicalSlotId != -1
menu.findItem(R.id.euicc_memory_reset).isVisible =
runBlocking { preferenceRepository.euiccMemoryResetFlow.first() }
}
R.id.euicc_info -> {
if (logicalSlotId != -1) {
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
startActivity(this)
}
}
true
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.show_notifications -> {
Intent(requireContext(), NotificationsActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
startActivity(this)
}
else -> super.onOptionsItemSelected(item)
true
}
R.id.euicc_info -> {
Intent(requireContext(), EuiccInfoActivity::class.java).apply {
putExtra("logicalSlotId", logicalSlotId)
startActivity(this)
}
true
}
R.id.euicc_memory_reset -> {
EuiccMemoryResetFragment.newInstance(slotId, portId, eid)
.show(childFragmentManager, EuiccMemoryResetFragment.TAG)
true
}
else -> super.onOptionsItemSelected(item)
}
protected open suspend fun onCreateFooterViews(
parent: ViewGroup,
profiles: List<LocalProfileInfo>
@ -192,6 +206,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
val profiles = withEuiccChannel { channel ->
logicalSlotId = channel.logicalSlotId
eid = channel.lpa.eID
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId)
if (unfilteredProfileListFlow.value)
channel.lpa.profiles

View file

@ -0,0 +1,126 @@
package im.angry.openeuicc.ui
import android.graphics.Typeface
import android.os.Bundle
import android.text.Editable
import android.util.Log
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.EuiccChannelFragmentMarker
import im.angry.openeuicc.util.EuiccProfilesChangedListener
import im.angry.openeuicc.util.ensureEuiccChannelManager
import im.angry.openeuicc.util.euiccChannelManagerService
import im.angry.openeuicc.util.newInstanceEuicc
import im.angry.openeuicc.util.notifyEuiccProfilesChanged
import im.angry.openeuicc.util.portId
import im.angry.openeuicc.util.slotId
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
class EuiccMemoryResetFragment : DialogFragment(), EuiccChannelFragmentMarker {
companion object {
const val TAG = "EuiccMemoryResetFragment"
private const val FIELD_EID = "eid"
fun newInstance(slotId: Int, portId: Int, eid: String) =
newInstanceEuicc(EuiccMemoryResetFragment::class.java, slotId, portId) {
putString(FIELD_EID, eid)
}
}
private val eid: String by lazy { requireArguments().getString(FIELD_EID)!! }
private val confirmText: String by lazy {
getString(R.string.euicc_memory_reset_confirm_text, eid.takeLast(8))
}
private inline val isMatched: Boolean
get() = editText.text.toString() == confirmText
private var confirmed = false
private var toast: Toast? = null
set(value) {
toast?.cancel()
field = value
value?.show()
}
private val editText by lazy {
EditText(requireContext()).apply {
isLongClickable = false
typeface = Typeface.MONOSPACE
hint = Editable.Factory.getInstance()
.newEditable(getString(R.string.euicc_memory_reset_hint_text, confirmText))
}
}
private inline val alertDialog: AlertDialog
get() = requireDialog() as AlertDialog
override fun onCreateDialog(savedInstanceState: Bundle?) =
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
.setTitle(R.string.euicc_memory_reset_title)
.setMessage(getString(R.string.euicc_memory_reset_message, eid, confirmText))
.setView(editText)
// Set listener to null to prevent auto closing
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.euicc_memory_reset_invoke_button, null)
.create()
override fun onResume() {
super.onResume()
alertDialog.setCanceledOnTouchOutside(false)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
.setOnClickListener { if (!confirmed) confirmation() }
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)
.setOnClickListener { if (!confirmed) dismiss() }
}
private fun confirmation() {
toast?.cancel()
if (!isMatched) {
Log.d(TAG, buildString {
appendLine("User input is mismatch:")
appendLine(editText.text)
appendLine(confirmText)
})
val resId = R.string.toast_euicc_memory_reset_confirm_text_mismatched
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
return
}
confirmed = true
preventUserAction()
requireParentFragment().lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchMemoryReset(slotId, portId)
.onStart {
parentFragment?.notifyEuiccProfilesChanged()
val resId = R.string.toast_euicc_memory_reset_finitshed
toast = Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG)
runCatching(::dismiss)
}
.waitDone()
}
}
private fun preventUserAction() {
editText.isEnabled = false
alertDialog.setCancelable(false)
alertDialog.setCanceledOnTouchOutside(false)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
}
}

View file

@ -0,0 +1,72 @@
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)
}
}

View file

@ -20,13 +20,10 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
private const val FIELD_ICCID = "iccid"
private const val FIELD_NAME = "name"
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment {
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId)
instance.requireArguments().apply {
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String) =
newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) {
putString(FIELD_ICCID, iccid)
putString(FIELD_NAME, name)
}
return instance
}
}
@ -91,19 +88,12 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
requireParentFragment().lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid).onStart {
if (parentFragment is EuiccProfilesChangedListener) {
// Trigger a refresh in the parent fragment -- it should wait until
// any foreground task is completed before actually doing a refresh
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
euiccChannelManagerService.launchProfileDeleteTask(slotId, portId, iccid)
.onStart {
parentFragment?.notifyEuiccProfilesChanged()
runCatching(::dismiss)
}
try {
dismiss()
} catch (e: IllegalStateException) {
// Ignored
}
}.waitDone()
.waitDone()
}
}
}

View file

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout
@ -18,16 +19,16 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
companion object {
private const val FIELD_ICCID = "iccid"
private const val FIELD_CURRENT_NAME = "currentName"
const val TAG = "ProfileRenameFragment"
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment {
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId)
instance.requireArguments().apply {
putString("iccid", iccid)
putString("currentName", currentName)
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String) =
newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) {
putString(FIELD_ICCID, iccid)
putString(FIELD_CURRENT_NAME, currentName)
}
return instance
}
}
private lateinit var toolbar: Toolbar
@ -36,6 +37,14 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
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(
inflater: LayoutInflater,
container: ViewGroup?,
@ -54,7 +63,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName"))
profileRenameNewName.editText!!.setText(currentName)
toolbar.apply {
setTitle(R.string.rename)
setNavigationOnClickListener {
@ -78,12 +87,8 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
}
}
private fun showErrorAndCancel(errorStrRes: Int) {
Toast.makeText(
requireContext(),
errorStrRes,
Toast.LENGTH_LONG
).show()
private fun showErrorAndCancel(@StringRes resId: Int) {
Toast.makeText(requireContext(), resId, Toast.LENGTH_LONG).show()
renaming = false
progress.visibility = View.GONE
@ -94,17 +99,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
progress.isIndeterminate = true
progress.visibility = View.VISIBLE
val newName = profileRenameNewName.editText!!.text.toString().trim()
lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
val res = euiccChannelManagerService.launchProfileRenameTask(
slotId,
portId,
requireArguments().getString("iccid")!!,
profileRenameNewName.editText!!.text.toString().trim()
).waitDone()
val response = euiccChannelManagerService
.launchProfileRenameTask(slotId, portId, iccid, newName).waitDone()
when (res) {
when (response) {
is LocalProfileAssistant.ProfileNameTooLongException -> {
showErrorAndCancel(R.string.profile_rename_too_long)
}
@ -118,15 +121,9 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
}
else -> {
if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
parentFragment?.notifyEuiccProfilesChanged()
try {
dismiss()
} catch (e: IllegalStateException) {
// Ignored
}
runCatching(::dismiss)
}
}
}

View file

@ -77,6 +77,16 @@ open class SettingsFragment: PreferenceFragmentCompat() {
requirePreference<CheckBoxPreference>("pref_developer_ignore_tls_certificate")
.bindBooleanFlow(preferenceRepository.ignoreTLSCertificateFlow)
requirePreference<CheckBoxPreference>("pref_developer_refresh_after_switch")
.bindBooleanFlow(preferenceRepository.refreshAfterSwitchFlow)
requirePreference<CheckBoxPreference>("pref_developer_euicc_memory_reset")
.bindBooleanFlow(preferenceRepository.euiccMemoryResetFlow)
requirePreference<Preference>("pref_developer_isdr_aid_list").apply {
intent = Intent(requireContext(), IsdrAidListActivity::class.java)
}
}
protected fun <T : Preference> requirePreference(key: CharSequence) =
@ -122,7 +132,7 @@ open class SettingsFragment: PreferenceFragmentCompat() {
return true
}
private fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
protected fun CheckBoxPreference.bindBooleanFlow(flow: PreferenceFlowWrapper<Boolean>) {
lifecycleScope.launch {
flow.collect { isChecked = it }
}

View file

@ -1,13 +1,16 @@
package im.angry.openeuicc.ui.wizard
import android.app.assist.AssistContent
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@ -19,7 +22,6 @@ import im.angry.openeuicc.ui.BaseEuiccAccessActivity
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileAssistant
class DownloadWizardActivity: BaseEuiccAccessActivity() {
@ -33,6 +35,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
var downloadStarted: Boolean,
var downloadTaskID: Long,
var downloadError: LocalProfileAssistant.ProfileDownloadException?,
var skipMethodSelect: Boolean,
var confirmationCodeRequired: Boolean,
)
private lateinit var state: DownloadWizardState
@ -61,17 +65,21 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
})
state = DownloadWizardState(
null,
intent.getIntExtra("selectedLogicalSlot", 0),
"",
null,
null,
null,
false,
-1,
null
currentStepFragmentClassName = null,
selectedLogicalSlot = intent.getIntExtra("selectedLogicalSlot", 0),
smdp = "",
matchingId = null,
confirmationCode = null,
imei = null,
downloadStarted = false,
downloadTaskID = -1,
downloadError = null,
skipMethodSelect = false,
confirmationCodeRequired = false,
)
handleDeepLink()
progressBar = requireViewById(R.id.progress)
nextButton = requireViewById(R.id.download_wizard_next)
prevButton = requireViewById(R.id.download_wizard_back)
@ -111,6 +119,35 @@ 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) {
super.onSaveInstanceState(outState)
outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName)
@ -121,6 +158,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
outState.putString("imei", state.imei)
outState.putBoolean("downloadStarted", state.downloadStarted)
outState.putLong("downloadTaskID", state.downloadTaskID)
outState.putBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@ -137,6 +175,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
state.downloadStarted =
savedInstanceState.getBoolean("downloadStarted", state.downloadStarted)
state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID)
state.confirmationCode = savedInstanceState.getString("confirmationCode", state.confirmationCode)
state.confirmationCodeRequired = savedInstanceState.getBoolean("confirmationCodeRequired", state.confirmationCodeRequired)
}
private fun onPrevPressed() {
@ -212,6 +252,14 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim)
.replace(R.id.step_fragment_container, nextFrag)
.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()
}
@ -241,6 +289,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() {
protected val state: DownloadWizardState
get() = (requireActivity() as DownloadWizardActivity).state
open val keepScreenOn = false
abstract val hasNext: Boolean
abstract val hasPrev: Boolean
abstract fun createNextFragment(): DownloadWizardStepFragment?

View file

@ -1,7 +1,6 @@
package im.angry.openeuicc.ui.wizard
import android.os.Bundle
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -36,7 +35,11 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
DownloadWizardProgressFragment()
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
DownloadWizardMethodSelectFragment()
if (state.skipMethodSelect) {
DownloadWizardSlotSelectFragment()
} else {
DownloadWizardMethodSelectFragment()
}
override fun onCreateView(
inflater: LayoutInflater,
@ -51,6 +54,9 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
smdp.editText!!.addTextChangedListener {
updateInputCompleteness()
}
confirmationCode.editText!!.addTextChangedListener {
updateInputCompleteness()
}
return view
}
@ -61,6 +67,15 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
confirmationCode.editText!!.setText(state.confirmationCode)
imei.editText!!.setText(state.imei)
updateInputCompleteness()
if (state.confirmationCodeRequired) {
confirmationCode.editText!!.requestFocus()
confirmationCode.editText!!.hint =
getString(R.string.profile_download_confirmation_code_required)
} else {
confirmationCode.editText!!.hint =
getString(R.string.profile_download_confirmation_code)
}
}
override fun onPause() {
@ -69,7 +84,34 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
}
private fun updateInputCompleteness() {
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
inputComplete = isValidAddress(smdp.editText!!.text)
if (state.confirmationCodeRequired) {
inputComplete = inputComplete && confirmationCode.editText!!.text.isNotEmpty()
}
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
}

View file

@ -42,21 +42,16 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
if (result == null) return@registerForActivityResult
lifecycleScope.launch(Dispatchers.IO) {
runCatching {
requireContext().contentResolver.openInputStream(result)?.let { input ->
val bmp = BitmapFactory.decodeStream(input)
input.close()
decodeQrFromBitmap(bmp)?.let {
withContext(Dispatchers.Main) {
processLpaString(it)
}
lifecycleScope.launch {
val decoded = withContext(Dispatchers.IO) {
runCatching {
requireContext().contentResolver.openInputStream(result)?.use { input ->
BitmapFactory.decodeStream(input).use(::decodeQrFromBitmap)
}
bmp.recycle()
}
}
decoded.getOrNull()?.let { processLpaString(it) }
}
}
@ -124,9 +119,14 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
processLpaString(text.toString())
}
private fun processLpaString(s: String) {
val components = s.split("$")
if (components.size < 3 || components[0] != "LPA:1") {
private fun processLpaString(input: String) {
try {
val parsed = LPAString.parse(input)
state.smdp = parsed.address
state.matchingId = parsed.matchingId
state.confirmationCodeRequired = parsed.confirmationCodeRequired
gotoNextFragment(DownloadWizardDetailsFragment())
} catch (e: IllegalArgumentException) {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.profile_download_incorrect_lpa_string)
setMessage(R.string.profile_download_incorrect_lpa_string_message)
@ -134,21 +134,22 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
setNegativeButton(android.R.string.cancel, null)
show()
}
return
}
state.smdp = components[1]
state.matchingId = components[2]
gotoNextFragment(DownloadWizardDetailsFragment())
}
private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
private inner class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) {
private val icon = root.requireViewById<ImageView>(R.id.download_method_icon)
private val title = root.requireViewById<TextView>(R.id.download_method_title)
fun bind(item: DownloadMethod) {
icon.setImageResource(item.iconRes)
title.setText(item.titleRes)
root.setOnClickListener { item.onClick() }
root.setOnClickListener {
// If the user elected to use another download method, reset the confirmation code flag
// too
state.confirmationCodeRequired = false
item.onClick()
}
}
}

View file

@ -59,6 +59,9 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep
private val adapter = ProgressItemAdapter()
// We don't want to turn off the screen during a download
override val keepScreenOn = true
private var isDone = false
override val hasNext: Boolean

View file

@ -49,7 +49,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
get() = true
override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment =
DownloadWizardMethodSelectFragment()
if (state.skipMethodSelect) {
DownloadWizardDetailsFragment()
} else {
DownloadWizardMethodSelectFragment()
}
override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null

View file

@ -7,43 +7,65 @@ import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
private const val FIELD_SLOT_ID = "slotId"
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"
// in the definition of an interface, so the only way is to limit where the extension functions
// can be applied.
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: Bundle.() -> Unit = {}): T where T: Fragment, T: EuiccChannelFragmentMarker {
val instance = clazz.newInstance()
instance.arguments = Bundle().apply {
putInt("slotId", slotId)
putInt("portId", portId)
addArguments()
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int, addArguments: BundleSetter = {}): T
where T : Fragment, T : EuiccChannelFragmentMarker =
clazz.getDeclaredConstructor().newInstance().apply {
arguments = Bundle()
arguments!!.putInt(FIELD_SLOT_ID, slotId)
arguments!!.putInt(FIELD_PORT_ID, portId)
arguments!!.addArguments()
}
return instance
}
// Convenient methods to avoid using `channel` for these
// `channel` requires that the channel actually exists in EuiccChannelManager, which is
// not always the case during operations such as switching
val <T> T.slotId: Int where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("slotId")
val <T> T.portId: Int where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("portId")
val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
get() = requireArguments().getInt("slotId") == EuiccChannelManager.USB_CHANNEL_ID
val <T> T.slotId: Int
where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt(FIELD_SLOT_ID)
val <T> T.portId: Int
where T : Fragment, T : EuiccChannelFragmentMarker
get() = requireArguments().getInt(FIELD_PORT_ID)
val <T> T.isUsb: Boolean
where T : Fragment, T : EuiccChannelFragmentMarker
get() = slotId == EuiccChannelManager.USB_CHANNEL_ID
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: OpenEuiccContextMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: OpenEuiccContextMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
private fun <T> T.requireEuiccActivity(): BaseEuiccAccessActivity
where T : Fragment, T : OpenEuiccContextMarker =
requireActivity() as BaseEuiccAccessActivity
suspend fun <T, R> T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker {
val <T> T.euiccChannelManager: EuiccChannelManager
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()
return euiccChannelManager.withEuiccChannel(slotId, portId, fn)
}
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: OpenEuiccContextMarker =
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
suspend fun <T> T.ensureEuiccChannelManager() where T : Fragment, T : OpenEuiccContextMarker =
requireEuiccActivity().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 {
fun onEuiccProfilesChanged()

View file

@ -0,0 +1,34 @@
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('$')
}
}

View file

@ -5,11 +5,13 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.fragment.app.Fragment
import im.angry.openeuicc.OpenEuiccApplication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.Base64
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs")
@ -31,11 +33,38 @@ internal object PreferenceKeys {
// ---- Developer Options ----
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 IGNORE_TLS_CERTIFICATE = booleanPreferencesKey("ignore_tls_certificate")
val EUICC_MEMORY_RESET = booleanPreferencesKey("euicc_memory_reset")
val ISDR_AID_LIST = stringPreferencesKey("isdr_aid_list")
}
class PreferenceRepository(private val context: Context) {
const val EUICC_DEFAULT_ISDR_AID = "A0000005591010FFFFFFFF8900000100"
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
// ---- Profile Notifications ----
val notificationDownloadFlow = bindFlow(PreferenceKeys.NOTIFICATION_DOWNLOAD, true)
@ -47,26 +76,50 @@ class PreferenceRepository(private val context: Context) {
val verboseLoggingFlow = bindFlow(PreferenceKeys.VERBOSE_LOGGING, false)
// ---- Developer Options ----
val refreshAfterSwitchFlow = bindFlow(PreferenceKeys.REFRESH_AFTER_SWITCH, true)
val developerOptionsEnabledFlow = bindFlow(PreferenceKeys.DEVELOPER_OPTIONS_ENABLED, false)
val unfilteredProfileListFlow = bindFlow(PreferenceKeys.UNFILTERED_PROFILE_LIST, false)
val ignoreTLSCertificateFlow = bindFlow(PreferenceKeys.IGNORE_TLS_CERTIFICATE, false)
val euiccMemoryResetFlow = bindFlow(PreferenceKeys.EUICC_MEMORY_RESET, false)
val isdrAidListFlow = bindFlow(
PreferenceKeys.ISDR_AID_LIST,
PreferenceConstants.DEFAULT_AID_LIST,
{ Base64.getEncoder().encodeToString(it.encodeToByteArray()) },
{ Base64.getDecoder().decode(it).decodeToString() })
private fun <T> bindFlow(key: Preferences.Key<T>, defaultValue: T): PreferenceFlowWrapper<T> =
PreferenceFlowWrapper(context, key, defaultValue)
protected fun <T> bindFlow(
key: Preferences.Key<T>,
defaultValue: T,
encoder: (T) -> T = { it },
decoder: (T) -> T = { it }
): PreferenceFlowWrapper<T> =
PreferenceFlowWrapper(context, key, defaultValue, encoder, decoder)
}
class PreferenceFlowWrapper<T> private constructor(
private val context: Context,
private val key: Preferences.Key<T>,
inner: Flow<T>
inner: Flow<T>,
private val encoder: (T) -> T,
) : Flow<T> by inner {
internal constructor(context: Context, key: Preferences.Key<T>, defaultValue: T) : this(
internal constructor(
context: Context,
key: Preferences.Key<T>,
defaultValue: T,
encoder: (T) -> T,
decoder: (T) -> T
) : this(
context,
key,
context.dataStore.data.map { it[key] ?: defaultValue }
context.dataStore.data.map { it[key]?.let(decoder) ?: defaultValue },
encoder
)
suspend fun updatePreference(value: T) {
context.dataStore.edit { it[key] = value }
context.dataStore.edit { it[key] = encoder(value) }
}
}
suspend fun removePreference() {
context.dataStore.edit { it.remove(key) }
}
}

View file

@ -1,7 +1,7 @@
package im.angry.openeuicc.util
fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
require(length % 2 == 0) { "Must have an even length" }
val decodedLength = length / 2
val out = ByteArray(decodedLength)
@ -29,6 +29,19 @@ fun formatFreeSpace(size: Int): String =
"$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 {
val ret = StringBuilder()
var inQuotes = false

View file

@ -54,6 +54,9 @@ interface OpenEuiccContextMarker {
val appContainer: AppContainer
get() = openEuiccApplication.appContainer
val preferenceRepository: PreferenceRepository
get() = appContainer.preferenceRepository
val telephonyManager: TelephonyManager
get() = appContainer.telephonyManager
}
@ -86,6 +89,13 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c
}
}
inline fun <T> Bitmap.use(f: (Bitmap) -> T): T =
try {
f(this)
} finally {
recycle()
}
fun decodeQrFromBitmap(bmp: Bitmap): String? =
runCatching {
val pixels = IntArray(bmp.width * bmp.height)

View file

@ -0,0 +1,112 @@
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
)
}
}

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="21"
android:viewportHeight="21">
<path
android:pathData="m3.578,6.487c1.385,-2.384 3.966,-3.987 6.922,-3.987 4.418,0 8,3.582 8,8s-3.582,8 -8,8 -8,-3.582 -8,-8"
android:strokeWidth="1"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="m7.5,6.5l-4,0l-0,-4"
android:strokeWidth="1"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View file

@ -0,0 +1,24 @@
<?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>

View file

@ -0,0 +1,15 @@
<?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>

View file

@ -10,4 +10,9 @@
android:id="@+id/euicc_info"
android:title="@string/euicc_info"
app:showAsAction="never" />
<item
android:id="@+id/euicc_memory_reset"
android:title="@string/euicc_memory_reset"
app:showAsAction="never" />
</menu>

View file

@ -3,13 +3,17 @@
<string name="no_euicc">このアプリでアクセスできるリムーバブル eUICC カードがデバイス上で検出されていません。互換性のあるカード挿入または USB リーダーを接続してください。</string>
<string name="no_profile">この eSIM にはプロファイルがありません。</string>
<string name="unknown">不明</string>
<string name="information_unavailable">情報なし</string>
<string name="information_unavailable">情報がありません</string>
<string name="help">ヘルプ</string>
<string name="reload">スロットを再読み込み</string>
<string name="channel_name_format">論理スロット %d</string>
<string name="enabled">有効済み</string>
<string name="disabled">無効済み</string>
<string name="provider">プロバイダー:</string>
<string name="profile_class">クラス:</string>
<string name="profile_class_testing">テスト中</string>
<string name="profile_class_provisioning">プロビジョニング</string>
<string name="profile_class_operational">稼働中</string>
<string name="enable">有効化</string>
<string name="disable">無効化</string>
<string name="delete">削除</string>
@ -17,8 +21,9 @@
<string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string>
<string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string>
<string name="toast_profile_enable_failed">新しい eSIM プロファイルに切り替えることができません。</string>
<string name="toast_profile_delete_confirm_text_mismatched">入力した確認用テキストは一致していません</string>
<string name="toast_profile_delete_confirm_text_mismatched">確認文字列が一致しません</string>
<string name="toast_iccid_copied">ICCID をクリップボードにコピーしました</string>
<string name="toast_sn_copied">シリアル番号をクリップボードにコピーしました</string>
<string name="toast_eid_copied">EID をクリップボードにコピーしました</string>
<string name="toast_atr_copied">ATR をクリップボードにコピーしました</string>
<string name="usb_permission">USB の権限を許可</string>
@ -37,16 +42,17 @@
<string name="profile_download_server">サーバー (RSP / SM-DP+)</string>
<string name="profile_download_code">アクティベーションコード</string>
<string name="profile_download_confirmation_code">確認コード (オプション)</string>
<string name="profile_download_confirmation_code_required">確認コード (必須)</string>
<string name="profile_download_imei">IMEI (オプション)</string>
<string name="profile_download_low_nvram_title">ダウンロードに失敗する可能性があります</string>
<string name="profile_download_low_nvram_title">残り容量が少ない</string>
<string name="profile_download_low_nvram_message">残り容量が少ないため、ダウンロードに失敗する可能性があります。</string>
<string name="profile_download_no_lpa_string">クリップボードに LPA コードが見つかりません</string>
<string name="profile_download_incorrect_lpa_string">LPA コードを解析できません</string>
<string name="profile_download_incorrect_lpa_string_message">クリップボードまたは QR コードの内容を LPA コードとして解析できません</string>
<string name="profile_download_no_lpa_string">クリップボードに LPA コードがりません</string>
<string name="profile_download_incorrect_lpa_string">解析できません</string>
<string name="profile_download_incorrect_lpa_string_message">QR コードまたはクリップボードの内容を LPA コードとして解析できませんでした。</string>
<string name="download_wizard">ダウンロードウィザード</string>
<string name="download_wizard_back">戻る</string>
<string name="download_wizard_next">次へ</string>
<string name="download_wizard_slot_removed">選択された SIM が取り外されました</string>
<string name="download_wizard_slot_removed">選択した SIM が削除されました</string>
<string name="download_wizard_slot_select">ダウンロードする eSIM を選択または確認:</string>
<string name="download_wizard_slot_type">タイプ:</string>
<string name="download_wizard_slot_type_removable">リムーバブル</string>
@ -76,12 +82,12 @@
<string name="download_wizard_diagnostics_last_apdu_response_fail">最終の APDU レスポンス (SIM) は失敗しました</string>
<string name="download_wizard_diagnostics_last_apdu_exception">最終の APDU 例外:</string>
<string name="download_wizard_diagnostics_save">保存</string>
<string name="download_wizard_diagnostics_file_template">%s のエラー診断</string>
<string name="logs_saved_message">ログは指定されたパスに保存しました。他のアプリにシェアしますか?</string>
<string name="download_wizard_diagnostics_file_template">「%s」での診断</string>
<string name="logs_saved_message">ログは共有したパスに保存されました。別のアプリで共有しますか?</string>
<string name="profile_rename_new_name">新しいニックネーム</string>
<string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできません</string>
<string name="profile_rename_too_long">ニックネームは 64 文字以内にしてください</string>
<string name="profile_rename_failure">ニックネームの変更で予期せぬエラーが発生しました</string>
<string name="profile_rename_encoding_error">ニックネームを UTF-8 にエンコードできませんでした</string>
<string name="profile_rename_too_long">ニックネームが 64 文字を超えています</string>
<string name="profile_rename_failure">プロファイルの名前変更時に不明なエラーが発生しました</string>
<string name="profile_delete_confirm">%s のプロファイルを削除してもよろしいですか?この操作は元に戻せません。</string>
<string name="profile_delete_confirm_input">削除を確認するには「%s」を入力してください</string>
<string name="profile_notifications">通知</string>
@ -98,50 +104,69 @@
<string name="euicc_info_activity_title">eUICC 情報 (%s)</string>
<string name="euicc_info_access_mode">アクセスモード</string>
<string name="euicc_info_removable">リムーバブル</string>
<string name="euicc_info_sku">製品名</string>
<string name="euicc_info_sn">製品シリアル番号</string>
<string name="euicc_info_bl_ver">製品ブートローダーバージョン</string>
<string name="euicc_info_fw_ver">製品ファームウェアバージョン</string>
<string name="euicc_info_sgp22_version">SGP.22 バージョン</string>
<string name="euicc_info_firmware_version">eUICC OS のバージョン</string>
<string name="euicc_info_firmware_version">eUICC OS バージョン</string>
<string name="euicc_info_globalplatform_version">グローバルプラットフォームのバージョン</string>
<string name="euicc_info_sas_accreditation_number">SAS 認定番号</string>
<string name="euicc_info_pp_version">Protected Profileのバージョン</string>
<string name="euicc_info_pp_version">保護されたプロファイルのバージョン</string>
<string name="euicc_info_free_nvram">NVRAM の空き容量 (eSIM プロファイルストレージ)</string>
<string name="euicc_info_ci_type">証明書発行者 (CI)</string>
<string name="euicc_info_ci_gsma_live">GSMA プロダクション CI</string>
<string name="euicc_info_ci_type">証明書発行者 (CI)</string>
<string name="euicc_info_ci_gsma_live">GSMA ライブ CI</string>
<string name="euicc_info_ci_gsma_test">GSMA テスト CI</string>
<string name="euicc_info_ci_unknown">未知の eSIM CI</string>
<string name="euicc_info_ci_unknown">不明な eSIM CI</string>
<string name="yes">はい</string>
<string name="no">いいえ</string>
<string name="logs_save">保存</string>
<string name="logs_filename_template">%s のログ</string>
<string name="developer_options_steps">開発者になるまであと %d ステップです。</string>
<string name="developer_options_enabled">あなたは開発者になりました!</string>
<string name="isdr_aid_list_saved">カスタム ISD-R AID リストが保存されました</string>
<string name="pref_settings">設定</string>
<string name="pref_notifications">通知</string>
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。ここでは、どのタイプの通知を送信するのかを微調整できます。</string>
<string name="pref_notifications_desc">eSIM のプロファイル操作により、通信事業者に通知が送信されます。必要に応じてこの動作を微調整できます。</string>
<string name="pref_notifications_download">ダウンロード</string>
<string name="pref_notifications_download_desc">プロファイル<i>ダウンロード済み</i>の通知を送信します</string>
<string name="pref_notifications_download_desc">プロファイル<i>ダウンロード中</i>の通知を送信します</string>
<string name="pref_notifications_delete">削除</string>
<string name="pref_notifications_delete_desc">プロファイル<i>削除済み</i>の通知を送信します</string>
<string name="pref_notifications_switch">切り替え</string>
<string name="pref_notifications_switch_desc">プロファイル<i>切り替え済み</i>の通知を送信します\nこのタイプの通知は有効化しても必ず送信するとは限らないことに注意してください。</string>
<string name="pref_notifications_delete_desc">プロファイル<i>削除中</i>の通知を送信します</string>
<string name="pref_notifications_switch">切り替え</string>
<string name="pref_notifications_switch_desc">プロファイル<i>切り替え中</i>の通知を送信します\nこのタイプの通知は信頼できないことに注意してください。</string>
<string name="pref_advanced">高度な設定</string>
<string name="pref_advanced_disable_safeguard_removable_esim">有効なプロファイルの無効化と削除を許可する</string>
<string name="pref_advanced_disable_safeguard_removable_esim">プロファイルの無効化と削除を許可</string>
<string name="pref_advanced_disable_safeguard_removable_esim_desc">デフォルトでは、このアプリでデバイスに挿入された取り外し可能な eSIM の有効なプロファイルを無効化することを防いでいます。なぜなのかというと<i>時々</i>アクセスができなくなるからです。\nこのチェックボックスを ON にすることで、この保護機能を<i>解除</i>します。</string>
<string name="pref_advanced_verbose_logging">詳細ログ</string>
<string name="pref_advanced_verbose_logging_desc">詳細ログを有効化します。これには個人的な情報が含まれている可能性があります。この機能を ON にした後は、信頼できるユーザーとのみログを共有してください。</string>
<string name="pref_advanced_language">言語</string>
<string name="pref_advanced_language_desc">アプリの言語</string>
<string name="pref_advanced_logs">ログ</string>
<string name="pref_advanced_logs_desc">アプリの最新デバッグログを表示します</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_desc">非運用のプロファイルも含めます</string>
<string name="pref_developer_ignore_tls_certificate">SM-DP+ TLS 証明書を無視する</string>
<string name="pref_developer_ignore_tls_certificate_desc">SM-DP+ TLS 証明書を無視して任意の RSP を許可します</string>
<string name="pref_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_app_version">アプリバージョン</string>
<string name="pref_info_source_code">ソースコード</string>
<string name="pref_advanced_language">言語</string>
<string name="pref_advanced_language_desc">アプリの言語を選択</string>
<string name="pref_developer_unfiltered_profile_list">すべてのプロファイルを表示</string>
<string name="pref_developer_unfiltered_profile_list_desc">プロダクション以外のプロファイルも表示する</string>
<string name="profile_class">タイプ:</string>
<string name="profile_class_testing">テスティング</string>
<string name="profile_class_provisioning">準備中</string>
<string name="profile_class_operational">動作中</string>
<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>

View file

@ -37,8 +37,14 @@
<string name="profile_download_server">服务器 (RSP / SM-DP+)</string>
<string name="profile_download_code">激活码</string>
<string name="profile_download_confirmation_code">确认码 (可选)</string>
<string name="toast_sn_copied">已复制序列号到剪贴板</string>
<string name="euicc_info_sku">产品名称</string>
<string name="euicc_info_sn">产品序列号</string>
<string name="euicc_info_bl_ver">产品 Bootloader 版本</string>
<string name="euicc_info_fw_ver">产品固件版本</string>
<string name="profile_download_confirmation_code_required">确认码 (必需)</string>
<string name="profile_download_imei">IMEI (可选)</string>
<string name="profile_download_low_nvram_title">本次下载可能会失败</string>
<string name="profile_download_low_nvram_title">剩余空间不足</string>
<string name="profile_download_low_nvram_message">当前芯片的剩余空间不足,可能导致配置下载失败。\n是否继续下载</string>
<string name="logs_saved_message">日志已保存到指定路径。需要通过其他 App 分享吗?</string>
<string name="profile_rename_new_name">新昵称</string>
@ -59,6 +65,7 @@
<string name="profile_notification_delete">删除</string>
<string name="logs_save">保存日志</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_notifications">通知</string>
<string name="pref_notifications_desc">操作 eSIM 配置文件会向运营商发送通知。根据需要在此处微调此行为。</string>
@ -75,6 +82,7 @@
<string name="pref_advanced_verbose_logging_desc">详细日志中包含敏感信息,开启此功能后请仅与你信任的人共享你的日志。</string>
<string name="pref_advanced_logs">日志</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_app_version">App 版本</string>
<string name="pref_info_source_code">源码</string>
@ -139,9 +147,26 @@
<string name="pref_advanced_language">语言</string>
<string name="pref_advanced_language_desc">选择 App 语言</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_desc">在配置文件列表中包括非生产环境的配置文件</string>
<string name="pref_developer_ignore_tls_certificate">无视 SM-DP+ 的 TLS 证书</string>
<string name="pref_developer_ignore_tls_certificate_desc">允许 RSP 服务器使用任意证书</string>
<string name="information_unavailable">无信息</string>
<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>

View file

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc">在此裝置上未檢測到此應用程式可訪問的可插拔 eUICC 卡。請插入相容卡或 USB 晶片讀卡機。</string>
<string name="no_profile">此 eSIM 上還沒有設定檔</string>
<string name="unknown">未知</string>
<string name="help">幫助</string>
<string name="reload">重新載入卡槽</string>
<string name="channel_name_format">虛擬卡槽 %d</string>
<string name="enabled">已啟用</string>
<string name="disabled">已停用</string>
<string name="provider">電信業者:</string>
<string name="profile_class">類型:</string>
<string name="enable">啟用</string>
<string name="disable">停用</string>
<string name="delete">刪除</string>
<string name="rename">重新命名</string>
<string name="enable_disable_timeout">等待 eSIM 切換設定檔時逾時。這可能是您手機基頻處理器韌體中的一個錯誤。請嘗試切換飛航模式、重新啟動應用程式或重新啟動手機</string>
<string name="switch_did_not_refresh">操作成功, 但是您手機的基頻處理器沒有重新整理。您可能需要切換飛航模式或重新啟動,以便使用新的設定檔。</string>
<string name="toast_profile_enable_failed">無法切換到新的 eSIM 設定檔。</string>
<string name="toast_profile_delete_confirm_text_mismatched">輸入的確認文字不匹配</string>
<string name="toast_iccid_copied">已複製 ICCID 到剪貼簿</string>
<string name="toast_eid_copied">已複製 EID 到剪貼簿</string>
<string name="toast_atr_copied">已複製 ATR 到剪貼簿</string>
<string name="usb_permission">授予 USB 權限</string>
<string name="usb_permission_needed">需要獲得訪問 USB 晶片讀卡機的權限。</string>
<string name="usb_failed">無法透過 USB 晶片讀卡機連線到 eSIM。</string>
<string name="task_notification">長時間運行的背景作業</string>
<string name="task_profile_download">正在下載 eSIM 設定檔</string>
<string name="task_profile_download_failure">無法下載 eSIM 設定檔</string>
<string name="task_profile_rename">正在重新命名 eSIM 設定檔</string>
<string name="task_profile_rename_failure">無法重新命名 eSIM 設定檔</string>
<string name="task_profile_delete">正在刪除 eSIM 設定檔</string>
<string name="task_profile_delete_failure">無法刪除 eSIM 設定檔</string>
<string name="task_profile_switch">正在切換 eSIM 設定檔</string>
<string name="task_profile_switch_failure">無法切換 eSIM 設定檔</string>
<string name="profile_download">新增新 eSIM</string>
<string name="profile_download_server">伺服器 (RSP / SM-DP+)</string>
<string name="profile_download_code">啟用碼</string>
<string name="profile_download_confirmation_code">確認碼 (可選)</string>
<string name="toast_sn_copied">已複製序號到剪貼簿</string>
<string name="euicc_info_sku">產品名稱</string>
<string name="euicc_info_sn">產品序號</string>
<string name="euicc_info_bl_ver">產品引導程式版本</string>
<string name="euicc_info_fw_ver">產品韌體版本</string>
<string name="profile_download_confirmation_code_required">確認碼 (必需)</string>
<string name="profile_download_imei">IMEI (可選)</string>
<string name="profile_download_low_nvram_title">剩餘空間不足</string>
<string name="profile_download_low_nvram_message">目前晶片的剩餘空間不足,可能導致配置下載失敗。\n是否繼續下載</string>
<string name="logs_saved_message">日誌已儲存到指定路徑。需要透過其他 App 分享嗎?</string>
<string name="profile_rename_new_name">新名稱</string>
<string name="profile_rename_encoding_error">無法將名稱編碼為 UTF-8</string>
<string name="profile_rename_too_long">名稱長於 64 字元</string>
<string name="profile_rename_failure">重新命名設定檔時發生了未知錯誤</string>
<string name="profile_delete_confirm">您確定要刪除 %s 嗎?此動作無法還原。</string>
<string name="profile_delete_confirm_input">請輸入\'%s\'以確認刪除</string>
<string name="profile_notifications">通知列表</string>
<string name="profile_notifications_detailed_format">通知列表 (%s)</string>
<string name="profile_notifications_show">管理通知</string>
<string name="profile_notifications_help">eSIM 設定檔可以在下載、刪除、啟用或停用時向電信業者傳送通知。此處列出了要傳送的這些通知的佇列。\n\n在\"設定\"中,您可以指定是否自動傳送每種型別的通知。請注意,即使通知已傳送,也不會自動從記錄中刪除,除非佇列空間不足。\n\n在這裡您可以手動傳送或刪除每個待處理的通知。</string>
<string name="profile_notification_operation_download">已下載</string>
<string name="profile_notification_operation_delete">已刪除</string>
<string name="profile_notification_operation_enable">已啟用</string>
<string name="profile_notification_operation_disable">已停用</string>
<string name="profile_notification_process">處理</string>
<string name="profile_notification_delete">刪除</string>
<string name="logs_save">儲存日誌</string>
<string name="logs_filename_template">%s 的日誌</string>
<string name="isdr_aid_list_saved">自訂 ISD-R AID 列表已儲存</string>
<string name="pref_settings">設定</string>
<string name="pref_notifications">通知</string>
<string name="pref_notifications_desc">變更 eSIM 設定檔會向電信業者傳送通知。根據需要在此處微調此行為。</string>
<string name="pref_notifications_download">下載</string>
<string name="pref_notifications_download_desc">傳送 <i>下載</i> 設定檔的通知</string>
<string name="pref_notifications_delete">刪除</string>
<string name="pref_notifications_delete_desc">傳送 <i>刪除</i> 設定檔的通知</string>
<string name="pref_notifications_switch">切換</string>
<string name="pref_advanced_verbose_logging">記錄詳細日誌</string>
<string name="pref_advanced_verbose_logging_desc">詳細日誌中包含敏感資訊,開啟此功能後請僅與你信任的人共享你的日誌。</string>
<string name="pref_advanced_logs">日誌</string>
<string name="pref_advanced_logs_desc">檢視應用程式的最新除錯日誌</string>
<string name="pref_notifications_switch_desc">傳送 <i>切換</i> 設定檔的通知\n注意這種型別的通知是不可靠的。</string>
<string name="pref_advanced">進階</string>
<string name="pref_advanced_disable_safeguard_removable_esim">允許 停用/刪除 已啟用的設定檔</string>
<string name="pref_advanced_disable_safeguard_removable_esim_desc">預設情況下,此應用程式會阻止您停用可插拔 eSIM 中已啟用的設定檔。\n因為這樣做 <i>有時</i> 會導致無法存取。\n勾選此框以 <i>移除</i> 此保護措施。</string>
<string name="pref_developer_isdr_aid_list_desc">某些品牌的可移除 eUICC 可能會使用自己的非標準 ISD-R AID導致第三方應用程式無法存取。此 App 可以嘗試使用此清單中新增的非標準 AID但不能保證它們一定有效。</string>
<string name="pref_info">資訊</string>
<string name="pref_info_app_version">App 版本</string>
<string name="pref_info_source_code">原始碼</string>
<string name="profile_class_testing">測試</string>
<string name="profile_class_provisioning">準備中</string>
<string name="profile_class_operational">可用</string>
<string name="profile_download_no_lpa_string">未在剪貼簿上發現 LPA 碼</string>
<string name="profile_download_incorrect_lpa_string">LPA 碼解析錯誤</string>
<string name="profile_download_incorrect_lpa_string_message">無法將二維碼或剪貼簿內容解析為 LPA 碼</string>
<string name="download_wizard">下載精靈</string>
<string name="download_wizard_back">返回</string>
<string name="download_wizard_next">下一步</string>
<string name="download_wizard_slot_removed">您選擇的 SIM 卡已被移除</string>
<string name="download_wizard_slot_select">請選擇或確認下載目標 eSIM 卡槽:</string>
<string name="download_wizard_slot_type">型別:</string>
<string name="download_wizard_slot_type_removable">可插拔</string>
<string name="download_wizard_slot_type_internal">內建</string>
<string name="download_wizard_slot_type_internal_port">內建, 埠 %d</string>
<string name="download_wizard_slot_active_profile">目前設定檔:</string>
<string name="download_wizard_slot_free_space">剩餘空間:</string>
<string name="download_wizard_method_select">您想要如何下載 eSIM 設定檔?</string>
<string name="download_wizard_method_qr_code">用相機掃描二維碼</string>
<string name="download_wizard_method_gallery">從相簿選擇二維碼</string>
<string name="download_wizard_method_clipboard">從剪貼簿讀取</string>
<string name="download_wizard_method_manual">手動輸入</string>
<string name="download_wizard_details">請輸入或確認下載 eSIM 的詳細資訊:</string>
<string name="download_wizard_progress">正在下載您的 eSIM...</string>
<string name="download_wizard_progress_step_preparing">準備中</string>
<string name="download_wizard_progress_step_connecting">正在連線到伺服器</string>
<string name="download_wizard_progress_step_authenticating">正在向伺服器驗證您的裝置</string>
<string name="download_wizard_progress_step_downloading">正在下載 eSIM 設定檔</string>
<string name="download_wizard_progress_step_finalizing">正在寫入 eSIM 設定檔</string>
<string name="download_wizard_diagnostics">錯誤診斷</string>
<string name="download_wizard_diagnostics_error_code">錯誤代碼: %s</string>
<string name="download_wizard_diagnostics_last_http_status">上次 HTTP 狀態碼 (來自伺服器): %d</string>
<string name="download_wizard_diagnostics_last_http_response">上次 HTTP 應答 (來自伺服器):</string>
<string name="download_wizard_diagnostics_last_http_exception">上次 HTTP 錯誤:</string>
<string name="download_wizard_diagnostics_last_apdu_response">上次 APDU 應答 (來自 SIM): %s</string>
<string name="download_wizard_diagnostics_last_apdu_response_success">上次 APDU 應答 (來自 SIM) 是成功的</string>
<string name="download_wizard_diagnostics_last_apdu_response_fail">上次 APDU 應答 (來自 SIM) 是失敗的</string>
<string name="download_wizard_diagnostics_last_apdu_exception">上次 APDU 錯誤:</string>
<string name="download_wizard_diagnostics_save">儲存</string>
<string name="download_wizard_diagnostics_file_template">%s 的錯誤診斷</string>
<string name="euicc_info">eUICC 詳情</string>
<string name="euicc_info_activity_title">eUICC 詳情 (%s)</string>
<string name="euicc_info_access_mode">訪問方式</string>
<string name="euicc_info_removable">可插拔</string>
<string name="euicc_info_sgp22_version">SGP.22 版本</string>
<string name="euicc_info_firmware_version">eUICC OS 版本</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform 版本</string>
<string name="euicc_info_sas_accreditation_number">SAS 認證號碼</string>
<string name="euicc_info_pp_version">Protected Profile 版本</string>
<string name="euicc_info_free_nvram">NVRAM 剩餘空間 (eSIM 儲存容量)</string>
<string name="euicc_info_ci_type">證書簽發者 (CI)</string>
<string name="euicc_info_ci_gsma_live">GSMA 生產環境 CI</string>
<string name="euicc_info_ci_gsma_test">GSMA 測試 CI</string>
<string name="euicc_info_ci_unknown">未知 eSIM CI</string>
<string name="yes"></string>
<string name="no"></string>
<string name="developer_options_steps">還有 %d 步成為開發者</string>
<string name="developer_options_enabled">您現在是開發者了!</string>
<string name="pref_advanced_language">語言</string>
<string name="pref_advanced_language_desc">選擇 App 語言</string>
<string name="pref_developer">開發人員選項</string>
<string name="pref_developer_refresh_after_switch_desc">切換設定檔後是否向基帶發送刷新命令。如果發現崩潰,請嘗試停用此功能。</string>
<string name="pref_developer_unfiltered_profile_list">顯示未經過濾的設定檔列表</string>
<string name="pref_developer_unfiltered_profile_list_desc">在設定檔列表中包括非生產環境的設定檔</string>
<string name="pref_developer_ignore_tls_certificate">忽略 SM-DP+ 的 TLS 證書</string>
<string name="pref_developer_ignore_tls_certificate_desc">允許 RSP 伺服器使用任意證書</string>
<string name="information_unavailable">無資訊</string>
<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>

View file

@ -30,7 +30,10 @@
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_euicc_memory_reset_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_euicc_memory_reset_finitshed">This chip has been erased</string>
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
<string name="toast_sn_copied">Serial number copied to clipboard</string>
<string name="toast_eid_copied">EID copied to clipboard</string>
<string name="toast_atr_copied">ATR copied to clipboard</string>
@ -47,15 +50,18 @@
<string name="task_profile_delete_failure">Failed to delete eSIM profile</string>
<string name="task_profile_switch">Switching eSIM profile</string>
<string name="task_profile_switch_failure">Failed to switch eSIM profile</string>
<string name="task_euicc_memory_reset">Erasing eSIM chip</string>
<string name="task_euicc_memory_reset_failure">Failed to erase eSIM chip</string>
<string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string>
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
<string name="profile_download_confirmation_code_required">Confirmation Code (Required)</string>
<string name="profile_download_imei">IMEI (Optional)</string>
<string name="profile_download_low_nvram_title">This download may fail</string>
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
<string name="profile_download_low_nvram_title">Low remaining capacity</string>
<string name="profile_download_low_nvram_message">This profile may fail to download due to low remaining capacity.</string>
<string name="profile_download_no_lpa_string">No LPA code found in clipboard</string>
<string name="profile_download_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>
@ -123,7 +129,12 @@
<string name="euicc_info_activity_title">eUICC Info (%s)</string>
<string name="euicc_info_access_mode">Access Mode</string>
<string name="euicc_info_removable">Removable</string>
<string name="euicc_info_sku">Product Name</string>
<string name="euicc_info_sn">Product Serial Number</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_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_firmware_version">eUICC OS Version</string>
<string name="euicc_info_globalplatform_version">GlobalPlatform Version</string>
@ -136,6 +147,13 @@
<string name="euicc_info_ci_unknown">Unknown eSIM CI</string>
<string name="euicc_info_atr" translatable="false">Answer To Reset (ATR)</string>
<string name="euicc_memory_reset">Erase eUICC</string>
<string name="euicc_memory_reset_title">Erase eUICC</string>
<string name="euicc_memory_reset_message">Please confirm to delete all profiles on this chip and understand that this operation is irreversible.\n\nEID: %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="no">No</string>
@ -145,6 +163,11 @@
<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="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_notifications">Notifications</string>
<string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
@ -164,10 +187,16 @@
<string name="pref_advanced_logs">Logs</string>
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
<string name="pref_developer">Developer Options</string>
<string name="pref_developer_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_desc">Include non-production profiles in the list</string>
<string name="pref_developer_ignore_tls_certificate">Ignore SM-DP+ TLS certificate</string>
<string name="pref_developer_ignore_tls_certificate_desc">Accept any TLS certificate used by the RSP server</string>
<string name="pref_developer_euicc_memory_reset">Allow erasing eUICC</string>
<string name="pref_developer_euicc_memory_reset_desc">This is a dangerous operation and hidden by default. As an alternative, you can delete all profiles manually.</string>
<string name="pref_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_app_version">App Version</string>
<string name="pref_info_source_code">Source Code</string>

View file

@ -3,4 +3,5 @@
<locale android:name="en-US" />
<locale android:name="ja" />
<locale android:name="zh-CN" />
<locale android:name="zh-TW" />
</locale-config>

View file

@ -52,11 +52,17 @@
</PreferenceCategory>
<PreferenceCategory
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
app:key="pref_developer"
app:title="@string/pref_developer"
app:iconSpaceReserved="false">
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_refresh_after_switch"
app:summary="@string/pref_developer_refresh_after_switch_desc"
app:title="@string/pref_developer_refresh_after_switch" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_unfiltered_profile_list"
@ -69,7 +75,19 @@
app:summary="@string/pref_developer_ignore_tls_certificate_desc"
app:title="@string/pref_developer_ignore_tls_certificate" />
</PreferenceCategory>
<CheckBoxPreference
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
app:key="pref_info"

View file

@ -1,7 +1,11 @@
package im.angry.openeuicc.ui
import android.content.pm.PackageManager
import android.provider.Settings
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import im.angry.easyeuicc.R
import im.angry.openeuicc.util.SIMToolkit
import im.angry.openeuicc.util.newInstanceEuicc
@ -23,9 +27,29 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.fragment_sim_toolkit, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.open_sim_toolkit).apply {
isVisible = stk.isAvailable(slotId)
intent = stk.intent(slotId)
intent = stk[slotId]?.intent
isVisible = intent != null
}
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.open_sim_toolkit -> {
SIMToolkit.getDisabledPackageName(item.intent)?.also { packageName ->
val label = requireContext().packageManager.getApplicationLabel(packageName)
val message = getString(R.string.toast_prompt_to_enable_sim_toolkit, label)
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
}
super.onOptionsItemSelected(item) // handling intent
}
else -> super.onOptionsItemSelected(item)
}
}
private fun PackageManager.getApplicationLabel(packageName: String): CharSequence =
getApplicationLabel(getApplicationInfo(packageName, 0))

View file

@ -128,10 +128,6 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck(
}
internal class IsdrChannelAccessCheck(private val context: Context): CompatibilityCheck(context) {
companion object {
val ISDR_AID = "A0000005591010FFFFFFFF8900000100".decodeHex()
}
override val title: String
get() = context.getString(R.string.compatibility_check_isdr_channel)
override val defaultDescription: String
@ -147,7 +143,10 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili
val (validSlotIds, result) = readers.map {
try {
it.openSession().openLogicalChannel(ISDR_AID)?.close()
// Note: we ONLY check the default ISD-R AID, because this test is for the _device_,
// 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)
} catch (_: SecurityException) {
// Ignore; this is expected when everything works

View file

@ -3,65 +3,84 @@ package im.angry.openeuicc.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.annotation.ArrayRes
import im.angry.easyeuicc.R
import im.angry.openeuicc.core.EuiccChannelManager
class SIMToolkit(private val context: Context) {
private val slotSelection = getComponentNames(R.array.sim_toolkit_slot_selection)
private val slots = buildMap {
fun getComponentNames(@ArrayRes id: Int) = context.resources
.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
put(-1, getComponentNames(R.array.sim_toolkit_slot_selection))
put(0, getComponentNames(R.array.sim_toolkit_slot_1))
put(1, getComponentNames(R.array.sim_toolkit_slot_2))
}
private val packageNames = buildSet {
addAll(slotSelection.map { it.packageName })
addAll(slots.values.flatten().map { it.packageName })
operator fun get(slotId: Int): Slot? = when (slotId) {
-1, EuiccChannelManager.USB_CHANNEL_ID -> null
else -> Slot(context.packageManager, buildSet {
addAll(slots.getOrDefault(slotId, emptySet()))
addAll(slots.getOrDefault(-1, emptySet()))
})
}
private val activities = packageNames.flatMap(::getActivities).toSet()
data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
private val packageNames: Iterable<String>
get() = components.map(ComponentName::getPackageName).toSet()
.filter(packageManager::isInstalledApp)
private val launchIntent by lazy {
packageNames.firstNotNullOfOrNull(::getLaunchIntent)
}
private val launchIntent: Intent?
get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
private fun getLaunchIntent(packageName: String) = try {
val pm = context.packageManager
pm.getLaunchIntentForPackage(packageName)
} catch (_: PackageManager.NameNotFoundException) {
null
}
private val activities: Iterable<ComponentName>
get() = packageNames.flatMap(packageManager::getActivities)
.filter(ActivityInfo::exported).map { ComponentName(it.packageName, it.name) }
private fun getActivities(packageName: String): List<ComponentName> {
return try {
val pm = context.packageManager
val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
val activities = packageInfo.activities
if (activities.isNullOrEmpty()) return emptyList()
activities.filter { it.exported }.map { ComponentName(it.packageName, it.name) }
} catch (_: PackageManager.NameNotFoundException) {
emptyList()
private fun getActivityIntent(): Intent? {
for (activity in activities) {
if (!components.contains(activity)) continue
if (isDisabledState(packageManager.getComponentEnabledSetting(activity))) continue
return Intent.makeMainActivity(activity)
}
return launchIntent
}
}
private fun getComponentNames(@ArrayRes id: Int) =
context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
fun isAvailable(slotId: Int) = when (slotId) {
-1 -> false
EuiccChannelManager.USB_CHANNEL_ID -> false
else -> intent(slotId) != null
}
fun intent(slotId: Int): Intent? {
val components = slots.getOrDefault(slotId, emptySet()) + slotSelection
val intent = Intent(Intent.ACTION_MAIN, null).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
component = components.find(activities::contains)
addCategory(Intent.CATEGORY_LAUNCHER)
private fun getDisabledPackageIntent(): Intent? {
val disabledPackageName = packageNames
.find { isDisabledState(packageManager.getApplicationEnabledSetting(it)) }
?: return null
val uri = Uri.fromParts("package", disabledPackageName, null)
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
}
val intent: Intent?
get() = getActivityIntent() ?: getDisabledPackageIntent()
}
companion object {
fun getDisabledPackageName(intent: Intent?): String? {
if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null
return intent.data?.schemeSpecificPart
}
return if (intent.component != null) intent else launchIntent
}
}
private fun isDisabledState(state: Int) = when (state) {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
else -> false
}
private fun PackageManager.isInstalledApp(packageName: String) = try {
getPackageInfo(packageName, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
}
private fun PackageManager.getActivities(packageName: String) =
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList()

View file

@ -2,33 +2,36 @@
<resources>
<string name="compatibility_check">互換性のチェック</string>
<string name="open_sim_toolkit">SIM ツールキットを開く</string>
<!-- Settings -->
<!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
<string name="toast_prompt_to_enable_sim_toolkit">「%s」アプリを有効化してください</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">システムの機能</string>
<string name="compatibility_check_system_features_desc">デバイスにリムーバブル eUICC カードの管理に必要なすべての機能が備わっているかどうか。例えば基本的な電話機能や OMAPI のサポートなど。</string>
<string name="compatibility_check_system_features_no_telephony">使用しているデバイスには電話機能がありません。</string>
<string name="compatibility_check_system_features_no_omapi">使用しているデバイスまたはシステムには OMAPI のサポートを宣言していません。これは、ハードウェアからのサポートが不足していることが原因の可能性があります。または、フラグが不足していることが原因の可能性もあります。OMAPI が実際にサポートされているかどうかを判断するには次の 2 つのチェック項目を参照してください。</string>
<string name="compatibility_check_omapi_connectivity">OMAPI の接続</string>
<string name="compatibility_check_omapi_connectivity_desc">使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可していますか?</string>
<string name="compatibility_check_omapi_connectivity_desc">使用しているデバイスは、OMAPI 経由で SIM カード上のセキュアエレメントへのアクセスを許可しているか否や。</string>
<string name="compatibility_check_omapi_connectivity_fail">OMAPI 経由で SIM カードのセキュアエレメントリーダーを検出できません。このデバイスに SIM を挿入していない場合は、SIM を挿入後にこのチェックを再試行してください。</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: <b>SIM%s</b>.</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">セキュアエレメントアクセスが正常に検出されましたが、次の SIM スロットでのみ有効です: &lt;b&gt;SIM%s&lt;/b&gt;</string>
<string name="compatibility_check_isdr_channel">ISD-R チャネルアクセス</string>
<string name="compatibility_check_isdr_channel_desc">使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしていますか?</string>
<string name="compatibility_check_isdr_channel_desc">使用しているデバイスは、OMAPI 経由で eSIM への ISD-R (管理) チャネルを開くことをサポートしているか否や。</string>
<string name="compatibility_check_isdr_channel_desc_unknown">OMAPI 経由での ISD-R アクセスがサポートされているかどうかを確認できません。まだ SIM カードが挿入されていない場合は、挿入した状態で再試行してください (どの SIM カードでも構いません)。</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: <b>SIM%s</b>.</string>
<string name="compatibility_check_known_broken">既知の破損リストに掲載されていない</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">ISD-R への OMAPI アクセスは、次のスロットでのみ可能です: &lt;b&gt;SIM%s&lt;/b&gt;</string>
<string name="compatibility_check_known_broken">既知の破損リストの記載されていない</string>
<string name="compatibility_check_known_broken_desc">取り外し可能な eSIM に関連するバグがデバイスに存在しないかを確認します。</string>
<string name="compatibility_check_known_broken_fail">おっと使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。</string>
<string name="compatibility_check_known_broken_fail">おっと...使用しているデバイスには、取り外し可能な eSIM へのアクセス時にバグが存在します。これは必ずしも全く機能しないことを意味するわけではありませんが、注意して進める必要があります。</string>
<string name="compatibility_check_usb">USB カードリーダーのサポート</string>
<string name="compatibility_check_usb_desc">使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしていますか?</string>
<string name="compatibility_check_usb_desc">使用しているデバイスは、USB カードリーダー経由の eSIM の管理をサポートしているか否や。</string>
<string name="compatibility_check_usb_ok">このデバイスの標準 USB CCID リーダーを介して eSIM を管理できます (ここで他のチェック項目に失敗した場合でも)。カードリーダーを挿入し、このアプリを開いてこの方法で eSIM を管理できます。</string>
<string name="compatibility_check_usb_fail">使用しているデバイスは USB ホストとしての機能をサポートしていません。</string>
<string name="compatibility_check_verdict">判定 (USB 以外)</string>
<string name="compatibility_check_verdict_desc">これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどのくらいありますか?</string>
<string name="compatibility_check_verdict_desc">これまでのすべてのチェック項目に基づいて、デバイスに挿入された取り外し可能な eSIM の管理と互換性がある可能性はどの程度かについて</string>
<string name="compatibility_check_verdict_ok">このデバイスに挿入された取り外し可能な eSIM の使用および管理が使用できる可能性があります。</string>
<string name="compatibility_check_verdict_known_broken">挿入された取り外し可能な eSIM にアクセスするとデバイスにバグが発生することが知られています。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_ok">挿入された取り外し可能な eSIM が使用しているデバイスで管理できるかはわかりません。ただし、このデバイスは OMAPI のサポートを宣言しているため、動作する可能性はわずかに高くなります。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string>
<string name="compatibility_check_verdict_unknown">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
</resources>

View file

@ -0,0 +1,32 @@
<resources>
<string name="compatibility_check">相容性檢查</string>
<string name="open_sim_toolkit">啟動 SIM 卡應用程式</string>
<string name="compatibility_check_system_features">系統功能</string>
<string name="compatibility_check_system_features_desc">您的裝置是否具有管理可插拔 eUICC 卡所需的所有功能。例如,基本的電話功能和 OMAPI 支援。</string>
<string name="compatibility_check_system_features_no_telephony">您的裝置沒有電話功能。</string>
<string name="compatibility_check_system_features_no_omapi">您的裝置/系統未宣告支援 OMAPI。這可能是由於缺少硬體支援或者可能僅僅是由於缺少標誌。請參閱以下兩項檢查以確定 OMAPI 是否確實受支援。</string>
<string name="compatibility_check_omapi_connectivity">OMAPI 連線</string>
<string name="compatibility_check_omapi_connectivity_desc">您的裝置是否允許透過 OMAPI 存取 SIM 卡上的安全元件?</string>
<string name="compatibility_check_omapi_connectivity_fail">無法透過 OMAPI 偵測到 SIM 卡的 Secure Element。如果您尚未在此裝置中插入 SIM 卡,請嘗試插入一張 SIM 卡並重試此檢查。</string>
<string name="compatibility_check_omapi_connectivity_partial_success_sim_number">已成功檢測到可存取 Secure Element 的卡槽,但僅限於以下 SIM 卡槽:<b>SIM%s</b></string>
<string name="compatibility_check_isdr_channel">ISD-R 通道存取</string>
<string name="compatibility_check_isdr_channel_desc">您的裝置是否支援透過 OMAPI 開啟 eSIM 的 ISD-R (管理) 通道?</string>
<string name="compatibility_check_isdr_channel_desc_unknown">無法確定是否支援透過 OMAPI 進行 ISD-R 的存取。如果尚未插入,您可能需要插入 SIM 卡 (任何 SIM 卡都可以) 重試。</string>
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI 只能在以下 SIM 插槽上存取 ISD-R<b>SIM%s</b></string>
<string name="compatibility_check_known_broken">不在已知錯誤清單中</string>
<string name="compatibility_check_known_broken_desc">確保您的裝置不存在與可插拔 eSIM 相關的錯誤。</string>
<string name="compatibility_check_known_broken_fail">很抱歉,您的裝置在存取可插拔 eSIM 時存在錯誤。這並不表示完全無法使用,但我們不保證該應用在您裝置上的行為。</string>
<string name="compatibility_check_usb">USB 晶片讀卡機支援</string>
<string name="compatibility_check_usb_desc">您的裝置是否支援透過 USB 晶片讀卡機管理 eSIM</string>
<string name="compatibility_check_usb_ok">您可以透過此裝置上的標準 USB CCID 讀卡機管理 eSIM (即使您在這裡有任何其他檢查項失敗)。請插入讀卡機,然後開啟此應用程式以這種方式管理 eSIM。</string>
<string name="compatibility_check_usb_fail">您的裝置不支援 USB 晶片讀卡機。</string>
<string name="compatibility_check_verdict">結論 (USB 晶片讀卡機以外)</string>
<string name="compatibility_check_verdict_desc">根據之前的所有檢查,您的裝置與可插拔 eSIM 卡相容的可能性有多大?</string>
<string name="compatibility_check_verdict_ok">您可以使用和管理插入此裝置的可插拔 eSIM 卡。</string>
<string name="compatibility_check_verdict_known_broken">已知您的裝置在存取可插拔 eSIM 卡時存在問題。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_ok">我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。不過,您的裝置確實宣告支援 OMAPI因此它工作的可能性略高。\n%s</string>
<string name="compatibility_check_verdict_unknown_likely_fail">我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。由於您的裝置未宣告支援OMAPI因此更有可能不支援在此裝置上管理可插拔 eSIM。\n%s</string>
<string name="compatibility_check_verdict_unknown">我們無法確定是否可以在您的裝置上管理可插拔 eSIM 卡。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">然而已經載入了eSIM設定檔的可插拔 eSIM 卡仍然可以工作; 即使無法在裝置上直接管理可插拔 eSIM 卡中的設定檔,您仍然可以使用 USB 卡讀卡機來管理設定檔。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 已複製到剪貼簿</string>
</resources>

View file

@ -5,12 +5,14 @@
<item>com.android.stk/.StkMainHide</item>
<item>com.android.stk/.StkListActivity</item>
<item>com.android.stk/.StkLauncherListActivity</item>
<item>com.android.stk/.StkSelectionActivity</item>
</string-array>
<string-array name="sim_toolkit_slot_1">
<item>com.android.stk/.StkMain1</item>
<item>com.android.stk/.PrimaryStkMain</item>
<item>com.android.stk/.StkLauncherActivity</item>
<item>com.android.stk/.StkLauncherActivity_Chn</item>
<item>com.android.stk/.StkLauncherActivity1</item>
<item>com.android.stk/.StkLauncherActivityI</item>
<item>com.android.stk/.OppoStkLauncherActivity1</item>
<item>com.android.stk/.OplusStkLauncherActivity1</item>

View file

@ -9,6 +9,7 @@
<!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
<string name="toast_prompt_to_enable_sim_toolkit">Please ENABLE your \"%s\" application</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string>

View file

@ -5,23 +5,27 @@ import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.first
import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
private val tm by lazy {
(context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager
}
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context),
PrivilegedEuiccContextMarker {
override val openEuiccMarkerContext: Context
get() = context
@Suppress("NAME_SHADOWING")
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
override suspend fun tryOpenEuiccChannel(
port: UiccPortInfoCompat,
isdrAid: ByteArray
): EuiccChannel? {
val port = port as RealUiccPortInfoCompat
if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannel(port)?.let { return it }
super.tryOpenEuiccChannel(port, isdrAid)?.let { return it }
}
if (port.card.isEuicc) {
if (port.card.isEuicc || preferenceRepository.removableTelephonyManagerFlow.first()) {
Log.i(
DefaultEuiccChannelManager.TAG,
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
@ -33,21 +37,22 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
intrinsicChannelName = null,
TelephonyManagerApduInterface(
port,
tm,
telephonyManager,
context.preferenceRepository.verboseLoggingFlow
),
isdrAid,
context.preferenceRepository.verboseLoggingFlow,
context.preferenceRepository.ignoreTLSCertificateFlow,
)
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// Failed
Log.w(
DefaultEuiccChannelManager.TAG,
"TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back"
"TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex} with ISD-R AID: ${isdrAid.encodeHex()}."
)
}
}
return super.tryOpenEuiccChannel(port)
return super.tryOpenEuiccChannel(port, isdrAid)
}
}

View file

@ -18,12 +18,10 @@ class TelephonyManagerApduInterface(
const val TAG = "TelephonyManagerApduInterface"
}
private var lastChannel: Int = -1
override val valid: Boolean
// TelephonyManager channels will never become truly "invalid",
// just that transactions might return errors or nonsense
get() = lastChannel != -1
get() = channels.isNotEmpty()
private var channels = mutableSetOf<Int>()
override fun connect() {
// Do nothing
@ -31,52 +29,39 @@ class TelephonyManagerApduInterface(
override fun disconnect() {
// Do nothing
lastChannel = -1
}
override fun logicalChannelOpen(aid: ByteArray): Int {
check(lastChannel == -1) { "Already initialized" }
val hex = aid.encodeHex()
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonyManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
}
lastChannel = channel.channel
return lastChannel
channels.add(channel.channel)
return channel.channel
}
override fun logicalChannelClose(handle: Int) {
check(handle == lastChannel) { "Invalid channel handle " }
check(channels.contains(handle)) {
"Invalid logical channel handle $handle"
}
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
lastChannel = -1
channels.remove(handle)
}
override fun transmit(tx: ByteArray): ByteArray {
check(lastChannel != -1) { "Uninitialized" }
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
check(channels.contains(handle)) {
"Invalid logical channel handle $handle"
}
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
}
val cla = tx[0].toUByte().toInt()
val instruction = tx[1].toUByte().toInt()
val p1 = tx[2].toUByte().toInt()
val p2 = tx[3].toUByte().toInt()
val p3 = tx[4].toUByte().toInt()
val p4 = tx.drop(5).toByteArray().encodeHex()
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
cla,
instruction,
p1,
p2,
p3,
p4
).also {
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "TelephonyManager APDU response: $it")
}
}?.decodeHex() ?: byteArrayOf()
val result = tm.iccTransmitApduLogicalChannelByPortCompat(
port.card.physicalSlotIndex, port.portIndex, handle,
tx,
)
if (runBlocking { verboseLoggingFlow.first() })
Log.d(TAG, "TelephonyManager APDU response: $result")
return result?.decodeHex() ?: byteArrayOf()
}
}

View file

@ -6,6 +6,7 @@ import im.angry.openeuicc.core.EuiccChannelManagerFactory
import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
import im.angry.openeuicc.core.PrivilegedEuiccChannelManagerFactory
import im.angry.openeuicc.util.*
class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
override val euiccChannelManager: EuiccChannelManager by lazy {
@ -27,4 +28,8 @@ class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
override val customizableTextProvider by lazy {
PrivilegedCustomizableTextProvider(context)
}
override val preferenceRepository by lazy {
PrivilegedPreferenceRepository(context)
}
}

View file

@ -1,11 +1,17 @@
package im.angry.openeuicc.ui
import android.os.Bundle
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
class PrivilegedSettingsFragment : SettingsFragment() {
class PrivilegedSettingsFragment : SettingsFragment(), PrivilegedEuiccContextMarker {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
addPreferencesFromResource(R.xml.pref_privileged_settings)
mergePreferenceOverlay("pref_developer_overlay", "pref_developer")
// It's stupid to _disable_ things for privileged, but for now, the per-app locale picker
// is not usable for apps signed with the platform key.
// ref: <https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-15.0.0_r6/src/com/android/settings/applications/AppLocaleUtil.java#60>
@ -13,5 +19,9 @@ class PrivilegedSettingsFragment : SettingsFragment() {
// eventually work for platform-signed apps. Or, at some point we might introduce our own
// locale picker, which hopefully works whether privileged or not.
requirePreference<Preference>("pref_advanced_language").isVisible = false
// Force use TelephonyManager API
requirePreference<CheckBoxPreference>("pref_developer_removable_telephony_manager")
.bindBooleanFlow(preferenceRepository.removableTelephonyManagerFlow)
}
}

View file

@ -0,0 +1,15 @@
package im.angry.openeuicc.util
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
internal object PrivilegedPreferenceKeys {
// ---- Developer Options ----
val REMOVABLE_TELEPHONY_MANAGER = booleanPreferencesKey("removable_telephony_manager")
}
class PrivilegedPreferenceRepository(context: Context) : PreferenceRepository(context) {
// ---- Developer Options ----
val removableTelephonyManagerFlow =
bindFlow(PrivilegedPreferenceKeys.REMOVABLE_TELEPHONY_MANAGER, false)
}

View file

@ -111,15 +111,26 @@ fun TelephonyManager.iccCloseLogicalChannelByPortCompat(
}
fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat(
slotIndex: Int, portIndex: Int, channel: Int,
cla: Int, inst: Int, p1: Int, p2: Int, p3: Int, data: String?
): String? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
slotIndex: Int,
portIndex: Int,
channel: Int,
tx: ByteArray
): String? {
val cla = tx[0].toUByte().toInt()
val ins = tx[1].toUByte().toInt()
val p1 = tx[2].toUByte().toInt()
val p2 = tx[3].toUByte().toInt()
val p3 = tx[4].toUByte().toInt()
val p4 = tx.drop(5).toByteArray().encodeHex()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
iccTransmitApduLogicalChannelByPort(
slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data
slotIndex, portIndex, channel,
cla, ins, p1, p2, p3, p4
)
} else {
iccTransmitApduLogicalChannelBySlot(
slotIndex, channel, cla, inst, p1, p2, p3, data
slotIndex, channel,
cla, ins, p1, p2, p3, p4
)
}
}

View file

@ -5,10 +5,16 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.fragment.app.Fragment
import java.util.concurrent.Executors
import kotlin.coroutines.resume
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> =
suspendCoroutine { cont ->
var binder: IBinder?

View file

@ -17,4 +17,6 @@
<string name="lui_desc">使用しているデバイスは eSIM をサポートしています。モバイルネットワークに接続するには通信事業者が発行した eSIM をダウンロードするか、物理 SIM を挿入してください。</string>
<string name="lui_skip">スキップ</string>
<string name="lui_download">eSIM をダウンロード</string>
<string name="pref_developer_telephony_manager_removable">TelephonyManagerをどこでも使用</string>
<string name="pref_developer_telephony_manager_removable_desc">デフォルトでは、非特権モード (EasyEUICC) と一致するように、取り外し可能な eUICC に対して OMAPI のみが試行されます。これは、一部のデバイスではうまく機能しない可能性があります。このオプションを選択する場合、取り外し可能な eUICC でも TelephonyManager を使用することになります。</string>
</resources>

View file

@ -17,4 +17,6 @@
<string name="lui_skip">跳过</string>
<string name="lui_download">下载 eSIM</string>
<string name="telephony_manager">TelephonyManager (特权)</string>
<string name="pref_developer_telephony_manager_removable">全局使用 TelephonyManager</string>
<string name="pref_developer_telephony_manager_removable_desc">在默认情况下,可移除 eUICC 将仅使用 OMAPI。这与非特权模式 (EasyEUICC) 一致。在某些设备上 OMAPI 可能存在问题 -- 选择此选项以强制使用 TelephonyManager。</string>
</resources>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc_priv">在此裝置上找不到 eUICC 晶片。\n在某些裝置上您可能需要先在此應用的選單中啟用雙卡支援。</string>
<string name="dsds">雙卡</string>
<string name="toast_dsds_switched">雙卡支援狀態已切換。請等待基頻處理器重新啟動。</string>
<string name="footer_mep">此卡槽支援多個啟用設定檔 (MEP)。要啟用或停用此功能,請使用\"卡槽對映\"工具。</string>
<string name="slot_mapping">卡槽對映</string>
<string name="slot_mapping_logical_slot">虛擬卡槽 %d:</string>
<string name="slot_mapping_port">卡槽 %1$d 端口 %2$d</string>
<string name="slot_mapping_help">您的手機有 %1$d 個虛擬 SIM 卡槽和 %2$d 個實體 SIM 卡槽。%3$s\n\n選擇您希望每個虛擬卡槽對應的實體卡槽 和/或 \"端口\"。請注意,並非所有對映模式都受硬體支援。</string>
<string name="slot_mapping_help_mep">\n\n實體卡槽 %1$d 支援多個啟用的設定檔 (MEP)。要使用此功能,請將其 %2$d 個虛擬\"端口\"分配給上面顯示的不同虛擬卡槽。\n\n啟用 MEP 後,\"端口\"會在 OpenEUICC 中顯示為共享 eSIM 設定檔的獨立的 eSIM 卡槽。</string>
<string name="slot_mapping_help_dsds">\n支援雙卡模式但已停用。如果您的裝置帶有內建 eSIM 晶片,則預設情況下可能不會啟用。更改上面的對映或啟用雙卡以訪問您的 eSIM。</string>
<string name="slot_mapping_completed">您的新卡槽對映已設定完畢。請等待基頻處理器重新整理卡槽。</string>
<string name="slot_mapping_failure">指定的對映可能無效或硬體不支援您指定的對映。</string>
<string name="lui_title">透過下載 eSIM 連線到行動網路</string>
<string name="lui_desc">您的裝置支援 eSIM。要連線到行動網路請下載電信業者釋出的 eSIM或插入實體 SIM 卡。</string>
<string name="lui_skip">跳過</string>
<string name="lui_download">下載 eSIM</string>
<string name="telephony_manager">TelephonyManager (特權)</string>
<string name="pref_developer_telephony_manager_removable">全域使用 TelephonyManager</string>
<string name="pref_developer_telephony_manager_removable_desc">在預設情況下,可移除 eUICC 將僅使用 OMAPI。這與非特權模式 (EasyEUICC) 一致。在某些裝置上 OMAPI 可能有問題 -- 選擇此選項以強制使用 TelephonyManager。</string>
</resources>

View file

@ -22,4 +22,8 @@
<string name="lui_desc">Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM.</string>
<string name="lui_skip">Skip</string>
<string name="lui_download">Download eSIM</string>
<!-- Preference -->
<string name="pref_developer_telephony_manager_removable">Use TelephonyManager everywhere</string>
<string name="pref_developer_telephony_manager_removable_desc">By default, only OMAPI is attempted for removable eUICCs to match what is done in unprivileged mode (i.e. EasyEUICC). This may not work well on some devices. Select this option to force the use of TelephonyManager even for removable eUICCs.</string>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:isPreferenceVisible="false"
app:key="pref_developer_overlay">
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="pref_developer_removable_telephony_manager"
app:summary="@string/pref_developer_telephony_manager_removable_desc"
app:title="@string/pref_developer_telephony_manager_removable" />
</PreferenceCategory>
</PreferenceScreen>

Binary file not shown.

View file

@ -1,6 +1,7 @@
#Wed Jun 08 13:28:20 EDT 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

297
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,69 +15,104 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# 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
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +122,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,88 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
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
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
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" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -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
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# 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" "$@"

37
gradlew.bat vendored
View file

@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View file

@ -69,11 +69,13 @@ fun TelephonyManager.iccOpenLogicalChannelByPort(
): IccOpenLogicalChannelResponse =
iccOpenLogicalChannelByPort.invoke(this, slotId, portId, appletId, p2) as IccOpenLogicalChannelResponse
fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int): Boolean =
iccCloseLogicalChannelBySlot.invoke(this, slotId, channel) as Boolean
fun TelephonyManager.iccCloseLogicalChannelBySlot(slotId: Int, channel: Int) {
iccCloseLogicalChannelBySlot.invoke(this, slotId, channel)
}
fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int): Boolean =
iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel) as Boolean
fun TelephonyManager.iccCloseLogicalChannelByPort(slotId: Int, portId: Int, channel: Int) {
iccCloseLogicalChannelByPort.invoke(this, slotId, portId, channel)
}
fun TelephonyManager.iccTransmitApduLogicalChannelBySlot(
slotId: Int, channel: Int, cla: Int, instruction: Int,

View file

@ -8,7 +8,7 @@ interface ApduInterface {
fun disconnect()
fun logicalChannelOpen(aid: ByteArray): Int
fun logicalChannelClose(handle: Int)
fun transmit(tx: ByteArray): ByteArray
fun transmit(handle: Int, tx: ByteArray): ByteArray
/**
* Is this APDU connection still valid?
@ -16,4 +16,13 @@ interface ApduInterface {
* callers should further check with the LPA to fully determine the validity of a channel
*/
val valid: Boolean
}
fun <T> withLogicalChannel(aid: ByteArray, cb: ((ByteArray) -> ByteArray) -> T): T {
val handle = logicalChannelOpen(aid)
return try {
cb { transmit(handle, it) }
} finally {
logicalChannelClose(handle)
}
}
}

View file

@ -2,14 +2,31 @@ package net.typeblog.lpac_jni
/* Corresponds to EuiccInfo2 in SGP.22 */
data class EuiccInfo2(
val sgp22Version: String,
val profileVersion: String,
val euiccFirmwareVersion: String,
val globalPlatformVersion: String,
val sgp22Version: Version,
val profileVersion: Version,
val euiccFirmwareVersion: Version,
val globalPlatformVersion: Version,
val sasAccreditationNumber: String,
val ppVersion: String,
val ppVersion: Version,
val freeNvram: Int,
val freeRam: Int,
val euiccCiPKIdListForSigning: Array<String>,
val euiccCiPKIdListForVerification: Array<String>,
)
val euiccCiPKIdListForSigning: Set<String>,
val euiccCiPKIdListForVerification: Set<String>,
)
data class Version(
val major: Int,
val minor: Int,
val patch: Int,
) {
constructor(version: String) : this(version.split('.').map(String::toInt))
private constructor(parts: List<Int>) : this(parts[0], parts[1], parts[2])
operator fun compareTo(other: Version): Int {
if (major != other.major) return major - other.major
if (minor != other.minor) return minor - other.minor
return patch - other.patch
}
override fun toString() = "$major.$minor.$patch"
}

View file

@ -5,7 +5,11 @@ internal object LpacJni {
System.loadLibrary("lpac-jni")
}
external fun createContext(apduInterface: ApduInterface, httpInterface: HttpInterface): Long
external fun createContext(
isdrAid: ByteArray,
apduInterface: ApduInterface,
httpInterface: HttpInterface
): Long
external fun destroyContext(handle: Long)
external fun euiccInit(handle: Long): Int

View file

@ -10,8 +10,10 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback
import net.typeblog.lpac_jni.Version
class LocalProfileAssistantImpl(
isdrAid: ByteArray,
rawApduInterface: ApduInterface,
rawHttpInterface: HttpInterface
): LocalProfileAssistant {
@ -27,9 +29,9 @@ class LocalProfileAssistantImpl(
var lastApduResponse: ByteArray? = null
var lastApduException: Exception? = null
override fun transmit(tx: ByteArray): ByteArray =
override fun transmit(handle: Int, tx: ByteArray): ByteArray =
try {
apduInterface.transmit(tx).also {
apduInterface.transmit(handle, tx).also {
lastApduException = null
lastApduResponse = it
}
@ -76,15 +78,15 @@ class LocalProfileAssistantImpl(
private val httpInterface = HttpInterfaceWrapper(rawHttpInterface)
private var finalized = false
private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface)
private var contextHandle: Long = LpacJni.createContext(isdrAid, apduInterface, httpInterface)
init {
if (LpacJni.euiccInit(contextHandle) < 0) {
throw IllegalArgumentException("Failed to initialize LPA")
}
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf()
httpInterface.usePublicKeyIds(pkids)
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf()
httpInterface.usePublicKeyIds(pkids.toTypedArray())
}
override fun setEs10xMss(mss: Byte) {
@ -156,31 +158,29 @@ class LocalProfileAssistantImpl(
val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
if (cInfo == 0L) return null
val euiccCiPKIdListForSigning = mutableListOf<String>()
var curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
while (curr != 0L) {
euiccCiPKIdListForSigning.add(LpacJni.stringDeref(curr))
curr = LpacJni.stringArrNext(curr)
}
val euiccCiPKIdListForVerification = mutableListOf<String>()
curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
while (curr != 0L) {
euiccCiPKIdListForVerification.add(LpacJni.stringDeref(curr))
curr = LpacJni.stringArrNext(curr)
}
val ret = EuiccInfo2(
LpacJni.euiccInfo2GetSGP22Version(cInfo),
LpacJni.euiccInfo2GetProfileVersion(cInfo),
LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo),
LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo),
Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
LpacJni.euiccInfo2GetPpVersion(cInfo),
Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
euiccCiPKIdListForSigning.toTypedArray(),
euiccCiPKIdListForVerification.toTypedArray()
buildSet {
var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo)
while (cursor != 0L) {
add(LpacJni.stringDeref(cursor))
cursor = LpacJni.stringArrNext(cursor)
}
},
buildSet {
var cursor = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo)
while (cursor != 0L) {
add(LpacJni.stringDeref(cursor))
cursor = LpacJni.stringArrNext(cursor)
}
},
)
LpacJni.euiccInfo2Free(cInfo)

View file

@ -14,7 +14,7 @@ const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795
// List of GSMA Live CIs
// https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/
val PKID_GSMA_LIVE_CI = arrayOf(
val PKID_GSMA_LIVE_CI = setOf(
// GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert)
// https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt
DEFAULT_PKID_GSMA_RSP2_ROOT_CI1,
@ -25,7 +25,7 @@ val PKID_GSMA_LIVE_CI = arrayOf(
// SGP.26 v3.0, 2023-12-01
// https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf
val PKID_GSMA_TEST_CI = arrayOf(
val PKID_GSMA_TEST_CI = setOf(
// Test CI (SGP.26, NIST P256)
// https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt
"34eecf13156518d48d30bdf06853404d115f955d",

@ -1 +1 @@
Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef
Subproject commit 90f7104847d4bb392b275746da20a55177a67573

View file

@ -22,7 +22,7 @@ void interface_wrapper_init() {
"([B)I");
method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose",
"(I)V");
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B");
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "(I[B)[B");
jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
@ -53,28 +53,34 @@ apdu_interface_logical_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, u
jint ret = (*env)->CallIntMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
method_apdu_logical_channel_open, jbarr);
LPAC_JNI_EXCEPTION_RETURN;
LPAC_JNI_CTX(ctx)->logical_channel_id = ret;
return ret;
}
static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, uint8_t channel) {
static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx,
__attribute__((unused)) uint8_t channel) {
LPAC_JNI_SETUP_ENV;
jint logical_channel_id = LPAC_JNI_CTX(ctx)->logical_channel_id;
(*env)->CallVoidMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
method_apdu_logical_channel_close, channel);
method_apdu_logical_channel_close, logical_channel_id);
(*env)->ExceptionClear(env);
}
static int
apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
uint32_t tx_len) {
const int logic_channel = LPAC_JNI_CTX(ctx)->logical_channel_id;
LPAC_JNI_SETUP_ENV;
jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
(*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
method_apdu_transmit, txArr);
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(
env, LPAC_JNI_CTX(ctx)->apdu_interface,
method_apdu_transmit, logic_channel, txArr
);
LPAC_JNI_EXCEPTION_RETURN;
*rx_len = (*env)->GetArrayLength(env, ret);
*rx = calloc(*rx_len, sizeof(uint8_t));
(*env)->GetByteArrayRegion(env, ret, 0, *rx_len, *rx);
(*env)->GetByteArrayRegion(env, ret, 0, *rx_len, (jbyte *) *rx);
(*env)->DeleteLocalRef(env, txArr);
(*env)->DeleteLocalRef(env, ret);
return 0;
@ -107,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);
*rx_len = (*env)->GetArrayLength(env, rxArr);
*rx = calloc(*rx_len, sizeof(uint8_t));
(*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, *rx);
(*env)->GetByteArrayRegion(env, rxArr, 0, *rx_len, (jbyte *) *rx);
(*env)->DeleteLocalRef(env, txArr);
(*env)->DeleteLocalRef(env, rxArr);
(*env)->DeleteLocalRef(env, headersArr);

View file

@ -126,8 +126,11 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason);
if (ret < 0) {
ret = - (int) es10b_load_bound_profile_package_result.errorReason;
goto out;
}
euicc_http_cleanup(ctx);
out:
// We expect Java side to call cancelSessions after any error -- thus, `euicc_http_cleanup` is done there
// This is so that Java side can access the last HTTP and/or APDU errors when we return.

View file

@ -28,7 +28,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
string_constructor = (*env)->GetMethodID(env, string_class, "<init>",
"([BLjava/lang/String;)V");
const char _unused[1];
const jchar _unused[1];
empty_string = (*env)->NewString(env, _unused, 0);
empty_string = (*env)->NewGlobalRef(env, empty_string);
@ -37,17 +37,30 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEXPORT jlong JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_createContext(JNIEnv *env, jobject thiz,
jbyteArray isdr_aid,
jobject apdu_interface,
jobject http_interface) {
struct lpac_jni_ctx *jni_ctx = NULL;
struct euicc_ctx *ctx = NULL;
jbyte *isdr_java = NULL;
uint32_t isdr_len = 0;
uint8_t *isdr_c = NULL;
ctx = calloc(1, sizeof(struct euicc_ctx));
jni_ctx = calloc(1, sizeof(struct lpac_jni_ctx));
isdr_java = (*env)->GetByteArrayElements(env, isdr_aid, JNI_FALSE);
isdr_len = (*env)->GetArrayLength(env, isdr_aid);
isdr_c = calloc(isdr_len, sizeof(uint8_t));
memcpy(isdr_c, isdr_java, isdr_len);
(*env)->ReleaseByteArrayElements(env, isdr_aid, isdr_java, JNI_ABORT);
ctx->apdu.interface = &lpac_jni_apdu_interface;
ctx->http.interface = &lpac_jni_http_interface;
jni_ctx->apdu_interface = (*env)->NewGlobalRef(env, apdu_interface);
jni_ctx->http_interface = (*env)->NewGlobalRef(env, http_interface);
ctx->aid = (const uint8_t *) isdr_c;
ctx->aid_len = isdr_len;
ctx->userdata = (void *) jni_ctx;
return (jlong) ctx;
}
@ -60,6 +73,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_destroyContext(JNIEnv *env, jobject thiz, jl
(*env)->DeleteGlobalRef(env, jni_ctx->apdu_interface);
(*env)->DeleteGlobalRef(env, jni_ctx->http_interface);
free(jni_ctx);
free((void *) ctx->aid);
free(ctx);
}

View file

@ -8,6 +8,7 @@ _Static_assert(sizeof(void *) <= sizeof(jlong),
"jlong must be big enough to hold a platform raw pointer");
struct lpac_jni_ctx {
jint logical_channel_id;
jobject apdu_interface;
jobject http_interface;
};