Compare commits

..

No commits in common. "master" and "lpac" have entirely different histories.
master ... lpac

163 changed files with 1019 additions and 4984 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/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store
/build
/captures
@ -16,5 +15,3 @@
.cxx
local.properties
/libs/**/build
/buildSrc/build
/app-deps/libs

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>

9
.idea/compiler.xml generated
View file

@ -3,14 +3,11 @@
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.7">
<module name="OpenEUICC.app" target="17" />
<module name="OpenEUICC.app-common" target="17" />
<module name="OpenEUICC.app-deps" target="17" />
<module name="OpenEUICC.app-unpriv" target="17" />
<module name="OpenEUICC.buildSrc" target="17" />
<module name="OpenEUICC.buildSrc.main" target="17" />
<module name="OpenEUICC.buildSrc.test" target="17" />
<module name="OpenEUICC.libs.hidden-apis-shim" target="17" />
<module name="OpenEUICC.libs.lpac-jni" target="17" />
<module name="OpenEUICC.libs.lpad-sm-dp-plus-connector" target="17" />
<module name="OpenEUICC.libs.lpad-sm-dp-plus-connector.main" target="17" />
<module name="OpenEUICC.libs.lpad-sm-dp-plus-connector.test" target="17" />
</bytecodeTargetLevel>
</component>
</project>

4
.idea/gradle.xml generated
View file

