Compare commits

...

72 commits

Author SHA1 Message Date
Peter Cai 01e1b2b9a4 fix: Show toasts from main thread
All checks were successful
/ build-debug (push) Successful in 6m50s
2024-06-13 20:24:42 -04:00
Peter Cai 1536343b3f fix: Add connection timeouts for notification handling
All checks were successful
/ build-debug (push) Successful in 5m22s
...and do not fail operations if notification handling fails --
notifications are best-effort.
2024-06-04 22:05:29 -04:00
Peter Cai 051bb9f1e3 ui: Fix download errors, again
All checks were successful
/ build-debug (push) Successful in 5m5s
Also improve comments so I don't keep forgetting what I did
2024-06-02 20:24:31 -04:00
Peter Cai 5498186cf1 ui: Stop refreshing profiles on every UI event
All checks were successful
/ build-debug (push) Successful in 5m6s
2024-06-02 20:12:28 -04:00
Peter Cai 2d312f2216 lpac-jni: Avoid reloading profiles from card every time
All checks were successful
/ build-debug (push) Successful in 5m57s
Some cards may run at a low baud rate which causes issues
2024-06-02 17:44:10 -04:00
Peter Cai 3869374140 ProfileDownloadFragment: make OK always appear as action
All checks were successful
/ build-debug (push) Successful in 5m19s
2024-05-20 18:09:49 -04:00
Peter Cai fc4e5739de feat: Scan QR code from gallery
Close #6.
2024-05-20 18:09:00 -04:00
Peter Cai 8eb36c77a8 workflows: Fix path
All checks were successful
/ build-debug (push) Successful in 5m25s
/ release (push) Successful in 4m56s
This is not JMP SIM Manager :)
2024-05-20 10:10:55 -04:00
Peter Cai 1a22854d05 refactor: Remove all usage of knownChannels
Some checks failed
/ build-debug (push) Has been cancelled
/ release (push) Failing after 5m29s
enumerateEuiccChannels() should return all discovered channels on its
own. Outside classes should never access the cached open channels
directly.
2024-05-09 16:08:00 -04:00
Peter Cai 5785fe2e7c EuiccChannelManager: Check for channel validity before returning
All checks were successful
/ build-debug (push) Successful in 4m32s
Related to #26. Sometimes we could open a channel but it somehow ends up
being invalid, for example for a slot that's not actually an eUICC
(???). This should be a bug somewhere else, but we should nevertheless
prevent OpenEUICC from crashing.
2024-05-07 10:06:08 -04:00
Peter Cai 043dff0f0a lpac: bump upstream
All checks were successful
/ build-debug (push) Successful in 6m10s
2024-05-04 22:02:11 -04:00
Peter Cai 59f3597874 refactor: Wrap EuiccChannelManager in an Android Service instance
All checks were successful
/ build-debug (push) Successful in 4m54s
This allows MUCH better lifecycle control over EuiccChannelManager. We
no longer have to keep all opened APDU channels open until the
application is destroyed. Instead, they can be closed as long as no
component is bound to this Service instance.

A catch is that other long-running services must bind to this service
as-needed, otherwise a binding is going to keep the service always
alive. This only affects the EuiccService implementation, and a
suspending/blocking helper function is added to deal with this case.
2024-05-04 17:29:10 -04:00
Peter Cai 0f655f1f1f workflows: Save debug symbols for releases
All checks were successful
/ build-debug (push) Successful in 5m0s
2024-04-29 19:22:30 -04:00
Peter Cai 1f6bad4222 app-unpriv: Set ndkVersion as well for stripping
Otherwise, the stripping step always fails in CI builds. This breaks
reproducibility as debug info contains path to NDK.
2024-04-29 19:22:19 -04:00
Peter Cai 03e6380570 Ditch REPRODUCIBLE_BUILD flag and set all prefix maps unconditionally 2024-04-29 19:22:12 -04:00
Peter Cai bf121e07a4 workflows: Correct build reproducibility
- REPRODUCIBLE_BUILD needs to be a string
- Fetch all history to generate versionCode correctly
2024-04-29 19:22:04 -04:00
Peter Cai 8b38a5a58d OpenEuiccService: prevent crashing when AOSP queries an unmapped slot
All checks were successful
/ build-debug (push) Successful in 5m53s
To properly fix this we need to temporarily enable disabled slots when
they are requested by AOSP. For now let's just stop OpenEUICC from
crashing.
2024-04-21 22:31:03 -04:00
Peter Cai 6e590cfd48 OpenEuiccService: stop confusing AOSP with multiple eUICCs
All checks were successful
/ build-debug (push) Successful in 4m12s
Unfortunately, AOSP is not really good at handling more than one eUICC
chips per device, even though the EuiccService interface should
technically allow for such a situation.

