Compare commits

..

No commits in common. "master" and "unpriv-decouple" have entirely different histories.

144 changed files with 771 additions and 4542 deletions

View file

@ -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

View file

@ -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
View file

@ -8,7 +8,6 @@
/.idea/workspace.xml /.idea/workspace.xml
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store .DS_Store
/build /build
/captures /captures
@ -16,5 +15,3 @@
.cxx .cxx
local.properties local.properties
/libs/**/build /libs/**/build
/buildSrc/build
/app-deps/libs

View file

@ -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>

View file

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

5
.idea/compiler.xml generated
View file

@ -4,11 +4,6 @@
<bytecodeTargetLevel target="1.7"> <bytecodeTargetLevel target="1.7">
<module name="OpenEUICC.app" target="17" /> <module name="OpenEUICC.app" target="17" />
<module name="OpenEUICC.app-common" 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.hidden-apis-shim" target="17" />
<module name="OpenEUICC.libs.lpac-jni" target="17" /> <module name="OpenEUICC.libs.lpac-jni" target="17" />
</bytecodeTargetLevel> </bytecodeTargetLevel>

3
.idea/gradle.xml generated
View file

@ -14,9 +14,6 @@
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/app-common" /> <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" />
<option value="$PROJECT_DIR$/libs/hidden-apis-shim" /> <option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
<option value="$PROJECT_DIR$/libs/hidden-apis-stub" /> <option value="$PROJECT_DIR$/libs/hidden-apis-stub" />

View file

@ -1,52 +1,30 @@
java_library {
name: "net.typeblog.lpac_jni",
srcs: [
"libs/lpac-jni/src/main/**/*.kt",
],
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",
"kotlinx_coroutines",
],
srcs: [
"app-common/src/main/**/*.kt",
],
optimize: {
enabled: false,
},
resource_dirs: [
"app-common/src/main/res",
],
kotlincflags: [
"-opt-in=kotlin.ExperimentalStdlibApi",
],
manifest: "app-common/src/main/AndroidManifest.xml",
system_ext_specific: true,
}
android_app { android_app {
name: "OpenEUICC", name: "OpenEUICC",
static_libs: [ static_libs: [
"OpenEUICC-common", // 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",
"gson",
"kotlinx_coroutines",
], ],
jni_libs: [ jni_libs: [
"liblpac-jni", "liblpac-jni",
], ],
srcs: [ srcs: [
// Main app
"app/src/main/**/*.kt", "app/src/main/**/*.kt",
// lpac-jni interface
"libs/lpac-jni/src/main/**/*.kt",
], ],
optimize: { optimize: {
enabled: false, enabled: false,

7
COPYING Normal file
View 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/>.

View file

@ -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
View 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'
}

View file

@ -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")
}

View file

@ -1,30 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android">
package="im.angry.openeuicc.common">
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:networkSecurityConfig="@xml/network_security_config"> 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 <activity
android:name="com.journeyapps.barcodescanner.CaptureActivity" android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor" android:screenOrientation="fullSensor"

View file

@ -3,18 +3,9 @@ package im.angry.openeuicc
import android.app.Application import android.app.Application
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import com.google.android.material.color.DynamicColors
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.PreferenceRepository
open class OpenEuiccApplication : Application() { open class OpenEuiccApplication : Application() {
override fun onCreate() {
super.onCreate()
// Observe dynamic colors changes
DynamicColors.applyToActivitiesIfAvailable(this)
}
val telephonyManager by lazy { val telephonyManager by lazy {
getSystemService(TelephonyManager::class.java)!! getSystemService(TelephonyManager::class.java)!!
} }
@ -26,8 +17,4 @@ open class OpenEuiccApplication : Application() {
val subscriptionManager by lazy { val subscriptionManager by lazy {
getSystemService(SubscriptionManager::class.java)!! getSystemService(SubscriptionManager::class.java)!!
} }
val preferenceRepository by lazy {
PreferenceRepository(this)
}
} }

View file

@ -1,18 +1,36 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
// A custom type to avoid compatibility issues with UiccCardInfo / UiccPortInfo
data class EuiccChannelInfo(
val slotId: Int,
val cardId: Int,
val name: String,
val imei: String,
val removable: Boolean
)
abstract class EuiccChannel( abstract class EuiccChannel(
val port: UiccPortInfoCompat info: EuiccChannelInfo
) { ) {
val slotId = port.card.physicalSlotIndex // PHYSICAL slot val slotId = info.slotId
val logicalSlotId = port.logicalSlotIndex val cardId = info.cardId
val portId = port.portIndex val name = info.name
val imei = info.imei
val removable = info.removable
abstract val lpa: LocalProfileAssistant abstract val lpa: LocalProfileAssistant
val valid: Boolean 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() fun close() = lpa.close()
} }

View file

@ -1,18 +1,23 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import android.se.omapi.SEService import android.se.omapi.SEService
import android.telephony.SubscriptionManager import android.telephony.UiccCardInfo
import android.util.Log import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@SuppressLint("MissingPermission") // We rely on ARA-based privileges, not READ_PRIVILEGED_PHONE_STATE
open class EuiccChannelManager(protected val context: Context) { open class EuiccChannelManager(protected val context: Context) {
companion object { companion object {
const val TAG = "EuiccChannelManager" const val TAG = "EuiccChannelManager"
@ -28,42 +33,36 @@ open class EuiccChannelManager(protected val context: Context) {
(context.applicationContext as OpenEuiccApplication).telephonyManager (context.applicationContext as OpenEuiccApplication).telephonyManager
} }
protected open val uiccCards: Collection<UiccCardInfoCompat> private val handler = Handler(HandlerThread("BaseEuiccChannelManager").also { it.start() }.looper)
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
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() { private suspend fun ensureSEService() {
if (seService == null) { if (seService == null) {
seService = connectSEService(context) seService = connectSEService()
} }
} }
protected open fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? { protected open fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? {
// No-op when unprivileged // No-op when unprivileged
return null return null
} }
protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? { private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfo): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
}
Log.i(TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try {
return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}.")
}
return null
}
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock { lock.withLock {
ensureSEService() ensureSEService()
val existing = channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } val existing = channels.find { it.slotId == uiccInfo.slotIndex }
if (existing != null) { if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) { if (existing.valid) {
return existing return existing
} else { } else {
existing.close() existing.close()
@ -71,15 +70,22 @@ open class EuiccChannelManager(protected val context: Context) {
} }
} }
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { val channelInfo = EuiccChannelInfo(
// We can only open channels on ports that are actually enabled uiccInfo.slotIndex,
return null uiccInfo.cardId,
} "SIM ${uiccInfo.slotIndex}",
tm.getImei(uiccInfo.slotIndex) ?: return null,
uiccInfo.isRemovable
)
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port) var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(uiccInfo, channelInfo)
if (euiccChannel == null) { if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(port) try {
euiccChannel = OmapiChannel(seService!!, channelInfo)
} catch (e: IllegalArgumentException) {
// Failed
}
} }
if (euiccChannel != null) { if (euiccChannel != null) {
@ -90,60 +96,28 @@ open class EuiccChannelManager(protected val context: Context) {
} }
} }
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? = private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? {
runBlocking { return tm.uiccCardsInfo.find { it.slotIndex == slotId }?.let {
tryOpenEuiccChannel(it)
}
}
fun findEuiccChannelBySlotBlocking(slotId: Int): EuiccChannel? = runBlocking {
if (!checkPrivileges()) return@runBlocking null
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
for (card in uiccCards) { findEuiccChannelBySlot(slotId)
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
null
}
}
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return@runBlocking null
}
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
} }
} }
suspend fun enumerateEuiccChannels() { suspend fun enumerateEuiccChannels() {
if (!checkPrivileges()) return
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
ensureSEService() ensureSEService()
for (uiccInfo in uiccCards) { for (uiccInfo in tm.uiccCardsInfo) {
for (port in uiccInfo.ports) { if (tryOpenEuiccChannel(uiccInfo) != null) {
if (tryOpenEuiccChannel(port) != null) { Log.d(TAG, "Found eUICC on slot ${uiccInfo.slotIndex}")
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
}
} }
} }
} }
@ -153,6 +127,8 @@ open class EuiccChannelManager(protected val context: Context) {
get() = channels.toList() get() = channels.toList()
fun invalidate() { fun invalidate() {
if (!checkPrivileges()) return
for (channel in channels) { for (channel in channels) {
channel.close() channel.close()
} }
@ -162,7 +138,7 @@ open class EuiccChannelManager(protected val context: Context) {
seService = null seService = null
} }
open fun notifyEuiccProfilesChanged(logicalSlotId: Int) { open fun notifyEuiccProfilesChanged(slotId: Int) {
// No-op for unprivileged // No-op for unprivileged
} }
} }

View file

@ -3,7 +3,6 @@ package im.angry.openeuicc.core
import android.se.omapi.Channel import android.se.omapi.Channel
import android.se.omapi.SEService import android.se.omapi.SEService
import android.se.omapi.Session import android.se.omapi.Session
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
@ -11,13 +10,13 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class OmapiApduInterface( class OmapiApduInterface(
private val service: SEService, private val service: SEService,
private val port: UiccPortInfoCompat private val info: EuiccChannelInfo
): ApduInterface { ): ApduInterface {
private lateinit var session: Session private lateinit var session: Session
private lateinit var lastChannel: Channel private lateinit var lastChannel: Channel
override fun connect() { override fun connect() {
session = service.getUiccReaderCompat(port.logicalSlotIndex + 1).openSession() session = service.getUiccReader(info.slotId + 1).openSession()
} }
override fun disconnect() { override fun disconnect() {
@ -29,11 +28,11 @@ class OmapiApduInterface(
"Can only open one channel" "Can only open one channel"
} }
lastChannel = session.openLogicalChannel(aid)!!; lastChannel = session.openLogicalChannel(aid)!!;
return 1; return 0;
} }
override fun logicalChannelClose(handle: Int) { override fun logicalChannelClose(handle: Int) {
check(handle == 1 && !this::lastChannel.isInitialized) { check(handle == 0 && !this::lastChannel.isInitialized) {
"Unknown channel" "Unknown channel"
} }
lastChannel.close() lastChannel.close()
@ -51,9 +50,9 @@ class OmapiApduInterface(
class OmapiChannel( class OmapiChannel(
service: SEService, service: SEService,
port: UiccPortInfoCompat, info: EuiccChannelInfo,
) : EuiccChannel(port) { ) : EuiccChannel(info) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl( override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
OmapiApduInterface(service, port), OmapiApduInterface(service, info),
HttpInterfaceImpl()) HttpInterfaceImpl())
} }

View file

@ -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)
}
}
}

View file

@ -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()
}

View file

@ -0,0 +1,31 @@
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): T where T: Fragment, T: EuiccFragmentMarker {
val instance = clazz.newInstance()
instance.arguments = Bundle().apply {
putInt("slotId", slotId)
}
return instance
}
val <T> T.slotId: Int where T: Fragment, T: EuiccFragmentMarker
get() = requireArguments().getInt("slotId")
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.findEuiccChannelBySlotBlocking(slotId)!!
interface EuiccProfilesChangedListener {
fun onEuiccProfilesChanged()
}

View file

