Compare commits
No commits in common. "master" and "mep" have entirely different histories.
|
@ -1,44 +0,0 @@
|
|||
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
|
||||
|
||||
- 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
|
|
@ -1,49 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
tags: '*'
|
||||
|
||||
env:
|
||||
# Enable reproducibility-related build system workarounds
|
||||
REPRODUCIBLE_BUILD: true
|
||||
|
||||
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
|
||||
|
||||
- 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: 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'
|
3
.gitignore
vendored
|
@ -8,7 +8,6 @@
|
|||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
@ -16,5 +15,3 @@
|
|||
.cxx
|
||||
local.properties
|
||||
/libs/**/build
|
||||
/buildSrc/build
|
||||
/app-deps/libs
|
117
.idea/codeStyles/Project.xml
generated
|
@ -1,117 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
|
@ -1,5 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
5
.idea/compiler.xml
generated
|
@ -4,11 +4,6 @@
|
|||
<bytecodeTargetLevel target="1.7">
|
||||
<module name="OpenEUICC.app" target="17" />
|
||||
<module name="OpenEUICC.app-common" target="17" />
|
||||
<module name="OpenEUICC.app-deps" target="17" />
|
||||
<module name="OpenEUICC.app-unpriv" target="17" />
|
||||
<module name="OpenEUICC.buildSrc" target="17" />
|
||||
<module name="OpenEUICC.buildSrc.main" target="17" />
|
||||
<module name="OpenEUICC.buildSrc.test" target="17" />
|
||||
<module name="OpenEUICC.libs.hidden-apis-shim" target="17" />
|
||||
<module name="OpenEUICC.libs.lpac-jni" target="17" />
|
||||
</bytecodeTargetLevel>
|
||||
|
|
3
.idea/gradle.xml
generated
|
@ -14,9 +14,6 @@
|
|||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/app-common" />
|
||||
<option value="$PROJECT_DIR$/app-deps" />
|
||||
<option value="$PROJECT_DIR$/app-unpriv" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/libs" />
|
||||
<option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
|
||||
<option value="$PROJECT_DIR$/libs/hidden-apis-stub" />
|
||||
|
|
22
Android.bp
|
@ -6,19 +6,26 @@ java_library {
|
|||
optimize: {
|
||||
enabled: false,
|
||||
},
|
||||
static_libs: [
|
||||
"kotlinx_coroutines",
|
||||
],
|
||||
system_ext_specific: true,
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "OpenEUICC-common",
|
||||
defaults: [
|
||||
"OpenEUICC-deps-defaults",
|
||||
],
|
||||
static_libs: [
|
||||
"net.typeblog.lpac_jni",
|
||||
|
||||
// Dependencies that must be pulled from maven,
|
||||
"zxing-core-prebuilt-jar",
|
||||
"zxing-android-embedded-prebuilt-aar",
|
||||
|
||||
// Dependencies included with AOSP
|
||||
"androidx.appcompat_appcompat",
|
||||
"androidx.cardview_cardview",
|
||||
"androidx-constraintlayout_constraintlayout",
|
||||
"androidx.core_core-ktx",
|
||||
"androidx.lifecycle_lifecycle-runtime-ktx",
|
||||
"androidx.swiperefreshlayout_swiperefreshlayout",
|
||||
"com.google.android.material_material",
|
||||
"kotlinx_coroutines",
|
||||
],
|
||||
srcs: [
|
||||
|
@ -30,9 +37,6 @@ android_library {
|
|||
resource_dirs: [
|
||||
"app-common/src/main/res",
|
||||
],
|
||||
kotlincflags: [
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
],
|
||||
manifest: "app-common/src/main/AndroidManifest.xml",
|
||||
system_ext_specific: true,
|
||||
}
|
||||
|
|
7
COPYING
Normal file
|
@ -0,0 +1,7 @@
|
|||
Copyright 2022 Peter Cai & Pierre-Hugues Husson
|
||||
|
||||
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.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
77
README.md
|
@ -1,77 +0,0 @@
|
|||
{Open,Easy}EUICC
|
||||
---
|
||||
|
||||
A fully free and open-source Local Profile Assistant implementation for Android devices.
|
||||
|
||||
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)
|
||||
- For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA`
|
||||
|
||||
Building
|
||||
===
|
||||
|
||||
Make sure you have all submodules cloned and updated by running
|
||||
|
||||
```shell
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
A file `keystore.properties` is required in the root directory. Template:
|
||||
|
||||
```ini
|
||||
storePassword=my-store-password
|
||||
keyPassword=my-password
|
||||
keyAlias=my-key
|
||||
unprivKeyPassword=my-unpriv-password
|
||||
unprivKeyAlias=my-unpriv-key
|
||||
storeFile=/path/to/android/keystore
|
||||
```
|
||||
|
||||
Note that you must have a Java-compatible keystore generated first.
|
||||
|
||||
To build the privileged OpenEUICC:
|
||||
|
||||
```shell
|
||||
./gradlew :app:assembleRelease
|
||||
```
|
||||
|
||||
For EasyEUICC:
|
||||
|
||||
```shell
|
||||
./gradlew :app-unpriv:assembleRelease
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
- 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.
|
||||
|
||||
- Q: Can EasyEUICC manage my phone's internal eSIM?
|
||||
- A: No. For EasyEUICC to work, the eSIM chip MUST proactively grant access via its ARA-M field.
|
||||
|
||||
- Q: Removable eSIMs? Are they a joke?
|
||||
- A: No, even though the name "removable embedded SIM" can sound like an oxymoron. In fact, there can be many advantages to these chips compared to fully embedded ones. For example, the ability to transfer eSIM profiles without carrier support or approval, or the ability to use eSIM on devices that do not and may never get the support, such as Wi-Fi hotspots.
|
||||
|
||||
Copyright
|
||||
===
|
||||
|
||||
```
|
||||
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 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/>.
|
||||
```
|
45
app-common/build.gradle
Normal file
|
@ -0,0 +1,45 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'im.angry.openeuicc.common'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk 30
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":libs:lpac-jni")
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.angry.openeuicc.common"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":libs:lpac-jni"))
|
||||
api(project(":app-deps"))
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
}
|
|
@ -8,23 +8,6 @@
|
|||
|
||||
<application
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.SettingsActivity"
|
||||
android:label="@string/pref_settings" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.NotificationsActivity"
|
||||
android:label="@string/profile_notifications" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
|
||||
android:label="@string/profile_download"
|
||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.LogsActivity"
|
||||
android:label="@string/pref_advanced_logs" />
|
||||
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
|
|
|
@ -3,18 +3,9 @@ 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
|
||||
|
||||
open class OpenEuiccApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Observe dynamic colors changes
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
val telephonyManager by lazy {
|
||||
getSystemService(TelephonyManager::class.java)!!
|
||||
}
|
||||
|
@ -26,8 +17,4 @@ open class OpenEuiccApplication : Application() {
|
|||
val subscriptionManager by lazy {
|
||||
getSystemService(SubscriptionManager::class.java)!!
|
||||
}
|
||||
|
||||
val preferenceRepository by lazy {
|
||||
PreferenceRepository(this)
|
||||
}
|
||||
}
|
|
@ -4,15 +4,26 @@ import im.angry.openeuicc.util.*
|
|||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
abstract class EuiccChannel(
|
||||
val port: UiccPortInfoCompat
|
||||
port: UiccPortInfoCompat
|
||||
) {
|
||||
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
|
||||
val logicalSlotId = port.logicalSlotIndex
|
||||
val portId = port.portIndex
|
||||
val cardId = port.card.cardId
|
||||
val removable = port.card.isRemovable
|
||||
val isMEP = port.card.isMultipleEnabledProfilesSupported
|
||||
|
||||
abstract val lpa: LocalProfileAssistant
|
||||
val valid: Boolean
|
||||
get() = lpa.valid
|
||||
get() {
|
||||
try {
|
||||
// Try to ping the eUICC card by reading the EID
|
||||
lpa.eID
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun close() = lpa.close()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.se.omapi.SEService
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.util.Log
|
||||
|
@ -12,6 +14,8 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.IllegalArgumentException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
open class EuiccChannelManager(protected val context: Context) {
|
||||
companion object {
|
||||
|
@ -28,12 +32,22 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
(context.applicationContext as OpenEuiccApplication).telephonyManager
|
||||
}
|
||||
|
||||
protected open val uiccCards: Collection<UiccCardInfoCompat>
|
||||
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
|
||||
private val handler = Handler(HandlerThread("BaseEuiccChannelManager").also { it.start() }.looper)
|
||||
|
||||
protected open fun checkPrivileges() = tm.hasCarrierPrivileges()
|
||||
|
||||
private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
|
||||
handler.post {
|
||||
var service: SEService? = null
|
||||
service = SEService(context, { handler.post(it) }) {
|
||||
cont.resume(service!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureSEService() {
|
||||
if (seService == null) {
|
||||
seService = connectSEService(context)
|
||||
seService = connectSEService()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +58,8 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
|
||||
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.w(TAG, "OMAPI channel attempted on non-zero portId, ignoring")
|
||||
return null
|
||||
}
|
||||
|
||||
Log.i(TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
|
||||
|
@ -92,8 +107,9 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
|
||||
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
|
||||
runBlocking {
|
||||
if (!checkPrivileges()) return@runBlocking null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (card in uiccCards) {
|
||||
for (card in tm.uiccCardsInfoCompat) {
|
||||
for (port in card.ports) {
|
||||
if (port.logicalSlotIndex == logicalSlotId) {
|
||||
return@withContext tryOpenEuiccChannel(port)
|
||||
|
@ -106,8 +122,9 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
}
|
||||
|
||||
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
|
||||
if (!checkPrivileges()) return@runBlocking null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (card in uiccCards) {
|
||||
for (card in tm.uiccCardsInfoCompat) {
|
||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||
for (port in card.ports) {
|
||||
tryOpenEuiccChannel(port)?.let { return@withContext it }
|
||||
|
@ -119,7 +136,8 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
}
|
||||
|
||||
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
|
||||
for (card in uiccCards) {
|
||||
if (!checkPrivileges()) return@runBlocking null
|
||||
for (card in tm.uiccCardsInfoCompat) {
|
||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
||||
.ifEmpty { null }
|
||||
|
@ -128,18 +146,21 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
}
|
||||
|
||||
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
|
||||
if (!checkPrivileges()) return@runBlocking null
|
||||
withContext(Dispatchers.IO) {
|
||||
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
|
||||
tm.uiccCardsInfoCompat.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
|
||||
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun enumerateEuiccChannels() {
|
||||
if (!checkPrivileges()) return
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
ensureSEService()
|
||||
|
||||
for (uiccInfo in uiccCards) {
|
||||
for (uiccInfo in tm.uiccCardsInfoCompat) {
|
||||
for (port in uiccInfo.ports) {
|
||||
if (tryOpenEuiccChannel(port) != null) {
|
||||
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
|
||||
|
@ -153,6 +174,8 @@ open class EuiccChannelManager(protected val context: Context) {
|
|||
get() = channels.toList()
|
||||
|
||||
fun invalidate() {
|
||||
if (!checkPrivileges()) return
|
||||
|
||||
for (channel in channels) {
|
||||
channel.close()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package im.angry.openeuicc.core
|
|||
import android.se.omapi.Channel
|
||||
import android.se.omapi.SEService
|
||||
import android.se.omapi.Session
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.UiccPortInfoCompat
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
||||
|
@ -17,7 +17,7 @@ class OmapiApduInterface(
|
|||
private lateinit var lastChannel: Channel
|
||||
|
||||
override fun connect() {
|
||||
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession()
|
||||
session = service.getUiccReader(port.logicalSlotIndex + 1).openSession()
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Window
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import im.angry.openeuicc.common.R
|
||||
|
||||
abstract class BaseMaterialDialogFragment: DialogFragment() {
|
||||
override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
|
||||
val inflater = super.onGetLayoutInflater(savedInstanceState)
|
||||
val wrappedContext = ContextThemeWrapper(requireContext(), R.style.Theme_OpenEUICC)
|
||||
val dynamicWrappedContext = DynamicColors.wrapContextIfAvailable(wrappedContext)
|
||||
return inflater.cloneInContext(dynamicWrappedContext)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).also {
|
||||
it.window?.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
it.window?.setBackgroundDrawableResource(R.drawable.dialog_background)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DirectProfileDownloadActivity : AppCompatActivity(), SlotSelectFragment.SlotSelectedListener, OpenEuiccContextMarker {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.enumerateEuiccChannels()
|
||||
}
|
||||
|
||||
when {
|
||||
euiccChannelManager.knownChannels.isEmpty() -> {
|
||||
finish()
|
||||
}
|
||||
euiccChannelManager.knownChannels.hasMultipleChips -> {
|
||||
SlotSelectFragment.newInstance()
|
||||
.show(supportFragmentManager, SlotSelectFragment.TAG)
|
||||
}
|
||||
else -> {
|
||||
// If the device has only one eSIM "chip" (but may be mapped to multiple slots),
|
||||
// we can skip the slot selection dialog since there is only one chip to save to.
|
||||
onSlotSelected(euiccChannelManager.knownChannels[0].slotId,
|
||||
euiccChannelManager.knownChannels[0].portId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlotSelected(slotId: Int, portId: Int) {
|
||||
ProfileDownloadFragment.newInstance(slotId, portId, finishWhenDone = true)
|
||||
.show(supportFragmentManager, ProfileDownloadFragment.TAG)
|
||||
}
|
||||
|
||||
override fun onSlotSelectCancelled() = finish()
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.openEuiccApplication
|
||||
|
||||
interface EuiccFragmentMarker
|
||||
|
||||
fun <T> newInstanceEuicc(clazz: Class<T>, slotId: Int, portId: Int): T where T: Fragment, T: EuiccFragmentMarker {
|
||||
val instance = clazz.newInstance()
|
||||
instance.arguments = Bundle().apply {
|
||||
putInt("slotId", slotId)
|
||||
putInt("portId", portId)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
val <T> T.slotId: Int where T: Fragment, T: EuiccFragmentMarker
|
||||
get() = requireArguments().getInt("slotId")
|
||||
val <T> T.portId: Int where T: Fragment, T: EuiccFragmentMarker
|
||||
get() = requireArguments().getInt("portId")
|
||||
|
||||
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccFragmentMarker
|
||||
get() = openEuiccApplication.euiccChannelManager
|
||||
|
||||
val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker
|
||||
get() =
|
||||
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
|
||||
|
||||
interface EuiccProfilesChangedListener {
|
||||
fun onEuiccProfilesChanged()
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -26,13 +23,11 @@ import net.typeblog.lpac_jni.LocalProfileInfo
|
|||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Exception
|
||||
|
||||
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
||||
EuiccChannelFragmentMarker {
|
||||
open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
|
||||
companion object {
|
||||
const val TAG = "EuiccManagementFragment"
|
||||
|
||||
|
@ -46,11 +41,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
private val adapter = EuiccProfileAdapter()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -87,23 +77,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
refresh()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_euicc, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.show_notifications -> {
|
||||
Intent(requireContext(), NotificationsActivity::class.java).apply {
|
||||
putExtra("logicalSlotId", channel.logicalSlotId)
|
||||
startActivity(this)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
protected open suspend fun onCreateFooterViews(parent: ViewGroup): List<View> = listOf()
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
|
@ -127,6 +100,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
|
||||
private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
|
||||
swipeRefresh.isRefreshing = true
|
||||
swipeRefresh.isEnabled = false
|
||||
fab.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
@ -136,37 +110,30 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
} else {
|
||||
doDisableProfile(iccid)
|
||||
}
|
||||
refresh()
|
||||
fab.isEnabled = true
|
||||
Toast.makeText(context, R.string.toast_profile_enabled, Toast.LENGTH_LONG).show()
|
||||
// The APDU channel will be invalid when the SIM reboots. For now, just exit the app
|
||||
euiccChannelManager.invalidate()
|
||||
requireActivity().finish()
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to enable / disable profile $iccid")
|
||||
Log.d(TAG, Log.getStackTraceString(e))
|
||||
fab.isEnabled = true
|
||||
swipeRefresh.isEnabled = true
|
||||
Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doEnableProfile(iccid: String) =
|
||||
channel.lpa.beginOperation {
|
||||
channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) &&
|
||||
preferenceRepository.notificationEnableFlow.first()
|
||||
withContext(Dispatchers.IO) {
|
||||
channel.lpa.enableProfile(iccid)
|
||||
}
|
||||
|
||||
private suspend fun doDisableProfile(iccid: String) =
|
||||
channel.lpa.beginOperation {
|
||||
channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) &&
|
||||
preferenceRepository.notificationDisableFlow.first()
|
||||
withContext(Dispatchers.IO) {
|
||||
channel.lpa.disableProfile(iccid)
|
||||
}
|
||||
|
||||
protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
|
||||
popup.inflate(R.menu.profile_options)
|
||||
if (profile.isEnabled) {
|
||||
popup.menu.findItem(R.id.enable).isVisible = false
|
||||
popup.menu.findItem(R.id.delete).isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
enum class Type(val value: Int) {
|
||||
PROFILE(0),
|
||||
|
@ -216,7 +183,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
name.text = profile.displayName
|
||||
|
||||
state.setText(
|
||||
if (profile.isEnabled) {
|
||||
if (isEnabled()) {
|
||||
R.string.enabled
|
||||
} else {
|
||||
R.string.disabled
|
||||
|
@ -227,10 +194,19 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
iccid.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
}
|
||||
|
||||
private fun isEnabled(): Boolean =
|
||||
profile.state == LocalProfileInfo.State.Enabled
|
||||
|
||||
private fun showOptionsMenu() {
|
||||
PopupMenu(root.context, profileMenu).apply {
|
||||
setOnMenuItemClickListener(::onMenuItemClicked)
|
||||
populatePopupWithProfileActions(this, profile)
|
||||
inflate(R.menu.profile_options)
|
||||
if (isEnabled()) {
|
||||
menu.findItem(R.id.enable).isVisible = false
|
||||
menu.findItem(R.id.delete).isVisible = false
|
||||
} else {
|
||||
menu.findItem(R.id.disable).isVisible = false
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LogsActivity : AppCompatActivity() {
|
||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||
private lateinit var scrollView: ScrollView
|
||||
private lateinit var logText: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_logs)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
swipeRefresh = findViewById(R.id.swipe_refresh)
|
||||
scrollView = findViewById(R.id.scroll_view)
|
||||
logText = findViewById(R.id.log_text)
|
||||
|
||||
swipeRefresh.setOnRefreshListener {
|
||||
lifecycleScope.launch {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleScope.launch {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reload() = withContext(Dispatchers.Main) {
|
||||
swipeRefresh.isRefreshing = true
|
||||
|
||||
val logStr = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
Runtime.getRuntime().exec("logcat -t 1024").inputStream.readBytes()
|
||||
.decodeToString()
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
logText.text = logStr
|
||||
|
||||
swipeRefresh.isRefreshing = false
|
||||
|
||||
scrollView.post {
|
||||
scrollView.fullScroll(View.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
|
@ -20,7 +18,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
|
||||
open class MainActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
const val TAG = "MainActivity"
|
||||
}
|
||||
|
@ -39,13 +37,11 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
|
||||
noEuiccPlaceholder = findViewById(R.id.no_euicc_placeholder)
|
||||
|
||||
tm = telephonyManager
|
||||
tm = openEuiccApplication.telephonyManager
|
||||
|
||||
manager = euiccChannelManager
|
||||
manager = openEuiccApplication.euiccChannelManager
|
||||
|
||||
spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
|
||||
|
||||
|
@ -57,43 +53,26 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
|
|||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_main, menu)
|
||||
|
||||
if (!this::spinner.isInitialized) {
|
||||
spinner = menu.findItem(R.id.spinner).actionView as Spinner
|
||||
spinner.adapter = spinnerAdapter
|
||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_root, fragments[position]).commit()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
|
||||
spinner = menu.findItem(R.id.spinner).actionView as Spinner
|
||||
spinner.adapter = spinnerAdapter
|
||||
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments[position]).commit()
|
||||
}
|
||||
} else {
|
||||
// Fragments may cause this menu to be inflated multiple times.
|
||||
// Simply reuse the action view in that case
|
||||
menu.findItem(R.id.spinner).actionView = spinner
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java));
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
protected open fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
|
||||
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
|
||||
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.ContextMenu
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MenuItem.OnMenuItemClickListener
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.forEach
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.LocalProfileNotification
|
||||
|
||||
class NotificationsActivity: AppCompatActivity(), OpenEuiccContextMarker {
|
||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||
private lateinit var notificationList: RecyclerView
|
||||
private val notificationAdapter = NotificationAdapter()
|
||||
|
||||
private lateinit var euiccChannel: EuiccChannel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_notifications)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
euiccChannel = euiccChannelManager
|
||||
.findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!!
|
||||
|
||||
swipeRefresh = findViewById(R.id.swipe_refresh)
|
||||
notificationList = findViewById(R.id.recycler_view)
|
||||
|
||||
notificationList.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
notificationList.adapter = notificationAdapter
|
||||
registerForContextMenu(notificationList)
|
||||
|
||||
swipeRefresh.setOnRefreshListener {
|
||||
refresh()
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.activity_notifications, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
R.id.help -> {
|
||||
AlertDialog.Builder(this, R.style.AlertDialogTheme).apply {
|
||||
setMessage(R.string.profile_notifications_help)
|
||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun launchTask(task: suspend () -> Unit) {
|
||||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
task()
|
||||
|
||||
swipeRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
launchTask {
|
||||
val profiles = withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.profiles
|
||||
}
|
||||
|
||||
notificationAdapter.notifications =
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.notifications.map {
|
||||
val profile = profiles.find { p -> p.iccid == it.iccid }
|
||||
LocalProfileNotificationWrapper(it, profile?.displayName ?: "???")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LocalProfileNotificationWrapper(
|
||||
val inner: LocalProfileNotification,
|
||||
val profileName: String
|
||||
)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
inner class NotificationViewHolder(private val root: View):
|
||||
RecyclerView.ViewHolder(root), View.OnCreateContextMenuListener, OnMenuItemClickListener {
|
||||
private val address: TextView = root.findViewById(R.id.notification_address)
|
||||
private val profileName: TextView = root.findViewById(R.id.notification_profile_name)
|
||||
|
||||
private lateinit var notification: LocalProfileNotificationWrapper
|
||||
|
||||
private var lastTouchX = 0f
|
||||
private var lastTouchY = 0f
|
||||
|
||||
init {
|
||||
root.isClickable = true
|
||||
root.setOnCreateContextMenuListener(this)
|
||||
root.setOnTouchListener { _, event ->
|
||||
lastTouchX = event.x
|
||||
lastTouchY = event.y
|
||||
false
|
||||
}
|
||||
root.setOnLongClickListener {
|
||||
root.showContextMenu(lastTouchX, lastTouchY)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun operationToLocalizedText(operation: LocalProfileNotification.Operation) =
|
||||
root.context.getText(
|
||||
when (operation) {
|
||||
LocalProfileNotification.Operation.Install -> R.string.profile_notification_operation_download
|
||||
LocalProfileNotification.Operation.Delete -> R.string.profile_notification_operation_delete
|
||||
LocalProfileNotification.Operation.Enable -> R.string.profile_notification_operation_enable
|
||||
LocalProfileNotification.Operation.Disable -> R.string.profile_notification_operation_disable
|
||||
})
|
||||
|
||||
fun updateNotification(value: LocalProfileNotificationWrapper) {
|
||||
notification = value
|
||||
|
||||
address.text = value.inner.notificationAddress
|
||||
profileName.text = Html.fromHtml(
|
||||
root.context.getString(R.string.profile_notification_name_format,
|
||||
operationToLocalizedText(value.inner.profileManagementOperation),
|
||||
value.profileName, value.inner.iccid),
|
||||
Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
menu: ContextMenu?,
|
||||
v: View?,
|
||||
menuInfo: ContextMenu.ContextMenuInfo?
|
||||
) {
|
||||
menuInflater.inflate(R.menu.notification_options, menu)
|
||||
|
||||
menu!!.forEach {
|
||||
it.setOnMenuItemClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.notification_process -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.handleNotification(notification.inner.seqNumber)
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
true
|
||||
}
|
||||
R.id.notification_delete -> {
|
||||
launchTask {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannel.lpa.deleteNotification(notification.inner.seqNumber)
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
inner class NotificationAdapter: RecyclerView.Adapter<NotificationViewHolder>() {
|
||||
var notifications: List<LocalProfileNotificationWrapper> = listOf()
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder {
|
||||
val root = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.notification_item, parent, false)
|
||||
return NotificationViewHolder(root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = notifications.size
|
||||
|
||||
override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) =
|
||||
holder.updateNotification(notifications[position])
|
||||
|
||||
}
|
||||
}
|
|
@ -7,12 +7,12 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Exception
|
||||
|
||||
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||
class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
|
||||
companion object {
|
||||
const val TAG = "ProfileDeleteFragment"
|
||||
|
||||
|
@ -29,7 +29,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
private var deleting = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme).apply {
|
||||
return AlertDialog.Builder(requireContext()).apply {
|
||||
setMessage(getString(R.string.profile_delete_confirm, requireArguments().getString("name")))
|
||||
setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -70,8 +70,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun doDelete() = channel.lpa.beginOperation {
|
||||
private suspend fun doDelete() = withContext(Dispatchers.IO) {
|
||||
channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
|
||||
preferenceRepository.notificationDeleteFlow.first()
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package im.angry.openeuicc.ui
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.format.Formatter
|
||||
|
@ -12,28 +11,26 @@ import android.widget.ProgressBar
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.openEuiccApplication
|
||||
import im.angry.openeuicc.util.setWidthPercent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
import kotlin.Exception
|
||||
|
||||
class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
||||
Toolbar.OnMenuItemClickListener, EuiccChannelFragmentMarker {
|
||||
class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
const val TAG = "ProfileDownloadFragment"
|
||||
|
||||
fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment =
|
||||
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) {
|
||||
putBoolean("finishWhenDone", finishWhenDone)
|
||||
}
|
||||
fun newInstance(slotId: Int, portId: Int): ProfileDownloadFragment =
|
||||
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId)
|
||||
}
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
|
@ -48,10 +45,6 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
|
||||
private var downloading = false
|
||||
|
||||
private val finishWhenDone by lazy {
|
||||
requireArguments().getBoolean("finishWhenDone", false)
|
||||
}
|
||||
|
||||
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||
result.contents?.let { content ->
|
||||
Log.d(TAG, content)
|
||||
|
@ -87,9 +80,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
toolbar.apply {
|
||||
setTitle(R.string.profile_download)
|
||||
setNavigationOnClickListener {
|
||||
if (!downloading) {
|
||||
dismiss()
|
||||
}
|
||||
if (!downloading) dismiss()
|
||||
}
|
||||
setOnMenuItemClickListener(this@ProfileDownloadFragment)
|
||||
}
|
||||
|
@ -142,6 +133,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).also {
|
||||
it.window?.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
it.setCanceledOnTouchOutside(false)
|
||||
}
|
||||
}
|
||||
|
@ -189,8 +181,8 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = channel.lpa.beginOperation {
|
||||
downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback {
|
||||
private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = withContext(Dispatchers.IO) {
|
||||
channel.lpa.downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
progress.isIndeterminate = false
|
||||
|
@ -198,23 +190,5 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If we get here, we are successful
|
||||
// Only send notifications if the user allowed us to
|
||||
preferenceRepository.notificationDownloadFlow.first()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
if (finishWhenDone) {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
if (finishWhenDone) {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,20 +6,22 @@ import android.util.Log
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.setWidthPercent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Exception
|
||||
import java.lang.RuntimeException
|
||||
|
||||
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
||||
class ProfileRenameFragment : DialogFragment(), EuiccFragmentMarker {
|
||||
companion object {
|
||||
const val TAG = "ProfileRenameFragment"
|
||||
|
||||
|
@ -81,6 +83,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).also {
|
||||
it.window?.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
it.setCanceledOnTouchOutside(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import im.angry.openeuicc.common.R
|
||||
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings_container, SettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class SettingsFragment: PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_settings, rootKey)
|
||||
|
||||
findPreference<Preference>("pref_info_app_version")
|
||||
?.summary = requireContext().selfAppVersion
|
||||
|
||||
findPreference<Preference>("pref_info_source_code")
|
||||
?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.summary.toString())))
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>("pref_advanced_logs")
|
||||
?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(requireContext(), LogsActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_download")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_delete")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_enable")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationEnableFlow, PreferenceKeys.NOTIFICATION_ENABLE)
|
||||
|
||||
findPreference<CheckBoxPreference>("pref_notifications_disable")
|
||||
?.bindBooleanFlow(preferenceRepository.notificationDisableFlow, PreferenceKeys.NOTIFICATION_DISABLE)
|
||||
}
|
||||
|
||||
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {
|
||||
lifecycleScope.launch {
|
||||
flow.collect { isChecked = it }
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
runBlocking {
|
||||
preferenceRepository.updatePreference(key, newValue as Boolean)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class SlotSelectFragment : BaseMaterialDialogFragment(), OpenEuiccContextMarker {
|
||||
companion object {
|
||||
const val TAG = "SlotSelectFragment"
|
||||
|
||||
fun newInstance(): SlotSelectFragment {
|
||||
return SlotSelectFragment()
|
||||
}
|
||||
}
|
||||
|
||||
interface SlotSelectedListener {
|
||||
fun onSlotSelected(slotId: Int, portId: Int)
|
||||
fun onSlotSelectCancelled()
|
||||
}
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var spinner: Spinner
|
||||
private val channels: List<EuiccChannel> by lazy {
|
||||
euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_slot_select, container, false)
|
||||
|
||||
toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.slot_select)
|
||||
toolbar.inflateMenu(R.menu.fragment_slot_select)
|
||||
|
||||
val adapter = ArrayAdapter<String>(inflater.context, R.layout.spinner_item)
|
||||
|
||||
spinner = view.findViewById(R.id.spinner)
|
||||
spinner.adapter = adapter
|
||||
|
||||
channels.forEach { channel ->
|
||||
adapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
||||
}
|
||||
toolbar.setOnMenuItemClickListener {
|
||||
val channel = channels[spinner.selectedItemPosition]
|
||||
(requireActivity() as SlotSelectedListener).onSlotSelected(channel.slotId, channel.portId)
|
||||
dismiss()
|
||||
true
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setWidthPercent(75)
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
(requireActivity() as SlotSelectedListener).onSlotSelectCancelled()
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package im.angry.openeuicc.ui.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
|
||||
@Suppress("unused")
|
||||
class LongSummaryPreferenceCategory: PreferenceCategory {
|
||||
constructor(ctx: Context): super(ctx)
|
||||
constructor(ctx: Context, attrs: AttributeSet): super(ctx, attrs)
|
||||
constructor(ctx: Context, attrs: AttributeSet, defStyle: Int): super(ctx, attrs, defStyle)
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
val summaryText = holder.findViewById(android.R.id.summary) as TextView
|
||||
summaryText.isSingleLine = false
|
||||
summaryText.maxLines = 10
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
|
||||
interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
|
||||
|
||||
// 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()
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
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.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
|
||||
get() =
|
||||
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
|
||||
|
||||
interface EuiccProfilesChangedListener {
|
||||
fun onEuiccProfilesChanged()
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
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.preferencesDataStore
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.OpenEuiccApplication
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs")
|
||||
|
||||
val Context.preferenceRepository: PreferenceRepository
|
||||
get() = (applicationContext as OpenEuiccApplication).preferenceRepository
|
||||
|
||||
val Fragment.preferenceRepository: PreferenceRepository
|
||||
get() = requireContext().preferenceRepository
|
||||
|
||||
object PreferenceKeys {
|
||||
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
|
||||
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
||||
val NOTIFICATION_ENABLE = booleanPreferencesKey("notification_enable")
|
||||
val NOTIFICATION_DISABLE = booleanPreferencesKey("notification_disable")
|
||||
}
|
||||
|
||||
class PreferenceRepository(context: Context) {
|
||||
private val dataStore = context.dataStore
|
||||
|
||||
// Expose flows so that we can also handle default values
|
||||
// ---- Profile Notifications ----
|
||||
val notificationDownloadFlow: Flow<Boolean> =
|
||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DOWNLOAD] ?: true }
|
||||
|
||||
val notificationDeleteFlow: Flow<Boolean> =
|
||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DELETE] ?: true }
|
||||
|
||||
// Enabling / disabling notifications are not sent by default
|
||||
val notificationEnableFlow: Flow<Boolean> =
|
||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_ENABLE] ?: false }
|
||||
|
||||
val notificationDisableFlow: Flow<Boolean> =
|
||||
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DISABLE] ?: false }
|
||||
|
||||
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
|
||||
dataStore.edit {
|
||||
it[key] = value
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +1,83 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.se.omapi.Reader
|
||||
import android.se.omapi.SEService
|
||||
import android.telephony.TelephonyManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import android.telephony.UiccCardInfo
|
||||
import android.telephony.UiccPortInfo
|
||||
import im.angry.openeuicc.util.*
|
||||
import java.lang.RuntimeException
|
||||
|
||||
val TelephonyManager.activeModemCountCompat: Int
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
activeModemCount
|
||||
} else {
|
||||
phoneCount
|
||||
}
|
||||
|
||||
fun SEService.getUiccReaderCompat(slotNumber: Int): Reader {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return getUiccReader(slotNumber)
|
||||
} else {
|
||||
return readers.first { it.name == "SIM${slotNumber}" || (slotNumber == 1 && it.name == "SIM") }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* In the privileged version, the EuiccChannelManager should work
|
||||
* based on real Uicc{Card,Port}Info reported by TelephonyManager.
|
||||
* However, when unprivileged, we cannot depend on the fact that
|
||||
* we can access TelephonyManager. ARA-M only grants access to
|
||||
* OMAPI, but not TelephonyManager APIs that are associated with
|
||||
* carrier privileges.
|
||||
*
|
||||
* To maximally share code between the two variants, we define
|
||||
* an interface of whatever information will be used in the shared
|
||||
* portion of EuiccChannelManager etc. When unprivileged, we
|
||||
* generate "fake" versions based solely on how many slots the phone
|
||||
* has, while the privileged version can populate the fields with
|
||||
* real information, extending whenever needed.
|
||||
*/
|
||||
interface UiccCardInfoCompat {
|
||||
@Suppress("DEPRECATION")
|
||||
class UiccCardInfoCompat(val inner: UiccCardInfo) {
|
||||
val physicalSlotIndex: Int
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
inner.physicalSlotIndex
|
||||
} else {
|
||||
inner.slotIndex
|
||||
}
|
||||
|
||||
val ports: Collection<UiccPortInfoCompat>
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
inner.ports.map { UiccPortInfoCompat(it, this) }
|
||||
} else {
|
||||
listOf(UiccPortInfoCompat(null, this))
|
||||
}
|
||||
|
||||
val isEuicc: Boolean
|
||||
get() = inner.isEuicc
|
||||
|
||||
val isRemovable: Boolean
|
||||
get() = inner.isRemovable
|
||||
|
||||
val isMultipleEnabledProfilesSupported: Boolean
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
inner.isMultipleEnabledProfilesSupported
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val cardId: Int
|
||||
get() = inner.cardId
|
||||
}
|
||||
|
||||
interface UiccPortInfoCompat {
|
||||
val card: UiccCardInfoCompat
|
||||
class UiccPortInfoCompat(private val _inner: Any?, val card: UiccCardInfoCompat) {
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
check(_inner != null && _inner is UiccPortInfo) {
|
||||
"_inner is not UiccPortInfo on TIRAMISU"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val inner: UiccPortInfo
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
_inner as UiccPortInfo
|
||||
} else {
|
||||
throw RuntimeException("UiccPortInfo does not exist before T")
|
||||
}
|
||||
|
||||
val portIndex: Int
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
inner.portIndex
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
val logicalSlotIndex: Int
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
inner.logicalSlotIndex
|
||||
} else {
|
||||
card.physicalSlotIndex // logical is the same as physical below TIRAMISU
|
||||
}
|
||||
}
|
||||
|
||||
data class FakeUiccCardInfoCompat(
|
||||
override val physicalSlotIndex: Int,
|
||||
): UiccCardInfoCompat {
|
||||
override val ports: Collection<UiccPortInfoCompat> =
|
||||
listOf(FakeUiccPortInfoCompat(this))
|
||||
}
|
||||
|
||||
data class FakeUiccPortInfoCompat(
|
||||
override val card: UiccCardInfoCompat
|
||||
): UiccPortInfoCompat {
|
||||
override val portIndex: Int = 0
|
||||
override val logicalSlotIndex: Int = card.physicalSlotIndex
|
||||
}
|
||||
val TelephonyManager.uiccCardsInfoCompat: List<UiccCardInfoCompat>
|
||||
@SuppressLint("MissingPermission")
|
||||
get() = uiccCardsInfo.map { UiccCardInfoCompat(it) }
|
|
@ -1,9 +1,18 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.OpenEuiccApplication
|
||||
|
||||
val Activity.openEuiccApplication: OpenEuiccApplication
|
||||
get() = application as OpenEuiccApplication
|
||||
|
||||
val Fragment.openEuiccApplication: OpenEuiccApplication
|
||||
get() = requireActivity().openEuiccApplication
|
||||
|
||||
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
||||
/**
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.se.omapi.SEService
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.OpenEuiccApplication
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import java.lang.RuntimeException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
val Context.selfAppVersion: String
|
||||
get() =
|
||||
try {
|
||||
val pInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
interface OpenEuiccContextMarker {
|
||||
val openEuiccMarkerContext: Context
|
||||
get() = when (this) {
|
||||
is Context -> this
|
||||
is Fragment -> requireContext()
|
||||
else -> throw RuntimeException("OpenEuiccUIContextMarker shall only be used on Fragments or UI types that derive from Context")
|
||||
}
|
||||
|
||||
val openEuiccApplication: OpenEuiccApplication
|
||||
get() = openEuiccMarkerContext.applicationContext as OpenEuiccApplication
|
||||
|
||||
val euiccChannelManager: EuiccChannelManager
|
||||
get() = openEuiccApplication.euiccChannelManager
|
||||
|
||||
val telephonyManager: TelephonyManager
|
||||
get() = openEuiccApplication.telephonyManager
|
||||
}
|
||||
|
||||
val LocalProfileInfo.isEnabled: Boolean
|
||||
get() = state == LocalProfileInfo.State.Enabled
|
||||
|
||||
val List<EuiccChannel>.hasMultipleChips: Boolean
|
||||
get() = distinctBy { it.slotId }.size > 1
|
||||
|
||||
// Create an instance of OMAPI SEService in a manner that "makes sense" without unpredictable callbacks
|
||||
suspend fun connectSEService(context: Context): SEService = suspendCoroutine { cont ->
|
||||
// Use a Mutex to make sure the continuation is run *after* the "service" variable is assigned
|
||||
val lock = Mutex()
|
||||
var service: SEService? = null
|
||||
val callback = {
|
||||
runBlocking {
|
||||
lock.withLock {
|
||||
cont.resume(service!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
// If this were not protected by a Mutex, callback might be run before service is even assigned
|
||||
// Yes, we are on Android, we could have used something like a Handler, but we cannot really
|
||||
// assume the coroutine is run on a thread that has a Handler. We either use our own HandlerThread
|
||||
// (and then cleanup becomes an issue), or we use a lock
|
||||
lock.withLock {
|
||||
try {
|
||||
service = SEService(context, { it.run() }, callback)
|
||||
} catch (e: Exception) {
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid
|
||||
android:color="?attr/colorSurface"/>
|
||||
<corners
|
||||
android:radius="?attr/dialogCornerRadius" />
|
||||
</shape>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
|
@ -1,47 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/log_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
android:textSize="10sp"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:longClickable="true"
|
||||
tools:ignore="SmallSp" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -6,22 +6,14 @@
|
|||
android:layout_height="match_parent"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_root"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<TextView
|
||||
android:id="@+id/no_euicc_placeholder"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,7 +4,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
|
@ -103,6 +103,6 @@
|
|||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</FrameLayout>
|
|
@ -2,13 +2,15 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Theme.OpenEUICC"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1"
|
||||
|
@ -41,6 +43,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:hint="@string/profile_download_server"
|
||||
style="@style/Widget.OpenEUICC.Input"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -48,7 +51,8 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:theme="@style/Theme.OpenEUICC.Input.Cursor"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
@ -58,6 +62,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="15dp"
|
||||
android:hint="@string/profile_download_code"
|
||||
style="@style/Widget.OpenEUICC.Input"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_download_server"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -67,7 +72,8 @@
|
|||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textPassword" />
|
||||
android:inputType="textPassword"
|
||||
android:theme="@style/Theme.OpenEUICC.Input.Cursor"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
@ -77,6 +83,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="15dp"
|
||||
android:hint="@string/profile_download_confirmation_code"
|
||||
style="@style/Widget.OpenEUICC.Input"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_download_code"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -86,7 +93,8 @@
|
|||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textPassword" />
|
||||
android:inputType="textPassword"
|
||||
android:theme="@style/Theme.OpenEUICC.Input.Cursor"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
@ -97,6 +105,7 @@
|
|||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:hint="@string/profile_download_imei"
|
||||
style="@style/Widget.OpenEUICC.Input"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_download_confirmation_code"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -107,7 +116,8 @@
|
|||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textPassword" />
|
||||
android:inputType="textPassword"
|
||||
android:theme="@style/Theme.OpenEUICC.Input.Cursor"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Theme.OpenEUICC"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1"
|
||||
|
@ -40,6 +43,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="15dp"
|
||||
android:hint="@string/profile_rename_new_name"
|
||||
style="@style/Widget.OpenEUICC.Input"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -48,7 +52,8 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:theme="@style/Theme.OpenEUICC.Input.Cursor"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="48dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_profile_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="marquee"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/notification_address"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -5,10 +5,6 @@
|
|||
android:id="@+id/spinner"
|
||||
android:title=""
|
||||
app:actionViewClass="android.widget.Spinner"
|
||||
android:background="?android:attr/colorPrimary"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:title="@string/pref_settings"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/help"
|
||||
android:icon="@drawable/ic_help_black"
|
||||
android:title="@string/help"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/show_notifications"
|
||||
android:title="@string/profile_notifications_show"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/ok"
|
||||
android:icon="@drawable/ic_check_black"
|
||||
android:title="@string/slot_select_select"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/notification_process"
|
||||
android:title="@string/profile_notification_process" />
|
||||
|
||||
<item
|
||||
android:id="@+id/notification_delete"
|
||||
android:title="@string/profile_notification_delete" />
|
||||
</menu>
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/disable"
|
||||
android:visible="false"
|
||||
android:title="@string/disable"/>
|
||||
|
||||
<item
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICUDCCAfegAwIBAgIJALh086v6bETTMAoGCCqGSM49BAMCMEQxEDAOBgNVBAMM
|
||||
B1Rlc3QgQ0kxETAPBgNVBAsMCFRFU1RDRVJUMRAwDgYDVQQKDAdSU1BURVNUMQsw
|
||||
CQYDVQQGEwJJVDAgFw0yMDA0MDEwODI3NTFaGA8yMDU1MDQwMTA4Mjc1MVowRDEQ
|
||||
MA4GA1UEAwwHVGVzdCBDSTERMA8GA1UECwwIVEVTVENFUlQxEDAOBgNVBAoMB1JT
|
||||
UFRFU1QxCzAJBgNVBAYTAklUMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElAZX
|
||||
pnPcKI+J1S6opHcEmSeR+cNLADbmM+LQy6lFTWXbMusXmBeZ0vJDiO4rlcEJRUbJ
|
||||
eQHOrrqWUJGaLiDSKaOBzzCBzDAdBgNVHQ4EFgQU9UFyvfmKldZcvriKOKHBHYAK
|
||||
hcMwDwYDVR0TAQH/BAUwAwEB/zAXBgNVHSABAf8EDTALMAkGB2eBEgECAQAwDgYD
|
||||
VR0PAQH/BAQDAgEGMA4GA1UdEQQHMAWIA4g3ATBhBgNVHR8EWjBYMCqgKKAmhiRo
|
||||
dHRwOi8vY2kudGVzdC5leGFtcGxlLmNvbS9DUkwtQS5jcmwwKqAooCaGJGh0dHA6
|
||||
Ly9jaS50ZXN0LmV4YW1wbGUuY29tL0NSTC1CLmNybDAKBggqhkjOPQQDAgNHADBE
|
||||
AiBSdWqvwgIKbOy/Ll88IIklEP8pdR0pi9OwFdlgWk/mfQIgV5goNuTSBd3S5sPB
|
||||
tFWTf2tuSTtgL9G2bDV0iak192s=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICUTCCAfigAwIBAgIJALh086v6bETTMAoGCCqGSM49BAMCMEQxEDAOBgNVBAMM
|
||||
B1Rlc3QgQ0kxETAPBgNVBAsMCFRFU1RDRVJUMRAwDgYDVQQKDAdSU1BURVNUMQsw
|
||||
CQYDVQQGEwJJVDAgFw0yMDA0MDEwODI3NTFaGA8yMDU1MDQwMTA4Mjc1MVowRDEQ
|
||||
MA4GA1UEAwwHVGVzdCBDSTERMA8GA1UECwwIVEVTVENFUlQxEDAOBgNVBAoMB1JT
|
||||
UFRFU1QxCzAJBgNVBAYTAklUMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABCeH
|
||||
tNVu2CSp5r4E4Yh/a5i6/rjHY/UoN/cBE+k2Tt2+E5vAx95+Fo8eXNDBhTT8UGTm
|
||||
T2htxTMnyn8dzqhaKZSjgc8wgcwwHQYDVR0OBBYEFMC8cLo2kp1DtGf/V1cFMOV6
|
||||
uPzYMA8GA1UdEwEB/wQFMAMBAf8wFwYDVR0gAQH/BA0wCzAJBgdngRIBAgEAMA4G
|
||||
A1UdDwEB/wQEAwIBBjAOBgNVHREEBzAFiAOINwEwYQYDVR0fBFowWDAqoCigJoYk
|
||||
aHR0cDovL2NpLnRlc3QuZXhhbXBsZS5jb20vQ1JMLUEuY3JsMCqgKKAmhiRodHRw
|
||||
Oi8vY2kudGVzdC5leGFtcGxlLmNvbS9DUkwtQi5jcmwwCgYIKoZIzj0EAwIDRwAw
|
||||
RAIgPYrf0CKl0FBMUaHx5xS1duTDbQ4wBZN3qKBeNniuux0CIHBek2vLfoANAdtt
|
||||
f5u5Ce6DVC2oIfpn5UnS24F3oMqM
|
||||
-----END CERTIFICATE-----
|
8
app-common/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="gray_300">#E0E0E0</color>
|
||||
<color name="pink_600">#D81B60</color>
|
||||
<color name="pink_800">#AD1457</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
|
@ -2,7 +2,6 @@
|
|||
<resources>
|
||||
<string name="no_euicc">No eUICC card on this device is accessible by this app.\nInsert a supported eUICC card, or try out the privileged OpenEUICC app instead.</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="help">Help</string>
|
||||
|
||||
<string name="channel_name_format">Logical Slot %d</string>
|
||||
|
||||
|
@ -16,12 +15,10 @@
|
|||
<string name="delete">Delete</string>
|
||||
<string name="rename">Rename</string>
|
||||
|
||||
<string name="toast_profile_enabled">eSIM profile switched. Please wait for a while when the card is restarting.</string>
|
||||
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
||||
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
|
||||
|
||||
<string name="slot_select">Select Slot</string>
|
||||
<string name="slot_select_select">Select</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>
|
||||
|
@ -35,34 +32,4 @@
|
|||
<string name="profile_rename_new_name">New nickname</string>
|
||||
|
||||
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
|
||||
|
||||
<string name="profile_notifications">Profile Notifications</string>
|
||||
<string name="profile_notifications_show">Manage Notifications</string>
|
||||
<string name="profile_notifications_help">eSIM profiles can send notifications to the carrier when they are downloaded, deleted, enabled, or disabled. The queue of these notifications to be sent is listed here.\n\nIn Settings, you can specify whether to send each type of notification automatically. Note that even if a notification has been sent, it will not be deleted automatically from the record, unless the queue runs out of space.\n\nHere, you can manually send or delete each pending notification.</string>
|
||||
<string name="profile_notification_operation_download">Downloaded</string>
|
||||
<string name="profile_notification_operation_delete">Deleted</string>
|
||||
<string name="profile_notification_operation_enable">Enabled</string>
|
||||
<string name="profile_notification_operation_disable">Disabled</string>
|
||||
<string name="profile_notification_name_format"><b>%1$s</b> %2$s (%3$s)</string>
|
||||
<string name="profile_notification_process">Process</string>
|
||||
<string name="profile_notification_delete">Delete</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>
|
||||
<string name="pref_notifications_download">Downloads</string>
|
||||
<string name="pref_notifications_download_desc">Send notifications for <i>downloading</i> profiles</string>
|
||||
<string name="pref_notifications_delete">Deletion</string>
|
||||
<string name="pref_notifications_delete_desc">Send notifications for <i>deleting</i> profiles</string>
|
||||
<string name="pref_notifications_enable">Enabling</string>
|
||||
<string name="pref_notifications_enable_desc">Send notifications for <i>enabling</i> profiles\nNote that this type of notification is unreliable.</string>
|
||||
<string name="pref_notifications_disable">Disabling</string>
|
||||
<string name="pref_notifications_disable_desc">Send notifications for <i>disabling</i> profiles\nNote that this type of notification is unreliable.</string>
|
||||
<string name="pref_advanced">Advanced</string>
|
||||
<string name="pref_advanced_logs">Logs</string>
|
||||
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
|
||||
<string name="pref_info">Info</string>
|
||||
<string name="pref_info_app_version">App Version</string>
|
||||
<string name="pref_info_source_code">Source Code</string>
|
||||
<string name="pref_info_source_code_url" translatable="false">https://gitea.angry.im/PeterCxy/OpenEUICC</string>
|
||||
</resources>
|
|
@ -1,41 +1,46 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.OpenEUICC" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
|
||||
<style name="Theme.OpenEUICC" parent="Theme.MaterialComponents.DayNight">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/white</item>
|
||||
<item name="colorPrimaryVariant">@color/gray_300</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/pink_600</item>
|
||||
<item name="colorSecondaryVariant">@color/pink_800</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
<item name="colorAccent">?attr/colorSecondary</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimary</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="alertDialogTheme">@style/AlertDialogTheme</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightNavigationBar">?attr/isLightTheme</item>
|
||||
<item name="toolbarStyle">@style/ToolbarTheme</item>
|
||||
<item name="android:statusBarColor">?attr/colorSurfaceVariant</item>
|
||||
<item name="android:colorBackground">?attr/colorSurface</item>
|
||||
<item name="dialogCornerRadius">28dp</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="ToolbarTheme" parent="Widget.Material3.Toolbar">
|
||||
<item name="android:background">?attr/colorSurfaceVariant</item>
|
||||
<style name="Theme.OpenEUICC.Input.Cursor" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
|
||||
<item name="colorControlActivated">?attr/colorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="AlertDialogTheme" parent="Theme.Material3.DayNight.Dialog.Alert">
|
||||
<style name="Widget.OpenEUICC.Input" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
<item name="boxBackgroundColor">@android:color/transparent</item>
|
||||
<item name="boxStrokeColor">?attr/colorSecondary</item>
|
||||
<item name="hintTextColor">?attr/colorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="AlertDialogTheme" parent="ThemeOverlay.MaterialComponents.Dialog.Alert">
|
||||
<item name="buttonBarNegativeButtonStyle">@style/NegativeButtonStyle</item>
|
||||
<item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item>
|
||||
<item name="dialogCornerRadius">28dp</item>
|
||||
</style>
|
||||
|
||||
<style name="NegativeButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
|
||||
<style name="NegativeButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
|
||||
<item name="android:textColor">?attr/colorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
|
||||
<style name="PositiveButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
|
||||
<item name="android:textColor">?attr/colorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:colorBackgroundCacheHint">@null</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowAnimationStyle">@android:style/Animation</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
|
||||
</resources>
|
|
@ -3,7 +3,6 @@
|
|||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="@raw/symantec_gsma_rspv2_root_ci1"/>
|
||||
<certificates src="@raw/gsma_sgp26"/>
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory
|
||||
app:title="@string/pref_notifications"
|
||||
app:summary="@string/pref_notifications_desc"
|
||||
app:iconSpaceReserved="false">
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_notifications_download"
|
||||
app:summary="@string/pref_notifications_download_desc"
|
||||
app:key="pref_notifications_download" />
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_notifications_delete"
|
||||
app:summary="@string/pref_notifications_delete_desc"
|
||||
app:key="pref_notifications_delete" />
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_notifications_enable"
|
||||
app:summary="@string/pref_notifications_enable_desc"
|
||||
app:key="pref_notifications_enable" />
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_notifications_disable"
|
||||
app:summary="@string/pref_notifications_disable_desc"
|
||||
app:key="pref_notifications_disable" />
|
||||
</im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/pref_advanced"
|
||||
app:iconSpaceReserved="false">
|
||||
<Preference
|
||||
app:key="pref_advanced_logs"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_advanced_logs"
|
||||
app:summary="@string/pref_advanced_logs_desc" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:title="@string/pref_info"
|
||||
app:iconSpaceReserved="false">
|
||||
<Preference
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_info_app_version"
|
||||
app:key="pref_info_app_version" />
|
||||
|
||||
<Preference
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_info_source_code"
|
||||
app:summary="@string/pref_info_source_code_url"
|
||||
app:key="pref_info_source_code"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
1
app-deps/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,16 +0,0 @@
|
|||
java_defaults {
|
||||
name: "OpenEUICC-deps-defaults",
|
||||
static_libs: [
|
||||
// DO NOT EDIT THIS SECTION MANUALLY
|
||||
"androidx.core_core-ktx",
|
||||
"androidx.appcompat_appcompat",
|
||||
"com.google.android.material_material",
|
||||
"androidx-constraintlayout_constraintlayout",
|
||||
"androidx.preference_preference",
|
||||
"androidx.lifecycle_lifecycle-runtime-ktx",
|
||||
"androidx.swiperefreshlayout_swiperefreshlayout",
|
||||
"androidx.cardview_cardview",
|
||||
"OpenEUICC_androidx.datastore_datastore-preferences",
|
||||
"OpenEUICC_com.journeyapps_zxing-android-embedded",
|
||||
],
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import org.lineageos.generatebp.GenerateBpPlugin
|
||||
import org.lineageos.generatebp.GenerateBpPluginExtension
|
||||
import org.lineageos.generatebp.models.Module
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
apply {
|
||||
plugin<GenerateBpPlugin>()
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.angry.openeuicc_deps"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("androidx.core:core-ktx:1.12.0")
|
||||
api("androidx.appcompat:appcompat:1.6.1")
|
||||
api("com.google.android.material:material:1.10.0")
|
||||
api("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
//noinspection KtxExtensionAvailable
|
||||
api("androidx.preference:preference:1.2.1")
|
||||
api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||
api("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
api("androidx.cardview:cardview:1.0.0")
|
||||
api("androidx.datastore:datastore-preferences:1.0.0")
|
||||
api("com.journeyapps:zxing-android-embedded:4.3.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
}
|
||||
|
||||
configure<GenerateBpPluginExtension> {
|
||||
targetSdk.set(android.compileSdk!!)
|
||||
availableInAOSP.set { module: Module ->
|
||||
when {
|
||||
module.group == "androidx.datastore" -> false
|
||||
module.group.startsWith("androidx") -> true
|
||||
module.group == "com.google.android.material" -> true
|
||||
module.group.startsWith("org.jetbrains") -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
21
app-deps/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,24 +0,0 @@
|
|||
package im.angry.openeuicc_deps
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("im.angry.openeuicc_deps.test", appContext.packageName)
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
|
@ -1,17 +0,0 @@
|
|||
package im.angry.openeuicc_deps
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
1
app-unpriv/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,45 +0,0 @@
|
|||
import im.angry.openeuicc.build.*
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
signingKeystoreProperties {
|
||||
keyAliasField = "unprivKeyAlias"
|
||||
keyPasswordField = "unprivKeyPassword"
|
||||
}
|
||||
|
||||
apply {
|
||||
plugin<MyVersioningPlugin>()
|
||||
plugin<MySigningPlugin>()
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.angry.easyeuicc"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "im.angry.easyeuicc_sgp26"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":app-common"))
|
||||
}
|
21
app-unpriv/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name="im.angry.openeuicc.OpenEuiccApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OpenEUICC">
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
||||
android:label="@string/compatibility_check"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,85 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import im.angry.easyeuicc.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CompatibilityCheckActivity: AppCompatActivity() {
|
||||
private lateinit var compatibilityCheckList: RecyclerView
|
||||
private val compatibilityChecks: List<CompatibilityCheck> by lazy { getCompatibilityChecks(this) }
|
||||
private val adapter = CompatibilityChecksAdapter()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_compatibility_check)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
compatibilityCheckList = findViewById(R.id.recycler_view)
|
||||
compatibilityCheckList.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
compatibilityCheckList.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleScope.launch {
|
||||
compatibilityChecks.executeAll { adapter.notifyDataSetChanged() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
inner class ViewHolder(private val root: View): RecyclerView.ViewHolder(root) {
|
||||
private val titleView: TextView = root.findViewById(R.id.compatibility_check_title)
|
||||
private val descView: TextView = root.findViewById(R.id.compatibility_check_desc)
|
||||
|
||||
fun bindItem(item: CompatibilityCheck) {
|
||||
titleView.text = item.title
|
||||
descView.text = item.description
|
||||
|
||||
when (item.state) {
|
||||
CompatibilityCheck.State.SUCCESS -> {
|
||||
root.findViewById<View>(R.id.compatibility_check_checkmark).visibility = View.VISIBLE
|
||||
}
|
||||
CompatibilityCheck.State.FAILURE -> {
|
||||
root.findViewById<View>(R.id.compatibility_check_error).visibility = View.VISIBLE
|
||||
}
|
||||
else -> {
|
||||
root.findViewById<View>(R.id.compatibility_check_progress_bar).visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class CompatibilityChecksAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
ViewHolder(layoutInflater.inflate(R.layout.compatibility_check_item, parent, false))
|
||||
|
||||
override fun getItemCount(): Int =
|
||||
compatibilityChecks.indexOfLast { it.state != CompatibilityCheck.State.NOT_STARTED } + 1
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bindItem(compatibilityChecks[position])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package im.angry.openeuicc.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import im.angry.easyeuicc.R
|
||||
|
||||
class UnprivilegedMainActivity: MainActivity() {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.activity_main_unprivileged, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
when (item.itemId) {
|
||||
R.id.compatibility_check -> {
|
||||
startActivity(Intent(this, CompatibilityCheckActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package im.angry.openeuicc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.se.omapi.Reader
|
||||
import android.telephony.TelephonyManager
|
||||
import im.angry.easyeuicc.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun getCompatibilityChecks(context: Context): List<CompatibilityCheck> =
|
||||
listOf(
|
||||
HasSystemFeaturesCheck(context),
|
||||
OmapiConnCheck(context),
|
||||
IsdrChannelAccessCheck(context),
|
||||
KnownBrokenCheck(context)
|
||||
)
|
||||
|
||||
suspend fun List<CompatibilityCheck>.executeAll(callback: () -> Unit) = withContext(Dispatchers.IO) {
|
||||
forEach {
|
||||
it.run()
|
||||
withContext(Dispatchers.Main) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Reader.isSIM: Boolean
|
||||
get() = name.startsWith("SIM")
|
||||
|
||||
private val Reader.slotIndex: Int
|
||||
get() = (name.replace("SIM", "").toIntOrNull() ?: 1)
|
||||
|
||||
abstract class CompatibilityCheck(context: Context) {
|
||||
enum class State {
|
||||
NOT_STARTED,
|
||||
IN_PROGRESS,
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
var state = State.NOT_STARTED
|
||||
|
||||
abstract val title: String
|
||||
protected abstract val defaultDescription: String
|
||||
protected lateinit var failureDescription: String
|
||||
|
||||
val description: String
|
||||
get() = when {
|
||||
state == State.FAILURE && this::failureDescription.isInitialized -> failureDescription
|
||||
else -> defaultDescription
|
||||
}
|
||||
|
||||
protected abstract suspend fun doCheck(): Boolean
|
||||
|
||||
suspend fun run() {
|
||||
state = State.IN_PROGRESS
|
||||
delay(200)
|
||||
state = try {
|
||||
if (doCheck()) {
|
||||
State.SUCCESS
|
||||
} else {
|
||||
State.FAILURE
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
State.FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class HasSystemFeaturesCheck(private val context: Context): CompatibilityCheck(context) {
|
||||
override val title: String
|
||||
get() = context.getString(R.string.compatibility_check_system_features)
|
||||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_system_features_desc)
|
||||
|
||||
override suspend fun doCheck(): Boolean {
|
||||
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_system_features_no_telephony)
|
||||
return false
|
||||
}
|
||||
|
||||
// We can check OMAPI UICC availability on R or later (if before R, we check OMAPI connectivity later)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !context.packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_SE_OMAPI_UICC
|
||||
)) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_system_features_no_omapi)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
internal class OmapiConnCheck(private val context: Context): CompatibilityCheck(context) {
|
||||
override val title: String
|
||||
get() = context.getString(R.string.compatibility_check_omapi_connectivity)
|
||||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_omapi_connectivity_desc)
|
||||
|
||||
override suspend fun doCheck(): Boolean {
|
||||
val seService = connectSEService(context)
|
||||
if (!seService.isConnected) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail)
|
||||
return false
|
||||
}
|
||||
|
||||
val tm = context.getSystemService(TelephonyManager::class.java)
|
||||
val simReaders = seService.readers.filter { it.isSIM }
|
||||
if (simReaders.isEmpty()) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail)
|
||||
return false
|
||||
} else if (simReaders.size < tm.activeModemCountCompat) {
|
||||
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail_sim_number,
|
||||
simReaders.map { it.slotIndex }.joinToString(", "))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
get() = context.getString(R.string.compatibility_check_isdr_channel_desc)
|
||||
|
||||
override suspend fun doCheck(): Boolean {
|
||||
val seService = connectSEService(context)
|
||||
val (validSlotIds, result) = seService.readers.filter { it.isSIM }.map {
|
||||
try {
|
||||
it.openSession().openLogicalChannel(ISDR_AID)?.close()
|
||||
Pair(it.slotIndex, true)
|
||||
} catch (_: SecurityException) {
|
||||
// Ignore; this is expected when everything works
|
||||
// ref: https://android.googlesource.com/platform/frameworks/base/+/4fe64fb4712a99d5da9c9a0eb8fd5169b252e1e1/omapi/java/android/se/omapi/Session.java#305
|
||||
// SecurityException is only thrown when Channel is constructed, which means everything else needs to succeed
|
||||
Pair(it.slotIndex, true)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(it.slotIndex, false)
|
||||
}
|
||||
}.fold(Pair(mutableListOf<Int>(), true)) { (ids, result), (id, ok) ->
|
||||
if (!ok) {
|
||||
Pair(ids, false)
|
||||
} else {
|
||||
Pair(ids.apply { add(id) }, result)
|
||||
}
|
||||
}
|
||||
|
||||
if (!result && validSlotIds.size > 0) {
|
||||
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
|
||||
failureDescription = context.getString(
|
||||
R.string.compatibility_check_isdr_channel_desc_partial_fail,
|
||||
validSlotIds.joinToString(", ")
|
||||
)
|
||||
} else {
|
||||
// If the device has embedded eSIMs, we can likely ignore the failure here;
|
||||
// the OMAPI failure likely resulted from trying to access internal eSIMs.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
internal class KnownBrokenCheck(private val context: Context): CompatibilityCheck(context) {
|
||||
companion object {
|
||||
val BROKEN_MANUFACTURERS = arrayOf("xiaomi")
|
||||
}
|
||||
|
||||
override val title: String
|
||||
get() = context.getString(R.string.compatibility_check_known_broken)
|
||||
override val defaultDescription: String
|
||||
get() = context.getString(R.string.compatibility_check_known_broken_desc)
|
||||
|
||||
init {
|
||||
failureDescription = context.getString(R.string.compatibility_check_known_broken_fail)
|
||||
}
|
||||
|
||||
override suspend fun doCheck(): Boolean =
|
||||
Build.MANUFACTURER.lowercase() !in BROKEN_MANUFACTURERS
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.59,7.58L10,14.17l-3.59,-3.58L5,12l5,5 8,-8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
|
@ -1,170 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#9C27B0"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="0.5162"
|
||||
android:scaleY="0.5162"
|
||||
android:translateX="5.8056"
|
||||
android:translateY="5.8056">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.99,4c0,-1.1 -0.89,-2 -1.99,-2h-8L4,8v12c0,1.1 0.9,2 2,2h12.01c1.1,0 1.99,-0.9 1.99,-2l-0.01,-16zM9,19L7,19v-2h2v2zM17,19h-2v-2h2v2zM9,15L7,15v-4h2v4zM13,19h-2v-4h2v4zM13,13h-2v-2h2v2zM17,15h-2v-4h2v4z"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,65 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/compatibility_check_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/compatibility_check_status_container"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/compatibility_check_desc"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/compatibility_check_status_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/compatibility_check_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/compatibility_check_status_container"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/compatibility_check_progress_bar"
|
||||
android:visibility="gone"
|
||||
android:indeterminate="true"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/compatibility_check_checkmark"
|
||||
android:src="@drawable/ic_checkmark_outline"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/compatibility_check_error"
|
||||
android:src="@drawable/ic_error_outline"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/compatibility_check"
|
||||
android:title="@string/compatibility_check"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 9.4 KiB |
|
@ -1,20 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">EasyEUICC SGP.26</string>
|
||||
<string name="compatibility_check">Compatibility Check</string>
|
||||
|
||||
<!-- Compatibility Check Descriptions -->
|
||||
<string name="compatibility_check_system_features">System Features</string>
|
||||
<string name="compatibility_check_system_features_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string>
|
||||
<string name="compatibility_check_system_features_no_telephony">Your device has no telephony features.</string>
|
||||
<string name="compatibility_check_system_features_no_omapi">Your device has no support for accessing SIM cards via OMAPI.</string>
|
||||
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
|
||||
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
|
||||
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI.</string>
|
||||
<string name="compatibility_check_omapi_connectivity_fail_sim_number">Only the following SIM slots are accessible via OMAPI: %s.</string>
|
||||
<string name="compatibility_check_isdr_channel">ISD-R Channel Access</string>
|
||||
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
|
||||
<string name="compatibility_check_isdr_channel_desc_partial_fail">OMAPI access to ISD-R is only possible on the following SIM slots: %s.</string>
|
||||
<string name="compatibility_check_known_broken">Known Broken?</string>
|
||||
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
|
||||
<string name="compatibility_check_known_broken_fail">Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution.</string>
|
||||
</resources>
|
|
@ -1,17 +0,0 @@
|
|||
package im.angry.easyeuicc
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
70
app/build.gradle
Normal file
|
@ -0,0 +1,70 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
apply from: '../helpers.gradle'
|
||||
|
||||
// Signing config, mainly intended for debug builds
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties");
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "im.angry.openeuicc"
|
||||
minSdk 30
|
||||
targetSdk 34
|
||||
versionCode getGitVersionCode()
|
||||
versionName getGitVersionName()
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
config {
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.config
|
||||
}
|
||||
debug {
|
||||
signingConfig signingConfigs.config
|
||||
}
|
||||
}
|
||||
applicationVariants.configureEach { variant ->
|
||||
if (variant.name == "debug") {
|
||||
variant.outputs.each { o -> o.versionCodeOverride = System.currentTimeSeconds() }
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
namespace 'im.angry.openeuicc'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
compileOnly project(':libs:hidden-apis-stub')
|
||||
implementation project(':libs:hidden-apis-shim')
|
||||
implementation project(':libs:lpac-jni')
|
||||
implementation project(":app-common")
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import im.angry.openeuicc.build.*
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
apply {
|
||||
plugin<MyVersioningPlugin>()
|
||||
plugin<MySigningPlugin>()
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.angry.openeuicc"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "im.angry.openeuicc_sgp26"
|
||||
minSdk = 30
|
||||
targetSdk = 34
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":libs:hidden-apis-stub"))
|
||||
implementation(project(":libs:hidden-apis-shim"))
|
||||
implementation(project(":libs:lpac-jni"))
|
||||
implementation(project(":app-common"))
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
}
|
|
@ -10,7 +10,6 @@ class PrivilegedOpenEuiccApplication: OpenEuiccApplication() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
(euiccChannelManager as PrivilegedEuiccChannelManager).closeAllStaleChannels()
|
||||
}
|
||||
}
|
|
@ -8,12 +8,9 @@ import java.lang.Exception
|
|||
import java.lang.IllegalArgumentException
|
||||
|
||||
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
|
||||
override val uiccCards: Collection<UiccCardInfoCompat>
|
||||
get() = tm.uiccCardsInfoCompat
|
||||
override fun checkPrivileges() = true // TODO: Implement proper system app check
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
|
||||
val port = port as RealUiccPortInfoCompat
|
||||
if (port.card.isRemovable) {
|
||||
// Attempt unprivileged (OMAPI) before TelephonyManager
|
||||
// but still try TelephonyManager in case OMAPI is broken
|
||||
|
@ -22,6 +19,7 @@ class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(conte
|
|||
|
||||
if (port.card.isEuicc) {
|
||||
Log.i(TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
|
||||
// TODO: On Tiramisu, we should also connect all available "ports" for MEP support
|
||||
try {
|
||||
return TelephonyManagerChannel(port, tm)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
|
|
|
@ -20,7 +20,6 @@ class TelephonyManagerApduInterface(
|
|||
|
||||
override fun disconnect() {
|
||||
// Do nothing
|
||||
lastChannel = -1
|
||||
}
|
||||
|
||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||
|
|