diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..384a1fc --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup JDK 21 + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: "21" + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Install requires platform + run: sdkmanager "platforms;android-33" "build-tools;33.0.2" + - name: Build with Gradle + run: ./gradlew assembleRelease + - uses: ilharp/sign-android-release@nightly + name: Sign app APK + id: sign_app + with: + releaseDir: app/build/outputs/apk/release + signingKey: ${{ secrets.KEYSTORE }} + keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} + keyAlias: ${{ secrets.KEYSTORE_ALIAS }} + - name: Upload Packages + uses: actions/upload-artifact@v3 + if: success() + with: + name: eUICC Probe + path: ${{ steps.sign_app.outputs.signedFile }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0996f26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +/app/debug/*.aab +/app/release/*.aab \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..daaef12 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +eUICC Probe \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..f9d4ff7 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,45 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..be86226 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..8d81632 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac40857 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# eUICC Probe + +eUICC Available Probe + +## Screenshots + +![Screenshot](docs/shot-1.png) + +## LICENSE + +[CC-0](LICENSE.txt) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..ca92958 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,53 @@ +import java.net.URI + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.septs.euiccprobe" + compileSdk = 34 + + defaultConfig { + applicationId = "app.septs.euiccprobe" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + 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 { + implementation(files("libs/org.simalliance.openmobileapi.jar")) + + implementation("io.noties.markwon:core:4.6.2") + implementation("io.noties.markwon:ext-tasklist:4.6.2") + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/app/libs/org.simalliance.openmobileapi.jar b/app/libs/org.simalliance.openmobileapi.jar new file mode 100644 index 0000000..5b98f3d Binary files /dev/null and b/app/libs/org.simalliance.openmobileapi.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/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 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..62b8947 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/app/septs/euiccprobe/MainActivity.kt b/app/src/main/java/app/septs/euiccprobe/MainActivity.kt new file mode 100644 index 0000000..66b44bb --- /dev/null +++ b/app/src/main/java/app/septs/euiccprobe/MainActivity.kt @@ -0,0 +1,113 @@ +package app.septs.euiccprobe + +import android.os.Build +import android.os.Bundle +import android.view.View +import android.widget.HorizontalScrollView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import io.noties.markwon.Markwon +import io.noties.markwon.ext.tasklist.TaskListPlugin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class MainActivity : AppCompatActivity() { + private lateinit var markwon: Markwon + private lateinit var scrollView: HorizontalScrollView + private lateinit var report: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + setSupportActionBar(findViewById(R.id.toolbar)) + + scrollView = findViewById(R.id.scroll_view) + report = findViewById(R.id.report) + markwon = Markwon.builder(this) + .usePlugin(TaskListPlugin.create(this)) + .build() + } + + override fun onStart() { + super.onStart() + lifecycleScope.launch { + try { + init() + } catch (e: Throwable) { + report.text = e.stackTraceToString() + } + } + } + + @Suppress("SpellCheckingInspection") + private suspend fun init() = withContext(Dispatchers.Main) { + val markdown = buildString { + appendLine("${Build.BRAND} ${Build.MODEL} (${Build.MANUFACTURER})") + appendLine() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val state = SystemService.getEuiccServiceState(applicationContext) + appendLine() + appendLine("eUICC System Service: $state") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val state = SystemService.getSEBypassState(applicationContext) + appendLine() + appendLine("Secure Elements: $state") + } + SystemService.getSystemFeatures(applicationContext).let { + appendLine() + appendLine("System Features:") + for (feature in it.entries) { + appendLine("- [${if (feature.value) "x" else " "}] ${feature.key}") + } + } + SystemApps.getSystemLPAs(applicationContext).let { apps -> + if (apps.isEmpty()) { + return@let + } + appendLine() + appendLine("System LPAs:") + for (app in apps) { + val label = packageManager.getApplicationLabel(app).let { + if (it.contentEquals(app.packageName)) { + it + } else { + "$it (${app.packageName})" + } + } + appendLine("- $label") + } + } + val properties = arrayOf( + "esim.enable_esim_system_ui_by_default", + "ro.telephony.sim_slots.count", + "ro.setupwizard.esim_cid_ignore", + ) + SystemProperties.pick(*properties).let { + if (it.isEmpty()) return@let + appendLine() + appendLine("System Properties:") + for (entry in it.entries) { + appendLine("- ${entry.key} = ${entry.value}") + } + } + OpenMobileAPI.getSlots(applicationContext).let { result -> + appendLine() + appendLine("Open Mobile API:") + appendLine("- Backend: ${result.backend}") + appendLine("- State: ${result.state}") + if (result.state == OpenMobileAPI.State.Available) { + for (slot in result.slots) { + val state = if (slot.value) "Available" else "Unavailable" + appendLine("- ${slot.key} Slot: $state") + } + } + } + } + markwon.setMarkdown(report, markdown) + scrollView.post { scrollView.fullScroll(View.FOCUS_DOWN) } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/septs/euiccprobe/OpenMobileAPI.kt b/app/src/main/java/app/septs/euiccprobe/OpenMobileAPI.kt new file mode 100644 index 0000000..ccf1050 --- /dev/null +++ b/app/src/main/java/app/septs/euiccprobe/OpenMobileAPI.kt @@ -0,0 +1,134 @@ +package app.septs.euiccprobe + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.telephony.TelephonyManager +import android.util.Log +import androidx.annotation.RequiresApi +import java.util.concurrent.Executors +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + + +object OpenMobileAPI { + @OptIn(ExperimentalStdlibApi::class) + private val ISD_R_APPLET_ID = "A0000005591010FFFFFFFF8900000100".hexToByteArray() + + data class Result( + val backend: Backend, + val state: State, + val slots: Map + ) + + enum class Backend { + Builtin, + SIMAlliance, + } + + enum class State { + Unsupported, + UnableToConnect, + Unavailable, + Available, + } + + suspend fun getSlots(context: Context): Result { + val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + fromBuiltin(context) + } else { + fromSIMAlliance(context) + } + return Result( + backend = result.backend, + state = result.state, + slots = buildMap { + putAll(getCardSlots(context)) + putAll(result.slots) + } + ) + } + + @RequiresApi(Build.VERSION_CODES.P) + private suspend fun fromBuiltin(context: Context): Result { + val pkgName = "com.android.se" + if (!SystemService.hasService(context, pkgName)) { + return Result(Backend.Builtin, State.Unsupported, emptyMap()) + } + val service = suspendCoroutine { + val executor = Executors.newSingleThreadExecutor() + lateinit var service: android.se.omapi.SEService + service = android.se.omapi.SEService(context, executor) { + it.resume(service) + } + } + if (!service.isConnected) { + return Result(Backend.Builtin, State.UnableToConnect, emptyMap()) + } + val slots = buildMap { + for (reader in service.readers) { + if (!reader.name.startsWith("SIM")) continue + try { + val session = reader.openSession() + val channel = session.openLogicalChannel(ISD_R_APPLET_ID) ?: continue + put(reader.name, channel.isOpen) + if (channel.isOpen) channel.close() + if (!session.isClosed) session.closeChannels() + } catch (_: SecurityException) { + put(reader.name, true) + } catch (e: Throwable) { + Log.e(javaClass.name, "${reader.name} = ${e.message}") + } + } + service.shutdown() + } + val state = if (slots.isEmpty()) State.Unavailable else State.Available + return Result(Backend.Builtin, state, slots) + } + + private suspend fun fromSIMAlliance(context: Context): Result { + val pkgName = "org.simalliance.openmobileapi.service" + if (!SystemService.hasService(context, pkgName)) { + return Result(Backend.SIMAlliance, State.Unsupported, emptyMap()) + } + val service = suspendCoroutine { + org.simalliance.openmobileapi.SEService(context, it::resume) + } + if (!service.isConnected) { + return Result(Backend.SIMAlliance, State.UnableToConnect, emptyMap()) + } + val slots = buildMap { + for (reader in service.readers) { + if (!reader.name.startsWith("SIM")) continue + try { + val session = reader.openSession() + val channel = session.openLogicalChannel(ISD_R_APPLET_ID) + if (!channel.isClosed) channel.close() + if (!session.isClosed) session.closeChannels() + put(reader.name, true) + } catch (_: SecurityException) { + put(reader.name, true) + } catch (e: Throwable) { + Log.e(javaClass.name, "${reader.name} = ${e.message}") + } + } + service.shutdown() + } + val state = if (slots.isEmpty()) State.Unavailable else State.Available + return Result(Backend.SIMAlliance, state, slots) + } + + private fun getCardSlots(context: Context): Map { + val service = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val count = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> service.activeModemCount + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> @Suppress("DEPRECATION") service.phoneCount + else -> return mapOf(Pair("SIM", false)) + } + return buildMap { + for (index in 1..count) { + put("SIM$index", false) + } + } + } +} diff --git a/app/src/main/java/app/septs/euiccprobe/SystemApps.kt b/app/src/main/java/app/septs/euiccprobe/SystemApps.kt new file mode 100644 index 0000000..58dcd36 --- /dev/null +++ b/app/src/main/java/app/septs/euiccprobe/SystemApps.kt @@ -0,0 +1,45 @@ +package app.septs.euiccprobe + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build + +object SystemApps { + private val perms = arrayOf( + "android.permission.BIND_EUICC_SERVICE", + "android.permission.SECURE_ELEMENT_PRIVILEGED_OPERATION", + "android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS", + "com.android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS", + ) + + fun getSystemLPAs(context: Context): List { + val namePattern = Regex("lpa|euicc|esim") + return getSystemApps(context.packageManager).filter { app -> + when { + app.packageName.startsWith("com.android") -> false + app.packageName.contains(namePattern) -> perms.any { + hasPermission(context.packageManager, it, app.packageName) + } + + else -> false + } + } + } + + private fun getSystemApps(pm: PackageManager): List { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return emptyList() + } + val flags = PackageManager.MATCH_SYSTEM_ONLY + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(flags.toLong())) + } else { + pm.getInstalledApplications(flags) + } + } + + private fun hasPermission(pm: PackageManager, permName: String, pkgName: String): Boolean { + return pm.checkPermission(permName, pkgName) == PackageManager.PERMISSION_GRANTED + } +} \ No newline at end of file diff --git a/app/src/main/java/app/septs/euiccprobe/SystemProperties.kt b/app/src/main/java/app/septs/euiccprobe/SystemProperties.kt new file mode 100644 index 0000000..7e66828 --- /dev/null +++ b/app/src/main/java/app/septs/euiccprobe/SystemProperties.kt @@ -0,0 +1,30 @@ +package app.septs.euiccprobe + +object SystemProperties { + fun get(name: String): String? { + val p = Runtime.getRuntime().exec(arrayOf("getprop", name)) + return p.inputStream.reader().readText().trim().ifEmpty { null } + } + + fun getAll(): Map { + val p = Runtime.getRuntime().exec("getprop") + return buildMap { + p.inputStream.reader().forEachLine { line -> + val name = line.indexOf('[') + .let { (it + 1).. { + val features = arrayOf( + "android.hardware.telephony", + "android.hardware.telephony.subscription", + "android.hardware.telephony.euicc", + "android.hardware.telephony.euicc.mep", + "android.hardware.se.omapi.uicc", + ) + return buildMap { + for (feature in features) { + put(feature, context.packageManager.hasSystemFeature(feature)) + } + } + } + + @RequiresApi(Build.VERSION_CODES.P) + fun getEuiccServiceState(context: Context): EuiccState { + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) { + return EuiccState.Unsupported + } + val service = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager? + ?: return EuiccState.Unimplemented + return if (service.isEnabled) EuiccState.Enabled else EuiccState.Disabled + } + + fun hasService(context: Context, name: String): Boolean { + val pm = context.packageManager + val flags = 0 + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getApplicationInfo(name, PackageManager.ApplicationInfoFlags.of(flags.toLong())) + } else { + pm.getApplicationInfo(name, flags) + } + true + } catch (_: PackageManager.NameNotFoundException) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..cda2927 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a5a4f87 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..55344e5 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..2bfd12a --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + eUICC Probe + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..0973c5f --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +