Compare commits

..

No commits in common. "47b2a7060b2837e8f45cf9a7120856a4a015d664" and "98e0b032d028e8ad0f0468c484f0dea8dc2380fd" have entirely different histories.

52 changed files with 163 additions and 357 deletions

4
.idea/compiler.xml generated
View file

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

1
.idea/gradle.xml generated
View file

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

View file

@ -1 +0,0 @@
/build

View file

@ -1,45 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'im.angry.openeuicc.common'
compileSdk 34
defaultConfig {
minSdk 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation project(":libs:lpac-jni")
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,24 +0,0 @@
package im.angry.openeuicc.common
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("im.angry.openeuicc.common.test", appContext.packageName)
}
}

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application>
</manifest>

View file

@ -1,11 +0,0 @@
package im.angry.openeuicc.util
import net.typeblog.lpac_jni.LocalProfileInfo
val LocalProfileInfo.displayName: String
get() = nickName.ifEmpty { name }
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
get() = filter {
it.profileClass == LocalProfileInfo.Clazz.Operational
}

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_euicc">No eUICC card on this device is accessible by this app.\nInsert a supported eUICC card, or try out the privileged OpenEUICC app instead.</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
<string name="provider">Provider:</string>
<string name="iccid">ICCID:</string>
<string name="enable">Enable</string>
<string name="disable">Disable</string>
<string name="delete">Delete</string>
<string name="rename">Rename</string>
<string name="toast_profile_enabled">eSIM profile switched. Please wait for a while when the card is restarting.</string>
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
<string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string>
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
<string name="profile_download_imei">IMEI (Optional)</string>
<string name="profile_download_scan">Scan QR Code</string>
<string name="profile_download_ok">Download</string>
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
<string name="profile_rename_new_name">New nickname</string>
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
</resources>

View file

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

View file