Let's do the next best thing -- only ever report one eUICC chip to AOSP.
If the device has an internal one, then only report that one; otherwise,
select the first available eUICC chip to report to the system.

We might make this more configurable in the future, but for now I think
this should work for most of the situations.

Note that this does NOT affect how the rest of OpenEUICC behaves. This
does mean however OpenEUICC will keep hold of some APDU channels that it
will never access via OpenEuiccService. A mitigation is to make
EuiccChannelManager close unused channels automatically after some
timeout.
2024-04-03 20:53:48 -04:00
Peter Cai 35e543ff70 unpriv: Use "SIM <id>" instead of "Logical Slot <id>"
All checks were successful
/ build-debug (push) Successful in 4m9s
It does not make sense to refer to logical slot in an unprivileged
context.
2024-03-30 18:55:14 -04:00
Peter Cai f046d40f2c privileged: Disable slot mapping support for pre-T
All checks were successful
/ build-debug (push) Successful in 4m22s
Fixes #15.
2024-03-30 15:36:30 -04:00
Peter Cai 80adac68c8 ui: Use KiB instead of KB for free space
All checks were successful
/ build-debug (push) Successful in 4m27s
Fixes #19.
2024-03-30 15:30:40 -04:00
Peter Cai 1ed5a4de38 chore: Uprev lpac
All checks were successful
/ build-debug (push) Successful in 3m55s
Fixes #20
2024-03-29 17:31:06 -04:00
Peter Cai 4b842c4afe feat: Add an "verdict" to compatibility checks
All checks were successful
/ build-debug (push) Successful in 3m42s
2024-03-23 20:56:42 -04:00
Peter Cai 2517fc817e ComaptibilityCheck: Clarify "Known Broken"
All checks were successful
/ build-debug (push) Successful in 3m54s
2024-03-23 11:17:10 -04:00
Peter Cai e48f9aa828 refactor: Channel validity, and reconnection
All checks were successful
/ build-debug (push) Successful in 4m51s
* ApduInterfaces also need a concept of validity based on the underlying
  APDU channel. For example, OMAPI depends on SEService being still
  connected.
* We then rely on this validity to wait for reconnection; we do not need
  to manually remove all channels under a slot because the rest will be
  invalid anyway, and the next attempt at connection will lazily
  recreate the channel.
* We had to manage channels manually before during reconnect because
  `valid` may result in SIGSEGV's when the underlying APDU channel has
  become invalid. This is avoided by the validity concept added to APDU
  channels.
2024-03-22 21:08:59 -04:00
Peter Cai 1ac683f9ab refactor: Reconnecting channels is a EuiccChannelManager responsibility
All checks were successful
/ build-debug (push) Successful in 4m13s
Reconnecting did not work properly for OMAPI, because in that case we
have to reconnect SEService as well.
2024-03-21 22:21:24 -04:00
Peter Cai 7834e0348a refactor: Move notification tracking logic to EuiccChannelFragmentUtils 2024-03-21 21:29:20 -04:00
Peter Cai 92d8f9079f EuiccManagementFragment: Show alert dialog if timed out waiting for SIM
All checks were successful
/ build-debug (push) Successful in 4m17s
2024-03-21 21:16:14 -04:00
Peter Cai d9d0cf2e75 CompatibilityCheck: Show unknown status if OMAPI feature flag is not found
All checks were successful
/ build-debug (push) Successful in 3m49s
2024-03-20 20:03:45 -04:00
Peter Cai 999462c294 ui: Hide spinner when no eSIM is found
All checks were successful
/ build-debug (push) Successful in 4m42s
2024-03-17 14:19:07 -04:00
Peter Cai 2061e6fea3 ui: Better placeholder for the no-eSIM case 2024-03-17 14:02:38 -04:00
Peter Cai 6977a32e80 refactor: EuiccChannel is not abstract
All checks were successful
/ build-debug (push) Successful in 4m19s
2024-03-17 11:19:37 -04:00
Peter Cai 3a0d805eb2 treewide: Nullability fixes for AOSP 14 r29
All checks were successful
/ build-debug (push) Successful in 4m48s
Mainly a treewide `findViewById` -> `requireViewById`, with
miscellaneous fixes.
2024-03-11 18:55:32 -04:00
Peter Cai 348395c48d chore: Bump lpac
All checks were successful
/ build-debug (push) Successful in 4m21s
/ release (push) Successful in 4m26s
2024-03-05 20:14:21 -05:00
Peter Cai 124d1690ab fix: Clear status icon when compat check items are recycled 2024-03-05 20:12:19 -05:00
Peter Cai 8ee3c53492 buildSrc: Use HEAD rev count as version code
In Actions, we do not always have a checkout of the master branch.