@ -1,17 +1,13 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
@ -26,30 +22,23 @@ import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.Exception import java.lang.Exception
open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
EuiccChannelFragmentMarker {
companion object { companion object {
const val TAG = "EuiccManagementFragment" const val TAG = "EuiccManagementFragment"
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment = fun newInstance(slotId: Int): EuiccManagementFragment =
newInstanceEuicc(EuiccManagementFragment::class.java, slotId, portId) newInstanceEuicc(EuiccManagementFragment::class.java, slotId)
} }
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var fab: FloatingActionButton private lateinit var fab: FloatingActionButton
private lateinit var profileList: RecyclerView private lateinit var profileList: RecyclerView
private val adapter = EuiccProfileAdapter() private val adapter = EuiccProfileAdapter(listOf())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -73,7 +62,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
fab.setOnClickListener { fab.setOnClickListener {
ProfileDownloadFragment.newInstance(slotId, portId) ProfileDownloadFragment.newInstance(slotId)
.show(childFragmentManager, ProfileDownloadFragment.TAG) .show(childFragmentManager, ProfileDownloadFragment.TAG)
} }
} }
@ -87,38 +76,18 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
refresh() 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") @SuppressLint("NotifyDataSetChanged")
private fun refresh() { private fun refresh() {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
val profiles = withContext(Dispatchers.IO) { val profiles = withContext(Dispatchers.IO) {
euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) euiccChannelManager.notifyEuiccProfilesChanged(slotId)
channel.lpa.profiles channel.lpa.profiles
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
adapter.profiles = profiles.operational adapter.profiles = profiles.operational
adapter.footerViews = onCreateFooterViews(profileList)
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
@ -127,6 +96,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
private fun enableOrDisableProfile(iccid: String, enable: Boolean) { private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
swipeRefresh.isEnabled = false
fab.isEnabled = false fab.isEnabled = false
lifecycleScope.launch { lifecycleScope.launch {
@ -136,61 +106,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
} else { } else {
doDisableProfile(iccid) doDisableProfile(iccid)
} }
refresh() Toast.makeText(context, R.string.toast_profile_enabled, Toast.LENGTH_LONG).show()
fab.isEnabled = true // The APDU channel will be invalid when the SIM reboots. For now, just exit the app
euiccChannelManager.invalidate()
requireActivity().finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "Failed to enable / disable profile $iccid") Log.d(TAG, "Failed to enable / disable profile $iccid")
Log.d(TAG, Log.getStackTraceString(e)) Log.d(TAG, Log.getStackTraceString(e))
fab.isEnabled = true fab.isEnabled = true
swipeRefresh.isEnabled = true
Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.toast_profile_enable_failed, Toast.LENGTH_LONG).show()
} }
} }
} }
private suspend fun doEnableProfile(iccid: String) = private suspend fun doEnableProfile(iccid: String) =
channel.lpa.beginOperation { withContext(Dispatchers.IO) {
channel.lpa.enableProfile(iccid, reconnectTimeout = 15 * 1000) && channel.lpa.enableProfile(iccid)
preferenceRepository.notificationEnableFlow.first()
} }
private suspend fun doDisableProfile(iccid: String) = private suspend fun doDisableProfile(iccid: String) =
channel.lpa.beginOperation { withContext(Dispatchers.IO) {
channel.lpa.disableProfile(iccid, reconnectTimeout = 15 * 1000) && channel.lpa.disableProfile(iccid)
preferenceRepository.notificationDisableFlow.first()
} }
protected open fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) { inner class ViewHolder(private val root: View) : RecyclerView.ViewHolder(root) {
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),
FOOTER(1);
companion object {
fun fromInt(value: Int) =
Type.values().first { it.value == value }
}
}
}
inner class FooterViewHolder: ViewHolder(FrameLayout(requireContext())) {
fun attach(view: View) {
view.parent?.let { (it as ViewGroup).removeView(view) }
(itemView as FrameLayout).addView(view)
}
fun detach() {
(itemView as FrameLayout).removeAllViews()
}
}
inner class ProfileViewHolder(private val root: View) : ViewHolder(root) {
private val iccid: TextView = root.findViewById(R.id.iccid) private val iccid: TextView = root.findViewById(R.id.iccid)
private val name: TextView = root.findViewById(R.id.name) private val name: TextView = root.findViewById(R.id.name)
private val state: TextView = root.findViewById(R.id.state) private val state: TextView = root.findViewById(R.id.state)
@ -216,7 +156,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
name.text = profile.displayName name.text = profile.displayName
state.setText( state.setText(
if (profile.isEnabled) { if (isEnabled()) {
R.string.enabled R.string.enabled
} else { } else {
R.string.disabled R.string.disabled
@ -227,10 +167,19 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
iccid.transformationMethod = PasswordTransformationMethod.getInstance() iccid.transformationMethod = PasswordTransformationMethod.getInstance()
} }
private fun isEnabled(): Boolean =
profile.state == LocalProfileInfo.State.Enabled
private fun showOptionsMenu() { private fun showOptionsMenu() {
PopupMenu(root.context, profileMenu).apply { PopupMenu(root.context, profileMenu).apply {
setOnMenuItemClickListener(::onMenuItemClicked) 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() show()
} }
} }
@ -246,12 +195,12 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
true true
} }
R.id.rename -> { R.id.rename -> {
ProfileRenameFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) ProfileRenameFragment.newInstance(slotId, profile.iccid, profile.displayName)
.show(childFragmentManager, ProfileRenameFragment.TAG) .show(childFragmentManager, ProfileRenameFragment.TAG)
true true
} }
R.id.delete -> { R.id.delete -> {
ProfileDeleteFragment.newInstance(slotId, portId, profile.iccid, profile.displayName) ProfileDeleteFragment.newInstance(slotId, profile.iccid, profile.displayName)
.show(childFragmentManager, ProfileDeleteFragment.TAG) .show(childFragmentManager, ProfileDeleteFragment.TAG)
true true
} }
@ -259,49 +208,16 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
} }
} }
inner class EuiccProfileAdapter : RecyclerView.Adapter<ViewHolder>() { inner class EuiccProfileAdapter(var profiles: List<LocalProfileInfo>) : RecyclerView.Adapter<ViewHolder>() {
var profiles: List<LocalProfileInfo> = listOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
var footerViews: List<View> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
when (ViewHolder.Type.fromInt(viewType)) {
ViewHolder.Type.PROFILE -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false) val view = LayoutInflater.from(parent.context).inflate(R.layout.euicc_profile, parent, false)
ProfileViewHolder(view) return ViewHolder(view)
}
ViewHolder.Type.FOOTER -> {
FooterViewHolder()
}
}
override fun getItemViewType(position: Int): Int =
when {
position < profiles.size -> {
ViewHolder.Type.PROFILE.value
}
position >= profiles.size && position < profiles.size + footerViews.size -> {
ViewHolder.Type.FOOTER.value
}
else -> -1
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is ProfileViewHolder -> {
holder.setProfile(profiles[position]) holder.setProfile(profiles[position])
} }
is FooterViewHolder -> {
holder.attach(footerViews[position - profiles.size])
}
}
}
override fun onViewRecycled(holder: ViewHolder) { override fun getItemCount(): Int = profiles.size
if (holder is FooterViewHolder) {
holder.detach()
}
}
override fun getItemCount(): Int = profiles.size + footerViews.size
} }
} }

