Introducing EasyEUICC, the unprivileged version of OpenEUICC
|
@ -4,6 +4,7 @@
|
||||||
<bytecodeTargetLevel target="1.7">
|
<bytecodeTargetLevel target="1.7">
|
||||||
<module name="OpenEUICC.app" target="17" />
|
<module name="OpenEUICC.app" target="17" />
|
||||||
<module name="OpenEUICC.app-common" target="17" />
|
<module name="OpenEUICC.app-common" target="17" />
|
||||||
|
<module name="OpenEUICC.app-unpriv" target="17" />
|
||||||
<module name="OpenEUICC.libs.hidden-apis-shim" target="17" />
|
<module name="OpenEUICC.libs.hidden-apis-shim" target="17" />
|
||||||
<module name="OpenEUICC.libs.lpac-jni" target="17" />
|
<module name="OpenEUICC.libs.lpac-jni" target="17" />
|
||||||
</bytecodeTargetLevel>
|
</bytecodeTargetLevel>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/app-common" />
|
<option value="$PROJECT_DIR$/app-common" />
|
||||||
|
<option value="$PROJECT_DIR$/app-unpriv" />
|
||||||
<option value="$PROJECT_DIR$/libs" />
|
<option value="$PROJECT_DIR$/libs" />
|
||||||
<option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
|
<option value="$PROJECT_DIR$/libs/hidden-apis-shim" />
|
||||||
<option value="$PROJECT_DIR$/libs/hidden-apis-stub" />
|
<option value="$PROJECT_DIR$/libs/hidden-apis-stub" />
|
||||||
|
|
|
@ -4,14 +4,11 @@ import im.angry.openeuicc.util.*
|
||||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||||
|
|
||||||
abstract class EuiccChannel(
|
abstract class EuiccChannel(
|
||||||
port: UiccPortInfoCompat
|
val port: UiccPortInfoCompat
|
||||||
) {
|
) {
|
||||||
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
|
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
|
||||||
val logicalSlotId = port.logicalSlotIndex
|
val logicalSlotId = port.logicalSlotIndex
|
||||||
val portId = port.portIndex
|
val portId = port.portIndex
|
||||||
val cardId = port.card.cardId
|
|
||||||
val removable = port.card.isRemovable
|
|
||||||
val isMEP = port.card.isMultipleEnabledProfilesSupported
|
|
||||||
|
|
||||||
abstract val lpa: LocalProfileAssistant
|
abstract val lpa: LocalProfileAssistant
|
||||||
val valid: Boolean
|
val valid: Boolean
|
||||||
|
|
|
@ -34,7 +34,8 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
|
|
||||||
private val handler = Handler(HandlerThread("BaseEuiccChannelManager").also { it.start() }.looper)
|
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 ->
|
private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
|
||||||
handler.post {
|
handler.post {
|
||||||
|
@ -107,9 +108,8 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
|
|
||||||
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
|
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
if (!checkPrivileges()) return@runBlocking null
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
for (card in tm.uiccCardsInfoCompat) {
|
for (card in uiccCards) {
|
||||||
for (port in card.ports) {
|
for (port in card.ports) {
|
||||||
if (port.logicalSlotIndex == logicalSlotId) {
|
if (port.logicalSlotIndex == logicalSlotId) {
|
||||||
return@withContext tryOpenEuiccChannel(port)
|
return@withContext tryOpenEuiccChannel(port)
|
||||||
|
@ -122,9 +122,8 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
|
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = runBlocking {
|
||||||
if (!checkPrivileges()) return@runBlocking null
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
for (card in tm.uiccCardsInfoCompat) {
|
for (card in uiccCards) {
|
||||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||||
for (port in card.ports) {
|
for (port in card.ports) {
|
||||||
tryOpenEuiccChannel(port)?.let { return@withContext it }
|
tryOpenEuiccChannel(port)?.let { return@withContext it }
|
||||||
|
@ -136,8 +135,7 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
|
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? = runBlocking {
|
||||||
if (!checkPrivileges()) return@runBlocking null
|
for (card in uiccCards) {
|
||||||
for (card in tm.uiccCardsInfoCompat) {
|
|
||||||
if (card.physicalSlotIndex != physicalSlotId) continue
|
if (card.physicalSlotIndex != physicalSlotId) continue
|
||||||
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
|
||||||
.ifEmpty { null }
|
.ifEmpty { null }
|
||||||
|
@ -146,21 +144,18 @@ open class EuiccChannelManager(protected val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
|
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? = runBlocking {
|
||||||
if (!checkPrivileges()) return@runBlocking null
|
|
||||||
withContext(Dispatchers.IO) {
|
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) }
|
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun enumerateEuiccChannels() {
|
suspend fun enumerateEuiccChannels() {
|
||||||
if (!checkPrivileges()) return
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ensureSEService()
|
ensureSEService()
|
||||||
|
|
||||||
for (uiccInfo in tm.uiccCardsInfoCompat) {
|
for (uiccInfo in uiccCards) {
|
||||||
for (port in uiccInfo.ports) {
|
for (port in uiccInfo.ports) {
|
||||||
if (tryOpenEuiccChannel(port) != null) {
|
if (tryOpenEuiccChannel(port) != null) {
|
||||||
Log.d(TAG, "Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}")
|
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()
|
get() = channels.toList()
|
||||||
|
|
||||||
fun invalidate() {
|
fun invalidate() {
|
||||||
if (!checkPrivileges()) return
|
|
||||||
|
|
||||||
for (channel in channels) {
|
for (channel in channels) {
|
||||||
channel.close()
|
channel.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +1,41 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
/*
|
||||||
import android.os.Build
|
* In the privileged version, the EuiccChannelManager should work
|
||||||
import android.telephony.TelephonyManager
|
* based on real Uicc{Card,Port}Info reported by TelephonyManager.
|
||||||
import android.telephony.UiccCardInfo
|
* However, when unprivileged, we cannot depend on the fact that
|
||||||
import android.telephony.UiccPortInfo
|
* we can access TelephonyManager. ARA-M only grants access to
|
||||||
import im.angry.openeuicc.util.*
|
* OMAPI, but not TelephonyManager APIs that are associated with
|
||||||
import java.lang.RuntimeException
|
* carrier privileges.
|
||||||
|
*
|
||||||
@Suppress("DEPRECATION")
|
* To maximally share code between the two variants, we define
|
||||||
class UiccCardInfoCompat(val inner: UiccCardInfo) {
|
* 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 physicalSlotIndex: Int
|
||||||
get() =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
inner.physicalSlotIndex
|
|
||||||
} else {
|
|
||||||
inner.slotIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
val ports: Collection<UiccPortInfoCompat>
|
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) {
|
interface UiccPortInfoCompat {
|
||||||
init {
|
val card: UiccCardInfoCompat
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
check(_inner != null && _inner is UiccPortInfo) {
|
|
||||||
"_inner is not UiccPortInfo on TIRAMISU"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val inner: UiccPortInfo
|
|
||||||
get() =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
_inner as UiccPortInfo
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("UiccPortInfo does not exist before T")
|
|
||||||
}
|
|
||||||
|
|
||||||
val portIndex: Int
|
val portIndex: Int
|
||||||
get() =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
inner.portIndex
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
val logicalSlotIndex: Int
|
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>
|
data class FakeUiccCardInfoCompat(
|
||||||
@SuppressLint("MissingPermission")
|
override val physicalSlotIndex: Int,
|
||||||
get() = uiccCardsInfo.map { UiccCardInfoCompat(it) }
|
): 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
|
||||||
|
}
|
|
@ -5,4 +5,7 @@
|
||||||
<color name="pink_800">#AD1457</color>
|
<color name="pink_800">#AD1457</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
|
||||||
|
<color name="brand">@color/pink_600</color>
|
||||||
|
<color name="brandSecondary">@color/pink_800</color>
|
||||||
</resources>
|
</resources>
|
|
@ -6,8 +6,8 @@
|
||||||
<item name="colorPrimaryVariant">@color/gray_300</item>
|
<item name="colorPrimaryVariant">@color/gray_300</item>
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
<item name="colorOnPrimary">@color/black</item>
|
||||||
<!-- Secondary brand color. -->
|
<!-- Secondary brand color. -->
|
||||||
<item name="colorSecondary">@color/pink_600</item>
|
<item name="colorSecondary">@color/brand</item>
|
||||||
<item name="colorSecondaryVariant">@color/pink_800</item>
|
<item name="colorSecondaryVariant">@color/brandSecondary</item>
|
||||||
<item name="colorOnSecondary">@color/white</item>
|
<item name="colorOnSecondary">@color/white</item>
|
||||||
<item name="colorAccent">?attr/colorSecondary</item>
|
<item name="colorAccent">?attr/colorSecondary</item>
|
||||||
<!-- Status bar color. -->
|
<!-- Status bar color. -->
|
||||||
|
|
1
app-unpriv/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
62
app-unpriv/build.gradle
Normal file
|
@ -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
app-unpriv/proguard-rules.pro
vendored
Normal file
|
@ -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
app-unpriv/src/main/AndroidManifest.xml
Normal file
|
@ -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
app-unpriv/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -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
app-unpriv/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
@ -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>
|
|
@ -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>
|
|
@ -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
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
app-unpriv/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app-unpriv/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
app-unpriv/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app-unpriv/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app-unpriv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
app-unpriv/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
app-unpriv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
app-unpriv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
app-unpriv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
8
app-unpriv/src/main/res/values/colors.xml
Normal file
|
@ -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
app-unpriv/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">EasyEUICC</string>
|
||||||
|
</resources>
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,9 +8,11 @@ import java.lang.Exception
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
|
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
|
||||||
override 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) {
|
if (port.card.isRemovable) {
|
||||||
// Attempt unprivileged (OMAPI) before TelephonyManager
|
// Attempt unprivileged (OMAPI) before TelephonyManager
|
||||||
// but still try TelephonyManager in case OMAPI is broken
|
// but still try TelephonyManager in case OMAPI is broken
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import im.angry.openeuicc.R
|
import im.angry.openeuicc.R
|
||||||
|
import im.angry.openeuicc.util.*
|
||||||
|
|
||||||
class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
|
class PrivilegedEuiccManagementFragment: EuiccManagementFragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,8 +1,93 @@
|
||||||
package im.angry.openeuicc.util
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.telephony.IccOpenLogicalChannelResponse
|
import android.telephony.IccOpenLogicalChannelResponse
|
||||||
import android.telephony.TelephonyManager
|
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
|
// 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?
|
// Maybe older versions should simply include hidden-apis-shim when building?
|
||||||
|
|
|
@ -3,6 +3,7 @@ package im.angry.openeuicc.util
|
||||||
import android.telephony.SubscriptionManager
|
import android.telephony.SubscriptionManager
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
import android.telephony.UiccSlotMapping
|
import android.telephony.UiccSlotMapping
|
||||||
|
import im.angry.openeuicc.core.EuiccChannel
|
||||||
import im.angry.openeuicc.core.EuiccChannelManager
|
import im.angry.openeuicc.core.EuiccChannelManager
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
@ -63,3 +64,13 @@ fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
|
@ -18,3 +18,4 @@ include ':libs:hidden-apis-stub'
|
||||||
include ':libs:hidden-apis-shim'
|
include ':libs:hidden-apis-shim'
|
||||||
include ':libs:lpac-jni'
|
include ':libs:lpac-jni'
|
||||||
include ':app-common'
|
include ':app-common'
|
||||||
|
include ':app-unpriv'
|
||||||
|
|