This only applies to release builds anyway. For debug builds, we always
use the timestamp.
2024-03-05 20:07:49 -05:00
Peter Cai ca0085e147 fix: Do not crash if a certificate key ID is not known
Some checks failed
/ build-debug (push) Successful in 3m25s
/ release (push) Failing after 2m2s
Fixes #17.
2024-03-04 20:07:43 -05:00
Peter Cai 09e19412e3 fix: Show less logs in UI than what we will save
All checks were successful
/ build-debug (push) Successful in 3m21s
...to avoid the UI getting stuck due to the sheer amount of lines.
2024-03-04 19:59:10 -05:00
Peter Cai 6c2b1675bd fixup: Infinite loop in PreferenceUtils after adopting DI
All checks were successful
/ build-debug (push) Successful in 3m48s
2024-03-04 19:35:17 -05:00
Peter Cai 1a69c5294b refactor: Use DI techniques for EuiccChannel's as well 2024-03-04 19:30:04 -05:00
Peter Cai 7c6b4ebee5 refactor: IEuiccChannelManager -> EuiccChannelManager
All checks were successful
/ build-debug (push) Successful in 3m27s
2024-03-04 19:06:05 -05:00
Peter Cai 4dd14d23f2 refactor: Add UiComponentFactory to manual DI
All checks were successful
/ build-debug (push) Successful in 4m3s
2024-03-04 18:51:10 -05:00
Peter Cai 6356601467 CompatibilityCheck: Make connectivity fail a "Unknown" failure
All checks were successful
/ build-debug (push) Successful in 3m36s
2024-03-04 18:43:37 -05:00
Peter Cai aef399dad0 CompatibilityCheck: Explain that the user might want to contact the ROM developer
All checks were successful
/ build-debug (push) Successful in 3m33s
2024-03-04 18:39:40 -05:00
Peter Cai a101ae6805 CompatibilityCheck: Improve OMAPI connectivity check
Some checks failed
/ build-debug (push) Has been cancelled
Stop failing the test if only some slots can be seen. Display a text
warning users of that, but don't appear as a failure.
2024-03-04 18:38:15 -05:00
Peter Cai 49af0ffee9 CompatibilityCheck: Return FAILURE_UNKNOWN when no SIM readers are found 2024-03-04 18:23:53 -05:00
Peter Cai 2d1c96023a refactor: Condense dependency management to a rudimentary dependency injection subpackage
All checks were successful
/ build-debug (push) Successful in 4m16s
2024-03-04 17:17:20 -05:00
Peter Cai 770083523d refactor: Extract an interface from EuiccChannelManager
All checks were successful
/ build-debug (push) Successful in 4m45s
Eventually, we would like EuiccChannelManager to become a Service
instead of just any random class.
2024-03-03 20:29:18 -05:00
Peter Cai e48a919335 feat: Allow exporting logs as txt
All checks were successful
/ build-debug (push) Successful in 4m2s
2024-03-03 13:22:46 -05:00
Peter Cai c0d1c29b7f feat: Show error logs on crash when unprivileged
All checks were successful
/ build-debug (push) Successful in 3m45s
...however, don't do this in privileged mode because OpenEuiccService is
supposed to be background, and we don't want to just randomly show up
when things go wrong.
2024-03-03 10:55:35 -05:00
Peter Cai 62e3e41c52 lpac-jni: Mark network-related LPA methods as synchronized
All checks were successful
/ build-debug (push) Successful in 4m3s
These methods rely on non-thread-safe internal states within lpac's
context.
2024-02-28 19:57:47 -05:00
Peter Cai a1b2643625 lpac-jni: Always add GSMA ROOT CI1
All checks were successful
/ build-debug (push) Successful in 3m48s
2024-02-27 20:06:08 -05:00
Peter Cai 6d200d14ac nuke asn1c in Android.bp and Android.mk
All checks were successful
/ build-debug (push) Successful in 3m54s
2024-02-27 19:50:29 -05:00
Peter Cai 12d02ee76c lpac-jni: malloc -> calloc
All checks were successful
/ build-debug (push) Successful in 3m45s
2024-02-25 15:00:24 -05:00
Peter Cai 412fd31477 lpac-jni: Uprev lpac libeuicc
* use `-z muldefs` temporarily to work around upstream bug.
2024-02-25 14:57:52 -05:00
Peter Cai 19c63113a1 ProfileDeleteFragment: Require confirmation via inputting profile name
All checks were successful
/ build-debug (push) Successful in 4m4s
2024-02-25 13:28:59 -05:00
Peter Cai 2a8fb99ed0 lpac-jni: Assert jlong is enough to hold a platform sized pointer
All checks were successful
/ build-debug (push) Successful in 3m48s
2024-02-24 16:18:15 -05:00
Peter Cai 18cd9acdb8 lpac-jni: Call es9p_ctx_free
All checks were successful
/ build-debug (push) Successful in 4m5s
2024-02-24 16:10:55 -05:00
Peter Cai 4ded234ed2 lpac-jni: Reformat JNI C code
Some checks failed
/ build-debug (push) Has been cancelled
2024-02-24 16:09:57 -05:00
Peter Cai 5aed27513f lpac-jni: Uprev lpac
All checks were successful
/ build-debug (push) Successful in 3m56s
2024-02-24 15:53:58 -05:00
Peter Cai 2b972badaa lpac-jni: Add Entrust CI to known list
All checks were successful
/ build-debug (push) Successful in 3m52s
2024-02-21 21:28:54 -05:00
Peter Cai ab76ae66e2 lpac-jni: Do not crash on unknown CIs
Some checks failed
/ build-debug (push) Has been cancelled
2024-02-21 21:26:45 -05:00
Peter Cai c033ef5ba9 refactor: Trust SM-DP+ TLS certs based on euiccCiPKIdListForVerification
All checks were successful
/ build-debug (push) Successful in 4m39s
Unfortunately, because there is no way to access the certificate itself
from the eUICC, we have to hard-code known & supported certificates
still.

However, this approach makes sure that only those certificates listed by
the eUICC are trusted during their SM-DP+ sessions. Were these added
directly as part of the Android security config, then all certificates
would be blindly trusted for all SM-DP+ sessions (and even normal TLS
connections if the app were to make them).

As a result we can now trust more known certificates, including GSMA
Test CIs. These are hard-coded as a hash map.
2024-02-21 21:09:20 -05:00
Peter Cai 252000660a CompatibilityCheck: show unknown status when "secure element is not present"
All checks were successful
/ build-debug (push) Successful in 4m3s
Some devices "optimize" their OMAPI by reporting this status when both
slots are empty. Even just inserting one SIM would fix this error for
both slots.

In this case, we should not imply that the device is incompatible.
2024-02-19 17:04:03 -05:00
Peter Cai 048764d305 refactor: Comaptibility checks should return the success / failure state directly 2024-02-19 16:42:39 -05:00
Peter Cai 1c0ddefad9 lpac-jni: Introduce convenience macros for linked lists
All checks were successful
/ build-debug (push) Successful in 4m41s
2024-02-18 21:08:37 -05:00
Peter Cai 1c7dc67803 chore: Synchronize with upstream lpac changes 2024-02-18 20:56:20 -05:00
Peter Cai 9f3977dc5e README: Fix fragments
All checks were successful
/ build-debug (push) Successful in 3m49s
2024-02-18 14:09:06 -05:00
Peter Cai 77fcc14dca Rewrite parts of README
Some checks failed
/ build-debug (push) Has been cancelled
2024-02-18 14:08:21 -05:00
Peter Cai f90f44ee53 Relicense lpac-jni to LGPLv2 to match lpac 2024-02-18 13:52:48 -05:00
Peter Cai e587af9714 workflows: Run only on runners with android app keystore
All checks were successful
/ build-debug (push) Successful in 3m45s
2024-02-15 19:07:30 -05:00
Peter Cai a18867cbaa feat: Add Forgejo Actions workflow
All checks were successful
/ build-debug (push) Successful in 4m32s
* Build debug artifacts for both EasyEUICC and OpenEUICC at every commit
  on master.
* Release EasyEUICC apk when a tag is created. No OpenEUICC release will
  be uploaded.
2024-02-11 21:20:00 -05:00
93 changed files with 2594 additions and 812 deletions

View file

@ -0,0 +1,45 @@
on:
push:
branches:
- 'master'
jobs:
build-debug:
runs-on: [docker, android-app-certs]
container:
volumes:
- android-app-keystore:/keystore
steps:
- name: Repository Checkout
uses: https://gitea.angry.im/actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Decode Secret Signing Configuration
uses: https://gitea.angry.im/actions/base64-to-file@v1
with:
fileName: keystore.properties
fileDir: ${{ env.GITHUB_WORKSPACE }}
encodedString: ${{ secrets.OPENEUICC_SIGNING_CONFIG }}
- name: Set up JDK 17
uses: https://gitea.angry.im/actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: https://gitea.angry.im/actions/setup-android@v3
- name: Build Debug APKs
run: ./gradlew --no-daemon assembleDebug
- name: Upload Artifacts
uses: https://gitea.angry.im/actions/upload-artifact@v3
with:
name: Debug APKs
compression-level: 0
path: |
app-unpriv/build/outputs/apk/debug/app-unpriv-debug.apk
app/build/outputs/apk/debug/app-debug.apk

View file

@ -0,0 +1,49 @@
on:
push:
tags: '*'
jobs:
release:
runs-on: [docker, android-app-certs]
container:
volumes:
- android-app-keystore:/keystore
steps:
- name: Repository Checkout
uses: https://gitea.angry.im/actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Decode Secret Signing Configuration
uses: https://gitea.angry.im/actions/base64-to-file@v1
with:
fileName: keystore.properties
fileDir: ${{ env.GITHUB_WORKSPACE }}
encodedString: ${{ secrets.OPENEUICC_SIGNING_CONFIG }}
- name: Set up JDK 17
uses: https://gitea.angry.im/actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: https://gitea.angry.im/actions/setup-android@v3
- name: Build Release APK (Unprivileged / EasyEUICC only)
run: ./gradlew --no-daemon :app-unpriv:assembleRelease
- name: Copy Debug Symbols to Release Path
run: cp app-unpriv/build/outputs/native-debug-symbols/release/native-debug-symbols.zip app-unpriv/build/outputs/apk/release/
- name: Create Release
uses: https://gitea.angry.im/actions/forgejo-release@v1
with:
direction: upload
release-dir: app-unpriv/build/outputs/apk/release
url: https://gitea.angry.im
token: ${{ secrets.FORGEJO_TOKEN }}
# Release details are expected to be edited manually
release-notes: TBD
prerelease: 'true'

View file

@ -5,16 +5,15 @@ A fully free and open-source Local Profile Assistant implementation for Android
There are two variants of this project:
- OpenEUICC: The full-fledged privileged variant. Intended to be run as a privileged system app (inside `/system/priv-app`) and serve as the system LPA. This can be used to manage all kinds of eSIM chips, embedded or removable.
- The privileged variant can be imported to build along with AOSP by simply placing this repository and its [dependencies](https://gitea.angry.im/PeterCxy/android_prebuilts_openeuicc-deps) inside the AOSP tree.
- Notes:
- This repository contains submodules. If inclusion in `manifest.xml` is required, remember to set the `sync-s` option.
- **Only the latest AOSP release** is supported for building. Older versions of AOSP are still compatible with the app itself, but it may not compile within the old AOSP trees. For older versions, consider building the app with `gradle` or a newer AOSP source tree and simply import as a prebuilt apk.
- EasyEUICC: Unprivileged version that can run as a user app. An eSIM chip must include the certificate of EasyEUICC in its ARA-M field in order to grant access without system privileges. This is intended for removable eSIM chips such as those provided by eSTK.
- Prebuilt EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases)
- 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).
- EasyEUICC: Unprivileged version that can run as a user app.
- 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.
- 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, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`
Building
Building (Gradle)
===
Make sure you have all submodules cloned and updated by running
@ -48,11 +47,24 @@ For EasyEUICC:
./gradlew :app-unpriv:assembleRelease
```
Building (AOSP)
===
There are two ways to include OpenEUICC in your AOSP-based system image:
1. Include this project and its [dependencies](https://gitea.angry.im/PeterCxy/android_prebuilts_openeuicc-deps) inside the AOSP tree.
- If inclusion in `manifest.xml` is required, remember to set the `sync-s` option to clone submodules.
- The module name is `OpenEUICC`. You can include it in `PRODUCT_PACKAGES`, or simply build it standalone using `mm`.
- Compilation of this project is **only** tested against the latest AOSP release version. The app itself should be compatible with older AOSP versions, but the source may not compile against an older AOSP source tree.
2. If compilation against AOSP source tree is not possible, consider [building with gradle](#building-gradle) and import the apk as a prebuilt.
- No official `Android.bp` is provided for this case but it should be straightforward to write.
- You might want to include `privapp_whitelist_im.angry.openeuicc.xml` as well.
FAQs
===
- Q: Do you provide prebuilt binaries for OpenEUICC?
- A: No. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want `privapp_whitelist_im.angry.openeuicc.xml` as well.
- A: Debug-mode APKs are available continuously as an artifact of the [Actions](https://gitea.angry.im/PeterCxy/OpenEUICC/actions) CI used by this project. However, these debug-mode APKs are **not** intended for inclusion inside system images, nor are they supported by the developer in any sense. If you are a custom ROM developer, either include the entire OpenEUICC repository in your AOSP source tree, or generate an APK using `gradle` and import that as a prebuilt system app. Note that you might want `privapp_whitelist_im.angry.openeuicc.xml` as well.
- Q: AOSP's Settings app seems to be confused by OpenEUICC (for example, disabling / enabling profiles from the Networks page do not work properly)
- A: When your device has internal eSIM chip(s) __and__ you have inserted a removable eSIM chip, the Settings app can misbehave since it was never designed for this scenario. __Please prefer using OpenEUICC's own management interface whenever possible.__ In the future, there might be an option to exclude removable SIMs from being reported to the Android system.
@ -66,12 +78,40 @@ FAQs
Copyright
===
Everything except `libs/lpac-jni`:
```
Copyright 2022-2024 OpenEUICC contributors
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, version 2.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
```
`libs/lpac-jni`:
```
Copyright (C) 2022-2024 OpenEUICC contributiors
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, version 2.1.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
```

View file

@ -6,8 +6,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:networkSecurityConfig="@xml/network_security_config">
<application>
<activity
android:name="im.angry.openeuicc.ui.SettingsActivity"
android:label="@string/pref_settings" />
@ -29,5 +28,9 @@
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<service
android:name="im.angry.openeuicc.service.EuiccChannelManagerService"
android:exported="false" />
</application>
</manifest>

View file

@ -1,33 +1,19 @@
package im.angry.openeuicc
import android.app.Application
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import com.google.android.material.color.DynamicColors
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.PreferenceRepository
import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.di.DefaultAppContainer
open class OpenEuiccApplication : Application() {
open val appContainer: AppContainer by lazy {
DefaultAppContainer(this)
}
override fun onCreate() {
super.onCreate()
// Observe dynamic colors changes
DynamicColors.applyToActivitiesIfAvailable(this)
}
val telephonyManager by lazy {
getSystemService(TelephonyManager::class.java)!!
}
open val euiccChannelManager: EuiccChannelManager by lazy {
EuiccChannelManager(this)
}
val subscriptionManager by lazy {
getSystemService(SubscriptionManager::class.java)!!
}
val preferenceRepository by lazy {
PreferenceRepository(this)
}
}

View file

@ -0,0 +1,43 @@
package im.angry.openeuicc.core
import android.content.Context
import android.se.omapi.SEService
import android.util.Log
import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
private var seService: SEService? = null
private suspend fun ensureSEService() {
if (seService == null || !seService!!.isConnected) {
seService = connectSEService(context)
}
}
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
if (port.portIndex != 0) {
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}")
try {
return EuiccChannel(port, OmapiApduInterface(seService!!, port))
} catch (e: IllegalArgumentException) {
// Failed
Log.w(
DefaultEuiccChannelManager.TAG,
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}."
)
}
return null
}
override fun cleanup() {
seService?.shutdown()
seService = null
}
}

View file

@ -0,0 +1,173 @@
package im.angry.openeuicc.core
import android.content.Context
import android.telephony.SubscriptionManager
import android.util.Log
import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
open class DefaultEuiccChannelManager(
protected val appContainer: AppContainer,
protected val context: Context
) : EuiccChannelManager {
companion object {
const val TAG = "EuiccChannelManager"
}
private val channelCache = mutableListOf<EuiccChannel>()
private val lock = Mutex()
protected val tm by lazy {
appContainer.telephonyManager
}
private val euiccChannelFactory by lazy {
appContainer.euiccChannelFactory
}
protected open val uiccCards: Collection<UiccCardInfoCompat>
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
val existing =
channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing
} else {
existing.close()
channelCache.remove(existing)
}
}
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// We can only open channels on ports that are actually enabled
return null
}
val channel = euiccChannelFactory.tryOpenEuiccChannel(port) ?: return null
if (channel.valid) {
channelCache.add(channel)
return channel
} else {
Log.i(
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
}
}
}
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
null
}
}
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return null
}
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
runBlocking {
findAllEuiccChannelsByPhysicalSlot(physicalSlotId)
}
override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
runBlocking {
findEuiccChannelByPort(physicalSlotId, portId)
}
override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) {
// If there is already a valid channel, we close it proactively
// Sometimes the current channel can linger on for a bit even after it should have become invalid
channelCache.find { it.slotId == physicalSlotId && it.portId == portId }?.apply {
if (valid) close()
}
withTimeout(timeoutMillis) {
while (true) {
try {
// tryOpenEuiccChannel() will automatically dispose of invalid channels
// and recreate when needed
val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId)!!
check(channel.valid) { "Invalid channel" }
break
} catch (e: Exception) {
Log.d(TAG, "Slot $physicalSlotId port $portId reconnect failure, retrying in 1000 ms")
}
delay(1000)
}
}
}
override suspend fun enumerateEuiccChannels(): List<EuiccChannel> =
withContext(Dispatchers.IO) {
uiccCards.flatMap { info ->
info.ports.mapNotNull { port ->
tryOpenEuiccChannel(port)?.also {
Log.d(
TAG,
"Found eUICC on slot ${info.physicalSlotIndex} port ${port.portIndex}"
)
}
}
}
}
override fun invalidate() {
for (channel in channelCache) {
channel.close()
}
channelCache.clear()
euiccChannelFactory.cleanup()
}
}

View file

@ -0,0 +1,10 @@
package im.angry.openeuicc.core
import android.app.Service
import im.angry.openeuicc.di.AppContainer
class DefaultEuiccChannelManagerFactory(private val appContainer: AppContainer) :
EuiccChannelManagerFactory {
override fun createEuiccChannelManager(serviceContext: Service) =
DefaultEuiccChannelManager(appContainer, serviceContext)
}

View file

@ -1,16 +1,21 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
abstract class EuiccChannel(
val port: UiccPortInfoCompat
class EuiccChannel(
val port: UiccPortInfoCompat,
apduInterface: ApduInterface,
) {
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
val logicalSlotId = port.logicalSlotIndex
val portId = port.portIndex
abstract val lpa: LocalProfileAssistant
val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl())
val valid: Boolean
get() = lpa.valid

View file

@ -0,0 +1,16 @@
package im.angry.openeuicc.core
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?
/**
* Release all resources used by this EuiccChannelFactory
* Note that the same instance may be reused; any resources allocated must be automatically
* re-acquired when this happens
*/
fun cleanup()
}

View file

@ -1,168 +1,66 @@
package im.angry.openeuicc.core
import android.content.Context
import android.se.omapi.SEService
import android.telephony.SubscriptionManager
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.lang.IllegalArgumentException
/**
* EuiccChannelManager holds references to, and manages the lifecycles of, individual
* APDU channels to SIM cards. The find* methods will create channels when needed, and
* all opened channels will be held in an internal cache until invalidate() is called
* or when this instance is destroyed.
*
* To precisely control the lifecycle of this object itself (and thus its cached channels),
* all other compoents must access EuiccChannelManager objects through EuiccChannelManagerService.
* Holding references independent of EuiccChannelManagerService is unsupported.
*/
interface EuiccChannelManager {
/**
* Scan all possible sources for EuiccChannels, return them and have all
* scanned channels cached; these channels will remain open for the entire lifetime of
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d
*/
suspend fun enumerateEuiccChannels(): List<EuiccChannel>
open class EuiccChannelManager(protected val context: Context) {
companion object {
const val TAG = "EuiccChannelManager"
}
/**
* Wait for a slot + port to reconnect (i.e. become valid again)
* If the port is currently valid, this function will return immediately.
* On timeout, the caller can decide to either try again later, or alert the user with an error
*/
suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long = 1000)
private val channels = mutableListOf<EuiccChannel>()
/**
* Returns the EuiccChannel corresponding to a **logical** slot
*/
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel?
private var seService: SEService? = null
/**
* Returns the first EuiccChannel corresponding to a **physical** slot
* If the physical slot supports MEP and has multiple ports, it is undefined
* which of the two channels will be returned.
*/
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel?
private val lock = Mutex()
/**
* Returns all EuiccChannels corresponding to a **physical** slot
* Multiple channels are possible in the case of MEP
*/
suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>?
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>?
protected val tm by lazy {
(context.applicationContext as OpenEuiccApplication).telephonyManager
}
/**
* Returns the EuiccChannel corresponding to a **physical** slot and a port ID
*/
suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel?
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
protected open val uiccCards: Collection<UiccCardInfoCompat>
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
/**
* Invalidate all EuiccChannels previously cached by this Manager
*/
fun invalidate()
private suspend fun ensureSEService() {
if (seService == null) {
seService = connectSEService(context)
}
}
protected open fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
// No-op when unprivileged
return null
}
protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
}
Log.i(TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try {
return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}.")
}
return null
}
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
ensureSEService()
val existing = channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing
} else {
existing.close()
channels.remove(existing)
}
}
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// We can only open channels on ports that are actually enabled
return null
}
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port)
if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(port)
}
if (euiccChannel != null) {
channels.add(euiccChannel)
}
return euiccChannel
}
}
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
null
}
}
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return@runBlocking null
}
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
}
suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) {
ensureSEService()
for (uiccInfo in uiccCards) {
for (port in uiccInfo.ports) {
if (tryOpenEuiccChannel(port) != null) {
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
}
}
}
}
}
val knownChannels: List<EuiccChannel>
get() = channels.toList()
fun invalidate() {
for (channel in channels) {
channel.close()
}
channels.clear()
seService?.shutdown()
seService = null
}
open fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
// No-op for unprivileged
/**
* If possible, trigger the system to update the cached list of profiles
* This is only expected to be implemented when the application is privileged
* TODO: Remove this from the common interface
*/
fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
// no-op by default
}
}

View file

@ -0,0 +1,7 @@
package im.angry.openeuicc.core
import android.app.Service
interface EuiccChannelManagerFactory {
fun createEuiccChannelManager(serviceContext: Service): EuiccChannelManager
}

View file