View file

@ -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)
}
}
}

View file

@ -1,11 +1,9 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
@ -13,14 +11,13 @@ import android.widget.Spinner
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker { open class MainActivity : AppCompatActivity() {
companion object { companion object {
const val TAG = "MainActivity" const val TAG = "MainActivity"
} }
@ -39,13 +36,11 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
noEuiccPlaceholder = findViewById(R.id.no_euicc_placeholder) 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) spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
@ -57,7 +52,6 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_main, menu) menuInflater.inflate(R.menu.activity_main, menu)
if (!this::spinner.isInitialized) {
spinner = menu.findItem(R.id.spinner).actionView as Spinner spinner = menu.findItem(R.id.spinner).actionView as Spinner
spinner.adapter = spinnerAdapter spinner.adapter = spinnerAdapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@ -67,53 +61,34 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
position: Int, position: Int,
id: Long id: Long
) { ) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments[position]).commit()
.replace(R.id.fragment_root, fragments[position]).commit()
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
} }
} }
} 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
}
return true 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)
private suspend fun init() { private suspend fun init() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
manager.enumerateEuiccChannels() manager.enumerateEuiccChannels()
manager.knownChannels.forEach { manager.knownChannels.forEach {
Log.d(TAG, "slot ${it.slotId} port ${it.portId}") Log.d(TAG, it.name)
Log.d(TAG, it.lpa.eID) Log.d(TAG, it.lpa.eID)
// Request the system to refresh the list of profiles every time we start // Request the system to refresh the list of profiles every time we start
// Note that this is currently supposed to be no-op when unprivileged, // Note that this is currently supposed to be no-op when unprivileged,
// but it could change in the future // but it could change in the future
manager.notifyEuiccProfilesChanged(it.logicalSlotId) manager.notifyEuiccProfilesChanged(it.slotId)
} }
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
manager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> manager.knownChannels.forEach { channel ->
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId)) spinnerAdapter.add(channel.name)
fragments.add(createEuiccManagementFragment(channel)) fragments.add(EuiccManagementFragment.newInstance(channel.slotId))
} }
if (fragments.isNotEmpty()) { if (fragments.isNotEmpty()) {

View file

@ -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])
}
}

View file