@ -13,10 +13,6 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/app-common" />
<option value="$PROJECT_DIR$/app-deps" />
<option value="$PROJECT_DIR$/app-unpriv" />
<option value="$PROJECT_DIR$/buildSrc" />
<option value="$PROJECT_DIR$/libs" />
<option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
<option value="$PROJECT_DIR$/libs/hidden-apis-stub" />

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 {
name: "OpenEUICC",
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: [
"liblpac-jni",
],
srcs: [
// Main app
"app/src/main/**/*.kt",
// lpac-jni interface
"libs/lpac-jni/src/main/**/*.kt",
],
optimize: {
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/>.
```

View file

@ -1 +0,0 @@
/build

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,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.common
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.common.test", appContext.packageName)
}
}

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
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.INTERNET" />
<application
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name="im.angry.openeuicc.ui.SettingsActivity"
android:label="@string/pref_settings" />
<activity
android:name="im.angry.openeuicc.ui.NotificationsActivity"
android:label="@string/profile_notifications" />
<activity
android:name="im.angry.openeuicc.ui.DirectProfileDownloadActivity"
android:label="@string/profile_download"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity
android:name="im.angry.openeuicc.ui.LogsActivity"
android:label="@string/pref_advanced_logs" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application>
</manifest>

View file

@ -1,18 +0,0 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileAssistant
abstract class EuiccChannel(
val port: UiccPortInfoCompat
) {
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
val logicalSlotId = port.logicalSlotIndex
val portId = port.portIndex
abstract val lpa: LocalProfileAssistant
val valid: Boolean
get() = lpa.valid
fun close() = lpa.close()
}

View file

@ -1,168 +0,0 @@
package im.angry.openeuicc.core
import android.content.Context
import android.se.omapi.SEService
import android.telephony.SubscriptionManager
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.lang.IllegalArgumentException
open class EuiccChannelManager(protected val context: Context) {
companion object {
const val TAG = "EuiccChannelManager"
}
private val channels = mutableListOf<EuiccChannel>()
private var seService: SEService? = null
private val lock = Mutex()
protected val tm by lazy {
(context.applicationContext as OpenEuiccApplication).telephonyManager
}
protected open val uiccCards: Collection<UiccCardInfoCompat>
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
private suspend fun ensureSEService() {
if (seService == null) {
seService = connectSEService(context)
}
}
protected open fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
// No-op when unprivileged
return null
}
protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
}
Log.i(TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try {
return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}.")
}
return null
}
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
ensureSEService()
val existing = channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing
} else {
existing.close()
channels.remove(existing)
}
}
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// We can only open channels on ports that are actually enabled
return null
}
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port)
if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(port)
}
if (euiccChannel != null) {
channels.add(euiccChannel)
}
return euiccChannel
}
}
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
null
}
}
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return@runBlocking null
}
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
}
suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) {
ensureSEService()
for (uiccInfo in uiccCards) {
for (port in uiccInfo.ports) {
if (tryOpenEuiccChannel(port) != null) {
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
}
}
}
}
}
val knownChannels: List<EuiccChannel>
get() = channels.toList()
fun invalidate() {
for (channel in channels) {
channel.close()
}
channels.clear()
seService?.shutdown()
seService = null
}
open fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
// No-op for unprivileged
}
}

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

@ -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,125 +0,0 @@
package im.angry.openeuicc.ui
import android.content.Intent
import android.os.Bundle
import android.telephony.TelephonyManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
companion object {
const val TAG = "MainActivity"
}
protected lateinit var manager: EuiccChannelManager
private lateinit var spinnerAdapter: ArrayAdapter<String>
private lateinit var spinner: Spinner
private val fragments = arrayListOf<EuiccManagementFragment>()
private lateinit var noEuiccPlaceholder: View
protected lateinit var tm: TelephonyManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
noEuiccPlaceholder = findViewById(R.id.no_euicc_placeholder)
tm = telephonyManager
manager = euiccChannelManager
spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
lifecycleScope.launch {
init()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_main, menu)
if (!this::spinner.isInitialized) {
spinner = menu.findItem(R.id.spinner).actionView as Spinner
spinner.adapter = spinnerAdapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_root, fragments[position]).commit()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
} 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
}
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() {
withContext(Dispatchers.IO) {
manager.enumerateEuiccChannels()
manager.knownChannels.forEach {
Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
Log.d(TAG, it.lpa.eID)
// 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,
// but it could change in the future
manager.notifyEuiccProfilesChanged(it.logicalSlotId)
}
}
withContext(Dispatchers.Main) {
manager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
fragments.add(createEuiccManagementFragment(channel))
}
if (fragments.isNotEmpty()) {
noEuiccPlaceholder.visibility = View.GONE
supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments.first()).commit()
}
}
}
}

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

@ -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,18 +0,0 @@
package im.angry.openeuicc.util
import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.LocalProfileInfo
val LocalProfileInfo.displayName: String
get() = nickName.ifEmpty { name }
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
get() = filter {
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,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,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

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

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

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

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

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc">No eUICC card on this device is accessible by this app.\nInsert a supported eUICC card, or try out the privileged OpenEUICC app instead.</string>
<string name="unknown">Unknown</string>
<string name="help">Help</string>
<string name="channel_name_format">Logical Slot %d</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
<string name="provider">Provider:</string>
<string name="iccid">ICCID:</string>
<string name="enable">Enable</string>
<string name="disable">Disable</string>
<string name="delete">Delete</string>
<string name="rename">Rename</string>
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
<string name="slot_select">Select Slot</string>
<string name="slot_select_select">Select</string>
<string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string>
<string name="profile_download_confirmation_code">Confirmation Code (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_ok">Download</string>
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</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_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>

View file

@ -1,41 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.OpenEUICC" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
<item name="alertDialogTheme">@style/AlertDialogTheme</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightNavigationBar">?attr/isLightTheme</item>
<item name="toolbarStyle">@style/ToolbarTheme</item>
<item name="android:statusBarColor">?attr/colorSurfaceVariant</item>
<item name="android:colorBackground">?attr/colorSurface</item>
<item name="dialogCornerRadius">28dp</item>
</style>
<style name="ToolbarTheme" parent="Widget.Material3.Toolbar">
<item name="android:background">?attr/colorSurfaceVariant</item>
</style>
<style name="AlertDialogTheme" parent="Theme.Material3.DayNight.Dialog.Alert">
<item name="buttonBarNegativeButtonStyle">@style/NegativeButtonStyle</item>
<item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item>
<item name="dialogCornerRadius">28dp</item>
</style>
<style name="NegativeButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
<item name="android:textColor">?attr/colorSecondary</item>
</style>
<style name="PositiveButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
<item name="android:textColor">?attr/colorSecondary</item>
</style>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

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>

View file

@ -1,17 +0,0 @@
package im.angry.openeuicc.common
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

1
app-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)
}
}

100
app/build.gradle Normal file
View file

@ -0,0 +1,100 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
def getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-list', '--first-parent', '--count', 'master'
standardOutput = stdout
}
return Integer.parseInt(stdout.toString().trim())
}
catch (ignored) {
return -1;
}
}
def getVersionName = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'describe', '--always', '--tags', '--dirty'
standardOutput = stdout
}
return stdout.toString().trim()
}
catch (ignored) {
return null;
}
}
// 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 31
versionCode getVersionCode()
versionName getVersionName()
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.all { 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 '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.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

@ -3,25 +3,24 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="ProtectedPermissions"
package="im.angry.openeuicc">
<uses-feature
android:name="android.hardware.telephony"
android:required="true" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SECURE_ELEMENT_PRIVILEGED_OPERATION" />
<application
android:name=".PrivilegedOpenEuiccApplication"
android:name=".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">
android:theme="@style/Theme.OpenEUICC"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".ui.PrivilegedMainActivity"
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -49,6 +48,11 @@
<category android:name="android.service.euicc.category.EUICC_UI" />
</intent-filter>
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application>
</manifest>

View file

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

View file

@ -1,16 +0,0 @@
package im.angry.openeuicc
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
class PrivilegedOpenEuiccApplication: OpenEuiccApplication() {
override val euiccChannelManager: EuiccChannelManager by lazy {
PrivilegedEuiccChannelManager(this)
}
override fun onCreate() {
super.onCreate()
(euiccChannelManager as PrivilegedEuiccChannelManager).closeAllStaleChannels()
}
}

View file

@ -0,0 +1,36 @@
package im.angry.openeuicc.core
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(
info: EuiccChannelInfo
) {
val slotId = info.slotId
val cardId = info.cardId
val name = info.name
val imei = info.imei
val removable = info.removable
abstract val lpa: LocalProfileAssistant
val valid: Boolean
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()
}

View file

@ -0,0 +1,153 @@
package im.angry.openeuicc.core
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import android.se.omapi.SEService
import android.telephony.UiccCardInfo
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.lang.Exception
import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class EuiccChannelManager(private val context: Context) {
companion object {
const val TAG = "EuiccChannelManager"
}
private val channels = mutableListOf<EuiccChannel>()
private var seService: SEService? = null
private val lock = Mutex()
private val tm by lazy {
(context.applicationContext as OpenEuiccApplication).telephonyManager
}
private val handler = Handler(HandlerThread("EuiccChannelManager").also { it.start() }.looper)
private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
handler.post {
var service: SEService? = null
service = SEService(context, { handler.post(it) }) {
cont.resume(service!!)
}
}
}
private suspend fun ensureSEService() {
if (seService == null) {
seService = connectSEService()
}
}
private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfo): EuiccChannel? {
lock.withLock {
ensureSEService()
val existing = channels.find { it.slotId == uiccInfo.slotIndex }
if (existing != null) {
if (existing.valid) {
return existing
} else {
existing.close()
channels.remove(existing)
}
}
val channelInfo = EuiccChannelInfo(
uiccInfo.slotIndex,
uiccInfo.cardId,
"SIM ${uiccInfo.slotIndex}",
tm.getImei(uiccInfo.slotIndex) ?: return null,
uiccInfo.isRemovable
)
var euiccChannel: EuiccChannel? = null
if (uiccInfo.isEuicc && !uiccInfo.isRemovable) {
Log.d(TAG, "Using TelephonyManager for slot ${uiccInfo.slotIndex}")
// TODO: On Tiramisu, we should also connect all available "ports" for MEP support
try {
euiccChannel = TelephonyManagerChannel(channelInfo, tm)
} catch (e: IllegalArgumentException) {
// Failed
}
}
if (euiccChannel == null) {
try {
euiccChannel = OmapiChannel(seService!!, channelInfo)
} catch (e: IllegalArgumentException) {
// Failed
}
}
if (euiccChannel != null) {
channels.add(euiccChannel)
}
return euiccChannel
}
}
private suspend fun findEuiccChannelBySlot(slotId: Int): EuiccChannel? {
return tm.uiccCardsInfo.find { it.slotIndex == slotId }?.let {
tryOpenEuiccChannel(it)
}
}
fun findEuiccChannelBySlotBlocking(slotId: Int): EuiccChannel? = runBlocking {
withContext(Dispatchers.IO) {
findEuiccChannelBySlot(slotId)
}
}
suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) {
ensureSEService()
for (uiccInfo in tm.uiccCardsInfo) {
if (tryOpenEuiccChannel(uiccInfo) != null) {
Log.d(TAG, "Found eUICC on slot ${uiccInfo.slotIndex}")
}
}
}
}
val knownChannels: List<EuiccChannel>
get() = channels.toList()
fun invalidate() {
for (channel in channels) {
channel.close()
}
channels.clear()
seService?.shutdown()
seService = null
}
// Clean up channels left open in TelephonyManager
// due to a (potentially) forced restart
// This should be called every time the application is restarted
fun closeAllStaleChannels() {
for (card in tm.uiccCardsInfo) {
for (channel in 0 until 10) {
try {
tm.iccCloseLogicalChannelBySlot(card.slotIndex, channel)
} catch (_: Exception) {
// We do not care
}
}
}
}
}

View file

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

View file

@ -1,57 +0,0 @@
package im.angry.openeuicc.core
import android.content.Context
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import java.lang.Exception
import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
override val uiccCards: Collection<UiccCardInfoCompat>
get() = tm.uiccCardsInfoCompat
@Suppress("NAME_SHADOWING")
override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
val port = port as RealUiccPortInfoCompat
if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannelUnprivileged(port)?.let { return it }
}
if (port.card.isEuicc) {
Log.i(TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
try {
return TelephonyManagerChannel(port, tm)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back")
}
}
return null
}
// Clean up channels left open in TelephonyManager
// due to a (potentially) forced restart
// This should be called every time the application is restarted
fun closeAllStaleChannels() {
for (card in tm.uiccCardsInfo) {
for (channel in 0 until 10) {
try {
tm.iccCloseLogicalChannelBySlot(card.slotIndex, channel)
} catch (_: Exception) {
// We do not care
}
}
}
}
override fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
(context.applicationContext as OpenEuiccApplication).subscriptionManager.apply {
findEuiccChannelBySlotBlocking(logicalSlotId)?.let {
tryRefreshCachedEuiccInfo(it.cardId)
}
}
}
}

View file

@ -9,7 +9,7 @@ import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class TelephonyManagerApduInterface(
private val port: UiccPortInfoCompat,
private val info: EuiccChannelInfo,
private val tm: TelephonyManager
): ApduInterface {
private var lastChannel: Int = -1
@ -20,15 +20,14 @@ class TelephonyManagerApduInterface(
override fun disconnect() {
// Do nothing
lastChannel = -1
}
override fun logicalChannelOpen(aid: ByteArray): Int {
check(lastChannel == -1) { "Already initialized" }
val hex = aid.encodeHex()
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
val channel = tm.iccOpenLogicalChannelBySlot(info.slotId, hex, 0)
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}");
throw IllegalArgumentException("Cannot open logical channel " + hex + " via TelephonManager on slot " + info.slotId);
}
lastChannel = channel.channel
return lastChannel
@ -36,7 +35,7 @@ class TelephonyManagerApduInterface(
override fun logicalChannelClose(handle: Int) {
check(handle == lastChannel) { "Invalid channel handle " }
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
tm.iccCloseLogicalChannelBySlot(info.slotId, handle)
lastChannel = -1
}
@ -50,18 +49,18 @@ class TelephonyManagerApduInterface(
val p3 = tx[4].toUByte().toInt()
val p4 = tx.drop(5).toByteArray().encodeHex()
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
return tm.iccTransmitApduLogicalChannelBySlot(info.slotId, lastChannel,
cla, instruction, p1, p2, p3, p4)?.decodeHex() ?: byteArrayOf()
}
}
class TelephonyManagerChannel(
port: UiccPortInfoCompat,
info: EuiccChannelInfo,
private val tm: TelephonyManager
) : EuiccChannel(port) {
) : EuiccChannel(info) {
override val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(
TelephonyManagerApduInterface(port, tm),
TelephonyManagerApduInterface(info, tm),
HttpInterfaceImpl()
)
}

View file

@ -1,28 +1,20 @@
package im.angry.openeuicc.service
import android.service.euicc.*
import android.telephony.UiccSlotMapping
import android.telephony.euicc.DownloadableSubscription
import android.telephony.euicc.EuiccInfo
import android.util.Log
import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.*
import java.lang.IllegalStateException
class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
companion object {
const val TAG = "OpenEuiccService"
}
class OpenEuiccService : EuiccService() {
private val openEuiccApplication
get() = application as OpenEuiccApplication
private fun findChannel(physicalSlotId: Int): EuiccChannel? =
euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
private fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
private fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
private fun findChannel(slotId: Int): EuiccChannel? =
openEuiccApplication.euiccChannelManager
.findEuiccChannelBySlotBlocking(slotId)
override fun onGetEid(slotId: Int): String? =
findChannel(slotId)?.lpa?.eID
@ -33,47 +25,6 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
private fun EuiccChannel.profileExists(iccid: String?) =
lpa.profiles.any { it.iccid == iccid }
private fun ensurePortIsMapped(slotId: Int, portId: Int) {
val mappings = telephonyManager.simSlotMapping.toMutableList()
mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId }?.let {
throw IllegalStateException("Slot $slotId port $portId has already been mapped")
}
val idx = mappings.indexOfFirst { it.physicalSlotIndex != slotId || it.portIndex != portId }
if (idx >= 0) {
mappings[idx] = UiccSlotMapping(portId, slotId, mappings[idx].logicalSlotIndex)
}
mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId } ?: run {
throw IllegalStateException("Cannot map slot $slotId port $portId")
}
try {
telephonyManager.simSlotMapping = mappings
return
} catch (_: Exception) {
}
// Sometimes hardware supports one ordering but not the reverse
telephonyManager.simSlotMapping = mappings.reversed()
}
private fun <T> retryWithTimeout(timeoutMillis: Int, backoff: Int = 1000, f: () -> T?): T? {
val startTimeMillis = System.currentTimeMillis()
do {
try {
f()?.let { return@retryWithTimeout it }
} catch (_: Exception) {
// Ignore
} finally {
Thread.sleep(backoff.toLong())
}
} while (System.currentTimeMillis() - startTimeMillis < timeoutMillis)
return null
}
override fun onGetOtaStatus(slotId: Int): Int {
// Not implemented
return 5 // EUICC_OTA_STATUS_UNAVAILABLE
@ -105,7 +56,6 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
}
override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult? {
Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
val channel = findChannel(slotId) ?: return null
val profiles = channel.lpa.profiles.operational.map {
EuiccProfileInfo.Builder(it.iccid).apply {
@ -136,16 +86,13 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
}
override fun onDeleteSubscription(slotId: Int, iccid: String): Int {
Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid")
try {
val channels = findAllChannels(slotId) ?: return RESULT_FIRST_USER
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
if (!channels[0].profileExists(iccid)) {
if (!channel.profileExists(iccid)) {
return RESULT_FIRST_USER
}
// If the profile is enabled by ANY channel (port), we cannot delete it
channels.forEach { channel ->
val profile = channel.lpa.profiles.find {
it.iccid == iccid
} ?: return RESULT_FIRST_USER
@ -154,9 +101,8 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
// Must disable the profile first
return RESULT_FIRST_USER
}
}
return if (channels[0].lpa.deleteProfile(iccid)) {
return if (channel.lpa.deleteProfile(iccid)) {
RESULT_OK
} else {
RESULT_FIRST_USER
@ -166,72 +112,48 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
}
}
// TODO: on some devices we need to update the mapping (and potentially disable a pSIM)
// for eSIM to be usable, in which case we will have to respect forceDeactivateSim.
// This is the same for our custom LUI. Both have to take this into consideration.
@Deprecated("Deprecated in Java")
override fun onSwitchToSubscription(
slotId: Int,
iccid: String?,
forceDeactivateSim: Boolean
): Int =
// -1 = any port
onSwitchToSubscriptionWithPort(slotId, -1, iccid, forceDeactivateSim)
override fun onSwitchToSubscriptionWithPort(
slotId: Int,
portIndex: Int,
iccid: String?,
forceDeactivateSim: Boolean
): Int {
Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim")
try {
// retryWithTimeout is needed here because this function may be called just after
// AOSP has switched slot mappings, in which case the slots may not be ready yet.
val channel = if (portIndex == -1) {
retryWithTimeout(5000) { findChannel(slotId) }
} else {
retryWithTimeout(5000) { findChannel(slotId, portIndex) }
} ?: run {
if (!forceDeactivateSim) {
// The user must select which SIM to deactivate
return@onSwitchToSubscriptionWithPort RESULT_MUST_DEACTIVATE_SIM
} else {
try {
// If we are allowed to deactivate any SIM we like, try mapping the indicated port first
ensurePortIsMapped(slotId, portIndex)
retryWithTimeout(5000) { findChannel(slotId, portIndex) }
} catch (e: Exception) {
// We cannot map the port (or it is already mapped)
// but we can also use any port available on the card
retryWithTimeout(5000) { findChannel(slotId) }
} ?: return@onSwitchToSubscriptionWithPort RESULT_FIRST_USER
}
}
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
if (iccid != null && !channel.profileExists(iccid)) {
Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found")
if (!channel.profileExists(iccid)) {
return RESULT_FIRST_USER
}
// Disable any active profile first if present
channel.lpa.profiles.find {
if (iccid == null) {
// Disable active profile
val activeProfile = channel.lpa.profiles.find {
it.state == LocalProfileInfo.State.Enabled
}?.let { if (!channel.lpa.disableProfile(it.iccid)) return RESULT_FIRST_USER }
} ?: return RESULT_OK
if (iccid != null) {
if (!channel.lpa.enableProfile(iccid)) {
return RESULT_FIRST_USER
return if (channel.lpa.disableProfile(activeProfile.iccid)) {
RESULT_OK
} else {
RESULT_FIRST_USER
}
} else {
return if (channel.lpa.enableProfile(iccid)) {
RESULT_OK
} else {
RESULT_FIRST_USER
}
}
return RESULT_OK
} catch (e: Exception) {
return RESULT_FIRST_USER
} finally {
euiccChannelManager.invalidate()
openEuiccApplication.euiccChannelManager.invalidate()
}
}
override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int {
Log.i(TAG, "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname")
val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
if (!channel.profileExists(iccid)) {
return RESULT_FIRST_USER

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

View file

@ -1,20 +1,12 @@
package im.angry.openeuicc.ui
import android.content.Intent
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import im.angry.openeuicc.R
class LuiActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
setContentView(R.layout.activity_lui)
findViewById<View>(R.id.lui_skip).setOnClickListener { finish() }
// TODO: Deactivate LuiActivity if there is no eSIM found.
// TODO: Support pre-filled download info (from carrier apps); UX
findViewById<View>(R.id.lui_download).setOnClickListener {
startActivity(Intent(this, DirectProfileDownloadActivity::class.java))
}
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View file

@ -0,0 +1,115 @@
package im.angry.openeuicc.ui
import android.os.Bundle
import android.telephony.TelephonyManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.R
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
private lateinit var manager: EuiccChannelManager
private lateinit var spinnerAdapter: ArrayAdapter<String>
private lateinit var spinner: Spinner
private val fragments = arrayListOf<EuiccManagementFragment>()
private lateinit var noEuiccPlaceholder: View
private lateinit var tm: TelephonyManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noEuiccPlaceholder = findViewById(R.id.no_euicc_placeholder)
tm = openEuiccApplication.telephonyManager
manager = openEuiccApplication.euiccChannelManager
spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
lifecycleScope.launch {
init()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_main, menu)
spinner = menu.findItem(R.id.spinner).actionView as Spinner
spinner.adapter = spinnerAdapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments[position]).commit()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
if (tm.supportsDSDS) {
val dsds = menu.findItem(R.id.dsds)
dsds.isVisible = true
dsds.isChecked = tm.dsdsEnabled
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.dsds -> {
tm.dsdsEnabled = !item.isChecked
Toast.makeText(this, R.string.toast_dsds_switched, Toast.LENGTH_LONG).show()
finish()
true
}
else -> false
}
private suspend fun init() {
withContext(Dispatchers.IO) {
manager.enumerateEuiccChannels()
manager.knownChannels.forEach {
Log.d(TAG, it.name)
Log.d(TAG, it.lpa.eID)
openEuiccApplication.subscriptionManager.tryRefreshCachedEuiccInfo(it.cardId)
}
}
withContext(Dispatchers.Main) {
manager.knownChannels.forEach { channel ->
spinnerAdapter.add(channel.name)
fragments.add(EuiccManagementFragment.newInstance(channel.slotId))
}
if (fragments.isNotEmpty()) {
noEuiccPlaceholder.visibility = View.GONE
supportFragmentManager.beginTransaction().replace(R.id.fragment_root, fragments.first()).commit()
}
}
}
}

View file

@ -1,39 +0,0 @@
package im.angry.openeuicc.ui
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.PopupMenu
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.LocalProfileInfo
class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
companion object {
fun newInstance(slotId: Int, portId: Int): EuiccManagementFragment =
newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId)
}
override suspend fun onCreateFooterViews(parent: ViewGroup): List<View> =
if (channel.isMEP) {
val view = layoutInflater.inflate(R.layout.footer_mep, parent, false)
view.findViewById<Button>(R.id.footer_mep_slot_mapping).setOnClickListener {
(requireActivity() as PrivilegedMainActivity).showSlotMappingFragment()
}
listOf(view)
} else {
listOf()
}
override fun populatePopupWithProfileActions(popup: PopupMenu, profile: LocalProfileInfo) {
super.populatePopupWithProfileActions(popup, profile)
if (profile.isEnabled && !channel.removable) {
// Only show the disable option for non-removable eUICCs
// Some devices without internal eUICCs have the "optimization" of ignoring SIM
// slots without a valid profile. This can lead to "bricking" of external eUICCs
// at least for that specific device.
// TODO: Maybe we can still make this option available in some sort of "advanced" mode.
popup.menu.findItem(im.angry.openeuicc.common.R.id.disable).isVisible = true
}
}
}

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