@ -3,7 +3,33 @@ plugins {
id 'org.jetbrains.kotlin.android'
}
apply from: '../helpers.gradle'
def getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-list', '--first-parent', '--count', 'master'
standardOutput = stdout
}
return Integer.parseInt(stdout.toString().trim())
}
catch (ignored) {
return -1;
}
}
def getVersionName = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'describe', '--always', '--tags', '--dirty'
standardOutput = stdout
}
return stdout.toString().trim()
}
catch (ignored) {
return null;
}
}
// Signing config, mainly intended for debug builds
def keystorePropertiesFile = rootProject.file("keystore.properties");
@ -16,9 +42,9 @@ android {
defaultConfig {
applicationId "im.angry.openeuicc"
minSdk 30
targetSdk 34
versionCode getGitVersionCode()
versionName getGitVersionName()
targetSdk 31
versionCode getVersionCode()
versionName getVersionName()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -41,7 +67,7 @@ android {
signingConfig signingConfigs.config
}
}
applicationVariants.configureEach { variant ->
applicationVariants.all { variant ->
if (variant.name == "debug") {
variant.outputs.each { o -> o.versionCodeOverride = System.currentTimeSeconds() }
}
@ -59,9 +85,15 @@ android {
dependencies {
compileOnly project(':libs:hidden-apis-stub')
implementation project(':libs:hidden-apis-shim')
implementation project(':libs:lpac-jni')
implementation project(":app-common")
implementation project(":libs:lpac-jni")
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View file

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

View file

@ -5,16 +5,21 @@ import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import im.angry.openeuicc.core.EuiccChannelManager
open class OpenEuiccApplication : Application() {
class OpenEuiccApplication : Application() {
val telephonyManager by lazy {
getSystemService(TelephonyManager::class.java)!!
}
open val euiccChannelManager: EuiccChannelManager by lazy {
val euiccChannelManager by lazy {
EuiccChannelManager(this)
}
val subscriptionManager by lazy {
getSystemService(SubscriptionManager::class.java)!!
}
override fun onCreate() {
super.onCreate()
euiccChannelManager.closeAllStaleChannels()
}
}

View file

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

View file

@ -1,6 +1,5 @@
package im.angry.openeuicc.core
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
@ -8,17 +7,18 @@ import android.se.omapi.SEService
import android.telephony.UiccCardInfo
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.lang.Exception
import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@SuppressLint("MissingPermission") // We rely on ARA-based privileges, not READ_PRIVILEGED_PHONE_STATE
open class EuiccChannelManager(protected val context: Context) {
class EuiccChannelManager(private val context: Context) {
companion object {
const val TAG = "EuiccChannelManager"
}
@ -29,13 +29,11 @@ open class EuiccChannelManager(protected val context: Context) {
private val lock = Mutex()
protected val tm by lazy {
private val tm by lazy {
(context.applicationContext as OpenEuiccApplication).telephonyManager
}
private val handler = Handler(HandlerThread("BaseEuiccChannelManager").also { it.start() }.looper)
protected open fun checkPrivileges() = tm.hasCarrierPrivileges()
private val handler = Handler(HandlerThread("EuiccChannelManager").also { it.start() }.looper)
private suspend fun connectSEService(): SEService = suspendCoroutine { cont ->
handler.post {
@ -52,11 +50,6 @@ open class EuiccChannelManager(protected val context: Context) {
}
}
protected open fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? {
// No-op when unprivileged
return null
}
private suspend fun tryOpenEuiccChannel(uiccInfo: UiccCardInfo): EuiccChannel? {
lock.withLock {
ensureSEService()
@ -78,7 +71,17 @@ open class EuiccChannelManager(protected val context: Context) {
uiccInfo.isRemovable
)
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(uiccInfo, channelInfo)
var euiccChannel: EuiccChannel? = null
if (uiccInfo.isEuicc && !uiccInfo.isRemovable) {
Log.d(TAG, "Using TelephonyManager for slot ${uiccInfo.slotIndex}")
// TODO: On Tiramisu, we should also connect all available "ports" for MEP support
try {
euiccChannel = TelephonyManagerChannel(channelInfo, tm)
} catch (e: IllegalArgumentException) {
// Failed
}
}
if (euiccChannel == null) {
try {
@ -103,15 +106,12 @@ open class EuiccChannelManager(protected val context: Context) {
}
fun findEuiccChannelBySlotBlocking(slotId: Int): EuiccChannel? = runBlocking {
if (!checkPrivileges()) return@runBlocking null
withContext(Dispatchers.IO) {
findEuiccChannelBySlot(slotId)
}
}
suspend fun enumerateEuiccChannels() {
if (!checkPrivileges()) return
withContext(Dispatchers.IO) {
ensureSEService()
@ -127,8 +127,6 @@ open class EuiccChannelManager(protected val context: Context) {
get() = channels.toList()
fun invalidate() {
if (!checkPrivileges()) return
for (channel in channels) {
channel.close()
}
@ -138,7 +136,18 @@ open class EuiccChannelManager(protected val context: Context) {
seService = null
}
open fun notifyEuiccProfilesChanged(slotId: Int) {
// No-op for unprivileged
// Clean up channels left open in TelephonyManager
// due to a (potentially) forced restart
// This should be called every time the application is restarted
fun closeAllStaleChannels() {
for (card in tm.uiccCardsInfo) {
for (channel in 0 until 10) {
try {
tm.iccCloseLogicalChannelBySlot(card.slotIndex, channel)
} catch (_: Exception) {
// We do not care
}
}
}
}
}

View file

@ -1,49 +0,0 @@
package im.angry.openeuicc.core
import android.content.Context
import android.telephony.UiccCardInfo
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import java.lang.Exception
import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) {
override fun checkPrivileges() = true // TODO: Implement proper system app check
override fun tryOpenEuiccChannelPrivileged(uiccInfo: UiccCardInfo, channelInfo: EuiccChannelInfo): EuiccChannel? {
if (uiccInfo.isEuicc && !uiccInfo.isRemovable) {
Log.d(TAG, "Using TelephonyManager for slot ${uiccInfo.slotIndex}")
// TODO: On Tiramisu, we should also connect all available "ports" for MEP support
try {
return TelephonyManagerChannel(channelInfo, tm)
} catch (e: IllegalArgumentException) {
// Failed
}
}
return null
}
// Clean up channels left open in TelephonyManager
// due to a (potentially) forced restart
// This should be called every time the application is restarted
fun closeAllStaleChannels() {
for (card in tm.uiccCardsInfo) {
for (channel in 0 until 10) {
try {
tm.iccCloseLogicalChannelBySlot(card.slotIndex, channel)
} catch (_: Exception) {
// We do not care
}
}
}
}
override fun notifyEuiccProfilesChanged(slotId: Int) {
(context.applicationContext as OpenEuiccApplication).subscriptionManager.apply {
findEuiccChannelBySlotBlocking(slotId)?.let {
tryRefreshCachedEuiccInfo(it.cardId)
}
}
}
}

View file

@ -2,8 +2,8 @@ package im.angry.openeuicc.ui
import android.os.Bundle
import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.openEuiccApplication
interface EuiccFragmentMarker

View file

@ -19,7 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -82,7 +82,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
lifecycleScope.launch {
val profiles = withContext(Dispatchers.IO) {
euiccChannelManager.notifyEuiccProfilesChanged(slotId)
openEuiccApplication.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
channel.lpa.profiles
}

View file

@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
class LuiActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
startActivity(Intent(this, PrivilegedMainActivity::class.java))
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

View file

@ -4,25 +4,27 @@ import android.os.Bundle
import android.telephony.TelephonyManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
import im.angry.openeuicc.R
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
protected lateinit var manager: EuiccChannelManager
private lateinit var manager: EuiccChannelManager
private lateinit var spinnerAdapter: ArrayAdapter<String>
private lateinit var spinner: Spinner
@ -31,7 +33,7 @@ open class MainActivity : AppCompatActivity() {
private lateinit var noEuiccPlaceholder: View
protected lateinit var tm: TelephonyManager
private lateinit var tm: TelephonyManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -69,19 +71,32 @@ open class MainActivity : AppCompatActivity() {
}
if (tm.supportsDSDS) {
val dsds = menu.findItem(R.id.dsds)
dsds.isVisible = true
dsds.isChecked = tm.dsdsEnabled
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.dsds -> {
tm.dsdsEnabled = !item.isChecked
Toast.makeText(this, R.string.toast_dsds_switched, Toast.LENGTH_LONG).show()
finish()
true
}
else -> false
}
private suspend fun init() {
withContext(Dispatchers.IO) {
manager.enumerateEuiccChannels()
manager.knownChannels.forEach {
Log.d(TAG, it.name)
Log.d(TAG, it.lpa.eID)
// Request the system to refresh the list of profiles every time we start
// Note that this is currently supposed to be no-op when unprivileged,
// but it could change in the future
manager.notifyEuiccProfilesChanged(it.slotId)
openEuiccApplication.subscriptionManager.tryRefreshCachedEuiccInfo(it.cardId)
}
}

View file

@ -1,32 +0,0 @@
package im.angry.openeuicc.ui
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import im.angry.openeuicc.R
import im.angry.openeuicc.util.*
class PrivilegedMainActivity : MainActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.activity_main_privileged, menu)
if (tm.supportsDSDS) {
val dsds = menu.findItem(R.id.dsds)
dsds.isVisible = true
dsds.isChecked = tm.dsdsEnabled
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.dsds -> {
tm.dsdsEnabled = !item.isChecked
Toast.makeText(this, R.string.toast_dsds_switched, Toast.LENGTH_LONG).show()
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}

View file

@ -6,7 +6,7 @@ import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
import im.angry.openeuicc.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

View file

@ -13,7 +13,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R
import im.angry.openeuicc.R
import im.angry.openeuicc.util.setWidthPercent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -132,8 +132,6 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
profileDownloadServer.editText!!.isEnabled = false
profileDownloadCode.editText!!.isEnabled = false
profileDownloadConfirmationCode.editText!!.isEnabled = false
profileDownloadIMEI.editText!!.isEnabled = false
progress.isIndeterminate = true
progress.visibility = View.VISIBLE

View file

@ -13,7 +13,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R
import im.angry.openeuicc.R
import im.angry.openeuicc.util.setWidthPercent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View file

@ -2,6 +2,7 @@ package im.angry.openeuicc.util
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import net.typeblog.lpac_jni.LocalProfileInfo
import java.lang.Exception
val TelephonyManager.supportsDSDS: Boolean
@ -21,4 +22,12 @@ fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
// Ignore
}
}
}
}
val LocalProfileInfo.displayName: String
get() = nickName.ifEmpty { name }
val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
get() = filter {
it.profileClass == LocalProfileInfo.Clazz.Operational
}

View file

@ -7,4 +7,11 @@
app:actionViewClass="android.widget.Spinner"
android:background="?android:attr/colorPrimary"
app:showAsAction="always" />
<item
android:id="@+id/dsds"
android:title="@string/dsds"
android:checkable="true"
android:visible="false"
app:showAsAction="never" />
</menu>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/dsds"
android:title="@string/dsds"
android:checkable="true"
android:visible="false"
app:showAsAction="never" />
</menu>

View file

@ -1,8 +1,35 @@
<resources>
<string name="app_name">OpenEUICC</string>
<string name="no_euicc">No eUICC found on this device.\nOn some devices, you may need to enable dual SIM first in the menu of this app.</string>
<string name="dsds">Dual SIM</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
<string name="provider">Provider:</string>
<string name="iccid">ICCID:</string>
<string name="enable">Enable</string>
<string name="disable">Disable</string>
<string name="delete">Delete</string>
<string name="rename">Rename</string>
<string name="toast_profile_enabled">eSIM profile switched. Please wait for a while when the card is restarting.</string>
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
<string name="toast_dsds_switched">DSDS state switched. Please wait until the modem restarts.</string>
<string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string>
<string name="profile_download_confirmation_code">Confirmation Code (Optional)</string>
<string name="profile_download_imei">IMEI (Optional)</string>
<string name="profile_download_scan">Scan QR Code</string>
<string name="profile_download_ok">Download</string>
<string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
<string name="profile_rename_new_name">New nickname</string>
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
</resources>

View file

@ -11,11 +11,9 @@
<item name="colorOnSecondary">@color/white</item>
<item name="colorAccent">?attr/colorSecondary</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimary</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="alertDialogTheme">@style/AlertDialogTheme</item>
<item name="android:navigationBarColor">?attr/colorSecondary</item>
</style>
<style name="Theme.OpenEUICC.Input.Cursor" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">

View file

@ -1,27 +0,0 @@
ext.getGitVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-list', '--first-parent', '--count', 'master'
standardOutput = stdout
}
return Integer.parseInt(stdout.toString().trim())
}
catch (ignored) {
return -1;
}
}
ext.getGitVersionName = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'describe', '--always', '--tags', '--dirty'
standardOutput = stdout
}
return stdout.toString().trim()
}
catch (ignored) {
return null;
}
}

View file

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