@ -7,17 +7,17 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R 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.launch
import kotlinx.coroutines.withContext
import java.lang.Exception import java.lang.Exception
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
companion object { companion object {
const val TAG = "ProfileDeleteFragment" const val TAG = "ProfileDeleteFragment"
fun newInstance(slotId: Int, portId: Int, iccid: String, name: String): ProfileDeleteFragment { fun newInstance(slotId: Int, iccid: String, name: String): ProfileDeleteFragment {
val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId, portId) val instance = newInstanceEuicc(ProfileDeleteFragment::class.java, slotId)
instance.requireArguments().apply { instance.requireArguments().apply {
putString("iccid", iccid) putString("iccid", iccid)
putString("name", name) putString("name", name)
@ -29,7 +29,7 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
private var deleting = false private var deleting = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 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"))) setMessage(getString(R.string.profile_delete_confirm, requireArguments().getString("name")))
setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing setPositiveButton(android.R.string.ok, null) // Set listener to null to prevent auto closing
setNegativeButton(android.R.string.cancel, null) 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")!!) channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
preferenceRepository.notificationDeleteFlow.first()
} }
} }

View file

@ -1,39 +1,32 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.format.Formatter
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.setWidthPercent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.ProfileDownloadCallback
import kotlin.Exception import java.lang.Exception
class ProfileDownloadFragment : BaseMaterialDialogFragment(), class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.OnMenuItemClickListener {
Toolbar.OnMenuItemClickListener, EuiccChannelFragmentMarker {
companion object { companion object {
const val TAG = "ProfileDownloadFragment" const val TAG = "ProfileDownloadFragment"
fun newInstance(slotId: Int, portId: Int, finishWhenDone: Boolean = false): ProfileDownloadFragment = fun newInstance(slotId: Int): ProfileDownloadFragment =
newInstanceEuicc(ProfileDownloadFragment::class.java, slotId, portId) { newInstanceEuicc(ProfileDownloadFragment::class.java, slotId)
putBoolean("finishWhenDone", finishWhenDone)
}
} }
private lateinit var toolbar: Toolbar private lateinit var toolbar: Toolbar
@ -41,17 +34,10 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
private lateinit var profileDownloadCode: TextInputLayout private lateinit var profileDownloadCode: TextInputLayout
private lateinit var profileDownloadConfirmationCode: TextInputLayout private lateinit var profileDownloadConfirmationCode: TextInputLayout
private lateinit var profileDownloadIMEI: TextInputLayout private lateinit var profileDownloadIMEI: TextInputLayout
private lateinit var profileDownloadFreeSpace: TextView
private lateinit var progress: ProgressBar private lateinit var progress: ProgressBar
private var freeNvram: Int = -1
private var downloading = false private var downloading = false
private val finishWhenDone by lazy {
requireArguments().getBoolean("finishWhenDone", false)
}
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
result.contents?.let { content -> result.contents?.let { content ->
Log.d(TAG, content) Log.d(TAG, content)
@ -74,7 +60,6 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
profileDownloadCode = view.findViewById(R.id.profile_download_code) profileDownloadCode = view.findViewById(R.id.profile_download_code)
profileDownloadConfirmationCode = view.findViewById(R.id.profile_download_confirmation_code) profileDownloadConfirmationCode = view.findViewById(R.id.profile_download_confirmation_code)
profileDownloadIMEI = view.findViewById(R.id.profile_download_imei) profileDownloadIMEI = view.findViewById(R.id.profile_download_imei)
profileDownloadFreeSpace = view.findViewById(R.id.profile_download_free_space)
progress = view.findViewById(R.id.progress) progress = view.findViewById(R.id.progress)
toolbar.inflateMenu(R.menu.fragment_profile_download) toolbar.inflateMenu(R.menu.fragment_profile_download)
@ -87,9 +72,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
toolbar.apply { toolbar.apply {
setTitle(R.string.profile_download) setTitle(R.string.profile_download)
setNavigationOnClickListener { setNavigationOnClickListener {
if (!downloading) { if (!downloading) dismiss()
dismiss()
}
} }
setOnMenuItemClickListener(this@ProfileDownloadFragment) setOnMenuItemClickListener(this@ProfileDownloadFragment)
} }
@ -116,32 +99,14 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
setWidthPercent(95) setWidthPercent(95)
} }
@SuppressLint("MissingPermission")
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable( profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable(channel.imei)
try {
openEuiccApplication.telephonyManager.getImei(channel.logicalSlotId)
} catch (e: Exception) {
""
}
)
lifecycleScope.launch(Dispatchers.IO) {
// Fetch remaining NVRAM
val str = channel.lpa.euiccInfo2?.freeNvram?.also {
freeNvram = it
}?.let { Formatter.formatShortFileSize(requireContext(), it.toLong()) }
withContext(Dispatchers.Main) {
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space,
str ?: getText(R.string.unknown))
}
}
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also { return super.onCreateDialog(savedInstanceState).also {
it.window?.requestFeature(Window.FEATURE_NO_TITLE)
it.setCanceledOnTouchOutside(false) it.setCanceledOnTouchOutside(false)
} }
} }
@ -189,8 +154,8 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
} }
} }
private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = channel.lpa.beginOperation { private suspend fun doDownloadProfile(server: String, code: String?, confirmationCode: String?, imei: String?) = withContext(Dispatchers.IO) {
downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback { channel.lpa.downloadProfile(server, code, imei, confirmationCode, object : ProfileDownloadCallback {
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
progress.isIndeterminate = false progress.isIndeterminate = false
@ -198,23 +163,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()
}
} }
} }

View file

