Browse Source

Introducing EasyEUICC, the unprivileged version of OpenEUICC

Peter Cai 2 years ago
parent
commit
554b43b101
33 changed files with 481 additions and 96 deletions
  1. 1 0
      .idea/compiler.xml
  2. 1 0
      .idea/gradle.xml
  3. 1 4
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt
  4. 7 14
      app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt
  5. 31 73
      app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt
  6. 3 0
      app-common/src/main/res/values/colors.xml
  7. 2 2
      app-common/src/main/res/values/themes.xml
  8. 1 0
      app-unpriv/.gitignore
  9. 62 0
      app-unpriv/build.gradle
  10. 21 0
      app-unpriv/proguard-rules.pro
  11. 25 0
      app-unpriv/src/main/AndroidManifest.xml
  12. 170 0
      app-unpriv/src/main/res/drawable/ic_launcher_background.xml
  13. 15 0
      app-unpriv/src/main/res/drawable/ic_launcher_foreground.xml
  14. 5 0
      app-unpriv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  15. 5 0
      app-unpriv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  16. BIN
      app-unpriv/src/main/res/mipmap-hdpi/ic_launcher.webp
  17. BIN
      app-unpriv/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  18. BIN
      app-unpriv/src/main/res/mipmap-mdpi/ic_launcher.webp
  19. BIN
      app-unpriv/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  20. BIN
      app-unpriv/src/main/res/mipmap-xhdpi/ic_launcher.webp
  21. BIN
      app-unpriv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  22. BIN
      app-unpriv/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  23. BIN
      app-unpriv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  24. BIN
      app-unpriv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  25. BIN
      app-unpriv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  26. 8 0
      app-unpriv/src/main/res/values/colors.xml
  27. 3 0
      app-unpriv/src/main/res/values/strings.xml
  28. 17 0
      app-unpriv/src/test/java/im/angry/easyeuicc/ExampleUnitTest.kt
  29. 4 2
      app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt
  30. 1 0
      app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt
  31. 85 0
      app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt
  32. 12 1
      app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt
  33. 1 0
      settings.gradle

+ 1 - 0
.idea/compiler.xml

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

+ 1 - 0
.idea/gradle.xml

@@ -14,6 +14,7 @@
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/app" />
             <option value="$PROJECT_DIR$/app-common" />
+            <option value="$PROJECT_DIR$/app-unpriv" />
             <option value="$PROJECT_DIR$/libs" />
             <option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
             <option value="$PROJECT_DIR$/libs/hidden-apis-stub" />

+ 1 - 4
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt

@@ -4,14 +4,11 @@ import im.angry.openeuicc.util.*
 import net.typeblog.lpac_jni.LocalProfileAssistant
 
 abstract class EuiccChannel(
-    port: UiccPortInfoCompat
+    val port: UiccPortInfoCompat
 ) {
     val slotId = port.card.physicalSlotIndex // PHYSICAL slot
     val logicalSlotId = port.logicalSlotIndex
     val portId = port.portIndex
-    val cardId = port.card.cardId
-    val removable = port.card.isRemovable
-    val isMEP = port.card.isMultipleEnabledProfilesSupported
 
     abstract val lpa: LocalProfileAssistant
     val valid: Boolean

+ 7 - 14
app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt

@@ -34,7 +34,8 @@ open class EuiccChannelManager(protected val context: Context) {
 
     private val handler = Handler(HandlerThread("BaseEuiccChannelManager").also { it.start() }.looper)
 
-    protected open fun checkPrivileges() = tm.hasCarrierPrivileges()
+    protected open val uiccCards: Collection<UiccCardInfoCompat>
+        get() = (0..<tm.activeModemCount).map { FakeUiccCardInfoCompat(it) }
 
     private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
         handler.post {
@@ -107,9 +108,8 @@ open class EuiccChannelManager(protected val context: Context) {
 
     fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
         runBlocking {
-            if (!checkPrivileges()) return@runBlocking null
             withContext(Dispatchers.IO) {
-                for (card in tm.uiccCardsInfoCompat) {
+                for (card in uiccCards) {
                     for (port in card.ports) {
                         if (port.logicalSlotIndex == logicalSlotId) {
                             return@withContext tryOpenEuiccChannel(port)
@@ -122,9 +122,8 @@ open class EuiccChannelManager(protected val context: Context) {
         }
 
     fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
-        if (!checkPrivileges()) return@runBlocking null
         withContext(Dispatchers.IO) {
-            for (card in tm.uiccCardsInfoCompat) {
+            for (card in uiccCards) {
                 if (card.physicalSlotIndex != physicalSlotId) continue
                 for (port in card.ports) {
                     tryOpenEuiccChannel(port)?.let { return@withContext it }
@@ -136,8 +135,7 @@ open class EuiccChannelManager(protected val context: Context) {
     }
 
     fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
-        if (!checkPrivileges()) return@runBlocking null
-        for (card in tm.uiccCardsInfoCompat) {
+        for (card in uiccCards) {
             if (card.physicalSlotIndex != physicalSlotId) continue
             return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
                 .ifEmpty { null }
@@ -146,21 +144,18 @@ open class EuiccChannelManager(protected val context: Context) {
     }
 
     fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
-        if (!checkPrivileges()) return@runBlocking null
         withContext(Dispatchers.IO) {
-            tm.uiccCardsInfoCompat.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
+            uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
                 card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
             }
         }
     }
 
     suspend fun enumerateEuiccChannels() {
-        if (!checkPrivileges()) return
-
         withContext(Dispatchers.IO) {
             ensureSEService()
 
-            for (uiccInfo in tm.uiccCardsInfoCompat) {
+            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}")
@@ -174,8 +169,6 @@ open class EuiccChannelManager(protected val context: Context) {
         get() = channels.toList()
 
     fun invalidate() {
-        if (!checkPrivileges()) return
-
         for (channel in channels) {
             channel.close()
         }

+ 31 - 73
app-common/src/main/java/im/angry/openeuicc/util/TelephonyCompat.kt

@@ -1,83 +1,41 @@
 package im.angry.openeuicc.util
 
-import android.annotation.SuppressLint
-import android.os.Build
-import android.telephony.TelephonyManager
-import android.telephony.UiccCardInfo
-import android.telephony.UiccPortInfo
-import im.angry.openeuicc.util.*
-import java.lang.RuntimeException
-
-@Suppress("DEPRECATION")
-class UiccCardInfoCompat(val inner: UiccCardInfo) {
+/*
+ * 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
-        get() =
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                inner.physicalSlotIndex
-            } else {
-                inner.slotIndex
-            }
-
     val ports: Collection<UiccPortInfoCompat>
-        get() =
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                inner.ports.map { UiccPortInfoCompat(it, this) }
-            } else {
-                listOf(UiccPortInfoCompat(null, this))
-            }
-
-    val isEuicc: Boolean
-        get() = inner.isEuicc
-
-    val isRemovable: Boolean
-        get() = inner.isRemovable
-
-    val isMultipleEnabledProfilesSupported: Boolean
-        get() =
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                inner.isMultipleEnabledProfilesSupported
-            } else {
-                false
-            }
-
-    val cardId: Int
-        get() = inner.cardId
 }
 
-class UiccPortInfoCompat(private val _inner: Any?, val card: UiccCardInfoCompat) {
-    init {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-            check(_inner != null && _inner is UiccPortInfo) {
-                "_inner is not UiccPortInfo on TIRAMISU"
-            }
-        }
-    }
-
-    val inner: UiccPortInfo
-        get() =
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                _inner as UiccPortInfo
-            } else {
-                throw RuntimeException("UiccPortInfo does not exist before T")
-            }
-
+interface UiccPortInfoCompat {
+    val card: UiccCardInfoCompat
     val portIndex: Int
-        get() =
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                inner.portIndex
-            } else {
-                0
-            }
-
     val logicalSlotIndex: Int
-        get() =
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                inner.logicalSlotIndex
-            } else {
-                card.physicalSlotIndex // logical is the same as physical below TIRAMISU
-            }
 }
 
-val TelephonyManager.uiccCardsInfoCompat: List<UiccCardInfoCompat>
-    @SuppressLint("MissingPermission")
-    get() = uiccCardsInfo.map { UiccCardInfoCompat(it) }
+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
+}

+ 3 - 0
app-common/src/main/res/values/colors.xml

@@ -5,4 +5,7 @@
     <color name="pink_800">#AD1457</color>
     <color name="black">#FF000000</color>
     <color name="white">#FFFFFFFF</color>
+
+    <color name="brand">@color/pink_600</color>
+    <color name="brandSecondary">@color/pink_800</color>
 </resources>

+ 2 - 2
app-common/src/main/res/values/themes.xml

@@ -6,8 +6,8 @@
         <item name="colorPrimaryVariant">@color/gray_300</item>
         <item name="colorOnPrimary">@color/black</item>
         <!-- Secondary brand color. -->
-        <item name="colorSecondary">@color/pink_600</item>
-        <item name="colorSecondaryVariant">@color/pink_800</item>
+        <item name="colorSecondary">@color/brand</item>
+        <item name="colorSecondaryVariant">@color/brandSecondary</item>
         <item name="colorOnSecondary">@color/white</item>
         <item name="colorAccent">?attr/colorSecondary</item>
         <!-- Status bar color. -->

+ 1 - 0
app-unpriv/.gitignore

@@ -0,0 +1 @@
+/build

+ 62 - 0
app-unpriv/build.gradle

@@ -0,0 +1,62 @@
+plugins {
+    id 'com.android.application'
+    id 'org.jetbrains.kotlin.android'
+}
+
+apply from: '../helpers.gradle'
+
+def keystorePropertiesFile = rootProject.file("keystore.properties");
+def keystoreProperties = new Properties()
+keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+
+android {
+    namespace 'im.angry.easyeuicc'
+    compileSdk 34
+
+    defaultConfig {
+        applicationId "im.angry.easyeuicc"
+        minSdk 30
+        targetSdk 34
+        versionCode getGitVersionCode()
+        versionName getGitVersionName()
+    }
+
+    signingConfigs {
+        config {
+            storeFile file(keystoreProperties['storeFile'])
+            storePassword keystoreProperties['storePassword']
+            keyAlias keystoreProperties['unprivKeyAlias']
+            keyPassword keystoreProperties['unprivKeyPassword']
+        }
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+            signingConfig signingConfigs.config
+        }
+        debug {
+            debuggable false
+            signingConfig signingConfigs.config
+        }
+    }
+    applicationVariants.configureEach { variant ->
+        if (variant.name == "debug") {
+            variant.outputs.each { o -> o.versionCodeOverride = System.currentTimeSeconds() }
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+}
+
+dependencies {
+    implementation project(":app-common")
+    implementation 'androidx.core:core-ktx:1.9.0'
+    implementation 'androidx.appcompat:appcompat:1.6.1'
+    implementation 'com.google.android.material:material:1.11.0'
+}

+ 21 - 0
app-unpriv/proguard-rules.pro

@@ -0,0 +1,21 @@
+# 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

+ 25 - 0
app-unpriv/src/main/AndroidManifest.xml

@@ -0,0 +1,25 @@
+<?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.MainActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>

+ 170 - 0
app-unpriv/src/main/res/drawable/ic_launcher_background.xml

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

+ 15 - 0
app-unpriv/src/main/res/drawable/ic_launcher_foreground.xml

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

+ 5 - 0
app-unpriv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

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

+ 5 - 0
app-unpriv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

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

BIN
app-unpriv/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
app-unpriv/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
app-unpriv/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
app-unpriv/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
app-unpriv/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
app-unpriv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
app-unpriv/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
app-unpriv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
app-unpriv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
app-unpriv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 8 - 0
app-unpriv/src/main/res/values/colors.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_500">#9C27B0</color>
+    <color name="purple_700">#7B1FA2</color>
+
+    <color name="brand">@color/purple_500</color>
+    <color name="brandSecondary">@color/purple_700</color>
+</resources>

+ 3 - 0
app-unpriv/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name" translatable="false">EasyEUICC</string>
+</resources>

+ 17 - 0
app-unpriv/src/test/java/im/angry/easyeuicc/ExampleUnitTest.kt

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

+ 4 - 2
app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelManager.kt

@@ -8,9 +8,11 @@ import java.lang.Exception
 import java.lang.IllegalArgumentException
 
 class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
-    override fun checkPrivileges() = true // TODO: Implement proper system app check
+    override val uiccCards: Collection<UiccCardInfoCompat>
+        get() = tm.uiccCardsInfoCompat
 
-    override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
+    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

+ 1 - 0
app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt

@@ -4,6 +4,7 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
 import im.angry.openeuicc.R
+import im.angry.openeuicc.util.*
 
 class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
     companion object {

+ 85 - 0
app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyCompat.kt

@@ -1,8 +1,93 @@
 package im.angry.openeuicc.util
 
+import android.annotation.SuppressLint
 import android.os.Build
 import android.telephony.IccOpenLogicalChannelResponse
 import android.telephony.TelephonyManager
+import android.telephony.UiccCardInfo
+import android.telephony.UiccPortInfo
+import java.lang.RuntimeException
+
+/*
+ * Implementation of Uicc{Card,Port}InfoCompat when privileged.
+ * Also handles compatibility with different platform API versions.
+ */
+@Suppress("DEPRECATION")
+class RealUiccCardInfoCompat(val inner: UiccCardInfo): UiccCardInfoCompat {
+    override val physicalSlotIndex: Int
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                inner.physicalSlotIndex
+            } else {
+                inner.slotIndex
+            }
+
+    override val ports: Collection<RealUiccPortInfoCompat>
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                inner.ports.map { RealUiccPortInfoCompat(it, this) }
+            } else {
+                listOf(RealUiccPortInfoCompat(null, this))
+            }
+
+    val isEuicc: Boolean
+        get() = inner.isEuicc
+
+    val isRemovable: Boolean
+        get() = inner.isRemovable
+
+    val isMultipleEnabledProfilesSupported: Boolean
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                inner.isMultipleEnabledProfilesSupported
+            } else {
+                false
+            }
+
+    val cardId: Int
+        get() = inner.cardId
+}
+
+class RealUiccPortInfoCompat(
+    private val _inner: Any?,
+    override val card: RealUiccCardInfoCompat
+): UiccPortInfoCompat {
+    init {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            check(_inner != null && _inner is UiccPortInfo) {
+                "_inner is not UiccPortInfo on TIRAMISU"
+            }
+        }
+    }
+
+    private val inner: UiccPortInfo
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                _inner as UiccPortInfo
+            } else {
+                throw RuntimeException("UiccPortInfo does not exist before T")
+            }
+
+    override val portIndex: Int
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                inner.portIndex
+            } else {
+                0
+            }
+
+    override val logicalSlotIndex: Int
+        get() =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                inner.logicalSlotIndex
+            } else {
+                card.physicalSlotIndex // logical is the same as physical below TIRAMISU
+            }
+}
+
+val TelephonyManager.uiccCardsInfoCompat: List<RealUiccCardInfoCompat>
+    @SuppressLint("MissingPermission")
+    get() = uiccCardsInfo.map { RealUiccCardInfoCompat(it) }
 
 // TODO: Usage of new APIs from T or later will still break build in-tree on lower AOSP versions
 //       Maybe older versions should simply include hidden-apis-shim when building?

+ 12 - 1
app/src/main/java/im/angry/openeuicc/util/PrivilegedTelephonyUtils.kt

@@ -3,6 +3,7 @@ package im.angry.openeuicc.util
 import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
 import android.telephony.UiccSlotMapping
+import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannelManager
 import kotlinx.coroutines.runBlocking
 import java.lang.Exception
@@ -62,4 +63,14 @@ fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
             // Ignore
         }
     }
-}
+}
+
+// Every EuiccChannel we use here should be backed by a RealUiccPortInfoCompat
+val EuiccChannel.removable
+    get() = (port as RealUiccPortInfoCompat).card.isRemovable
+
+val EuiccChannel.cardId
+    get() = (port as RealUiccPortInfoCompat).card.cardId
+
+val EuiccChannel.isMEP
+    get() = (port as RealUiccPortInfoCompat).card.isMultipleEnabledProfilesSupported

+ 1 - 0
settings.gradle

@@ -18,3 +18,4 @@ include ':libs:hidden-apis-stub'
 include ':libs:hidden-apis-shim'
 include ':libs:lpac-jni'
 include ':app-common'
+include ':app-unpriv'