@ -5,9 +5,6 @@ import android.se.omapi.SEService
import android.se.omapi.Session
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class OmapiApduInterface(
private val service: SEService,
@ -16,6 +13,9 @@ class OmapiApduInterface(
private lateinit var session: Session
private lateinit var lastChannel: Channel
override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
override fun connect() {
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession()
}
@ -47,13 +47,4 @@ class OmapiApduInterface(
return lastChannel.transmit(tx)
}
}
class OmapiChannel(
service: SEService,
port: UiccPortInfoCompat,
) : EuiccChannel(port) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
OmapiApduInterface(service, port),
HttpInterfaceImpl())
}
}

View file

@ -0,0 +1,18 @@
package im.angry.openeuicc.di
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import im.angry.openeuicc.core.EuiccChannelFactory
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.core.EuiccChannelManagerFactory
import im.angry.openeuicc.util.*
interface AppContainer {
val telephonyManager: TelephonyManager
val euiccChannelManager: EuiccChannelManager
val euiccChannelManagerFactory: EuiccChannelManagerFactory
val subscriptionManager: SubscriptionManager
val preferenceRepository: PreferenceRepository
val uiComponentFactory: UiComponentFactory
val euiccChannelFactory: EuiccChannelFactory
}

View file

@ -0,0 +1,41 @@
package im.angry.openeuicc.di
import android.content.Context
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import im.angry.openeuicc.core.DefaultEuiccChannelFactory
import im.angry.openeuicc.core.DefaultEuiccChannelManager
import im.angry.openeuicc.core.DefaultEuiccChannelManagerFactory
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.core.EuiccChannelManagerFactory
import im.angry.openeuicc.util.*
open class DefaultAppContainer(context: Context) : AppContainer {
override val telephonyManager by lazy {
context.getSystemService(TelephonyManager::class.java)!!
}
override val euiccChannelManager: EuiccChannelManager by lazy {
DefaultEuiccChannelManager(this, context)
}
override val euiccChannelManagerFactory: EuiccChannelManagerFactory by lazy {
DefaultEuiccChannelManagerFactory(this)
}
override val subscriptionManager by lazy {
context.getSystemService(SubscriptionManager::class.java)!!
}
override val preferenceRepository by lazy {
PreferenceRepository(context)
}
override val uiComponentFactory by lazy {
DefaultUiComponentFactory()
}
override val euiccChannelFactory by lazy {
DefaultEuiccChannelFactory(context)
}
}

View file

@ -0,0 +1,13 @@
package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.NoEuiccPlaceholderFragment
open class DefaultUiComponentFactory : UiComponentFactory {
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
override fun createNoEuiccPlaceholderFragment(): Fragment = NoEuiccPlaceholderFragment()
}

View file

@ -0,0 +1,10 @@
package im.angry.openeuicc.di
import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
interface UiComponentFactory {
fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment
fun createNoEuiccPlaceholderFragment(): Fragment
}

View file

@ -0,0 +1,41 @@
package im.angry.openeuicc.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
/**
* An Android Service wrapper for EuiccChannelManager.
* The purpose of this wrapper is mainly lifecycle-wise: having a Service allows the manager
* instance to have its own independent lifecycle. This way it can be created as requested and
* destroyed when no other components are bound to this service anymore.
* This behavior allows us to avoid keeping the APDU channels open at all times. For example,
* the EuiccService implementation should *only* bind to this service when it requires an
* instance of EuiccChannelManager. UI components can keep being bound to this service for
* their entire lifecycles, since the whole purpose of them is to expose the current state
* to the user.
*/
class EuiccChannelManagerService : Service(), OpenEuiccContextMarker {
inner class LocalBinder : Binder() {
val service = this@EuiccChannelManagerService
}
private val euiccChannelManagerDelegate = lazy {
appContainer.euiccChannelManagerFactory.createEuiccChannelManager(this)
}
val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
override fun onBind(intent: Intent?): IBinder = LocalBinder()
override fun onDestroy() {
super.onDestroy()
// This is the whole reason of the existence of this service:
// we can clean up opened channels when no one is using them
if (euiccChannelManagerDelegate.isInitialized()) {
euiccChannelManager.invalidate()
}
}
}

View file

@ -0,0 +1,48 @@
package im.angry.openeuicc.ui
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import androidx.appcompat.app.AppCompatActivity
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService
abstract class BaseEuiccAccessActivity : AppCompatActivity() {
lateinit var euiccChannelManager: EuiccChannelManager
private val euiccChannelManagerServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
euiccChannelManager =
(service!! as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager
onInit()
}
override fun onServiceDisconnected(name: ComponentName?) {
// These activities should never lose the EuiccChannelManagerService connection
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindService(
Intent(this, EuiccChannelManagerService::class.java),
euiccChannelManagerServiceConnection,
Context.BIND_AUTO_CREATE
)
}
override fun onDestroy() {
super.onDestroy()
unbindService(euiccChannelManagerServiceConnection)
}
/**
* When called, euiccChannelManager is guaranteed to have been initialized
*/
abstract fun onInit()