@ -6,25 +6,27 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.setWidthPercent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.Exception import java.lang.Exception
import java.lang.RuntimeException import java.lang.RuntimeException
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { class ProfileRenameFragment : DialogFragment(), EuiccFragmentMarker {
companion object { companion object {
const val TAG = "ProfileRenameFragment" const val TAG = "ProfileRenameFragment"
fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment { fun newInstance(slotId: Int, iccid: String, currentName: String): ProfileRenameFragment {
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId)
instance.requireArguments().apply { instance.requireArguments().apply {
putString("iccid", iccid) putString("iccid", iccid)
putString("currentName", currentName) putString("currentName", currentName)
@ -81,6 +83,7 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also { return super.onCreateDialog(savedInstanceState).also {
it.window?.requestFeature(Window.FEATURE_NO_TITLE)
it.setCanceledOnTouchOutside(false) it.setCanceledOnTouchOutside(false)
} }
} }

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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
}
}
}

View file

@ -1,68 +0,0 @@
package im.angry.openeuicc.util
import android.content.Context
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
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 {
val physicalSlotIndex: Int
val ports: Collection<UiccPortInfoCompat>
}
interface UiccPortInfoCompat {
val card: UiccCardInfoCompat
val portIndex: Int
val logicalSlotIndex: Int
}
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
}

View file

@ -1,6 +1,5 @@
package im.angry.openeuicc.util package im.angry.openeuicc.util
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
val LocalProfileInfo.displayName: String val LocalProfileInfo.displayName: String
@ -10,9 +9,3 @@ val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
get() = filter { get() = filter {
it.profileClass == LocalProfileInfo.Clazz.Operational it.profileClass == LocalProfileInfo.Clazz.Operational
} }
fun LocalProfileAssistant.disableActiveProfileWithUndo(): () -> Unit =
profiles.find { it.state == LocalProfileInfo.State.Enabled }?.let {
disableProfile(it.iccid)
return { enableProfile(it.iccid) }
} ?: { }

View file

@ -1,9 +1,18 @@
package im.angry.openeuicc.util package im.angry.openeuicc.util
import android.app.Activity
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment 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> // Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
/** /**

View file

@ -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)
}
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -6,22 +6,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.MainActivity"> 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 <FrameLayout
android:id="@+id/fragment_root" android:id="@+id/fragment_root"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"> app:layout_constraintTop_toTopOf="parent">
<TextView <TextView
android:id="@+id/no_euicc_placeholder" android:id="@+id/no_euicc_placeholder"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -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>

View file

@ -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>

View file

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView <androidx.cardview.widget.CardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp" android:layout_marginHorizontal="12dp"
@ -103,6 +103,6 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </androidx.cardview.widget.CardView>
</FrameLayout> </FrameLayout>

View file

@ -2,13 +2,15 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:background="?attr/colorSurface">
<com.google.android.material.appbar.MaterialToolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/Theme.OpenEUICC"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" app:layout_constraintWidth_percent="1"
@ -41,6 +43,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="15dp" android:layout_marginTop="15dp"
android:hint="@string/profile_download_server" android:hint="@string/profile_download_server"
style="@style/Widget.OpenEUICC.Input"
app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
@ -48,7 +51,8 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" 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> </com.google.android.material.textfield.TextInputLayout>
@ -58,6 +62,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="15dp" android:layout_marginVertical="15dp"
android:hint="@string/profile_download_code" android:hint="@string/profile_download_code"
style="@style/Widget.OpenEUICC.Input"
app:layout_constraintTop_toBottomOf="@id/profile_download_server" app:layout_constraintTop_toBottomOf="@id/profile_download_server"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
@ -67,7 +72,8 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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> </com.google.android.material.textfield.TextInputLayout>
@ -77,6 +83,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="15dp" android:layout_marginVertical="15dp"
android:hint="@string/profile_download_confirmation_code" android:hint="@string/profile_download_confirmation_code"
style="@style/Widget.OpenEUICC.Input"
app:layout_constraintTop_toBottomOf="@id/profile_download_code" app:layout_constraintTop_toBottomOf="@id/profile_download_code"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
@ -86,7 +93,8 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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> </com.google.android.material.textfield.TextInputLayout>
@ -94,33 +102,22 @@
android:id="@+id/profile_download_imei" android:id="@+id/profile_download_imei"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="15dp" android:layout_marginVertical="15dp"
android:layout_marginBottom="6dp"
android:hint="@string/profile_download_imei" android:hint="@string/profile_download_imei"
style="@style/Widget.OpenEUICC.Input"
app:layout_constraintTop_toBottomOf="@id/profile_download_confirmation_code" app:layout_constraintTop_toBottomOf="@id/profile_download_confirmation_code"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/profile_download_free_space" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent=".8" app:layout_constraintWidth_percent=".8"
app:passwordToggleEnabled="true"> app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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> </com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/profile_download_free_space"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="11sp"
android:layout_marginBottom="4dp"
app:layout_constraintTop_toBottomOf="@id/profile_download_imei"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,10 +4,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/Theme.OpenEUICC"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" app:layout_constraintWidth_percent="1"
@ -40,6 +43,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="15dp" android:layout_marginVertical="15dp"
android:hint="@string/profile_rename_new_name" android:hint="@string/profile_rename_new_name"
style="@style/Widget.OpenEUICC.Input"
app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
@ -48,7 +52,8 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" 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> </com.google.android.material.textfield.TextInputLayout>

View file

@ -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>

View file

@ -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>

View file

@ -5,10 +5,6 @@
android:id="@+id/spinner" android:id="@+id/spinner"
android:title="" android:title=""
app:actionViewClass="android.widget.Spinner" app:actionViewClass="android.widget.Spinner"
android:background="?android:attr/colorPrimary"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/settings"
android:title="@string/pref_settings"
app:showAsAction="never" />
</menu> </menu>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -6,7 +6,6 @@
<item <item
android:id="@+id/disable" android:id="@+id/disable"
android:visible="false"
android:title="@string/disable"/> android:title="@string/disable"/>
<item <item

View file

@ -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-----

View 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>

View file

@ -1,10 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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="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>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
@ -16,18 +12,15 @@
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="rename">Rename</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_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string> <string name="toast_profile_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">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string> <string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string> <string name="profile_download_code">Activation Code</string>
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string> <string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
<string name="profile_download_imei">IMEI (Optional)</string> <string name="profile_download_imei">IMEI (Optional)</string>
<string name="profile_download_free_space">Space remaining: %s</string>
<string name="profile_download_scan">Scan QR Code</string> <string name="profile_download_scan">Scan QR Code</string>
<string name="profile_download_ok">Download</string> <string name="profile_download_ok">Download</string>
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string> <string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
@ -35,34 +28,4 @@
<string name="profile_rename_new_name">New nickname</string> <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_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">&lt;b&gt;%1$s&lt;/b&gt; %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> </resources>

View file

@ -1,41 +1,46 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.OpenEUICC" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.OpenEUICC" parent="Theme.MaterialComponents.DayNight">
<item name="android:windowLightStatusBar">?attr/isLightTheme</item> <!-- 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="alertDialogTheme">@style/AlertDialogTheme</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">?attr/colorSecondary</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>
</style> </style>
<style name="ToolbarTheme" parent="Widget.Material3.Toolbar"> <style name="Theme.OpenEUICC.Input.Cursor" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
<item name="android:background">?attr/colorSurfaceVariant</item> <item name="colorControlActivated">?attr/colorSecondary</item>
</style> </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="buttonBarNegativeButtonStyle">@style/NegativeButtonStyle</item>
<item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item> <item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item>
<item name="dialogCornerRadius">28dp</item>
</style> </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> <item name="android:textColor">?attr/colorSecondary</item>
</style> </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> <item name="android:textColor">?attr/colorSecondary</item>
</style> </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> </resources>

View file

@ -3,7 +3,6 @@
<base-config> <base-config>
<trust-anchors> <trust-anchors>
<certificates src="@raw/symantec_gsma_rspv2_root_ci1"/> <certificates src="@raw/symantec_gsma_rspv2_root_ci1"/>
<certificates src="@raw/gsma_sgp26"/>
<certificates src="system"/> <certificates src="system"/>
</trust-anchors> </trust-anchors>
</base-config> </base-config>

View file

@ -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
View file

@ -1 +0,0 @@
/build

View file

@ -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",
],
}

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -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)
}
}

View file

@ -1 +0,0 @@
/build

View file

@ -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"))
}

View file

@ -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

View file

@ -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>

View file

@ -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])
}
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -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>

View file

@ -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)
}
}

68
app/build.gradle Normal file
View file

@ -0,0 +1,68 @@
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 {
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'
}

View file

@ -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")
}

View file

@ -10,7 +10,6 @@ class PrivilegedOpenEuiccApplication: OpenEuiccApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
(euiccChannelManager as PrivilegedEuiccChannelManager).closeAllStaleChannels() (euiccChannelManager as PrivilegedEuiccChannelManager).closeAllStaleChannels()
} }
} }

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.content.Context import android.content.Context
import android.telephony.UiccCardInfo
import android.util.Log import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
@ -8,25 +9,16 @@ import java.lang.Exception
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) { class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
override val uiccCards: Collection<UiccCardInfoCompat> override fun checkPrivileges() = true // TODO: Implement proper system app check
get() = tm.uiccCardsInfoCompat
@Suppress("NAME_SHADOWING") override fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? {
override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? { if (uiccInfo.isEuicc && !uiccInfo.isRemovable) {
val port = port as RealUiccPortInfoCompat Log.d(TAG, "Using TelephonyManager for slot ${uiccInfo.slotIndex}")
if (port.card.isRemovable) { // TODO: On Tiramisu, we should also connect all available "ports" for MEP support
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannelUnprivileged(port)?.let { return it }
}
if (port.card.isEuicc) {
Log.i(TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
try { try {
return TelephonyManagerChannel(port, tm) return TelephonyManagerChannel(channelInfo, tm)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
// Failed // Failed
Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back")
} }
} }
return null return null
@ -47,9 +39,9 @@ class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(conte
} }
} }
override fun notifyEuiccProfilesChanged(logicalSlotId: Int) { override fun notifyEuiccProfilesChanged(slotId: Int) {
(context.applicationContext as OpenEuiccApplication).subscriptionManager.apply { (context.applicationContext as OpenEuiccApplication).subscriptionManager.apply {
findEuiccChannelBySlotBlocking(logicalSlotId)?.let { findEuiccChannelBySlotBlocking(slotId)?.let {
tryRefreshCachedEuiccInfo(it.cardId) tryRefreshCachedEuiccInfo(it.cardId)
} }
} }

Some files were not shown because too many files have changed in this diff Show more