From 01fc07fd78026afbf524bd26c85b529c9be1e3fb Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 11 Aug 2024 20:50:57 -0400 Subject: [PATCH 001/342] EuiccManagementFragment: Add nonnull assertion for platform types Fixes build within AOSP 14 source tree. --- .../main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 3374f07..ac0a99f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -296,7 +296,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } iccid.setOnLongClickListener { - requireContext().getSystemService(ClipboardManager::class.java) + requireContext().getSystemService(ClipboardManager::class.java)!! .setPrimaryClip(ClipData.newPlainText("iccid", iccid.text)) Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT) .show() From 44b85ffdea4b208513a78b86d90f2f90c680201a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 14 Aug 2024 20:15:47 -0400 Subject: [PATCH 002/342] lpac-jni: Log load bpp error reason --- libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c index f8b27ff..91676da 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c @@ -114,7 +114,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j (*env)->CallVoidMethod(env, callback, on_state_update, download_state_finalizing); // TODO: Expose error code as Java-side exceptions? ret = es10b_load_bound_profile_package(ctx, &es10b_load_bound_profile_package_result); - syslog(LOG_INFO, "es10b_load_bound_profile_package %d", ret); + syslog(LOG_INFO, "es10b_load_bound_profile_package %d, reason %d", ret, es10b_load_bound_profile_package_result.errorReason); out: euicc_http_cleanup(ctx); From 87ea017b36d7abec34838632dd69d8d0a897dfc4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 14 Aug 2024 20:43:54 -0400 Subject: [PATCH 003/342] OmapiApduInterface: Log all APDU exchanges --- .../angry/openeuicc/core/OmapiApduInterface.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index 227977c..711658a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -3,6 +3,7 @@ package im.angry.openeuicc.core import android.se.omapi.Channel import android.se.omapi.SEService import android.se.omapi.Session +import android.util.Log import im.angry.openeuicc.util.* import net.typeblog.lpac_jni.ApduInterface @@ -10,6 +11,10 @@ class OmapiApduInterface( private val service: SEService, private val port: UiccPortInfoCompat ): ApduInterface { + companion object { + const val TAG = "OmapiApduInterface" + } + private lateinit var session: Session private lateinit var lastChannel: Channel @@ -44,7 +49,17 @@ class OmapiApduInterface( "Unknown channel" } - return lastChannel.transmit(tx) + Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}") + + try { + return lastChannel.transmit(tx).also { + Log.d(TAG, "OMAPI APDU response: ${it.encodeHex()}") + } + } catch (e: Exception) { + Log.e(TAG, "OMAPI APDU exception") + e.printStackTrace() + throw e + } } } \ No newline at end of file From f073261b603c620f99cc081aba1ed80564591380 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 17 Aug 2024 20:55:03 -0400 Subject: [PATCH 004/342] unpriv: Add Huawei and Honor into the blocklist --- .../src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt index 49811bc..8424c55 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt @@ -193,7 +193,7 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili internal class KnownBrokenCheck(private val context: Context): CompatibilityCheck(context) { companion object { - val BROKEN_MANUFACTURERS = arrayOf("xiaomi") + val BROKEN_MANUFACTURERS = arrayOf("xiaomi", "huawei", "honor") } override val title: String From 7c07db0aab03925bb5e41f5c9602b238601b03f9 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 17 Aug 2024 20:57:25 -0400 Subject: [PATCH 005/342] README: Warn about non-standard external eSIMs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5616a32..24f4c9f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ There are two variants of this project: - OpenEUICC: The full-fledged privileged variant. - Due to its privilege requirement, OpenEUICC must be placed inside `/system/priv-app` and be signed with the platform certificate. - The preferred way to including OpenEUICC in a system image is to [build it along with AOSP](#building-aosp). + - __Note__: When privileged, OpenEUICC supports any eUICC chip that implements the SGP.22 standard, internal or external. However, there is __no guarantee__ that external (removable) eSIMs actually follow the standard. Please __DO NOT__ submit bug reports for non-functioning removable eSIMs. They are __NOT__ officially supported unless they also support / are supported by EasyEUICC, the unprivileged variant. - EasyEUICC: Unprivileged version that can run as a user app. - This version supports two modes of operation: 1. Inserted, removable eSIMs: Due to obvious security requirements, EasyEUICC is only able to access eSIM chips whose [ARF/ARA](https://source.android.com/docs/core/connect/uicc#arf) contains the hash of EasyEUICC's signing certificate. From 7f67000074b5146945a7dd822ef8dd64f6f1a681 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 2 Sep 2024 17:03:20 -0400 Subject: [PATCH 006/342] refactor: lpac-jni: Handle notification struct / linked list in Kotlin JNI does not seem to like a ton of local references very much on armv7 (32-bit), even if env->EnsureLocalCapacity is called. Let's just avoid doing this by moving most of this logic to Kotlin. This also needs to be done for LocalProfileInfo. --- .../java/net/typeblog/lpac_jni/LpacJni.kt | 11 +- .../impl/LocalProfileAssistantImpl.kt | 19 ++- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 1 - .../main/jni/lpac-jni/lpac-notifications.c | 139 ++++++++---------- .../main/jni/lpac-jni/lpac-notifications.h | 2 - 5 files changed, 89 insertions(+), 83 deletions(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 5a706b9..884c81a 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -21,7 +21,7 @@ internal object LpacJni { external fun es10cSetNickname(handle: Long, iccid: String, nick: String): Int // es10b - external fun es10bListNotification(handle: Long): Array? + external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null) external fun es10bDeleteNotification(handle: Long, seqNumber: Long): Int // es9p + es10b @@ -32,4 +32,13 @@ internal object LpacJni { // es10cex (actually part of es10b) external fun es10cexGetEuiccInfo2(handle: Long): EuiccInfo2? + + // C <-> Java struct / linked list handling + // Notifications + external fun notificationNext(curr: Long): Long + external fun notificationGetSeq(curr: Long): Long + external fun notificationGetOperationString(curr: Long): String + external fun notificationGetAddress(curr: Long): String + external fun notificationGetIccid(curr: Long): String + external fun notificationsFree(head: Long) } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 361c594..0b6aa1d 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -45,9 +45,22 @@ class LocalProfileAssistantImpl( get() = LpacJni.es10cGetProfilesInfo(contextHandle)?.asList() ?: listOf() override val notifications: List - get() = - (LpacJni.es10bListNotification(contextHandle) ?: arrayOf()) - .sortedBy { it.seqNumber }.reversed() + get() { + val head = LpacJni.es10bListNotification(contextHandle) + var curr = head + val ret = mutableListOf() + while (curr != 0L) { + ret.add(LocalProfileNotification( + LpacJni.notificationGetSeq(curr), + LocalProfileNotification.Operation.fromString(LpacJni.notificationGetOperationString(curr)), + LpacJni.notificationGetAddress(curr), + LpacJni.notificationGetIccid(curr), + )) + curr = LpacJni.notificationNext(curr) + } + LpacJni.notificationsFree(head) + return ret.sortedBy { it.seqNumber }.reversed() + } override val eID: String get() = LpacJni.es10cGetEid(contextHandle)!! diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 6ba8ebf..561e027 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -33,7 +33,6 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { jvm = vm; interface_wrapper_init(); lpac_download_init(); - lpac_notifications_init(); LPAC_JNI_SETUP_ENV; string_class = (*env)->FindClass(env, "java/lang/String"); diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c index 1a5ac7c..cfb1e59 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c @@ -4,88 +4,15 @@ #include #include -jclass local_profile_notification_class; -jmethodID local_profile_notification_constructor; - -jclass local_profile_notification_operation_class; -jmethodID local_profile_notification_operation_from_string; - -void lpac_notifications_init() { - LPAC_JNI_SETUP_ENV; - - local_profile_notification_class = - (*env)->FindClass(env, "net/typeblog/lpac_jni/LocalProfileNotification"); - local_profile_notification_class = - (*env)->NewGlobalRef(env, local_profile_notification_class); - local_profile_notification_constructor = - (*env)->GetMethodID(env, local_profile_notification_class, "", - "(JLnet/typeblog/lpac_jni/LocalProfileNotification$Operation;Ljava/lang/String;Ljava/lang/String;)V"); - - local_profile_notification_operation_class = - (*env)->FindClass(env, "net/typeblog/lpac_jni/LocalProfileNotification$Operation"); - local_profile_notification_operation_class = - (*env)->NewGlobalRef(env, local_profile_notification_operation_class); - local_profile_notification_operation_from_string = - (*env)->GetStaticMethodID(env, local_profile_notification_operation_class, "fromString", - "(Ljava/lang/String;)Lnet/typeblog/lpac_jni/LocalProfileNotification$Operation;"); -} - -JNIEXPORT jobject JNICALL +JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_es10bListNotification(JNIEnv *env, jobject thiz, jlong handle) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; struct es10b_notification_metadata_list *info = NULL; - struct es10b_notification_metadata_list *curr = NULL; - const char *profileManagementOperationStr = NULL; - jobject notification = NULL; - jobject operation = NULL; - jobjectArray ret = NULL; - int count = 0; if (es10b_list_notification(ctx, &info) < 0) - return NULL; + return 0; - count = LPAC_JNI_LINKED_LIST_COUNT(info, curr); - - ret = (*env)->NewObjectArray(env, count, local_profile_notification_class, NULL); - - LPAC_JNI_LINKED_LIST_FOREACH(info, curr, { - switch (curr->profileManagementOperation) { - case ES10B_PROFILE_MANAGEMENT_OPERATION_INSTALL: - profileManagementOperationStr = "install"; - break; - case ES10B_PROFILE_MANAGEMENT_OPERATION_DELETE: - profileManagementOperationStr = "delete"; - break; - case ES10B_PROFILE_MANAGEMENT_OPERATION_ENABLE: - profileManagementOperationStr = "enable"; - break; - case ES10B_PROFILE_MANAGEMENT_OPERATION_DISABLE: - profileManagementOperationStr = "disable"; - break; - default: - profileManagementOperationStr = "unknown"; - } - - operation = - (*env)->CallStaticObjectMethod(env, local_profile_notification_operation_class, - local_profile_notification_operation_from_string, - toJString(env, profileManagementOperationStr)); - - notification = - (*env)->NewObject(env, local_profile_notification_class, - local_profile_notification_constructor, curr->seqNumber, - operation, - toJString(env, curr->notificationAddress), - toJString(env, curr->iccid)); - - (*env)->SetObjectArrayElement(env, ret, i, notification); - - (*env)->DeleteLocalRef(env, operation); - (*env)->DeleteLocalRef(env, notification); - }); - - es10b_notification_metadata_list_free_all(info); - return ret; + return (jlong) info; } JNIEXPORT jint JNICALL @@ -117,4 +44,64 @@ Java_net_typeblog_lpac_1jni_LpacJni_es10bDeleteNotification(JNIEnv *env, jobject jlong seq_number) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; return es10b_remove_notification_from_list(ctx, (unsigned long) seq_number); +} + +JNIEXPORT jlong JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_notificationNext(JNIEnv *env, jobject thiz, jlong curr) { + struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; + if (info == NULL) { + return 0; + } + return (jlong) info->next; +} + +JNIEXPORT jlong JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_notificationGetSeq(JNIEnv *env, jobject thiz, jlong curr) { + struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; + return info->seqNumber; +} + +JNIEXPORT jstring JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_notificationGetOperationString(JNIEnv *env, jobject thiz, + jlong curr) { + struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; + const char *profileManagementOperationStr = NULL; + switch (info->profileManagementOperation) { + case ES10B_PROFILE_MANAGEMENT_OPERATION_INSTALL: + profileManagementOperationStr = "install"; + break; + case ES10B_PROFILE_MANAGEMENT_OPERATION_DELETE: + profileManagementOperationStr = "delete"; + break; + case ES10B_PROFILE_MANAGEMENT_OPERATION_ENABLE: + profileManagementOperationStr = "enable"; + break; + case ES10B_PROFILE_MANAGEMENT_OPERATION_DISABLE: + profileManagementOperationStr = "disable"; + break; + default: + profileManagementOperationStr = "unknown"; + break; + } + + return toJString(env, profileManagementOperationStr); +} + +JNIEXPORT jstring JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_notificationGetAddress(JNIEnv *env, jobject thiz, jlong curr) { + struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; + return toJString(env, info->notificationAddress); +} + +JNIEXPORT jstring JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_notificationGetIccid(JNIEnv *env, jobject thiz, jlong curr) { + struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; + return toJString(env, info->iccid); +} + +JNIEXPORT void JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_notificationsFree(JNIEnv *env, jobject thiz, jlong head) { + struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) head; + if (info == NULL) return; + es10b_notification_metadata_list_free_all(info); } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.h index 0b88555..511de2f 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.h +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.h @@ -2,5 +2,3 @@ #include #include "lpac-jni.h" - -void lpac_notifications_init(); \ No newline at end of file From 394cad2eac64eeb8feef50b9e1e2fd71346af1a2 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 3 Sep 2024 21:35:41 -0400 Subject: [PATCH 007/342] refactor: lpac-jni: Use C macros to generate struct-exposing functions --- .../java/net/typeblog/lpac_jni/LpacJni.kt | 2 +- .../impl/LocalProfileAssistantImpl.kt | 2 +- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.h | 29 +++++++++++++- .../main/jni/lpac-jni/lpac-notifications.c | 38 +++---------------- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 884c81a..37406a7 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -35,7 +35,7 @@ internal object LpacJni { // C <-> Java struct / linked list handling // Notifications - external fun notificationNext(curr: Long): Long + external fun notificationsNext(curr: Long): Long external fun notificationGetSeq(curr: Long): Long external fun notificationGetOperationString(curr: Long): String external fun notificationGetAddress(curr: Long): String diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 0b6aa1d..0b03b64 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -56,7 +56,7 @@ class LocalProfileAssistantImpl( LpacJni.notificationGetAddress(curr), LpacJni.notificationGetIccid(curr), )) - curr = LpacJni.notificationNext(curr) + curr = LpacJni.notificationsNext(curr) } LpacJni.notificationsFree(head) return ret.sortedBy { it.seqNumber }.reversed() diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h index 1915c5e..4d537ee 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h @@ -50,4 +50,31 @@ struct lpac_jni_ctx { extern JavaVM *jvm; extern jclass string_class; -jstring toJString(JNIEnv *env, const char *pat); \ No newline at end of file +jstring toJString(JNIEnv *env, const char *pat); + +#define LPAC_JNI_STRUCT_GETTER_LINKED_LIST_NEXT(st, st_jname) \ + JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_jname##Next(JNIEnv *env, jobject thiz, jlong raw) { \ + st *p = (st *) raw; \ + if (p == NULL) return 0; \ + return (jlong) p->next; \ + } + +#define LPAC_JNI_STRUCT_LINKED_LIST_FREE(st, st_jname, free_func) \ + JNIEXPORT void JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_jname##Free(JNIEnv *env, jobject thiz, jlong raw) { \ + st *p = (st *) raw; \ + if (p == NULL) return; \ + free_func(p); \ + } + +#define LPAC_JNI_STRUCT_GETTER_LONG(st, name, jname) \ + JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_notificationGet##jname(JNIEnv *env, jobject thiz, jlong raw) { \ + st *p = (st *) raw; \ + if (p == NULL) return 0; \ + return (jlong) p->name; \ + } + +#define LPAC_JNI_STRUCT_GETTER_STRING(st, name, jname) \ + JNIEXPORT jstring JNICALL Java_net_typeblog_lpac_1jni_LpacJni_notificationGet##jname(JNIEnv *env, jobject thiz, jlong raw) { \ + st *p = (st *) raw; \ + return toJString(env, p->name); \ + } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c index cfb1e59..bd4dffd 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c @@ -46,21 +46,6 @@ Java_net_typeblog_lpac_1jni_LpacJni_es10bDeleteNotification(JNIEnv *env, jobject return es10b_remove_notification_from_list(ctx, (unsigned long) seq_number); } -JNIEXPORT jlong JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_notificationNext(JNIEnv *env, jobject thiz, jlong curr) { - struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; - if (info == NULL) { - return 0; - } - return (jlong) info->next; -} - -JNIEXPORT jlong JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_notificationGetSeq(JNIEnv *env, jobject thiz, jlong curr) { - struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; - return info->seqNumber; -} - JNIEXPORT jstring JNICALL Java_net_typeblog_lpac_1jni_LpacJni_notificationGetOperationString(JNIEnv *env, jobject thiz, jlong curr) { @@ -87,21 +72,8 @@ Java_net_typeblog_lpac_1jni_LpacJni_notificationGetOperationString(JNIEnv *env, return toJString(env, profileManagementOperationStr); } -JNIEXPORT jstring JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_notificationGetAddress(JNIEnv *env, jobject thiz, jlong curr) { - struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; - return toJString(env, info->notificationAddress); -} - -JNIEXPORT jstring JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_notificationGetIccid(JNIEnv *env, jobject thiz, jlong curr) { - struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) curr; - return toJString(env, info->iccid); -} - -JNIEXPORT void JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_notificationsFree(JNIEnv *env, jobject thiz, jlong head) { - struct es10b_notification_metadata_list *info = (struct es10b_notification_metadata_list *) head; - if (info == NULL) return; - es10b_notification_metadata_list_free_all(info); -} \ No newline at end of file +LPAC_JNI_STRUCT_GETTER_LINKED_LIST_NEXT(struct es10b_notification_metadata_list, notifications) +LPAC_JNI_STRUCT_LINKED_LIST_FREE(struct es10b_notification_metadata_list, notifications, es10b_notification_metadata_list_free_all) +LPAC_JNI_STRUCT_GETTER_LONG(struct es10b_notification_metadata_list, seqNumber, Seq) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, notificationAddress, Address) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, iccid, Iccid) \ No newline at end of file From 5ab07d626276e6381e4ce48ad70449d8d2a77b36 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 3 Sep 2024 21:45:50 -0400 Subject: [PATCH 008/342] README: Mention more clearly it's GNU GPL v3 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 24f4c9f..f8019b2 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ There are two variants of this project: - Prebuilt release-mode EasyEUICC apks can be downloaded [here](https://gitea.angry.im/PeterCxy/OpenEUICC/releases) - For removable eSIM chip vendors: to have your chip supported by official builds of EasyEUICC when inserted, include the ARA-M hash `2A2FA878BC7C3354C2CF82935A5945A3EDAE4AFA` +__This project is Free Software licensed under GNU GPL v3, WITHOUT the "or later" clause.__ Any modification and derivative work __MUST__ be released under the SAME license, which means, at the very least, that the source code __MUST__ be available upon request. + +__If you are releasing a modification of this app, you are kindly asked to make changes to at least the app name and package name.__ + Building (Gradle) === From b101b01228231714e298e09a293a91b99892a6cd Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 7 Sep 2024 15:51:37 -0400 Subject: [PATCH 009/342] chore: Bump all subproject compileSdks ...yes, we'll probably need to bump these again very soon. --- app-deps/build.gradle.kts | 2 +- libs/lpac-jni/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app-deps/build.gradle.kts b/app-deps/build.gradle.kts index 2111436..6cde72d 100644 --- a/app-deps/build.gradle.kts +++ b/app-deps/build.gradle.kts @@ -13,7 +13,7 @@ apply { android { namespace = "im.angry.openeuicc_deps" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 28 diff --git a/libs/lpac-jni/build.gradle.kts b/libs/lpac-jni/build.gradle.kts index b50a953..7f45177 100644 --- a/libs/lpac-jni/build.gradle.kts +++ b/libs/lpac-jni/build.gradle.kts @@ -5,7 +5,7 @@ plugins { android { namespace = "net.typeblog.lpac_jni" - compileSdk = 33 + compileSdk = 34 ndkVersion = "26.1.10909125" defaultConfig { From 0afbece3b5b5551fc1735a79c72527ce2b565c33 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 7 Sep 2024 15:53:11 -0400 Subject: [PATCH 010/342] .idea: Whatever updates Android Studio wants --- .idea/deploymentTargetSelector.xml | 10 ++++++++++ .idea/gradle.xml | 16 +++++++++++++--- .idea/migrations.xml | 10 ++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/migrations.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..913d49d --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 163d6b4..589fc6c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,11 +4,20 @@ 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 From c681e99e4762be890b81fb419b799aca3c935d12 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 7 Sep 2024 16:02:28 -0400 Subject: [PATCH 011/342] refactor: lpac-jni: Move profiles linked list to Kotlin handling too --- .../java/net/typeblog/lpac_jni/LpacJni.kt | 12 +- .../impl/LocalProfileAssistantImpl.kt | 23 +++- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 121 +++++------------- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.h | 8 +- .../main/jni/lpac-jni/lpac-notifications.c | 6 +- 5 files changed, 70 insertions(+), 100 deletions(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 37406a7..d4ea8b2 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -14,7 +14,7 @@ internal object LpacJni { // es10c // null returns signify errors external fun es10cGetEid(handle: Long): String? - external fun es10cGetProfilesInfo(handle: Long): Array? + external fun es10cGetProfilesInfo(handle: Long): Long external fun es10cEnableProfile(handle: Long, iccid: String, refresh: Boolean): Int external fun es10cDisableProfile(handle: Long, iccid: String, refresh: Boolean): Int external fun es10cDeleteProfile(handle: Long, iccid: String): Int @@ -34,6 +34,16 @@ internal object LpacJni { external fun es10cexGetEuiccInfo2(handle: Long): EuiccInfo2? // C <-> Java struct / linked list handling + // Profiles + external fun profilesNext(curr: Long): Long + external fun profilesFree(head: Long): Long + external fun profileGetIccid(curr: Long): String + external fun profileGetIsdpAid(curr: Long): String + external fun profileGetName(curr: Long): String + external fun profileGetNickname(curr: Long): String + external fun profileGetServiceProvider(curr: Long): String + external fun profileGetStateString(curr: Long): String + external fun profileGetClassString(curr: Long): String // Notifications external fun notificationsNext(curr: Long): Long external fun notificationGetSeq(curr: Long): Long diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 0b03b64..4515ea6 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -42,7 +42,28 @@ class LocalProfileAssistantImpl( } override val profiles: List - get() = LpacJni.es10cGetProfilesInfo(contextHandle)?.asList() ?: listOf() + get() { + val head = LpacJni.es10cGetProfilesInfo(contextHandle) + var curr = head + val ret = mutableListOf() + while (curr != 0L) { + val state = LocalProfileInfo.State.fromString(LpacJni.profileGetStateString(curr)) + val clazz = LocalProfileInfo.Clazz.fromString(LpacJni.profileGetClassString(curr)) + ret.add(LocalProfileInfo( + LpacJni.profileGetIccid(curr), + state, + LpacJni.profileGetName(curr), + LpacJni.profileGetNickname(curr), + LpacJni.profileGetServiceProvider(curr), + LpacJni.profileGetIsdpAid(curr), + clazz + )) + curr = LpacJni.profilesNext(curr) + } + + LpacJni.profilesFree(curr) + return ret + } override val notifications: List get() { diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 561e027..5dcddce 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -12,15 +12,6 @@ JavaVM *jvm = NULL; -jclass local_profile_info_class; -jmethodID local_profile_info_constructor; - -jclass local_profile_state_class; -jmethodID local_profile_state_from_string; - -jclass local_profile_class_class; -jmethodID local_profile_class_from_string; - jstring empty_string; jclass string_class; @@ -40,25 +31,6 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { string_constructor = (*env)->GetMethodID(env, string_class, "", "([BLjava/lang/String;)V"); - local_profile_info_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/LocalProfileInfo"); - local_profile_info_class = (*env)->NewGlobalRef(env, local_profile_info_class); - local_profile_info_constructor = (*env)->GetMethodID(env, local_profile_info_class, "", - "(Ljava/lang/String;Lnet/typeblog/lpac_jni/LocalProfileInfo$State;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lnet/typeblog/lpac_jni/LocalProfileInfo$Clazz;)V"); - - local_profile_state_class = (*env)->FindClass(env, - "net/typeblog/lpac_jni/LocalProfileInfo$State"); - local_profile_state_class = (*env)->NewGlobalRef(env, local_profile_state_class); - local_profile_state_from_string = (*env)->GetStaticMethodID(env, local_profile_state_class, - "fromString", - "(Ljava/lang/String;)Lnet/typeblog/lpac_jni/LocalProfileInfo$State;"); - - local_profile_class_class = (*env)->FindClass(env, - "net/typeblog/lpac_jni/LocalProfileInfo$Clazz"); - local_profile_class_class = (*env)->NewGlobalRef(env, local_profile_class_class); - local_profile_class_from_string = (*env)->GetStaticMethodID(env, local_profile_class_class, - "fromString", - "(Ljava/lang/String;)Lnet/typeblog/lpac_jni/LocalProfileInfo$Clazz;"); - euicc_info2_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/EuiccInfo2"); euicc_info2_class = (*env)->NewGlobalRef(env, euicc_info2_class); euicc_info2_constructor = (*env)->GetMethodID(env, euicc_info2_class, "", @@ -144,25 +116,23 @@ Java_net_typeblog_lpac_1jni_LpacJni_es10cGetEid(JNIEnv *env, jobject thiz, jlong return ret; } -jobject profile_info_native_to_java(JNIEnv *env, struct es10c_profile_info_list *info) { +JNIEXPORT jlong JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_es10cGetProfilesInfo(JNIEnv *env, jobject thiz, jlong handle) { + struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + struct es10c_profile_info_list *info = NULL; + + if (es10c_get_profiles_info(ctx, &info) < 0) { + return 0; + } + + return (jlong) info; +} + +JNIEXPORT jstring JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_profileGetStateString(JNIEnv *env, jobject thiz, jlong curr) { + struct es10c_profile_info_list *info = (struct es10c_profile_info_list *) curr; const char *profileStateStr = NULL; - const char *profileClassStr = NULL; - jstring serviceProvider = NULL; - jstring nickName = NULL; - jstring isdpAid = NULL; - jstring iccid = NULL; - jstring name = NULL; - jobject state = NULL; - jobject class = NULL; - jobject jinfo = NULL; - iccid = toJString(env, info->iccid); - isdpAid = toJString(env, info->isdpAid); - name = toJString(env, info->profileName); - nickName = toJString(env, info->profileNickname); - serviceProvider = toJString(env, info->serviceProviderName); - - // TODO: Maybe we should pass a Java object directly here? switch (info->profileState) { case ES10C_PROFILE_STATE_ENABLED: profileStateStr = "enabled"; @@ -174,9 +144,13 @@ jobject profile_info_native_to_java(JNIEnv *env, struct es10c_profile_info_list profileStateStr = "unknown"; } - state = (*env)->CallStaticObjectMethod(env, local_profile_state_class, - local_profile_state_from_string, - toJString(env, profileStateStr)); + return toJString(env, profileStateStr); +} + +JNIEXPORT jstring JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_profileGetClassString(JNIEnv *env, jobject thiz, jlong curr) { + struct es10c_profile_info_list *info = (struct es10c_profile_info_list *) curr; + const char *profileClassStr = NULL; switch (info->profileClass) { case ES10C_PROFILE_CLASS_TEST: @@ -193,51 +167,16 @@ jobject profile_info_native_to_java(JNIEnv *env, struct es10c_profile_info_list break; } - class = (*env)->CallStaticObjectMethod(env, local_profile_class_class, - local_profile_class_from_string, - toJString(env, profileClassStr)); - - jinfo = (*env)->NewObject(env, local_profile_info_class, local_profile_info_constructor, - iccid, state, name, nickName, serviceProvider, isdpAid, class); - - (*env)->DeleteLocalRef(env, class); - (*env)->DeleteLocalRef(env, state); - (*env)->DeleteLocalRef(env, serviceProvider); - (*env)->DeleteLocalRef(env, nickName); - (*env)->DeleteLocalRef(env, name); - (*env)->DeleteLocalRef(env, isdpAid); - (*env)->DeleteLocalRef(env, iccid); - - return jinfo; + return toJString(env, profileClassStr); } -JNIEXPORT jobjectArray JNICALL -Java_net_typeblog_lpac_1jni_LpacJni_es10cGetProfilesInfo(JNIEnv *env, jobject thiz, jlong handle) { - struct euicc_ctx *ctx = (struct euicc_ctx *) handle; - struct es10c_profile_info_list *info = NULL; - struct es10c_profile_info_list *curr = NULL; - jobjectArray ret = NULL; - jobject jinfo = NULL; - int count = 0; - - if (es10c_get_profiles_info(ctx, &info) < 0) { - return NULL; - } - - count = LPAC_JNI_LINKED_LIST_COUNT(info, curr); - - ret = (*env)->NewObjectArray(env, count, local_profile_info_class, NULL); - - // Convert the native info array to Java - LPAC_JNI_LINKED_LIST_FOREACH(info, curr, { - jinfo = profile_info_native_to_java(env, curr); - (*env)->SetObjectArrayElement(env, ret, i, jinfo); - (*env)->DeleteLocalRef(env, jinfo); - }); - - es10c_profile_info_list_free_all(info); - return ret; -} +LPAC_JNI_STRUCT_GETTER_LINKED_LIST_NEXT(struct es10c_profile_info_list, profiles) +LPAC_JNI_STRUCT_LINKED_LIST_FREE(struct es10c_profile_info_list, profiles, es10c_profile_info_list_free_all) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, iccid, Iccid) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, isdpAid, IsdpAid) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, profileName, Name) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, profileNickname, Nickname) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, serviceProviderName, ServiceProvider) JNIEXPORT jint JNICALL Java_net_typeblog_lpac_1jni_LpacJni_es10cEnableProfile(JNIEnv *env, jobject thiz, jlong handle, diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h index 4d537ee..d185551 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h @@ -66,15 +66,15 @@ jstring toJString(JNIEnv *env, const char *pat); free_func(p); \ } -#define LPAC_JNI_STRUCT_GETTER_LONG(st, name, jname) \ - JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_notificationGet##jname(JNIEnv *env, jobject thiz, jlong raw) { \ +#define LPAC_JNI_STRUCT_GETTER_LONG(st, st_name, name, jname) \ + JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_name##Get##jname(JNIEnv *env, jobject thiz, jlong raw) { \ st *p = (st *) raw; \ if (p == NULL) return 0; \ return (jlong) p->name; \ } -#define LPAC_JNI_STRUCT_GETTER_STRING(st, name, jname) \ - JNIEXPORT jstring JNICALL Java_net_typeblog_lpac_1jni_LpacJni_notificationGet##jname(JNIEnv *env, jobject thiz, jlong raw) { \ +#define LPAC_JNI_STRUCT_GETTER_STRING(st, st_name, name, jname) \ + JNIEXPORT jstring JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_name##Get##jname(JNIEnv *env, jobject thiz, jlong raw) { \ st *p = (st *) raw; \ return toJString(env, p->name); \ } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c index bd4dffd..b2c4825 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c @@ -74,6 +74,6 @@ Java_net_typeblog_lpac_1jni_LpacJni_notificationGetOperationString(JNIEnv *env, LPAC_JNI_STRUCT_GETTER_LINKED_LIST_NEXT(struct es10b_notification_metadata_list, notifications) LPAC_JNI_STRUCT_LINKED_LIST_FREE(struct es10b_notification_metadata_list, notifications, es10b_notification_metadata_list_free_all) -LPAC_JNI_STRUCT_GETTER_LONG(struct es10b_notification_metadata_list, seqNumber, Seq) -LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, notificationAddress, Address) -LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, iccid, Iccid) \ No newline at end of file +LPAC_JNI_STRUCT_GETTER_LONG(struct es10b_notification_metadata_list, notification, seqNumber, Seq) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, notification, notificationAddress, Address) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, notification, iccid, Iccid) From a43ceea39f09867be95d8e0828fff0a25b602197 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 7 Sep 2024 16:26:33 -0400 Subject: [PATCH 012/342] lpac-jni: linked list free -> struct free --- libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 2 +- libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h | 2 +- libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 5dcddce..c4ca95c 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -171,7 +171,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_profileGetClassString(JNIEnv *env, jobject t } LPAC_JNI_STRUCT_GETTER_LINKED_LIST_NEXT(struct es10c_profile_info_list, profiles) -LPAC_JNI_STRUCT_LINKED_LIST_FREE(struct es10c_profile_info_list, profiles, es10c_profile_info_list_free_all) +LPAC_JNI_STRUCT_FREE(struct es10c_profile_info_list, profiles, es10c_profile_info_list_free_all) LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, iccid, Iccid) LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, isdpAid, IsdpAid) LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_profile_info_list, profile, profileName, Name) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h index d185551..87b0716 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h @@ -59,7 +59,7 @@ jstring toJString(JNIEnv *env, const char *pat); return (jlong) p->next; \ } -#define LPAC_JNI_STRUCT_LINKED_LIST_FREE(st, st_jname, free_func) \ +#define LPAC_JNI_STRUCT_FREE(st, st_jname, free_func) \ JNIEXPORT void JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_jname##Free(JNIEnv *env, jobject thiz, jlong raw) { \ st *p = (st *) raw; \ if (p == NULL) return; \ diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c index b2c4825..cf402cf 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-notifications.c @@ -73,7 +73,7 @@ Java_net_typeblog_lpac_1jni_LpacJni_notificationGetOperationString(JNIEnv *env, } LPAC_JNI_STRUCT_GETTER_LINKED_LIST_NEXT(struct es10b_notification_metadata_list, notifications) -LPAC_JNI_STRUCT_LINKED_LIST_FREE(struct es10b_notification_metadata_list, notifications, es10b_notification_metadata_list_free_all) +LPAC_JNI_STRUCT_FREE(struct es10b_notification_metadata_list, notifications, es10b_notification_metadata_list_free_all) LPAC_JNI_STRUCT_GETTER_LONG(struct es10b_notification_metadata_list, notification, seqNumber, Seq) LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, notification, notificationAddress, Address) LPAC_JNI_STRUCT_GETTER_STRING(struct es10b_notification_metadata_list, notification, iccid, Iccid) From bf361882195d003b1b143d1d65f0f45c3237acbe Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 7 Sep 2024 20:32:27 -0400 Subject: [PATCH 013/342] refactor: lpac-jni: Move EuiccInfo2 handling to Kotlin too --- .../java/net/typeblog/lpac_jni/LpacJni.kt | 17 +++- .../impl/LocalProfileAssistantImpl.kt | 31 ++++++- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 82 ++++++------------- .../lpac-jni/src/main/jni/lpac-jni/lpac-jni.h | 38 ++------- 4 files changed, 81 insertions(+), 87 deletions(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index d4ea8b2..32fbb0a 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -31,9 +31,12 @@ internal object LpacJni { external fun handleNotification(handle: Long, seqNumber: Long): Int // es10cex (actually part of es10b) - external fun es10cexGetEuiccInfo2(handle: Long): EuiccInfo2? + external fun es10cexGetEuiccInfo2(handle: Long): Long // C <-> Java struct / linked list handling + // C String arrays + external fun stringArrNext(curr: Long): Long + external fun stringDeref(curr: Long): String // Profiles external fun profilesNext(curr: Long): Long external fun profilesFree(head: Long): Long @@ -51,4 +54,16 @@ internal object LpacJni { external fun notificationGetAddress(curr: Long): String external fun notificationGetIccid(curr: Long): String external fun notificationsFree(head: Long) + // EuiccInfo2 + external fun euiccInfo2Free(info: Long) + external fun euiccInfo2GetProfileVersion(info: Long): String + external fun euiccInfo2GetEuiccFirmwareVersion(info: Long): String + external fun euiccInfo2GetGlobalPlatformVersion(info: Long): String + external fun euiccInfo2GetSasAcreditationNumber(info: Long): String + external fun euiccInfo2GetPpVersion(info: Long): String + external fun euiccInfo2GetFreeNonVolatileMemory(info: Long): Long + external fun euiccInfo2GetFreeVolatileMemory(info: Long): Long + // C String Arrays + external fun euiccInfo2GetEuiccCiPKIdListForSigning(info: Long): Long + external fun euiccInfo2GetEuiccCiPKIdListForVerification(info: Long): Long } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 4515ea6..08748d3 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -87,7 +87,36 @@ class LocalProfileAssistantImpl( get() = LpacJni.es10cGetEid(contextHandle)!! override val euiccInfo2: EuiccInfo2? - get() = LpacJni.es10cexGetEuiccInfo2(contextHandle) + get() { + val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle) + if (cInfo == 0L) return null + + val euiccCiPKIdListForSigning = mutableListOf() + var curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForSigning(cInfo) + while (curr != 0L) { + euiccCiPKIdListForSigning.add(LpacJni.stringDeref(curr)) + curr = LpacJni.stringArrNext(curr) + } + + val euiccCiPKIdListForVerification = mutableListOf() + curr = LpacJni.euiccInfo2GetEuiccCiPKIdListForVerification(cInfo) + while (curr != 0L) { + euiccCiPKIdListForVerification.add(LpacJni.stringDeref(curr)) + curr = LpacJni.stringArrNext(curr) + } + + return EuiccInfo2( + LpacJni.euiccInfo2GetProfileVersion(cInfo), + LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo), + LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo), + LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo), + LpacJni.euiccInfo2GetPpVersion(cInfo), + LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(), + LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(), + euiccCiPKIdListForSigning.toTypedArray(), + euiccCiPKIdListForVerification.toTypedArray() + ) + } override fun enableProfile(iccid: String, refresh: Boolean): Boolean = LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0 diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index c4ca95c..1721b77 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -17,9 +17,6 @@ jstring empty_string; jclass string_class; jmethodID string_constructor; -jclass euicc_info2_class; -jmethodID euicc_info2_constructor; - jint JNI_OnLoad(JavaVM *vm, void *reserved) { jvm = vm; interface_wrapper_init(); @@ -31,11 +28,6 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { string_constructor = (*env)->GetMethodID(env, string_class, "", "([BLjava/lang/String;)V"); - euicc_info2_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/EuiccInfo2"); - euicc_info2_class = (*env)->NewGlobalRef(env, euicc_info2_class); - euicc_info2_constructor = (*env)->GetMethodID(env, euicc_info2_class, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;II[Ljava/lang/String;[Ljava/lang/String;)V"); - const char _unused[1]; empty_string = (*env)->NewString(env, _unused, 0); empty_string = (*env)->NewGlobalRef(env, empty_string); @@ -233,58 +225,38 @@ Java_net_typeblog_lpac_1jni_LpacJni_es10cDeleteProfile(JNIEnv *env, jobject thiz return ret; } -JNIEXPORT jobject JNICALL +JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_es10cexGetEuiccInfo2(JNIEnv *env, jobject thiz, jlong handle) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; - struct es10c_ex_euiccinfo2 info = {0}; - jobjectArray euiccCiPKIdListForVerification = NULL; - jobjectArray euiccCiPKIdListForSigning = NULL; - jstring sas_accreditation_number = NULL; - jstring global_platform_version = NULL; - jstring euicc_firmware_version = NULL; - jstring profile_version = NULL; - jstring pp_version = NULL; - jobject ret = NULL; - char **curr = NULL; - int count = 0; + struct es10c_ex_euiccinfo2 *info = malloc(sizeof(struct es10c_ex_euiccinfo2)); - if (es10c_ex_get_euiccinfo2(ctx, &info) < 0) - goto out; + if (es10c_ex_get_euiccinfo2(ctx, info) < 0) { + free(info); + return 0; + } - profile_version = toJString(env, info.profileVersion); - euicc_firmware_version = toJString(env, info.euiccFirmwareVer); - global_platform_version = toJString(env, info.globalplatformVersion); - sas_accreditation_number = toJString(env, info.sasAcreditationNumber); - pp_version = toJString(env, info.ppVersion); + return (jlong) info; +} - count = LPAC_JNI_NULL_TERM_LIST_COUNT(info.euiccCiPKIdListForSigning, curr); - euiccCiPKIdListForSigning = (*env)->NewObjectArray(env, count, string_class, NULL); - LPAC_JNI_NULL_TERM_LIST_FOREACH(info.euiccCiPKIdListForSigning, curr, { - (*env)->SetObjectArrayElement(env, euiccCiPKIdListForSigning, i, toJString(env, *curr)); - }); +JNIEXPORT jstring JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_stringDeref(JNIEnv *env, jobject thiz, jlong curr) { + return toJString(env, *((char **) curr)); +} - count = LPAC_JNI_NULL_TERM_LIST_COUNT(info.euiccCiPKIdListForVerification, curr); - euiccCiPKIdListForVerification = (*env)->NewObjectArray(env, count, string_class, NULL); - LPAC_JNI_NULL_TERM_LIST_FOREACH(info.euiccCiPKIdListForVerification, curr, { - (*env)->SetObjectArrayElement(env, euiccCiPKIdListForVerification, i, - toJString(env, *curr)); - }); +void lpac_jni_euiccinfo2_free(struct es10c_ex_euiccinfo2 *info) { + es10c_ex_euiccinfo2_free(info); + free(info); +} - ret = (*env)->NewObject(env, euicc_info2_class, euicc_info2_constructor, - profile_version, euicc_firmware_version, - global_platform_version, - sas_accreditation_number, pp_version, - info.extCardResource.freeNonVolatileMemory, - info.extCardResource.freeVolatileMemory, - euiccCiPKIdListForSigning, - euiccCiPKIdListForVerification); +LPAC_JNI_STRUCT_GETTER_NULL_TERM_LIST_NEXT(char*, stringArr) +LPAC_JNI_STRUCT_FREE(struct es10c_ex_euiccinfo2, euiccInfo2, lpac_jni_euiccinfo2_free) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_ex_euiccinfo2, euiccInfo2, profileVersion, ProfileVersion) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_ex_euiccinfo2, euiccInfo2, euiccFirmwareVer, EuiccFirmwareVersion) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_ex_euiccinfo2, euiccInfo2, globalplatformVersion, GlobalPlatformVersion) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_ex_euiccinfo2, euiccInfo2, sasAcreditationNumber, SasAcreditationNumber) +LPAC_JNI_STRUCT_GETTER_STRING(struct es10c_ex_euiccinfo2, euiccInfo2, ppVersion, PpVersion) +LPAC_JNI_STRUCT_GETTER_LONG(struct es10c_ex_euiccinfo2, euiccInfo2, extCardResource.freeNonVolatileMemory, FreeNonVolatileMemory) +LPAC_JNI_STRUCT_GETTER_LONG(struct es10c_ex_euiccinfo2, euiccInfo2, extCardResource.freeVolatileMemory, FreeVolatileMemory) - out: - (*env)->DeleteLocalRef(env, profile_version); - (*env)->DeleteLocalRef(env, euicc_firmware_version); - (*env)->DeleteLocalRef(env, global_platform_version); - (*env)->DeleteLocalRef(env, sas_accreditation_number); - (*env)->DeleteLocalRef(env, pp_version); - es10c_ex_euiccinfo2_free(&info); - return ret; -} \ No newline at end of file +LPAC_JNI_STRUCT_GETTER_LONG(struct es10c_ex_euiccinfo2, euiccInfo2, euiccCiPKIdListForSigning, EuiccCiPKIdListForSigning) +LPAC_JNI_STRUCT_GETTER_LONG(struct es10c_ex_euiccinfo2, euiccInfo2, euiccCiPKIdListForVerification, EuiccCiPKIdListForVerification) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h index 87b0716..a5d6262 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.h @@ -17,36 +17,6 @@ struct lpac_jni_ctx { JNIEnv *env; \ (*jvm)->AttachCurrentThread(jvm, &env, NULL) -#define __LPAC_JNI_LINKED_LIST_FOREACH(list, curr, body, after) { \ - int i = 0; \ - curr = list; \ - while (curr != NULL) { \ - body; \ - curr = curr->next; \ - i++; \ - }; \ - after; \ -} -#define LPAC_JNI_LINKED_LIST_FOREACH(list, curr, body) \ - __LPAC_JNI_LINKED_LIST_FOREACH(list, curr, body, {}) -#define LPAC_JNI_LINKED_LIST_COUNT(list, curr) \ - (__LPAC_JNI_LINKED_LIST_FOREACH(list, curr, {}, i)) - -#define __LPAC_JNI_NULL_TERM_LIST_FOREACH(list, curr, body, after) { \ - int i = 0; \ - curr = list; \ - while (*curr != NULL) { \ - body; \ - curr++; \ - i++; \ - }; \ - after; \ -} -#define LPAC_JNI_NULL_TERM_LIST_FOREACH(list, curr, body) \ - __LPAC_JNI_NULL_TERM_LIST_FOREACH(list, curr, body, {}) -#define LPAC_JNI_NULL_TERM_LIST_COUNT(list, curr) \ - (__LPAC_JNI_NULL_TERM_LIST_FOREACH(list, curr, {}, i)) - extern JavaVM *jvm; extern jclass string_class; @@ -59,6 +29,14 @@ jstring toJString(JNIEnv *env, const char *pat); return (jlong) p->next; \ } +#define LPAC_JNI_STRUCT_GETTER_NULL_TERM_LIST_NEXT(st, st_jname) \ + JNIEXPORT jlong JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_jname##Next(JNIEnv *env, jobject thiz, jlong raw) { \ + st *p = (st *) raw; \ + p++; \ + if (*p == NULL) return 0; \ + return (jlong) p; \ + } + #define LPAC_JNI_STRUCT_FREE(st, st_jname, free_func) \ JNIEXPORT void JNICALL Java_net_typeblog_lpac_1jni_LpacJni_##st_jname##Free(JNIEnv *env, jobject thiz, jlong raw) { \ st *p = (st *) raw; \ From 68f1e370fc1b277b9a00b0a266374e24d227bb37 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 7 Sep 2024 20:35:13 -0400 Subject: [PATCH 014/342] lpac-jni: Make sure to free EuiccInfo2 --- .../net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 08748d3..61491d7 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -105,7 +105,7 @@ class LocalProfileAssistantImpl( curr = LpacJni.stringArrNext(curr) } - return EuiccInfo2( + val ret = EuiccInfo2( LpacJni.euiccInfo2GetProfileVersion(cInfo), LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo), LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo), @@ -116,6 +116,10 @@ class LocalProfileAssistantImpl( euiccCiPKIdListForSigning.toTypedArray(), euiccCiPKIdListForVerification.toTypedArray() ) + + LpacJni.euiccInfo2Free(cInfo) + + return ret } override fun enableProfile(iccid: String, refresh: Boolean): Boolean = From 97bc0a08273ffdd982e1b292bf98e446521964c3 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 24 Sep 2024 22:22:18 -0400 Subject: [PATCH 015/342] chore: uprev to targetSDK 35 and basic edge2edge inset fixes --- .idea/deploymentTargetSelector.xml | 3 ++ app-common/build.gradle.kts | 2 +- .../im/angry/openeuicc/ui/LogsActivity.kt | 3 ++ .../im/angry/openeuicc/ui/MainActivity.kt | 3 ++ .../openeuicc/ui/NotificationsActivity.kt | 3 ++ .../im/angry/openeuicc/ui/SettingsActivity.kt | 4 +++ .../java/im/angry/openeuicc/util/UiUtils.kt | 30 +++++++++++++++++++ .../src/main/res/layout/activity_logs.xml | 9 ++++++ .../src/main/res/layout/activity_main.xml | 9 ++++++ .../res/layout/activity_notifications.xml | 9 ++++++ .../src/main/res/layout/activity_settings.xml | 9 ++++++ app-unpriv/build.gradle.kts | 4 +-- .../ui/CompatibilityCheckActivity.kt | 3 ++ .../layout/activity_compatibility_check.xml | 9 ++++++ app/build.gradle.kts | 4 +-- .../java/im/angry/openeuicc/ui/LuiActivity.kt | 14 +++++++++ app/src/main/res/layout/activity_lui.xml | 1 + libs/lpac-jni/build.gradle.kts | 2 +- 18 files changed, 115 insertions(+), 6 deletions(-) diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 913d49d..8096d6c 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,6 +5,9 @@ + + \ No newline at end of file diff --git a/app-common/build.gradle.kts b/app-common/build.gradle.kts index 5af1799..d74fcec 100644 --- a/app-common/build.gradle.kts +++ b/app-common/build.gradle.kts @@ -5,7 +5,7 @@ plugins { android { namespace = "im.angry.openeuicc.common" - compileSdk = 34 + compileSdk = 35 defaultConfig { minSdk = 28 diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt index 2b36468..558e69a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import android.view.View import android.widget.ScrollView import android.widget.TextView +import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope @@ -37,9 +38,11 @@ class LogsActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_logs) setSupportActionBar(requireViewById(R.id.toolbar)) + setupToolbarInsets() supportActionBar!!.setDisplayHomeAsUpEnabled(true) swipeRefresh = requireViewById(R.id.swipe_refresh) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 0f504eb..183bbbd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -13,6 +13,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ProgressBar +import androidx.activity.enableEdgeToEdge import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter @@ -64,9 +65,11 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { @SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag") override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(requireViewById(R.id.toolbar)) + setupToolbarInsets() loadingProgress = requireViewById(R.id.loading) tabs = requireViewById(R.id.main_tabs) viewPager = requireViewById(R.id.view_pager) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index 59abbc5..dd14d8e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -11,6 +11,7 @@ import android.view.MenuItem.OnMenuItemClickListener import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AlertDialog import androidx.core.view.forEach import androidx.lifecycle.lifecycleScope @@ -35,9 +36,11 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { private lateinit var euiccChannel: EuiccChannel override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_notifications) setSupportActionBar(requireViewById(R.id.toolbar)) + setupToolbarInsets() supportActionBar!!.setDisplayHomeAsUpEnabled(true) } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt index 426c546..52e3272 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt @@ -2,14 +2,18 @@ package im.angry.openeuicc.ui import android.os.Bundle import android.view.MenuItem +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import im.angry.openeuicc.common.R +import im.angry.openeuicc.util.* class SettingsActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) setSupportActionBar(requireViewById(R.id.toolbar)) + setupToolbarInsets() supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportFragmentManager.beginTransaction() .replace(R.id.settings_container, SettingsFragment()) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index 24c1ad8..6aa62c2 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -2,8 +2,16 @@ package im.angry.openeuicc.util import android.content.res.Resources import android.graphics.Rect +import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment +import im.angry.openeuicc.common.R // Source: /** @@ -26,3 +34,25 @@ fun DialogFragment.setWidthPercent(percentage: Int) { fun DialogFragment.setFullScreen() { dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) } + +fun AppCompatActivity.setupToolbarInsets() { + val spacer = requireViewById(R.id.toolbar_spacer) + ViewCompat.setOnApplyWindowInsetsListener(requireViewById(R.id.toolbar)) { v, insets -> + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + or WindowInsetsCompat.Type.displayCutout() + ) + + v.updateLayoutParams { + topMargin = bars.top + } + v.updatePadding(bars.left, v.paddingTop, bars.right, v.paddingBottom) + + spacer.updateLayoutParams { + height = v.top + } + + android.util.Log.d("aaa", "${(v as Toolbar).minimumHeight}") + WindowInsetsCompat.CONSUMED + } +} diff --git a/app-common/src/main/res/layout/activity_logs.xml b/app-common/src/main/res/layout/activity_logs.xml index c305d0a..8210d07 100644 --- a/app-common/src/main/res/layout/activity_logs.xml +++ b/app-common/src/main/res/layout/activity_logs.xml @@ -5,6 +5,15 @@ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + + + + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + or WindowInsetsCompat.Type.displayCutout() + ) + v.updatePadding(bars.left, bars.top, bars.right, bars.bottom) + WindowInsetsCompat.CONSUMED + } + requireViewById(R.id.lui_skip).setOnClickListener { finish() } // TODO: Deactivate LuiActivity if there is no eSIM found. // TODO: Support pre-filled download info (from carrier apps); UX diff --git a/app/src/main/res/layout/activity_lui.xml b/app/src/main/res/layout/activity_lui.xml index ff4a302..b331156 100644 --- a/app/src/main/res/layout/activity_lui.xml +++ b/app/src/main/res/layout/activity_lui.xml @@ -2,6 +2,7 @@ diff --git a/libs/lpac-jni/build.gradle.kts b/libs/lpac-jni/build.gradle.kts index 7f45177..ff34d56 100644 --- a/libs/lpac-jni/build.gradle.kts +++ b/libs/lpac-jni/build.gradle.kts @@ -5,7 +5,7 @@ plugins { android { namespace = "net.typeblog.lpac_jni" - compileSdk = 34 + compileSdk = 35 ndkVersion = "26.1.10909125" defaultConfig { From 4a32f53c0661c91a86a094cf1b61a2eb64df769e Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 28 Sep 2024 10:28:24 -0400 Subject: [PATCH 016/342] Commonize activity toolbar layouts --- .../src/main/res/layout/activity_logs.xml | 17 +------------- .../src/main/res/layout/activity_main.xml | 17 +------------- .../res/layout/activity_notifications.xml | 17 +------------- .../src/main/res/layout/activity_settings.xml | 17 +------------- .../src/main/res/layout/toolbar_activity.xml | 22 +++++++++++++++++++ .../layout/activity_compatibility_check.xml | 17 +------------- 6 files changed, 27 insertions(+), 80 deletions(-) create mode 100644 app-common/src/main/res/layout/toolbar_activity.xml diff --git a/app-common/src/main/res/layout/activity_logs.xml b/app-common/src/main/res/layout/activity_logs.xml index 8210d07..b37328d 100644 --- a/app-common/src/main/res/layout/activity_logs.xml +++ b/app-common/src/main/res/layout/activity_logs.xml @@ -5,22 +5,7 @@ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> - - - + - - - + - - - + - - - + + + + + + + + \ No newline at end of file diff --git a/app-unpriv/src/main/res/layout/activity_compatibility_check.xml b/app-unpriv/src/main/res/layout/activity_compatibility_check.xml index c284f93..4d622e3 100644 --- a/app-unpriv/src/main/res/layout/activity_compatibility_check.xml +++ b/app-unpriv/src/main/res/layout/activity_compatibility_check.xml @@ -4,22 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - + Date: Sat, 28 Sep 2024 10:44:29 -0400 Subject: [PATCH 017/342] Remove unused log --- app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index 6aa62c2..5810dff 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -52,7 +52,6 @@ fun AppCompatActivity.setupToolbarInsets() { height = v.top } - android.util.Log.d("aaa", "${(v as Toolbar).minimumHeight}") WindowInsetsCompat.CONSUMED } } From dc70f7ca4626e02d53908489cc9de1283b180b99 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 28 Sep 2024 10:58:46 -0400 Subject: [PATCH 018/342] Fix toolbar id in app-unpriv --- .../java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt index 5ff7b65..8ac43c1 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt @@ -26,7 +26,7 @@ class CompatibilityCheckActivity: AppCompatActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) setContentView(R.layout.activity_compatibility_check) - setSupportActionBar(requireViewById(R.id.toolbar)) + setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar)) setupToolbarInsets() supportActionBar!!.setDisplayHomeAsUpEnabled(true) From a6777d1d1780a157cc0d8e31bc5aad5bcc072d93 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 28 Sep 2024 11:05:18 -0400 Subject: [PATCH 019/342] Apply layout insets to more activities --- .../openeuicc/ui/EuiccManagementFragment.kt | 18 ++++++++++++++++++ .../java/im/angry/openeuicc/ui/LogsActivity.kt | 2 ++ .../openeuicc/ui/NotificationsActivity.kt | 8 +++++--- .../im/angry/openeuicc/ui/SettingsFragment.kt | 11 +++++++++++ .../java/im/angry/openeuicc/util/UiUtils.kt | 14 ++++++++++++++ .../openeuicc/ui/CompatibilityCheckActivity.kt | 2 ++ 6 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index ac0a99f..b9b5e59 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -19,6 +19,9 @@ import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -76,6 +79,21 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, fab = view.requireViewById(R.id.fab) profileList = view.requireViewById(R.id.profile_list) + val origFabMarginRight = (fab.layoutParams as ViewGroup.MarginLayoutParams).rightMargin + val origFabMarginBottom = (fab.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin + ViewCompat.setOnApplyWindowInsetsListener(fab) { v, insets -> + val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + + v.updateLayoutParams { + rightMargin = origFabMarginRight + bars.right + bottomMargin = origFabMarginBottom + bars.bottom + } + + WindowInsetsCompat.CONSUMED + } + + setupRootViewInsets(profileList) + return view } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt index 558e69a..e1c9ea1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt @@ -49,6 +49,8 @@ class LogsActivity : AppCompatActivity() { scrollView = requireViewById(R.id.scroll_view) logText = requireViewById(R.id.log_text) + setupRootViewInsets(scrollView) + swipeRefresh.setOnRefreshListener { lifecycleScope.launch { reload() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index dd14d8e..01de1fc 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -42,15 +42,17 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { setSupportActionBar(requireViewById(R.id.toolbar)) setupToolbarInsets() supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + swipeRefresh = requireViewById(R.id.swipe_refresh) + notificationList = requireViewById(R.id.recycler_view) + + setupRootViewInsets(notificationList) } override fun onInit() { euiccChannel = euiccChannelManager .findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!! - swipeRefresh = requireViewById(R.id.swipe_refresh) - notificationList = requireViewById(R.id.recycler_view) - notificationList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index d471efa..841b8bd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -3,6 +3,9 @@ package im.angry.openeuicc.ui import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference @@ -46,6 +49,14 @@ class SettingsFragment: PreferenceFragmentCompat() { ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM) } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return super.onCreateView(inflater, container, savedInstanceState).also(::setupRootViewInsets) + } + private fun CheckBoxPreference.bindBooleanFlow(flow: Flow, key: Preferences.Key) { lifecycleScope.launch { flow.collect { isChecked = it } diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index 5810dff..b54b494 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -55,3 +55,17 @@ fun AppCompatActivity.setupToolbarInsets() { WindowInsetsCompat.CONSUMED } } + +fun setupRootViewInsets(view: View) { + ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + or WindowInsetsCompat.Type.displayCutout() + ) + + // Don't set padding bottom because we do want scrolling root views to extend into nav bar + v.updatePadding(bars.left, v.paddingTop, bars.right, v.paddingBottom) + + WindowInsetsCompat.CONSUMED + } +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt index 8ac43c1..06b46df 100644 --- a/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt @@ -35,6 +35,8 @@ class CompatibilityCheckActivity: AppCompatActivity() { LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) compatibilityCheckList.adapter = adapter + + setupRootViewInsets(compatibilityCheckList) } override fun onStart() { From b94eedac0a0e2542f275d34832108f676cc50eb4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 28 Sep 2024 11:15:51 -0400 Subject: [PATCH 020/342] All LPA methods should be synchronized Calling them from multiple threads is undefined --- .../typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 61491d7..6b3055d 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -42,6 +42,7 @@ class LocalProfileAssistantImpl( } override val profiles: List + @Synchronized get() { val head = LpacJni.es10cGetProfilesInfo(contextHandle) var curr = head @@ -66,6 +67,7 @@ class LocalProfileAssistantImpl( } override val notifications: List + @Synchronized get() { val head = LpacJni.es10bListNotification(contextHandle) var curr = head @@ -84,9 +86,11 @@ class LocalProfileAssistantImpl( } override val eID: String + @Synchronized get() = LpacJni.es10cGetEid(contextHandle)!! override val euiccInfo2: EuiccInfo2? + @Synchronized get() { val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle) if (cInfo == 0L) return null @@ -122,12 +126,15 @@ class LocalProfileAssistantImpl( return ret } + @Synchronized override fun enableProfile(iccid: String, refresh: Boolean): Boolean = LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0 + @Synchronized override fun disableProfile(iccid: String, refresh: Boolean): Boolean = LpacJni.es10cDisableProfile(contextHandle, iccid, refresh) == 0 + @Synchronized override fun deleteProfile(iccid: String): Boolean = LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0 @@ -144,6 +151,7 @@ class LocalProfileAssistantImpl( ) == 0 } + @Synchronized override fun deleteNotification(seqNumber: Long): Boolean = LpacJni.es10bDeleteNotification(contextHandle, seqNumber) == 0 @@ -153,6 +161,7 @@ class LocalProfileAssistantImpl( Log.d(TAG, "handleNotification $seqNumber = $it") } == 0 + @Synchronized override fun setNickname(iccid: String, nickname: String): Boolean = LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 From 324dcdc563ba45e69c8a0423119764202407c96a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 28 Sep 2024 16:15:17 -0400 Subject: [PATCH 021/342] EuiccManagementFragment: prevent crashes on configuration change This is... not supposed to happen. We made too many assumptions about the Fragment lifecycle. --- .../java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt | 3 +++ .../java/im/angry/openeuicc/ui/EuiccManagementFragment.kt | 5 +++++ .../im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt | 3 +++ 3 files changed, 11 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt index d760c76..3c699d7 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt @@ -9,14 +9,17 @@ import android.os.IBinder import androidx.appcompat.app.AppCompatActivity import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.service.EuiccChannelManagerService +import kotlinx.coroutines.CompletableDeferred abstract class BaseEuiccAccessActivity : AppCompatActivity() { + val euiccChannelManagerLoaded = CompletableDeferred() lateinit var euiccChannelManager: EuiccChannelManager private val euiccChannelManagerServiceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { euiccChannelManager = (service!! as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager + euiccChannelManagerLoaded.complete(Unit) onInit() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index b9b5e59..fc2aa5c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -108,7 +108,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, ProfileDownloadFragment.newInstance(slotId, portId) .show(childFragmentManager, ProfileDownloadFragment.TAG) } + } + override fun onStart() { + super.onStart() refresh() } @@ -151,6 +154,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, swipeRefresh.isRefreshing = true lifecycleScope.launch { + ensureEuiccChannelManager() + if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { disableSafeguardFlow = preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index a324ac3..667ce6e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -39,6 +39,9 @@ val T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker get() = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! +suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker = + (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() + interface EuiccProfilesChangedListener { fun onEuiccProfilesChanged() } From 3add3ffa909a4c84dd90c0d34accc516a3751aa5 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 14:09:17 -0400 Subject: [PATCH 022/342] refactor: Launch profile download task inside EuiccChannelManagerService This task is too long to run directly inside the fragment lifecycle. Instead, let's launch it inside the service lifecycle scope and use a MutableStateFlow to notify the UI of progress. This interface is designed to be extensible to other use cases. --- app-common/src/main/AndroidManifest.xml | 3 + .../service/EuiccChannelManagerService.kt | 191 +++++++++++++++++- .../openeuicc/ui/BaseEuiccAccessActivity.kt | 5 +- .../im/angry/openeuicc/ui/MainActivity.kt | 17 ++ .../openeuicc/ui/ProfileDownloadFragment.kt | 40 ++-- .../util/EuiccChannelFragmentUtils.kt | 3 + .../drawable/ic_task_sim_card_download.xml | 5 + app-common/src/main/res/values/strings.xml | 3 + app-deps/Android.bp | 1 + app-deps/build.gradle.kts | 1 + privapp_whitelist_im.angry.openeuicc.xml | 2 + 11 files changed, 242 insertions(+), 29 deletions(-) create mode 100644 app-common/src/main/res/drawable/ic_task_sim_card_download.xml diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index 8163299..a59110b 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -3,8 +3,10 @@ xmlns:android="http://schemas.android.com/apk/res/android" package="im.angry.openeuicc.common"> + + diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 75c58db..9a30df3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -1,11 +1,27 @@ package im.angry.openeuicc.service -import android.app.Service import android.content.Intent +import android.content.pm.PackageManager import android.os.Binder import android.os.IBinder +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import im.angry.openeuicc.common.R import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.typeblog.lpac_jni.ProfileDownloadCallback /** * An Android Service wrapper for EuiccChannelManager. @@ -17,8 +33,20 @@ import im.angry.openeuicc.util.* * instance of EuiccChannelManager. UI components can keep being bound to this service for * their entire lifecycles, since the whole purpose of them is to expose the current state * to the user. + * + * Additionally, this service is also responsible for long-running "foreground" tasks that + * are not suitable to be managed by UI components. This includes profile downloading, etc. + * When a UI component needs to run one of these tasks, they have to bind to this service + * and call one of the `launch*` methods, which will run the task inside this service's + * lifecycle context and return a Flow instance for the UI component to subscribe to its + * progress. */ -class EuiccChannelManagerService : Service(), OpenEuiccContextMarker { +class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { + companion object { + private const val CHANNEL_ID = "tasks" + private const val FOREGROUND_ID = 1000 + } + inner class LocalBinder : Binder() { val service = this@EuiccChannelManagerService } @@ -28,14 +56,167 @@ class EuiccChannelManagerService : Service(), OpenEuiccContextMarker { } val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate - override fun onBind(intent: Intent?): IBinder = LocalBinder() + /** + * The state of a "foreground" task (named so due to the need to startForeground()) + */ + sealed interface ForegroundTaskState { + data object Idle : ForegroundTaskState + data class InProgress(val progress: Int) : ForegroundTaskState + data class Done(val error: Throwable?) : ForegroundTaskState + } + + /** + * This flow emits whenever the service has had a start command, from startService() + * The service self-starts when foreground is required, because other components + * only bind to this service and do not start it per-se. + */ + private val foregroundStarted: MutableSharedFlow = MutableSharedFlow() + + /** + * This flow is used to emit progress updates when a foreground task is running. + */ + private val foregroundTaskState: MutableStateFlow = + MutableStateFlow(ForegroundTaskState.Idle) + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return LocalBinder() + } override fun onDestroy() { super.onDestroy() - // This is the whole reason of the existence of this service: - // we can clean up opened channels when no one is using them if (euiccChannelManagerDelegate.isInitialized()) { euiccChannelManager.invalidate() } } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return super.onStartCommand(intent, flags, startId).also { + lifecycleScope.launch { + foregroundStarted.emit(Unit) + } + } + } + + private fun updateForegroundNotification(title: String, iconRes: Int) { + val channel = + NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) + .setName(getString(R.string.task_notification)) + .setVibrationEnabled(false) + .build() + NotificationManagerCompat.from(this).createNotificationChannel(channel) + + val state = foregroundTaskState.value + + if (state is ForegroundTaskState.InProgress) { + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(title) + .setProgress(100, state.progress, state.progress == 0) + .setSmallIcon(iconRes) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + + if (state.progress == 0) { + startForeground(FOREGROUND_ID, notification) + } else if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + NotificationManagerCompat.from(this).notify(FOREGROUND_ID, notification) + } + } else { + stopForeground(STOP_FOREGROUND_REMOVE) + } + } + + /** + * Launch a potentially blocking foreground task in this service's lifecycle context. + * This function does not block, but returns a Flow that emits ForegroundTaskState + * updates associated with this task. + * The task closure is expected to update foregroundTaskState whenever appropriate. + * If a foreground task is already running, this function returns null. + */ + private fun launchForegroundTask( + title: String, + iconRes: Int, + task: suspend EuiccChannelManagerService.() -> Unit + ): Flow? { + // Atomically set the state to InProgress. If this returns true, we are + // the only task currently in progress. + if (!foregroundTaskState.compareAndSet( + ForegroundTaskState.Idle, + ForegroundTaskState.InProgress(0) + ) + ) { + return null + } + + lifecycleScope.launch(Dispatchers.Main) { + // Wait until our self-start command has succeeded. + // We can only call startForeground() after that + foregroundStarted.first() + updateForegroundNotification(title, iconRes) + + try { + withContext(Dispatchers.IO) { + this@EuiccChannelManagerService.task() + } + foregroundTaskState.value = ForegroundTaskState.Done(null) + } catch (t: Throwable) { + foregroundTaskState.value = ForegroundTaskState.Done(t) + } finally { + stopSelf() + } + + updateForegroundNotification(title, iconRes) + } + + // We 've launched the coroutine, now we can self-start + // This is required in order to use startForeground() + // This will end up calling onStartCommand(), which will emit + // into foregroundStarted and unblock the coroutine above + startForegroundService(Intent(this, this::class.java)) + + // We should be the only task running, so we can subscribe to foregroundTaskState + // until we encounter ForegroundTaskState.Done. + return foregroundTaskState.transformWhile { + // Also update our notification when we see an update + updateForegroundNotification(title, iconRes) + emit(it) + it !is ForegroundTaskState.Done + }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } + } + + fun launchProfileDownloadTask( + slotId: Int, + portId: Int, + smdp: String, + matchingId: String?, + confirmationCode: String?, + imei: String? + ): Flow? = + launchForegroundTask( + getString(R.string.task_profile_download), + R.drawable.ic_task_sim_card_download + ) { + euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { + val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId) + val res = channel!!.lpa.downloadProfile( + smdp, + matchingId, + imei, + confirmationCode, + object : ProfileDownloadCallback { + override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { + if (state.progress == 0) return + foregroundTaskState.value = + ForegroundTaskState.InProgress(state.progress) + } + }) + + if (!res) { + // TODO: Provide more details on the error + throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.") + } + + preferenceRepository.notificationDownloadFlow.first() + } + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt index 3c699d7..ae13962 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/BaseEuiccAccessActivity.kt @@ -14,11 +14,12 @@ import kotlinx.coroutines.CompletableDeferred abstract class BaseEuiccAccessActivity : AppCompatActivity() { val euiccChannelManagerLoaded = CompletableDeferred() lateinit var euiccChannelManager: EuiccChannelManager + lateinit var euiccChannelManagerService: EuiccChannelManagerService private val euiccChannelManagerServiceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - euiccChannelManager = - (service!! as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager + euiccChannelManagerService = (service!! as EuiccChannelManagerService.LocalBinder).service + euiccChannelManager = euiccChannelManagerService.euiccChannelManager euiccChannelManagerLoaded.complete(Unit) onInit() } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 183bbbd..1307513 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -5,7 +5,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.hardware.usb.UsbManager +import android.os.Build import android.os.Bundle import android.telephony.TelephonyManager import android.util.Log @@ -30,6 +32,8 @@ import kotlinx.coroutines.withContext open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { companion object { const val TAG = "MainActivity" + + const val PERMISSION_REQUEST_CODE = 1000 } private lateinit var loadingProgress: ProgressBar @@ -116,6 +120,15 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { } } + private fun ensureNotificationPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), + PERMISSION_REQUEST_CODE + ) + } + } + private suspend fun init(fromUsbEvent: Boolean = false) { refreshing = true // We don't check this here -- the check happens in refresh() loadingProgress.visibility = View.VISIBLE @@ -173,6 +186,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { viewPager.currentItem = 0 } + if (pages.size > 0) { + ensureNotificationPermissions() + } + refreshing = false } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index f8399a9..475dd92 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -18,12 +18,13 @@ 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.service.EuiccChannelManagerService import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import net.typeblog.lpac_jni.ProfileDownloadCallback import kotlin.Exception class ProfileDownloadFragment : BaseMaterialDialogFragment(), @@ -224,30 +225,25 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), code: String?, confirmationCode: String?, imei: String? - ) = beginTrackedOperation { - val res = channel.lpa.downloadProfile( + ) = withContext(Dispatchers.Main) { + // The service is responsible for launching the actual blocking part on the IO context + val res = euiccChannelManagerService.launchProfileDownloadTask( + slotId, + portId, server, code, - imei, confirmationCode, - object : ProfileDownloadCallback { - override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { - lifecycleScope.launch(Dispatchers.Main) { - progress.isIndeterminate = false - progress.progress = state.progress - } - } - }) + imei + )!!.onEach { + progress.isIndeterminate = false + if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) { + progress.progress = it.progress + } else { + progress.progress = 100 + } + }.last() - if (!res) { - // TODO: Provide more details on the error - throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.") - } - - // If we get here, we are successful - // This function is wrapped in beginTrackedOperation, so by returning the settings value, - // We only send notifications if the user allowed us to - preferenceRepository.notificationDownloadFlow.first() + (res as? EuiccChannelManagerService.ForegroundTaskState.Done)?.error?.let { throw it } } override fun onDismiss(dialog: DialogInterface) { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index 667ce6e..f6f20d4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager +import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.ui.BaseEuiccAccessActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -35,6 +36,8 @@ val T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker val T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager +val T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker + get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService val T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker get() = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! diff --git a/app-common/src/main/res/drawable/ic_task_sim_card_download.xml b/app-common/src/main/res/drawable/ic_task_sim_card_download.xml new file mode 100644 index 0000000..a2eadb2 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_task_sim_card_download.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 7647815..647a2c4 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -33,6 +33,9 @@ Permission is needed to access the USB smart card reader. Cannot connect to eSIM via a USB smart card reader. + Long-running Tasks + Downloading eSIM profile + New eSIM Server (RSP / SM-DP+) Activation Code diff --git a/app-deps/Android.bp b/app-deps/Android.bp index 3143e3f..b98d862 100644 --- a/app-deps/Android.bp +++ b/app-deps/Android.bp @@ -8,6 +8,7 @@ java_defaults { "androidx-constraintlayout_constraintlayout", "androidx.preference_preference", "androidx.lifecycle_lifecycle-runtime-ktx", + "androidx.lifecycle_lifecycle-service", "androidx.swiperefreshlayout_swiperefreshlayout", "androidx.cardview_cardview", "androidx.viewpager2_viewpager2", diff --git a/app-deps/build.gradle.kts b/app-deps/build.gradle.kts index 6cde72d..2f4d70e 100644 --- a/app-deps/build.gradle.kts +++ b/app-deps/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { //noinspection KtxExtensionAvailable api("androidx.preference:preference:1.2.1") api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") + api("androidx.lifecycle:lifecycle-service:2.6.2") api("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") api("androidx.cardview:cardview:1.0.0") api("androidx.viewpager2:viewpager2:1.1.0") diff --git a/privapp_whitelist_im.angry.openeuicc.xml b/privapp_whitelist_im.angry.openeuicc.xml index 88d35cc..0f117b6 100644 --- a/privapp_whitelist_im.angry.openeuicc.xml +++ b/privapp_whitelist_im.angry.openeuicc.xml @@ -5,5 +5,7 @@ + + From 9a77824f7943203e80634d9ca573e1343715f69b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 14:40:15 -0400 Subject: [PATCH 023/342] Enforce updateForegroundNotification to run in the main thread --- .../im/angry/openeuicc/service/EuiccChannelManagerService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 9a30df3..c9a252a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -178,7 +178,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // until we encounter ForegroundTaskState.Done. return foregroundTaskState.transformWhile { // Also update our notification when we see an update - updateForegroundNotification(title, iconRes) + withContext(Dispatchers.Main) { + updateForegroundNotification(title, iconRes) + } emit(it) it !is ForegroundTaskState.Done }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } From 64a350d2714a177cb14d1735e2d0656706ad1c6b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 15:12:18 -0400 Subject: [PATCH 024/342] lpac-jni: Force reduce connect/read timeout for notification requests --- .../java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt index 356ccb2..8193ac3 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt @@ -34,6 +34,12 @@ class HttpInterfaceImpl: HttpInterface { val conn = parsedUrl.openConnection() as HttpsURLConnection conn.connectTimeout = 2000 + + if (url.contains("handleNotification")) { + conn.connectTimeout = 1000 + conn.readTimeout = 1000 + } + conn.sslSocketFactory = sslContext.socketFactory conn.requestMethod = "POST" conn.doInput = true From 8de0d868954aada198a8b135947dd1b62b41a678 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 15:14:43 -0400 Subject: [PATCH 025/342] ProfileDownloadFragment: Wait for euiccChannelManager to load --- .../main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 475dd92..99ac003 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -159,6 +159,8 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), ) lifecycleScope.launch(Dispatchers.IO) { + ensureEuiccChannelManager() + // Fetch remaining NVRAM val str = channel.lpa.euiccInfo2?.freeNvram?.also { freeNvram = it From fe1319537ace15ab2f323e7fce951c155eeffe73 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 15:30:49 -0400 Subject: [PATCH 026/342] Make foreground tasks block UI reloads --- .../openeuicc/service/EuiccChannelManagerService.kt | 10 ++++++++++ .../im/angry/openeuicc/ui/EuiccManagementFragment.kt | 1 + .../main/java/im/angry/openeuicc/ui/MainActivity.kt | 2 ++ .../im/angry/openeuicc/ui/ProfileDownloadFragment.kt | 6 ++++++ 4 files changed, 19 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index c9a252a..97a7841 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -16,8 +16,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -186,6 +188,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } } + val isForegroundTaskRunning: Boolean + get() = foregroundTaskState.value != ForegroundTaskState.Idle + + suspend fun waitForForegroundTask() { + foregroundTaskState.takeWhile { it != ForegroundTaskState.Idle } + .collect() + } + fun launchProfileDownloadTask( slotId: Int, portId: Int, diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index fc2aa5c..14d958d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -155,6 +155,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, lifecycleScope.launch { ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { disableSafeguardFlow = diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 1307513..17c99ff 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -134,6 +134,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { loadingProgress.visibility = View.VISIBLE viewPager.visibility = View.GONE tabs.visibility = View.GONE + // Prevent concurrent access with any running foreground task + euiccChannelManagerService.waitForForegroundTask() val knownChannels = withContext(Dispatchers.IO) { euiccChannelManager.enumerateEuiccChannels().onEach { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 99ac003..35825bf 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -160,6 +160,12 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), lifecycleScope.launch(Dispatchers.IO) { ensureEuiccChannelManager() + if (euiccChannelManagerService.isForegroundTaskRunning) { + withContext(Dispatchers.Main) { + dismiss() + } + return@launch + } // Fetch remaining NVRAM val str = channel.lpa.euiccInfo2?.freeNvram?.also { From f71da0e4ffefbc74d6b75a843885acb74109b0a4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 15:32:46 -0400 Subject: [PATCH 027/342] Update documentation --- .../openeuicc/service/EuiccChannelManagerService.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 97a7841..f177f1a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -131,9 +131,13 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { /** * Launch a potentially blocking foreground task in this service's lifecycle context. * This function does not block, but returns a Flow that emits ForegroundTaskState - * updates associated with this task. + * updates associated with this task. The last update the returned flow will emit is + * always ForegroundTaskState.Done. + * * The task closure is expected to update foregroundTaskState whenever appropriate. * If a foreground task is already running, this function returns null. + * + * The function will set the state back to Idle once it sees ForegroundTaskState.Done. */ private fun launchForegroundTask( title: String, @@ -178,6 +182,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // We should be the only task running, so we can subscribe to foregroundTaskState // until we encounter ForegroundTaskState.Done. + // Then, we complete the returned flow, but we also set the state back to Idle. + // The state update back to Idle won't show up in the returned stream, because + // it has been completed by that point. return foregroundTaskState.transformWhile { // Also update our notification when we see an update withContext(Dispatchers.Main) { From cf5704be4220e11221bdf71c88ddfe4527c8c3a7 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 16:32:58 -0400 Subject: [PATCH 028/342] Move profile download to the new foreground flow ...and fix race condition introduced by the new flow --- .../service/EuiccChannelManagerService.kt | 36 +++++++++++++++---- .../openeuicc/ui/ProfileDownloadFragment.kt | 2 ++ .../openeuicc/ui/ProfileRenameFragment.kt | 35 ++++++++---------- .../src/main/res/drawable/ic_task_rename.xml | 5 +++ app-common/src/main/res/values/strings.xml | 1 + 5 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 app-common/src/main/res/drawable/ic_task_rename.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index f177f1a..c004c5f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch @@ -174,12 +175,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { updateForegroundNotification(title, iconRes) } - // We 've launched the coroutine, now we can self-start - // This is required in order to use startForeground() - // This will end up calling onStartCommand(), which will emit - // into foregroundStarted and unblock the coroutine above - startForegroundService(Intent(this, this::class.java)) - // We should be the only task running, so we can subscribe to foregroundTaskState // until we encounter ForegroundTaskState.Done. // Then, we complete the returned flow, but we also set the state back to Idle. @@ -192,6 +187,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } emit(it) it !is ForegroundTaskState.Done + }.onStart { + // When this Flow is started, we unblock the coroutine launched above by + // self-starting as a foreground service. + startForegroundService( + Intent( + this@EuiccChannelManagerService, + this@EuiccChannelManagerService::class.java + ) + ) }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } } @@ -238,4 +242,24 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { preferenceRepository.notificationDownloadFlow.first() } } + + fun launchProfileRenameTask( + slotId: Int, + portId: Int, + iccid: String, + name: String + ): Flow? = + launchForegroundTask( + getString(R.string.task_profile_rename), + R.drawable.ic_task_rename + ) { + val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname( + iccid, + name + ) + + if (!res) { + throw RuntimeException("Profile not renamed") + } + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 35825bf..c61fe3a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -213,6 +213,8 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), progress.visibility = View.VISIBLE lifecycleScope.launch { + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() try { doDownloadProfile(server, code, confirmationCode, imei) } catch (e: Exception) { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index 7987117..84fd0cd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -2,7 +2,6 @@ package im.angry.openeuicc.ui import android.app.Dialog import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,11 +12,8 @@ import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.lang.Exception -import java.lang.RuntimeException class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { companion object { @@ -97,23 +93,20 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment progress.visibility = View.VISIBLE lifecycleScope.launch { - try { - doRename(name) - } catch (e: Exception) { - Log.d(TAG, "Failed to rename profile") - Log.d(TAG, Log.getStackTraceString(e)) - } finally { - if (parentFragment is EuiccProfilesChangedListener) { - (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() - } - dismiss() - } - } - } + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() + euiccChannelManagerService.launchProfileRenameTask( + slotId, + portId, + requireArguments().getString("iccid")!!, + name + )?.collect() - private suspend fun doRename(name: String) = withContext(Dispatchers.IO) { - if (!channel.lpa.setNickname(requireArguments().getString("iccid")!!, name)) { - throw RuntimeException("Profile nickname not changed") + if (parentFragment is EuiccProfilesChangedListener) { + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + } + + dismiss() } } } \ No newline at end of file diff --git a/app-common/src/main/res/drawable/ic_task_rename.xml b/app-common/src/main/res/drawable/ic_task_rename.xml new file mode 100644 index 0000000..3c53db7 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_task_rename.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 647a2c4..bedfe21 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -35,6 +35,7 @@ Long-running Tasks Downloading eSIM profile + Renaming eSIM profile New eSIM Server (RSP / SM-DP+) From 31c06470c6e2b1592322d4b30e9988a8c73cd9d8 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 16:49:21 -0400 Subject: [PATCH 029/342] Move profile deletion to new flow --- .../service/EuiccChannelManagerService.kt | 24 +++++++++++++- .../openeuicc/ui/ProfileDeleteFragment.kt | 31 +++++++++---------- .../src/main/res/drawable/ic_task_delete.xml | 5 +++ app-common/src/main/res/values/strings.xml | 1 + 4 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 app-common/src/main/res/drawable/ic_task_delete.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index c004c5f..6061a94 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -133,11 +133,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { * Launch a potentially blocking foreground task in this service's lifecycle context. * This function does not block, but returns a Flow that emits ForegroundTaskState * updates associated with this task. The last update the returned flow will emit is - * always ForegroundTaskState.Done. + * always ForegroundTaskState.Done. The returned flow MUST be started in order for the + * foreground task to run. * * The task closure is expected to update foregroundTaskState whenever appropriate. * If a foreground task is already running, this function returns null. * + * To wait for foreground tasks to be available, use waitForForegroundTask(). + * * The function will set the state back to Idle once it sees ForegroundTaskState.Done. */ private fun launchForegroundTask( @@ -262,4 +265,23 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { throw RuntimeException("Profile not renamed") } } + + fun launchProfileDeleteTask( + slotId: Int, + portId: Int, + iccid: String + ): Flow? = + launchForegroundTask( + getString(R.string.task_profile_delete), + R.drawable.ic_task_delete + ) { + euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { + euiccChannelManager.findEuiccChannelByPort( + slotId, + portId + )!!.lpa.deleteProfile(iccid) + + preferenceRepository.notificationDeleteFlow.first() + } + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 7586354..467132f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -3,16 +3,15 @@ package im.angry.openeuicc.ui import android.app.Dialog import android.os.Bundle import android.text.Editable -import android.util.Log import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import java.lang.Exception class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { companion object { @@ -67,23 +66,23 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false - lifecycleScope.launch { - try { - doDelete() - } catch (e: Exception) { - Log.d(ProfileDownloadFragment.TAG, "Error deleting profile") - Log.d(ProfileDownloadFragment.TAG, Log.getStackTraceString(e)) - } finally { + requireParentFragment().lifecycleScope.launch { + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() + + euiccChannelManagerService.launchProfileDeleteTask( + slotId, + portId, + requireArguments().getString("iccid")!! + )!!.onStart { if (parentFragment is EuiccProfilesChangedListener) { + // Trigger a refresh in the parent fragment -- it should wait until + // any foreground task is completed before actually doing a refresh (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() } + dismiss() - } + }.collect() } } - - private suspend fun doDelete() = beginTrackedOperation { - channel.lpa.deleteProfile(requireArguments().getString("iccid")!!) - preferenceRepository.notificationDeleteFlow.first() - } } \ No newline at end of file diff --git a/app-common/src/main/res/drawable/ic_task_delete.xml b/app-common/src/main/res/drawable/ic_task_delete.xml new file mode 100644 index 0000000..883bcaa --- /dev/null +++ b/app-common/src/main/res/drawable/ic_task_delete.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index bedfe21..cf7ce59 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -36,6 +36,7 @@ Long-running Tasks Downloading eSIM profile Renaming eSIM profile + Deleting eSIM profile New eSIM Server (RSP / SM-DP+) From 48b5f8ce06ac75f664e81374d2bda002a824c1ba Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 16:54:49 -0400 Subject: [PATCH 030/342] Impose timeout on waiting for foreground start --- .../service/EuiccChannelManagerService.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 6061a94..950247c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull import net.typeblog.lpac_jni.ProfileDownloadCallback /** @@ -161,7 +163,19 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { lifecycleScope.launch(Dispatchers.Main) { // Wait until our self-start command has succeeded. // We can only call startForeground() after that - foregroundStarted.first() + val res = withTimeoutOrNull(30 * 1000) { + foregroundStarted.first() + } + + if (res == null) { + // The only case where the wait above could time out is if the subscriber + // to the flow is stuck. Or we failed to start foreground. + // In that case, we should just set our state back to Idle -- setting it + // to Done wouldn't help much because nothing is going to then set it Idle. + foregroundTaskState.value = ForegroundTaskState.Idle + return@launch + } + updateForegroundNotification(title, iconRes) try { From 653123939c53e9c9d9261e2073810f4c9c251264 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 17:07:15 -0400 Subject: [PATCH 031/342] Set notifications to ongoing --- .../im/angry/openeuicc/service/EuiccChannelManagerService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 950247c..212194f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import net.typeblog.lpac_jni.ProfileDownloadCallback @@ -119,6 +118,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { .setProgress(100, state.progress, state.progress == 0) .setSmallIcon(iconRes) .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) .build() if (state.progress == 0) { From 2721f9127723d3d123109f452e6cb368243f5283 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 17:36:58 -0400 Subject: [PATCH 032/342] Make foreground notifications much more reliable --- .../service/EuiccChannelManagerService.kt | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 212194f..6bef221 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.coroutines.yield import net.typeblog.lpac_jni.ProfileDownloadCallback /** @@ -102,7 +103,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } } - private fun updateForegroundNotification(title: String, iconRes: Int) { + private suspend fun updateForegroundNotification(title: String, iconRes: Int) { val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) .setName(getString(R.string.task_notification)) @@ -126,6 +127,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } else if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { NotificationManagerCompat.from(this).notify(FOREGROUND_ID, notification) } + + // Yield out so that the main looper can handle the notification event + // Without this yield, the notification sent above will not be shown in time. + yield() } else { stopForeground(STOP_FOREGROUND_REMOVE) } @@ -182,14 +187,13 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { withContext(Dispatchers.IO) { this@EuiccChannelManagerService.task() } + // This update will be sent by the subscriber (as shown below) foregroundTaskState.value = ForegroundTaskState.Done(null) } catch (t: Throwable) { foregroundTaskState.value = ForegroundTaskState.Done(t) } finally { stopSelf() } - - updateForegroundNotification(title, iconRes) } // We should be the only task running, so we can subscribe to foregroundTaskState @@ -199,20 +203,26 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // it has been completed by that point. return foregroundTaskState.transformWhile { // Also update our notification when we see an update - withContext(Dispatchers.Main) { - updateForegroundNotification(title, iconRes) + // But ignore the first progress = 0 update -- that is the current value. + // we need that to be handled by the main coroutine after it finishes. + if (it !is ForegroundTaskState.InProgress || it.progress != 0) { + withContext(Dispatchers.Main) { + updateForegroundNotification(title, iconRes) + } } emit(it) it !is ForegroundTaskState.Done }.onStart { // When this Flow is started, we unblock the coroutine launched above by // self-starting as a foreground service. - startForegroundService( - Intent( - this@EuiccChannelManagerService, - this@EuiccChannelManagerService::class.java + withContext(Dispatchers.Main) { + startForegroundService( + Intent( + this@EuiccChannelManagerService, + this@EuiccChannelManagerService::class.java + ) ) - ) + } }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } } From 8573834a0349975e87c2328930208f0a55f1523b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 17:39:32 -0400 Subject: [PATCH 033/342] Improve download progress bar UI --- .../main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index c61fe3a..b7ad5ee 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -245,11 +245,12 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), confirmationCode, imei )!!.onEach { - progress.isIndeterminate = false if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) { progress.progress = it.progress + progress.isIndeterminate = it.progress == 0 } else { progress.progress = 100 + progress.isIndeterminate = false } }.last() From 79f43e2fda94b3ebbb463594233c3ff978a752e8 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 17:40:58 -0400 Subject: [PATCH 034/342] Set notification to alert only once --- .../im/angry/openeuicc/service/EuiccChannelManagerService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 6bef221..98a8722 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -120,6 +120,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { .setSmallIcon(iconRes) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) + .setOnlyAlertOnce(true) .build() if (state.progress == 0) { From 479e0ff34ade33af57204964aa0e6596202de99c Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 21:44:32 -0400 Subject: [PATCH 035/342] Move profile switching to use the new foreground task flow --- .../service/EuiccChannelManagerService.kt | 54 +++++++++ .../openeuicc/ui/EuiccManagementFragment.kt | 107 +++++++++--------- .../src/main/res/drawable/ic_task_switch.xml | 5 + app-common/src/main/res/values/strings.xml | 1 + 4 files changed, 113 insertions(+), 54 deletions(-) create mode 100644 app-common/src/main/res/drawable/ic_task_switch.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 98a8722..788a251 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -13,6 +13,7 @@ import im.angry.openeuicc.common.R import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -309,4 +310,57 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { preferenceRepository.notificationDeleteFlow.first() } } + + class SwitchingProfilesRefreshException : Exception() + + fun launchProfileSwitchTask( + slotId: Int, + portId: Int, + iccid: String, + enable: Boolean, // Enable or disable the profile indicated in iccid + reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers + ): Flow? = + launchForegroundTask( + getString(R.string.task_profile_switch), + R.drawable.ic_task_switch + ) { + euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { + val channel = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! + val (res, refreshed) = + if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) { + // Sometimes, we *can* enable or disable the profile, but we cannot + // send the refresh command to the modem because the profile somehow + // makes the modem "busy". In this case, we can still switch by setting + // refresh to false, but then the switch cannot take effect until the + // user resets the modem manually by toggling airplane mode or rebooting. + Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false) + } else { + Pair(true, true) + } + + if (!res) { + throw RuntimeException("Could not switch profile") + } + + if (!refreshed) { + // We may have switched the profile, but we could not refresh. Tell the caller about this + throw SwitchingProfilesRefreshException() + } + + if (reconnectTimeoutMillis > 0) { + // Add an unconditional delay first to account for any race condition between + // the card sending the refresh command and the modem actually refreshing + delay(reconnectTimeoutMillis / 10) + + // This throws TimeoutCancellationException if timed out + euiccChannelManager.waitForReconnect( + slotId, + portId, + reconnectTimeoutMillis / 10 * 9 + ) + } + + preferenceRepository.notificationSwitchFlow.first() + } + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 14d958d..da29123 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -30,12 +30,12 @@ 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.core.EuiccChannelManager +import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -176,67 +176,47 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, } } + private suspend fun showSwitchFailureText() = withContext(Dispatchers.Main) { + Toast.makeText( + context, + R.string.toast_profile_enable_failed, + Toast.LENGTH_LONG + ).show() + } + private fun enableOrDisableProfile(iccid: String, enable: Boolean) { swipeRefresh.isRefreshing = true fab.isEnabled = false lifecycleScope.launch { - beginTrackedOperation { - val (res, refreshed) = - if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) { - // Sometimes, we *can* enable or disable the profile, but we cannot - // send the refresh command to the modem because the profile somehow - // makes the modem "busy". In this case, we can still switch by setting - // refresh to false, but then the switch cannot take effect until the - // user resets the modem manually by toggling airplane mode or rebooting. - Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false) - } else { - Pair(true, true) - } + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() - if (!res) { - Log.d(TAG, "Failed to enable / disable profile $iccid") - withContext(Dispatchers.Main) { - Toast.makeText( - context, - R.string.toast_profile_enable_failed, - Toast.LENGTH_LONG - ).show() - } - return@beginTrackedOperation false + val res = euiccChannelManagerService.launchProfileSwitchTask( + slotId, + portId, + iccid, + enable, + reconnectTimeoutMillis = if (isUsb) { + 0 + } else { + 30 * 1000 } + )?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done - if (!refreshed && !isUsb) { - withContext(Dispatchers.Main) { - AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.switch_did_not_refresh) - setPositiveButton(android.R.string.ok) { dialog, _ -> - dialog.dismiss() - requireActivity().finish() - } - setOnDismissListener { _ -> - requireActivity().finish() - } - show() - } - } - return@beginTrackedOperation true - } + if (res == null) { + showSwitchFailureText() + return@launch + } - if (!isUsb) { - try { - euiccChannelManager.waitForReconnect( - slotId, - portId, - timeoutMillis = 30 * 1000 - ) - } catch (e: TimeoutCancellationException) { + when (res.error) { + null -> {} + is EuiccChannelManagerService.SwitchingProfilesRefreshException -> { + // This is only really fatal for internal eSIMs + if (!isUsb) { withContext(Dispatchers.Main) { - // Prevent this Fragment from being used again - invalid = true - // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.enable_disable_timeout) + setMessage(R.string.switch_did_not_refresh) setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() requireActivity().finish() @@ -247,12 +227,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, show() } } - return@beginTrackedOperation false } } - preferenceRepository.notificationSwitchFlow.first() + is TimeoutCancellationException -> { + withContext(Dispatchers.Main) { + // Prevent this Fragment from being used again + invalid = true + // Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid + AlertDialog.Builder(requireContext()).apply { + setMessage(R.string.enable_disable_timeout) + setPositiveButton(android.R.string.ok) { dialog, _ -> + dialog.dismiss() + requireActivity().finish() + } + setOnDismissListener { _ -> + requireActivity().finish() + } + show() + } + } + } + + else -> showSwitchFailureText() } + refresh() fab.isEnabled = true } diff --git a/app-common/src/main/res/drawable/ic_task_switch.xml b/app-common/src/main/res/drawable/ic_task_switch.xml new file mode 100644 index 0000000..86504d0 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_task_switch.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index cf7ce59..34fda1c 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ Downloading eSIM profile Renaming eSIM profile Deleting eSIM profile + Switching eSIM profile New eSIM Server (RSP / SM-DP+) From 7661b4b84f2c21f52ac13069a31e7e12b687de6b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 21:45:55 -0400 Subject: [PATCH 036/342] Output any foreground task error to Log --- .../im/angry/openeuicc/service/EuiccChannelManagerService.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 788a251..841458d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Binder import android.os.IBinder +import android.util.Log import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -49,6 +50,7 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback */ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { companion object { + private const val TAG = "EuiccChannelManagerService" private const val CHANNEL_ID = "tasks" private const val FOREGROUND_ID = 1000 } @@ -192,6 +194,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // This update will be sent by the subscriber (as shown below) foregroundTaskState.value = ForegroundTaskState.Done(null) } catch (t: Throwable) { + Log.e(TAG, "Foreground task encountered an error") + Log.e(TAG, Log.getStackTraceString(t)) foregroundTaskState.value = ForegroundTaskState.Done(t) } finally { stopSelf() From 54b4f61fd7e5560bd49578640ea25bd3943fa3a5 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 29 Sep 2024 21:50:47 -0400 Subject: [PATCH 037/342] Improve notification channel creation --- .../service/EuiccChannelManagerService.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 841458d..fa22dc1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -107,12 +107,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } private suspend fun updateForegroundNotification(title: String, iconRes: Int) { - val channel = - NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) - .setName(getString(R.string.task_notification)) - .setVibrationEnabled(false) - .build() - NotificationManagerCompat.from(this).createNotificationChannel(channel) + val nm = NotificationManagerCompat.from(this) + if (nm.getNotificationChannelCompat(CHANNEL_ID) == null) { + val channel = + NotificationChannelCompat.Builder( + CHANNEL_ID, + NotificationManagerCompat.IMPORTANCE_LOW + ) + .setName(getString(R.string.task_notification)) + .setVibrationEnabled(false) + .build() + nm.createNotificationChannel(channel) + } val state = foregroundTaskState.value @@ -129,7 +135,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { if (state.progress == 0) { startForeground(FOREGROUND_ID, notification) } else if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - NotificationManagerCompat.from(this).notify(FOREGROUND_ID, notification) + nm.notify(FOREGROUND_ID, notification) } // Yield out so that the main looper can handle the notification event From 42942c2816225880f797ab0f6435d76a1157f222 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 30 Sep 2024 19:49:24 -0400 Subject: [PATCH 038/342] ui: Handle navbar insets properly --- .../main/java/im/angry/openeuicc/ui/SettingsFragment.kt | 9 +++------ .../src/main/java/im/angry/openeuicc/util/UiUtils.kt | 7 ++++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index 841b8bd..2c99641 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -49,12 +49,9 @@ class SettingsFragment: PreferenceFragmentCompat() { ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return super.onCreateView(inflater, container, savedInstanceState).also(::setupRootViewInsets) + override fun onStart() { + super.onStart() + setupRootViewInsets(requireView().requireViewById(androidx.preference.R.id.recycler_view)) } private fun CheckBoxPreference.bindBooleanFlow(flow: Flow, key: Preferences.Key) { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt index b54b494..fbede87 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt @@ -56,15 +56,16 @@ fun AppCompatActivity.setupToolbarInsets() { } } -fun setupRootViewInsets(view: View) { +fun setupRootViewInsets(view: ViewGroup) { + // Disable clipToPadding to make sure content actually display + view.clipToPadding = false ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> val bars = insets.getInsets( WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() ) - // Don't set padding bottom because we do want scrolling root views to extend into nav bar - v.updatePadding(bars.left, v.paddingTop, bars.right, v.paddingBottom) + v.updatePadding(bars.left, v.paddingTop, bars.right, bars.bottom) WindowInsetsCompat.CONSUMED } From 165f685abb91a861b4ce14cecfaeb498121ab811 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 30 Sep 2024 19:53:25 -0400 Subject: [PATCH 039/342] Remove unused beginTrackedOperation in UI --- .../angry/openeuicc/util/EuiccChannelFragmentUtils.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index f6f20d4..e92be40 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -6,8 +6,6 @@ import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.ui.BaseEuiccAccessActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext interface EuiccChannelFragmentMarker: OpenEuiccContextMarker @@ -47,12 +45,4 @@ suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannel interface EuiccProfilesChangedListener { fun onEuiccProfilesChanged() -} - -suspend fun T.beginTrackedOperation(op: suspend () -> Boolean) where T: Fragment, T: EuiccChannelFragmentMarker { - withContext(Dispatchers.IO) { - euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { - op() - } - } } \ No newline at end of file From 6b71a746a4b30ca50221d68887446b839a81f3e3 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 9 Oct 2024 18:10:14 -0400 Subject: [PATCH 040/342] Add monochrome icons for v31+ --- app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher.xml | 6 ++++++ .../src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml | 6 ++++++ app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml | 6 ++++++ app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher.xml create mode 100644 app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml diff --git a/app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher.xml b/app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher.xml new file mode 100644 index 0000000..50ec886 --- /dev/null +++ b/app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml b/app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml new file mode 100644 index 0000000..50ec886 --- /dev/null +++ b/app-unpriv/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml new file mode 100644 index 0000000..50ec886 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml new file mode 100644 index 0000000..50ec886 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From ff266a4a9bb041f5a8085e633b53c989a3e1ac6a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 9 Oct 2024 18:13:51 -0400 Subject: [PATCH 041/342] Un-confusing-ify the "safeguards" description --- app-common/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 34fda1c..4b3f9ae 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -80,7 +80,7 @@ Switching Send notifications for switching profiles\nNote that this type of notification is unreliable. Advanced - Disable Safeguards for Removable eSIMs + Allow Deleting the Last Profile By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may sometimes render it inaccessible.\nCheck this box to remove this safeguard. Logs View recent debug logs of the application From 5c8bbeb217e5532ac27ebfd4f191901ca282994d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 9 Oct 2024 18:14:54 -0400 Subject: [PATCH 042/342] Fix removable eSIM safeguards description again --- app-common/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 4b3f9ae..b3a6eb8 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -80,7 +80,7 @@ Switching Send notifications for switching profiles\nNote that this type of notification is unreliable. Advanced - Allow Deleting the Last Profile + Allow Disabling / Deleting Active Profile By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may sometimes render it inaccessible.\nCheck this box to remove this safeguard. Logs View recent debug logs of the application From 290bdca75a34a0e5f9d4b3123ce9e3ab1d508d3b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 9 Oct 2024 20:20:32 -0400 Subject: [PATCH 043/342] feat: Introduce option for global verbose logging --- .../core/DefaultEuiccChannelFactory.kt | 18 +++++++++++-- .../im/angry/openeuicc/core/EuiccChannel.kt | 5 +++- .../openeuicc/core/OmapiApduInterface.kt | 15 ++++++++--- .../openeuicc/core/usb/UsbApduInterface.kt | 6 +++-- .../openeuicc/core/usb/UsbCcidTransceiver.kt | 10 +++++-- .../im/angry/openeuicc/ui/MainActivity.kt | 5 +++- .../openeuicc/ui/ProfileDownloadFragment.kt | 1 - .../im/angry/openeuicc/ui/SettingsFragment.kt | 3 +++ .../angry/openeuicc/util/PreferenceUtils.kt | 4 +++ app-common/src/main/res/values/strings.xml | 2 ++ app-common/src/main/res/xml/pref_settings.xml | 6 +++++ .../core/PrivilegedEuiccChannelFactory.kt | 10 ++++++- .../core/TelephonyManagerApduInterface.kt | 27 +++++++++++++++++-- .../lpac_jni/impl/HttpInterfaceImpl.kt | 20 ++++++++++++-- 14 files changed, 115 insertions(+), 17 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index e653a07..ae984b3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -33,7 +33,15 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}") try { - return EuiccChannel(port, OmapiApduInterface(seService!!, port)) + return EuiccChannel( + port, + OmapiApduInterface( + seService!!, + port, + context.preferenceRepository.verboseLoggingFlow + ), + context.preferenceRepository.verboseLoggingFlow + ) } catch (e: IllegalArgumentException) { // Failed Log.w( @@ -52,7 +60,13 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha if (!conn.claimInterface(usbInterface, true)) return null return EuiccChannel( FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), - UsbApduInterface(conn, bulkIn, bulkOut) + UsbApduInterface( + conn, + bulkIn, + bulkOut, + context.preferenceRepository.verboseLoggingFlow + ), + context.preferenceRepository.verboseLoggingFlow ) } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index 35cf0ad..e106535 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -1,6 +1,7 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.Flow import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.impl.HttpInterfaceImpl @@ -9,12 +10,14 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl class EuiccChannel( val port: UiccPortInfoCompat, apduInterface: ApduInterface, + verboseLoggingFlow: Flow ) { val slotId = port.card.physicalSlotIndex // PHYSICAL slot val logicalSlotId = port.logicalSlotIndex val portId = port.portIndex - val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl()) + val lpa: LocalProfileAssistant = + LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow)) val valid: Boolean get() = lpa.valid diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index 711658a..f71c876 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -5,11 +5,16 @@ import android.se.omapi.SEService import android.se.omapi.Session import android.util.Log import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.ApduInterface class OmapiApduInterface( private val service: SEService, - private val port: UiccPortInfoCompat + private val port: UiccPortInfoCompat, + private val verboseLoggingFlow: Flow ): ApduInterface { companion object { const val TAG = "OmapiApduInterface" @@ -49,11 +54,15 @@ class OmapiApduInterface( "Unknown channel" } - Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}") + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}") + } try { return lastChannel.transmit(tx).also { - Log.d(TAG, "OMAPI APDU response: ${it.encodeHex()}") + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d(TAG, "OMAPI APDU response: ${it.encodeHex()}") + } } } catch (e: Exception) { Log.e(TAG, "OMAPI APDU exception") diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt index fb93ff6..9894343 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbApduInterface.kt @@ -4,12 +4,14 @@ import android.hardware.usb.UsbDeviceConnection import android.hardware.usb.UsbEndpoint import android.util.Log import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.Flow import net.typeblog.lpac_jni.ApduInterface class UsbApduInterface( private val conn: UsbDeviceConnection, private val bulkIn: UsbEndpoint, - private val bulkOut: UsbEndpoint + private val bulkOut: UsbEndpoint, + private val verboseLoggingFlow: Flow ): ApduInterface { companion object { private const val TAG = "UsbApduInterface" @@ -27,7 +29,7 @@ class UsbApduInterface( throw IllegalArgumentException("Unsupported card reader; T=0 support is required") } - transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription) + transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow) try { transceiver.iccPowerOn() diff --git a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt index 09d324b..5ef35af 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/usb/UsbCcidTransceiver.kt @@ -5,6 +5,9 @@ import android.hardware.usb.UsbEndpoint import android.os.SystemClock import android.util.Log import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import java.nio.ByteBuffer import java.nio.ByteOrder @@ -18,7 +21,8 @@ class UsbCcidTransceiver( private val usbConnection: UsbDeviceConnection, private val usbBulkIn: UsbEndpoint, private val usbBulkOut: UsbEndpoint, - private val usbCcidDescription: UsbCcidDescription + private val usbCcidDescription: UsbCcidDescription, + private val verboseLoggingFlow: Flow ) { companion object { private const val TAG = "UsbCcidTransceiver" @@ -178,7 +182,9 @@ class UsbCcidTransceiver( readBytes = usbConnection.bulkTransfer( usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS ) - Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex()) + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex()) + } } while (readBytes <= 0 && attempts-- > 0) if (readBytes < CCID_HEADER_LENGTH) { throw UsbTransportException("USB-CCID error - failed to receive CCID header") diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt index 17c99ff..e432f6c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt @@ -25,6 +25,7 @@ import com.google.android.material.tabs.TabLayoutMediator import im.angry.openeuicc.common.R import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -140,7 +141,9 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { val knownChannels = withContext(Dispatchers.IO) { euiccChannelManager.enumerateEuiccChannels().onEach { Log.d(TAG, "slot ${it.slotId} port ${it.portId}") - Log.d(TAG, it.lpa.eID) + if (preferenceRepository.verboseLoggingFlow.first()) { + 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 diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index b7ad5ee..64d64c4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -56,7 +56,6 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> result.contents?.let { content -> - Log.d(TAG, content) onScanResult(content) } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt index 2c99641..5ed4348 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt @@ -47,6 +47,9 @@ class SettingsFragment: PreferenceFragmentCompat() { findPreference("pref_advanced_disable_safeguard_removable_esim") ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM) + + findPreference("pref_advanced_verbose_logging") + ?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING) } override fun onStart() { diff --git a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt index 5315340..262482a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt @@ -24,6 +24,7 @@ object PreferenceKeys { val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete") val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch") val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim") + val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging") } class PreferenceRepository(context: Context) { @@ -44,6 +45,9 @@ class PreferenceRepository(context: Context) { val disableSafeguardFlow: Flow = dataStore.data.map { it[PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM] ?: false } + val verboseLoggingFlow: Flow = + dataStore.data.map { it[PreferenceKeys.VERBOSE_LOGGING] ?: false } + suspend fun updatePreference(key: Preferences.Key, value: T) { dataStore.edit { it[key] = value diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index b3a6eb8..30d2fbe 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -82,6 +82,8 @@ Advanced Allow Disabling / Deleting Active Profile By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may sometimes render it inaccessible.\nCheck this box to remove this safeguard. + Verbose Logging + Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on. Logs View recent debug logs of the application Info diff --git a/app-common/src/main/res/xml/pref_settings.xml b/app-common/src/main/res/xml/pref_settings.xml index db348fc..53395ed 100644 --- a/app-common/src/main/res/xml/pref_settings.xml +++ b/app-common/src/main/res/xml/pref_settings.xml @@ -30,6 +30,12 @@ app:title="@string/pref_advanced_disable_safeguard_removable_esim" app:summary="@string/pref_advanced_disable_safeguard_removable_esim_desc" /> + + ): ApduInterface { + companion object { + const val TAG = "TelephonyManagerApduInterface" + } + private var lastChannel: Int = -1 override val valid: Boolean @@ -45,6 +54,10 @@ class TelephonyManagerApduInterface( override fun transmit(tx: ByteArray): ByteArray { check(lastChannel != -1) { "Uninitialized" } + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}") + } + val cla = tx[0].toUByte().toInt() val instruction = tx[1].toUByte().toInt() val p1 = tx[2].toUByte().toInt() @@ -53,7 +66,17 @@ class TelephonyManagerApduInterface( val p4 = tx.drop(5).toByteArray().encodeHex() return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel, - cla, instruction, p1, p2, p3, p4)?.decodeHex() ?: byteArrayOf() + cla, + instruction, + p1, + p2, + p3, + p4 + ).also { + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d(TAG, "TelephonyManager APDU response: $it") + } + }?.decodeHex() ?: byteArrayOf() } } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt index 8193ac3..dd72ab3 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt @@ -1,6 +1,9 @@ package net.typeblog.lpac_jni.impl import android.util.Log +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import net.typeblog.lpac_jni.HttpInterface import java.net.URL import java.security.SecureRandom @@ -9,7 +12,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory -class HttpInterfaceImpl: HttpInterface { +class HttpInterfaceImpl(private val verboseLoggingFlow: Flow) : HttpInterface { companion object { private const val TAG = "HttpInterfaceImpl" } @@ -23,6 +26,10 @@ class HttpInterfaceImpl: HttpInterface { ): HttpInterface.HttpResponse { Log.d(TAG, "transmit(url = $url)") + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d(TAG, "HTTP tx = ${tx.decodeToString(throwOnInvalidSequence = false)}") + } + val parsedUrl = URL(url) if (parsedUrl.protocol != "https") { throw IllegalArgumentException("SM-DP+ servers must use the HTTPS protocol") @@ -56,7 +63,16 @@ class HttpInterfaceImpl: HttpInterface { Log.d(TAG, "transmit responseCode = ${conn.responseCode}") - return HttpInterface.HttpResponse(conn.responseCode, conn.inputStream.readBytes()) + val bytes = conn.inputStream.readBytes().also { + if (runBlocking { verboseLoggingFlow.first() }) { + Log.d( + TAG, + "HTTP response body = ${it.decodeToString(throwOnInvalidSequence = false)}" + ) + } + } + + return HttpInterface.HttpResponse(conn.responseCode, bytes) } catch (e: Exception) { e.printStackTrace() throw e From 69e63b0a8b97647b34241d4d348beab4dd4ce11f Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 9 Oct 2024 21:46:49 -0400 Subject: [PATCH 044/342] Stop using runBlocking inside suspend funs --- .../java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index b943820..55fb53d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -173,7 +173,7 @@ open class DefaultEuiccChannelManager( try { // tryOpenEuiccChannel() will automatically dispose of invalid channels // and recreate when needed - val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId)!! + val channel = findEuiccChannelByPort(physicalSlotId, portId)!! check(channel.valid) { "Invalid channel" } break } catch (e: Exception) { From ddc421dae7ebc5de421e030a854376760d1b672b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 9 Oct 2024 22:45:09 -0400 Subject: [PATCH 045/342] omapi: Retry on 0x6601 checksum errors Some removable eUICCs don't play nicely with some modems. When they emit 0x6601 checksum errors, retry at least a few times before giving up. This fixes support for eSTK.me / JMP eSIM Adapters on Pixel 7 Pro. --- .../im/angry/openeuicc/core/OmapiApduInterface.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt index f71c876..b63f343 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/OmapiApduInterface.kt @@ -59,16 +59,25 @@ class OmapiApduInterface( } try { - return lastChannel.transmit(tx).also { + for (i in 0..10) { + val res = lastChannel.transmit(tx) if (runBlocking { verboseLoggingFlow.first() }) { - Log.d(TAG, "OMAPI APDU response: ${it.encodeHex()}") + Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}") } + + if (res.size == 2 && res[0] == 0x66.toByte() && res[1] == 0x01.toByte()) { + Log.d(TAG, "Received checksum error 0x6601, retrying (count = $i)") + continue + } + + return res } + + throw RuntimeException("Retransmit attempts exhausted; this was likely caused by checksum errors") } catch (e: Exception) { Log.e(TAG, "OMAPI APDU exception") e.printStackTrace() throw e } } - } \ No newline at end of file From 19dc215b3f78617172e1d71fad9d44960711ae14 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Thu, 10 Oct 2024 21:52:57 -0400 Subject: [PATCH 046/342] lpac-jni: Update lpac --- libs/lpac-jni/src/main/jni/lpac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/jni/lpac b/libs/lpac-jni/src/main/jni/lpac index 0011ea6..a5a0516 160000 --- a/libs/lpac-jni/src/main/jni/lpac +++ b/libs/lpac-jni/src/main/jni/lpac @@ -1 +1 @@ -Subproject commit 0011ea6cc4c045c84f7aac839c1cce7804422355 +Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef From d3a04b94a97eb2b81a864cc120bba3b100334308 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Thu, 10 Oct 2024 22:08:32 -0400 Subject: [PATCH 047/342] Set MSS on es10x commands to 60 for OMAPI channels This seems to help a LOT with 6601 checksum errors on phones like Pixel 7 Pro. I am seeing virtually none with MSS = 60. --- .../im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt | 5 ++++- .../java/net/typeblog/lpac_jni/LocalProfileAssistant.kt | 7 +++++++ .../src/main/java/net/typeblog/lpac_jni/LpacJni.kt | 1 + .../typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt | 4 ++++ libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c | 7 +++++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index ae984b3..7f3abb0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -41,7 +41,10 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha context.preferenceRepository.verboseLoggingFlow ), context.preferenceRepository.verboseLoggingFlow - ) + ).also { + Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60") + it.lpa.setEs10xMss(60) + } } catch (e: IllegalArgumentException) { // Failed Log.w( diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index 7e0a31b..f256caf 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -8,6 +8,13 @@ interface LocalProfileAssistant { // Extended EuiccInfo for use with LUIs, containing information such as firmware version val euiccInfo2: EuiccInfo2? + /** + * Set the max segment size (mss) for all es10x commands. This can help with removable + * eUICCs that may run at a baud rate too fast for the modem. + * By default, this is set to 60 by libeuicc. + */ + fun setEs10xMss(mss: Byte) + // All blocking functions in this class assume that they are executed on non-Main threads // The IO context in Kotlin's coroutine library is recommended. fun enableProfile(iccid: String, refresh: Boolean = true): Boolean diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 32fbb0a..f9a2f90 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -9,6 +9,7 @@ internal object LpacJni { external fun destroyContext(handle: Long) external fun euiccInit(handle: Long): Int + external fun euiccSetMss(handle: Long, mss: Byte) external fun euiccFini(handle: Long) // es10c diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 6b3055d..51039cd 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -30,6 +30,10 @@ class LocalProfileAssistantImpl( httpInterface.usePublicKeyIds(pkids) } + override fun setEs10xMss(mss: Byte) { + LpacJni.euiccSetMss(contextHandle, mss) + } + override val valid: Boolean get() = !finalized && apduInterface.valid && try { // If we can read both eID and euiccInfo2 properly, we are likely looking at diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 1721b77..f081f8e 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -75,6 +75,13 @@ Java_net_typeblog_lpac_1jni_LpacJni_euiccFini(JNIEnv *env, jobject thiz, jlong h euicc_fini(ctx); } +JNIEXPORT void JNICALL +Java_net_typeblog_lpac_1jni_LpacJni_euiccSetMss(JNIEnv *env, jobject thiz, jlong handle, + jbyte mss) { + struct euicc_ctx *ctx = (struct euicc_ctx *) handle; + ctx->es10x_mss = (uint8_t) mss; +} + jstring toJString(JNIEnv *env, const char *pat) { jbyteArray bytes = NULL; jstring encoding = NULL; From 84dd16c16923f5cad93d4ec7bb38b9f3d0ff2337 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 12 Oct 2024 10:39:48 -0400 Subject: [PATCH 048/342] Enable predicative back gestures --- app-common/src/main/AndroidManifest.xml | 4 +++- .../src/main/java/im/angry/openeuicc/ui/LogsActivity.kt | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/AndroidManifest.xml b/app-common/src/main/AndroidManifest.xml index a59110b..aa301cd 100644 --- a/app-common/src/main/AndroidManifest.xml +++ b/app-common/src/main/AndroidManifest.xml @@ -8,7 +8,9 @@ - + diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt index e1c9ea1..49bfa0f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt @@ -71,6 +71,10 @@ class LogsActivity : AppCompatActivity() { } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + finish() + true + } R.id.save -> { saveLogs.launch(getString(R.string.logs_filename_template, SimpleDateFormat.getDateTimeInstance().format(Date()) From eab96dde0558ff45d577dfa3c06c1421133a3ac4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 12 Oct 2024 10:49:47 -0400 Subject: [PATCH 049/342] ui: Reduce blocking operations on transition --- .../angry/openeuicc/ui/NotificationsActivity.kt | 7 ++++--- .../angry/openeuicc/ui/ProfileDownloadFragment.kt | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index 01de1fc..d7a7b66 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -50,8 +50,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } override fun onInit() { + val logicalSlotId = intent.getIntExtra("logicalSlotId", 0) euiccChannel = euiccChannelManager - .findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!! + .findEuiccChannelBySlotBlocking(logicalSlotId)!! notificationList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) @@ -61,10 +62,10 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { // This is slightly different from the MainActivity logic // due to the length (we don't want to display the full USB product name) - val channelTitle = if (euiccChannel.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { + val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { getString(R.string.usb) } else { - getString(R.string.channel_name_format, euiccChannel.logicalSlotId) + getString(R.string.channel_name_format, logicalSlotId) } title = getString(R.string.profile_notifications_detailed_format, channelTitle) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 64d64c4..8d6f99a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -149,13 +149,6 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), @SuppressLint("MissingPermission") override fun onStart() { super.onStart() - profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable( - try { - telephonyManager.getImei(channel.logicalSlotId) ?: "" - } catch (e: Exception) { - "" - } - ) lifecycleScope.launch(Dispatchers.IO) { ensureEuiccChannelManager() @@ -166,6 +159,12 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), return@launch } + val imei = try { + telephonyManager.getImei(channel.logicalSlotId) ?: "" + } catch (e: Exception) { + "" + } + // Fetch remaining NVRAM val str = channel.lpa.euiccInfo2?.freeNvram?.also { freeNvram = it @@ -174,6 +173,8 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), withContext(Dispatchers.Main) { profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, str ?: getText(R.string.unknown)) + profileDownloadIMEI.editText!!.text = + Editable.Factory.getInstance().newEditable(imei) } } } From 16b6aceedf90fecdca944de85ceafda566eef448 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 12 Oct 2024 11:09:16 -0400 Subject: [PATCH 050/342] ui: Remove more blocking operations in NotificationsActivity --- .../openeuicc/ui/NotificationsActivity.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt index d7a7b66..884e223 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt @@ -50,16 +50,14 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { } override fun onInit() { - val logicalSlotId = intent.getIntExtra("logicalSlotId", 0) - euiccChannel = euiccChannelManager - .findEuiccChannelBySlotBlocking(logicalSlotId)!! - notificationList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) notificationList.adapter = notificationAdapter registerForContextMenu(notificationList) + val logicalSlotId = intent.getIntExtra("logicalSlotId", 0) + // This is slightly different from the MainActivity logic // due to the length (we don't want to display the full USB product name) val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { @@ -106,6 +104,18 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker { swipeRefresh.isRefreshing = true lifecycleScope.launch { + if (!this@NotificationsActivity::euiccChannel.isInitialized) { + withContext(Dispatchers.IO) { + euiccChannelManagerLoaded.await() + euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking( + intent.getIntExtra( + "logicalSlotId", + 0 + ) + )!! + } + } + task() swipeRefresh.isRefreshing = false From 349c8179b0ba83715ce41ad73e13d7376722a699 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 19 Oct 2024 19:09:35 -0400 Subject: [PATCH 051/342] Force foreground tasks to always complete (i.e. not cancelled) --- .../im/angry/openeuicc/service/EuiccChannelManagerService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index fa22dc1..0dbd4e7 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -14,6 +14,7 @@ import im.angry.openeuicc.common.R import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -194,7 +195,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { updateForegroundNotification(title, iconRes) try { - withContext(Dispatchers.IO) { + withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete this@EuiccChannelManagerService.task() } // This update will be sent by the subscriber (as shown below) From 4709b6994fa043b6a1981ec5e4c463d0d7ed98b9 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 19 Oct 2024 19:14:14 -0400 Subject: [PATCH 052/342] Don't call stopSelf() if the coroutine is cancelled --- .../im/angry/openeuicc/service/EuiccChannelManagerService.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 0dbd4e7..73c3896 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull @@ -205,7 +206,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { Log.e(TAG, Log.getStackTraceString(t)) foregroundTaskState.value = ForegroundTaskState.Done(t) } finally { - stopSelf() + if (isActive) { + stopSelf() + } } } From 7197501cca1d283c646afa2c3bb4dd859f9e723f Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 19 Oct 2024 21:13:07 -0400 Subject: [PATCH 053/342] ProfileDownloadFragment: Stop catching all exceptions ..instead, use the error value returned in the foreground task result. Catching all exceptions will result in cancellation being ignored when the fragment is destroyed. --- .../openeuicc/ui/ProfileDownloadFragment.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 8d6f99a..a45a1ea 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -215,18 +215,22 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - try { - doDownloadProfile(server, code, confirmationCode, imei) - } catch (e: Exception) { + val res = doDownloadProfile(server, code, confirmationCode, imei) + + if (res == null || res.error != null) { Log.d(TAG, "Error downloading profile") - Log.d(TAG, Log.getStackTraceString(e)) - Toast.makeText(context, R.string.profile_download_failed, Toast.LENGTH_LONG).show() - } finally { - if (parentFragment is EuiccProfilesChangedListener) { - (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + + if (res?.error != null) { + Log.d(TAG, Log.getStackTraceString(res.error)) } - dismiss() + + Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show() } + + if (parentFragment is EuiccProfilesChangedListener) { + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + } + dismiss() } } @@ -254,7 +258,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), } }.last() - (res as? EuiccChannelManagerService.ForegroundTaskState.Done)?.error?.let { throw it } + res as? EuiccChannelManagerService.ForegroundTaskState.Done } override fun onDismiss(dialog: DialogInterface) { From 2337ad035db31d87ed706aafbeb93f61dd96d570 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 20 Oct 2024 11:44:20 -0400 Subject: [PATCH 054/342] Post a notification to signify task failure from service --- .../service/EuiccChannelManagerService.kt | 29 ++++++++++++++++++- .../src/main/res/drawable/ic_x_black.xml | 5 ++++ app-common/src/main/res/values/strings.xml | 4 +++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 app-common/src/main/res/drawable/ic_x_black.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 73c3896..74845db 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -55,6 +55,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { private const val TAG = "EuiccChannelManagerService" private const val CHANNEL_ID = "tasks" private const val FOREGROUND_ID = 1000 + private const val TASK_FAILURE_ID = 1001 } inner class LocalBinder : Binder() { @@ -108,7 +109,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } } - private suspend fun updateForegroundNotification(title: String, iconRes: Int) { + private fun ensureForegroundTaskNotificationChannel() { val nm = NotificationManagerCompat.from(this) if (nm.getNotificationChannelCompat(CHANNEL_ID) == null) { val channel = @@ -121,7 +122,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { .build() nm.createNotificationChannel(channel) } + } + private suspend fun updateForegroundNotification(title: String, iconRes: Int) { + ensureForegroundTaskNotificationChannel() + + val nm = NotificationManagerCompat.from(this) val state = foregroundTaskState.value if (state is ForegroundTaskState.InProgress) { @@ -148,6 +154,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } } + private fun postForegroundTaskFailureNotification(title: String) { + if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + return + } + + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(title) + .setSmallIcon(R.drawable.ic_x_black) + .build() + NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification) + } + /** * Launch a potentially blocking foreground task in this service's lifecycle context. * This function does not block, but returns a Flow that emits ForegroundTaskState @@ -164,6 +182,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { */ private fun launchForegroundTask( title: String, + failureTitle: String, iconRes: Int, task: suspend EuiccChannelManagerService.() -> Unit ): Flow? { @@ -205,6 +224,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { Log.e(TAG, "Foreground task encountered an error") Log.e(TAG, Log.getStackTraceString(t)) foregroundTaskState.value = ForegroundTaskState.Done(t) + + if (isActive) { + postForegroundTaskFailureNotification(failureTitle) + } } finally { if (isActive) { stopSelf() @@ -260,6 +283,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ): Flow? = launchForegroundTask( getString(R.string.task_profile_download), + getString(R.string.task_profile_download_failure), R.drawable.ic_task_sim_card_download ) { euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { @@ -294,6 +318,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ): Flow? = launchForegroundTask( getString(R.string.task_profile_rename), + getString(R.string.task_profile_rename_failure), R.drawable.ic_task_rename ) { val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname( @@ -313,6 +338,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ): Flow? = launchForegroundTask( getString(R.string.task_profile_delete), + getString(R.string.task_profile_delete_failure), R.drawable.ic_task_delete ) { euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { @@ -336,6 +362,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ): Flow? = launchForegroundTask( getString(R.string.task_profile_switch), + getString(R.string.task_profile_switch_failure), R.drawable.ic_task_switch ) { euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { diff --git a/app-common/src/main/res/drawable/ic_x_black.xml b/app-common/src/main/res/drawable/ic_x_black.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_x_black.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 30d2fbe..0278ffa 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -35,9 +35,13 @@ Long-running Tasks Downloading eSIM profile + Failed to download eSIM profile Renaming eSIM profile + Failed to rename eSIM profile Deleting eSIM profile + Failed to delete eSIM profile Switching eSIM profile + Failed to switch eSIM profile New eSIM Server (RSP / SM-DP+) From aac457f4b5739cb8d6ef0251e5680fba7e312d00 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 20 Oct 2024 11:45:27 -0400 Subject: [PATCH 055/342] Don't dismiss DialogFragment's when in background They shall be dismissed automatically. Doing it here may cause IllegalStateException. Note that even if they are dismissed, the corresponding tasks will continue to be executed by EuiccChannelManagerService and will not be cancelled. --- .../java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt | 6 +++++- .../java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt | 7 ++++++- .../java/im/angry/openeuicc/ui/ProfileRenameFragment.kt | 6 +++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt index 467132f..901f263 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt @@ -81,7 +81,11 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() } - dismiss() + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } }.collect() } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index a45a1ea..843c9e5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -230,7 +230,12 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), if (parentFragment is EuiccProfilesChangedListener) { (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() } - dismiss() + + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index 84fd0cd..278ea43 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -106,7 +106,11 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() } - dismiss() + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } } } } \ No newline at end of file From d26a8ddc7867fcd941502a3f174ccb89ca74b84b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 20 Oct 2024 20:37:44 -0400 Subject: [PATCH 056/342] EuiccChannelManagerService: stop using blocking variants unnecessarily --- .../service/EuiccChannelManagerService.kt | 8 ++--- .../java/im/angry/openeuicc/util/LPAUtils.kt | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 74845db..8db3bbe 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -286,7 +286,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_download_failure), R.drawable.ic_task_sim_card_download ) { - euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { + euiccChannelManager.beginTrackedOperation(slotId, portId) { val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId) val res = channel!!.lpa.downloadProfile( smdp, @@ -341,7 +341,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_delete_failure), R.drawable.ic_task_delete ) { - euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { + euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.findEuiccChannelByPort( slotId, portId @@ -365,8 +365,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_switch_failure), R.drawable.ic_task_switch ) { - euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) { - val channel = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! + euiccChannelManager.beginTrackedOperation(slotId, portId) { + val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!! val (res, refreshed) = if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) { // Sometimes, we *can* enable or disable the profile, but we cannot diff --git a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt index 50b2077..e7a3322 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt @@ -73,6 +73,42 @@ fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean * should be the concern of op() itself, and this function assumes that when * op() returns, the slotId and portId will correspond to a valid channel again. */ +suspend inline fun EuiccChannelManager.beginTrackedOperation( + slotId: Int, + portId: Int, + op: () -> Boolean +) { + val latestSeq = + findEuiccChannelByPort(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber + ?: 0 + Log.d(TAG, "Latest notification is $latestSeq before operation") + if (op()) { + Log.d(TAG, "Operation has requested notification handling") + try { + // Note that the exact instance of "channel" might have changed here if reconnected; + // so we MUST use the automatic getter for "channel" + findEuiccChannelByPort( + slotId, + portId + )?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach { + Log.d(TAG, "Handling notification $it") + findEuiccChannelByPort( + slotId, + portId + )?.lpa?.handleNotification(it.seqNumber) + } + } catch (e: Exception) { + // Ignore any error during notification handling + e.printStackTrace() + } + } + Log.d(TAG, "Operation complete") +} + +/** + * Same as beginTrackedOperation but uses blocking primitives. + * TODO: This function needs to be phased out of use. + */ inline fun EuiccChannelManager.beginTrackedOperationBlocking( slotId: Int, portId: Int, From 65c9a7dc39bc88e23c9a6b8b4c7bd6091f30f5ac Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 23 Oct 2024 04:17:48 +0200 Subject: [PATCH 057/342] fix: refer url (#53) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/53 Co-authored-by: septs Co-committed-by: septs --- .../main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt index 13848b6..5167da5 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/RootCertificates.kt @@ -35,7 +35,7 @@ internal fun keyIdToKeystore(keyIds: Array): KeyStore { return ret } -// ref: +// ref: internal val KNOWN_CI_CERTS = hashMapOf( // GSM Association - RSP2 Root CI1 (CA: DigiCert) // Specs: SGP.21 and SGP.22 version 2 and version 3 From 7cb872a664eabc25ee9ccd176154aa2fae2d0bad Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 15:23:49 -0400 Subject: [PATCH 058/342] Add new withEuiccChannel() method to EuiccChannelManager --- .../angry/openeuicc/core/DefaultEuiccChannelManager.kt | 10 ++++++++++ .../im/angry/openeuicc/core/EuiccChannelManager.kt | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 55fb53d..af69c12 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -159,6 +159,16 @@ open class DefaultEuiccChannelManager( findEuiccChannelByPort(physicalSlotId, portId) } + override suspend fun withEuiccChannel( + physicalSlotId: Int, + portId: Int, + fn: (EuiccChannel) -> R + ): R { + val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId) + ?: throw EuiccChannelManager.EuiccChannelNotFoundException() + return fn(channel) + } + override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index b21ccf6..304269d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -64,6 +64,16 @@ interface EuiccChannelManager { suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? + class EuiccChannelNotFoundException: Exception("EuiccChannel not found") + + /** + * Find a EuiccChannel by its slot and port, then run a callback with a reference to it. + * The reference is not supposed to be held outside of the callback. + * + * If a channel for that slot / port is not found, EuiccChannelNotFoundException is thrown + */ + suspend fun withEuiccChannel(physicalSlotId: Int, portId: Int, fn: (EuiccChannel) -> R): R + /** * Invalidate all EuiccChannels previously cached by this Manager */ From d54fcf2589c4e94dc041b4d5d857a56a8e7dbd80 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 15:31:52 -0400 Subject: [PATCH 059/342] refactor: Make EuiccChannel abstract This allows wrapping to control reference lifetime outside of EuiccChannelManager. --- .../core/DefaultEuiccChannelFactory.kt | 4 +-- .../im/angry/openeuicc/core/EuiccChannel.kt | 26 +++++++------------ .../angry/openeuicc/core/EuiccChannelImpl.kt | 26 +++++++++++++++++++ .../core/PrivilegedEuiccChannelFactory.kt | 2 +- 4 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt index 7f3abb0..2365b07 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelFactory.kt @@ -33,7 +33,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}") try { - return EuiccChannel( + return EuiccChannelImpl( port, OmapiApduInterface( seService!!, @@ -61,7 +61,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha if (bulkIn == null || bulkOut == null) return null val conn = usbManager.openDevice(usbDevice) ?: return null if (!conn.claimInterface(usbInterface, true)) return null - return EuiccChannel( + return EuiccChannelImpl( FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), UsbApduInterface( conn, diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt index e106535..11ac7b0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannel.kt @@ -1,26 +1,18 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.* -import kotlinx.coroutines.flow.Flow -import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.LocalProfileAssistant -import net.typeblog.lpac_jni.impl.HttpInterfaceImpl -import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl -class EuiccChannel( - val port: UiccPortInfoCompat, - apduInterface: ApduInterface, - verboseLoggingFlow: Flow -) { - val slotId = port.card.physicalSlotIndex // PHYSICAL slot - val logicalSlotId = port.logicalSlotIndex - val portId = port.portIndex +interface EuiccChannel { + val port: UiccPortInfoCompat - val lpa: LocalProfileAssistant = - LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow)) + val slotId: Int // PHYSICAL slot + val logicalSlotId: Int + val portId: Int + + val lpa: LocalProfileAssistant val valid: Boolean - get() = lpa.valid - fun close() = lpa.close() -} + fun close() +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt new file mode 100644 index 0000000..0c0b00e --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelImpl.kt @@ -0,0 +1,26 @@ +package im.angry.openeuicc.core + +import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.Flow +import net.typeblog.lpac_jni.ApduInterface +import net.typeblog.lpac_jni.LocalProfileAssistant +import net.typeblog.lpac_jni.impl.HttpInterfaceImpl +import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl + +class EuiccChannelImpl( + override val port: UiccPortInfoCompat, + apduInterface: ApduInterface, + verboseLoggingFlow: Flow +) : EuiccChannel { + override val slotId = port.card.physicalSlotIndex + override val logicalSlotId = port.logicalSlotIndex + override val portId = port.portIndex + + override val lpa: LocalProfileAssistant = + LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow)) + + override val valid: Boolean + get() = lpa.valid + + override fun close() = lpa.close() +} diff --git a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt index ce57fb8..14e0020 100644 --- a/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt +++ b/app/src/main/java/im/angry/openeuicc/core/PrivilegedEuiccChannelFactory.kt @@ -26,7 +26,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}" ) try { - return EuiccChannel( + return EuiccChannelImpl( port, TelephonyManagerApduInterface( port, From 76e8fbd56b1561965d616e7998cd3f2172811c33 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 15:46:43 -0400 Subject: [PATCH 060/342] Use wrappers to enforce that withEuiccChannel can't leak references --- .../core/DefaultEuiccChannelManager.kt | 7 +- .../openeuicc/core/EuiccChannelManager.kt | 3 +- .../openeuicc/core/EuiccChannelWrapper.kt | 41 ++++++++++++ .../core/LocalProfileAssistantWrapper.kt | 64 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt create mode 100644 app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index af69c12..4585525 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -166,7 +166,12 @@ open class DefaultEuiccChannelManager( ): R { val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() - return fn(channel) + val wrapper = EuiccChannelWrapper(channel) + try { + return fn(wrapper) + } finally { + wrapper.invalidateWrapper() + } } override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 304269d..171c215 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -68,7 +68,8 @@ interface EuiccChannelManager { /** * Find a EuiccChannel by its slot and port, then run a callback with a reference to it. - * The reference is not supposed to be held outside of the callback. + * The reference is not supposed to be held outside of the callback. This is enforced via + * a wrapper object. * * If a channel for that slot / port is not found, EuiccChannelNotFoundException is thrown */ diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt new file mode 100644 index 0000000..826ba41 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -0,0 +1,41 @@ +package im.angry.openeuicc.core + +import im.angry.openeuicc.util.* +import net.typeblog.lpac_jni.LocalProfileAssistant + +class EuiccChannelWrapper(private val _inner: EuiccChannel) : EuiccChannel { + private var wrapperInvalidated = false + + private val channel: EuiccChannel + get() { + if (wrapperInvalidated) { + throw IllegalStateException("This wrapper has been invalidated") + } + + return _inner + } + override val port: UiccPortInfoCompat + get() = channel.port + override val slotId: Int + get() = channel.slotId + override val logicalSlotId: Int + get() = channel.logicalSlotId + override val portId: Int + get() = channel.portId + private val lpaDelegate = lazy { + LocalProfileAssistantWrapper(_inner.lpa) + } + override val lpa: LocalProfileAssistant by lpaDelegate + override val valid: Boolean + get() = channel.valid + + override fun close() = channel.close() + + fun invalidateWrapper() { + wrapperInvalidated = true + + if (lpaDelegate.isInitialized()) { + (lpa as LocalProfileAssistantWrapper).invalidateWrapper() + } + } +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt new file mode 100644 index 0000000..660dbc2 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -0,0 +1,64 @@ +package im.angry.openeuicc.core + +import net.typeblog.lpac_jni.EuiccInfo2 +import net.typeblog.lpac_jni.LocalProfileAssistant +import net.typeblog.lpac_jni.LocalProfileInfo +import net.typeblog.lpac_jni.LocalProfileNotification +import net.typeblog.lpac_jni.ProfileDownloadCallback + +class LocalProfileAssistantWrapper(private val _inner: LocalProfileAssistant) : + LocalProfileAssistant { + + private var wrapperInvalidated = false + + private val lpa: LocalProfileAssistant + get() { + if (wrapperInvalidated) { + throw IllegalStateException("This wrapper has been invalidated") + } + + return _inner + } + + override val valid: Boolean + get() = lpa.valid + override val profiles: List + get() = lpa.profiles + override val notifications: List + get() = lpa.notifications + override val eID: String + get() = lpa.eID + override val euiccInfo2: EuiccInfo2? + get() = lpa.euiccInfo2 + + override fun setEs10xMss(mss: Byte) = lpa.setEs10xMss(mss) + + override fun enableProfile(iccid: String, refresh: Boolean): Boolean = + lpa.enableProfile(iccid, refresh) + + override fun disableProfile(iccid: String, refresh: Boolean): Boolean = + lpa.disableProfile(iccid, refresh) + + override fun deleteProfile(iccid: String): Boolean = lpa.deleteProfile(iccid) + + override fun downloadProfile( + smdp: String, + matchingId: String?, + imei: String?, + confirmationCode: String?, + callback: ProfileDownloadCallback + ): Boolean = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) + + override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber) + + override fun handleNotification(seqNumber: Long): Boolean = lpa.handleNotification(seqNumber) + + override fun setNickname(iccid: String, nickname: String): Boolean = + lpa.setNickname(iccid, nickname) + + override fun close() = lpa.close() + + fun invalidateWrapper() { + wrapperInvalidated = true + } +} \ No newline at end of file From ef622740573d3d2fea0cac410a3e0f9991b096e5 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 15:49:38 -0400 Subject: [PATCH 061/342] Wrappers shouldn't hold references indefinitely --- .../im/angry/openeuicc/core/EuiccChannelWrapper.kt | 12 ++++++------ .../openeuicc/core/LocalProfileAssistantWrapper.kt | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt index 826ba41..0488813 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelWrapper.kt @@ -3,16 +3,16 @@ package im.angry.openeuicc.core import im.angry.openeuicc.util.* import net.typeblog.lpac_jni.LocalProfileAssistant -class EuiccChannelWrapper(private val _inner: EuiccChannel) : EuiccChannel { - private var wrapperInvalidated = false +class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { + private var _inner: EuiccChannel? = orig private val channel: EuiccChannel get() { - if (wrapperInvalidated) { + if (_inner == null) { throw IllegalStateException("This wrapper has been invalidated") } - return _inner + return _inner!! } override val port: UiccPortInfoCompat get() = channel.port @@ -23,7 +23,7 @@ class EuiccChannelWrapper(private val _inner: EuiccChannel) : EuiccChannel { override val portId: Int get() = channel.portId private val lpaDelegate = lazy { - LocalProfileAssistantWrapper(_inner.lpa) + LocalProfileAssistantWrapper(channel.lpa) } override val lpa: LocalProfileAssistant by lpaDelegate override val valid: Boolean @@ -32,7 +32,7 @@ class EuiccChannelWrapper(private val _inner: EuiccChannel) : EuiccChannel { override fun close() = channel.close() fun invalidateWrapper() { - wrapperInvalidated = true + _inner = null if (lpaDelegate.isInitialized()) { (lpa as LocalProfileAssistantWrapper).invalidateWrapper() diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index 660dbc2..e6a648a 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -6,18 +6,17 @@ import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback -class LocalProfileAssistantWrapper(private val _inner: LocalProfileAssistant) : +class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : LocalProfileAssistant { - - private var wrapperInvalidated = false + private var _inner: LocalProfileAssistant? = orig private val lpa: LocalProfileAssistant get() { - if (wrapperInvalidated) { + if (_inner == null) { throw IllegalStateException("This wrapper has been invalidated") } - return _inner + return _inner!! } override val valid: Boolean @@ -59,6 +58,6 @@ class LocalProfileAssistantWrapper(private val _inner: LocalProfileAssistant) : override fun close() = lpa.close() fun invalidateWrapper() { - wrapperInvalidated = true + _inner = null } } \ No newline at end of file From 95b24e61514002065d8333fb2a8fda22568c7bbc Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 21:29:09 -0400 Subject: [PATCH 062/342] Add withEuiccChannel helper for EuiccChannelFragment --- .../im/angry/openeuicc/core/DefaultEuiccChannelManager.kt | 6 ++++-- .../java/im/angry/openeuicc/core/EuiccChannelManager.kt | 8 +++++++- .../im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt | 5 +++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 4585525..4bf634f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -162,13 +162,15 @@ open class DefaultEuiccChannelManager( override suspend fun withEuiccChannel( physicalSlotId: Int, portId: Int, - fn: (EuiccChannel) -> R + fn: suspend (EuiccChannel) -> R ): R { val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { - return fn(wrapper) + return withContext(Dispatchers.IO) { + fn(wrapper) + } } finally { wrapper.invalidateWrapper() } diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 171c215..0626c80 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -71,9 +71,15 @@ interface EuiccChannelManager { * The reference is not supposed to be held outside of the callback. This is enforced via * a wrapper object. * + * The callback is run on Dispatchers.IO by default. + * * If a channel for that slot / port is not found, EuiccChannelNotFoundException is thrown */ - suspend fun withEuiccChannel(physicalSlotId: Int, portId: Int, fn: (EuiccChannel) -> R): R + suspend fun withEuiccChannel( + physicalSlotId: Int, + portId: Int, + fn: suspend (EuiccChannel) -> R + ): R /** * Invalidate all EuiccChannels previously cached by this Manager diff --git a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt index e92be40..3a74fbb 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/EuiccChannelFragmentUtils.kt @@ -40,6 +40,11 @@ val T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker get() = euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! +suspend fun T.withEuiccChannel(fn: suspend (EuiccChannel) -> R): R where T : Fragment, T : EuiccChannelFragmentMarker { + ensureEuiccChannelManager() + return euiccChannelManager.withEuiccChannel(slotId, portId, fn) +} + suspend fun T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await() From 3b868e4f9abeb8e4746f1be83e2a66bfdfd46da0 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 21:41:56 -0400 Subject: [PATCH 063/342] Move some fragments to withEuiccChannel() --- .../openeuicc/ui/EuiccManagementFragment.kt | 13 +++++--- .../openeuicc/ui/ProfileDownloadFragment.kt | 32 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index da29123..7d47ae1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -6,7 +6,6 @@ import android.content.ClipboardManager import android.content.Intent import android.os.Bundle import android.text.method.PasswordTransformationMethod -import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -52,6 +51,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var fab: FloatingActionButton private lateinit var profileList: RecyclerView + private var logicalSlotId: Int = -1 private val adapter = EuiccProfileAdapter() @@ -127,9 +127,11 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { R.id.show_notifications -> { - Intent(requireContext(), NotificationsActivity::class.java).apply { - putExtra("logicalSlotId", channel.logicalSlotId) - startActivity(this) + if (logicalSlotId != -1) { + Intent(requireContext(), NotificationsActivity::class.java).apply { + putExtra("logicalSlotId", logicalSlotId) + startActivity(this) + } } true } @@ -162,7 +164,8 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) } - val profiles = withContext(Dispatchers.IO) { + val profiles = withEuiccChannel { channel -> + logicalSlotId = channel.logicalSlotId euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) channel.lpa.profiles.operational } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt index 843c9e5..9aa5506 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt @@ -159,22 +159,26 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(), return@launch } - val imei = try { - telephonyManager.getImei(channel.logicalSlotId) ?: "" - } catch (e: Exception) { - "" - } + withEuiccChannel { channel -> + val imei = try { + telephonyManager.getImei(channel.logicalSlotId) ?: "" + } catch (e: Exception) { + "" + } - // Fetch remaining NVRAM - val str = channel.lpa.euiccInfo2?.freeNvram?.also { - freeNvram = it - }?.let { formatFreeSpace(it) } + // Fetch remaining NVRAM + val str = channel.lpa.euiccInfo2?.freeNvram?.also { + freeNvram = it + }?.let { formatFreeSpace(it) } - withContext(Dispatchers.Main) { - profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, - str ?: getText(R.string.unknown)) - profileDownloadIMEI.editText!!.text = - Editable.Factory.getInstance().newEditable(imei) + withContext(Dispatchers.Main) { + profileDownloadFreeSpace.text = getString( + R.string.profile_download_free_space, + str ?: getText(R.string.unknown) + ) + profileDownloadIMEI.editText!!.text = + Editable.Factory.getInstance().newEditable(imei) + } } } } From 0961ef70f400825cf061bec0957dc71378ee3dba Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 21:45:14 -0400 Subject: [PATCH 064/342] New withEuiccChannel() variant with logical slot ID --- .../openeuicc/core/DefaultEuiccChannelManager.kt | 16 ++++++++++++++++ .../angry/openeuicc/core/EuiccChannelManager.kt | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 4bf634f..53418b7 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -176,6 +176,22 @@ open class DefaultEuiccChannelManager( } } + override suspend fun withEuiccChannel( + logicalSlotId: Int, + fn: suspend (EuiccChannel) -> R + ): R { + val channel = findEuiccChannelBySlotBlocking(logicalSlotId) + ?: throw EuiccChannelManager.EuiccChannelNotFoundException() + val wrapper = EuiccChannelWrapper(channel) + try { + return withContext(Dispatchers.IO) { + fn(wrapper) + } + } finally { + wrapper.invalidateWrapper() + } + } + override suspend fun waitForReconnect(physicalSlotId: Int, portId: Int, timeoutMillis: Long) { if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) return diff --git a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt index 0626c80..a4968c7 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/EuiccChannelManager.kt @@ -81,6 +81,14 @@ interface EuiccChannelManager { fn: suspend (EuiccChannel) -> R ): R + /** + * Same as withEuiccChannel(Int, Int, (EuiccChannel) -> R) but instead uses logical slot ID + */ + suspend fun withEuiccChannel( + logicalSlotId: Int, + fn: suspend (EuiccChannel) -> R + ): R + /** * Invalidate all EuiccChannels previously cached by this Manager */ From 8ac46bd77890f0243edb190910e0452fa56fc5fd Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 21:46:13 -0400 Subject: [PATCH 065/342] Move findEuiccChannelBySlot to non-blocking --- .../core/DefaultEuiccChannelManager.kt | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt index 53418b7..91a859d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/DefaultEuiccChannelManager.kt @@ -88,23 +88,26 @@ open class DefaultEuiccChannelManager( } } - override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? = - runBlocking { - withContext(Dispatchers.IO) { - if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { - return@withContext usbChannel - } + private suspend fun findEuiccChannelBySlot(logicalSlotId: Int): EuiccChannel? = + withContext(Dispatchers.IO) { + if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { + return@withContext usbChannel + } - for (card in uiccCards) { - for (port in card.ports) { - if (port.logicalSlotIndex == logicalSlotId) { - return@withContext tryOpenEuiccChannel(port) - } + for (card in uiccCards) { + for (port in card.ports) { + if (port.logicalSlotIndex == logicalSlotId) { + return@withContext tryOpenEuiccChannel(port) } } - - null } + + null + } + + override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? = + runBlocking { + findEuiccChannelBySlot(logicalSlotId) } override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = @@ -164,7 +167,7 @@ open class DefaultEuiccChannelManager( portId: Int, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId) + val channel = findEuiccChannelByPort(physicalSlotId, portId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { @@ -180,7 +183,7 @@ open class DefaultEuiccChannelManager( logicalSlotId: Int, fn: suspend (EuiccChannel) -> R ): R { - val channel = findEuiccChannelBySlotBlocking(logicalSlotId) + val channel = findEuiccChannelBySlot(logicalSlotId) ?: throw EuiccChannelManager.EuiccChannelNotFoundException() val wrapper = EuiccChannelWrapper(channel) try { From 6a2d4d66dd8547053ae6d9d1624cf460cdaa850c Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 21:52:33 -0400 Subject: [PATCH 066/342] Move EuiccChannelManagerService to withEuiccChannel() --- .../service/EuiccChannelManagerService.kt | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 8db3bbe..fce3d30 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -287,19 +287,20 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { R.drawable.ic_task_sim_card_download ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId) - val res = channel!!.lpa.downloadProfile( - smdp, - matchingId, - imei, - confirmationCode, - object : ProfileDownloadCallback { - override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { - if (state.progress == 0) return - foregroundTaskState.value = - ForegroundTaskState.InProgress(state.progress) - } - }) + val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.downloadProfile( + smdp, + matchingId, + imei, + confirmationCode, + object : ProfileDownloadCallback { + override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { + if (state.progress == 0) return + foregroundTaskState.value = + ForegroundTaskState.InProgress(state.progress) + } + }) + } if (!res) { // TODO: Provide more details on the error @@ -321,10 +322,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_rename_failure), R.drawable.ic_task_rename ) { - val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname( - iccid, - name - ) + val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.setNickname( + iccid, + name + ) + } if (!res) { throw RuntimeException("Profile not renamed") @@ -342,10 +345,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { R.drawable.ic_task_delete ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - euiccChannelManager.findEuiccChannelByPort( - slotId, - portId - )!!.lpa.deleteProfile(iccid) + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.deleteProfile(iccid) + } preferenceRepository.notificationDeleteFlow.first() } @@ -366,8 +368,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { R.drawable.ic_task_switch ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!! - val (res, refreshed) = + val (res, refreshed) = euiccChannelManager.withEuiccChannel( + slotId, + portId + ) { channel -> if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) { // Sometimes, we *can* enable or disable the profile, but we cannot // send the refresh command to the modem because the profile somehow @@ -378,6 +382,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } else { Pair(true, true) } + } if (!res) { throw RuntimeException("Could not switch profile") From 3d4704e77b6e882a9a74c113aa47d316990149fb Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 26 Oct 2024 22:15:02 -0400 Subject: [PATCH 067/342] Remove more EuiccChannel usage in PrivilegedEuiccManagementFragment --- .../openeuicc/ui/EuiccManagementFragment.kt | 40 ++++++++++--------- .../ui/PrivilegedEuiccManagementFragment.kt | 19 ++++++--- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 7d47ae1..0d61581 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -150,32 +150,36 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener, listOf() } - @SuppressLint("NotifyDataSetChanged") private fun refresh() { if (invalid) return swipeRefresh.isRefreshing = true lifecycleScope.launch { - ensureEuiccChannelManager() - euiccChannelManagerService.waitForForegroundTask() + doRefresh() + } + } - if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { - disableSafeguardFlow = - preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) - } + @SuppressLint("NotifyDataSetChanged") + protected open suspend fun doRefresh() { + ensureEuiccChannelManager() + euiccChannelManagerService.waitForForegroundTask() - val profiles = withEuiccChannel { channel -> - logicalSlotId = channel.logicalSlotId - euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) - channel.lpa.profiles.operational - } + if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { + disableSafeguardFlow = + preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) + } - withContext(Dispatchers.Main) { - adapter.profiles = profiles - adapter.footerViews = onCreateFooterViews(profileList, profiles) - adapter.notifyDataSetChanged() - swipeRefresh.isRefreshing = false - } + val profiles = withEuiccChannel { channel -> + logicalSlotId = channel.logicalSlotId + euiccChannelManager.notifyEuiccProfilesChanged(channel.logicalSlotId) + channel.lpa.profiles.operational + } + + withContext(Dispatchers.Main) { + adapter.profiles = profiles + adapter.footerViews = onCreateFooterViews(profileList, profiles) + adapter.notifyDataSetChanged() + swipeRefresh.isRefreshing = false } } diff --git a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt index 7d055f4..6a8fede 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/PrivilegedEuiccManagementFragment.kt @@ -6,8 +6,6 @@ import android.widget.Button import android.widget.PopupMenu import im.angry.openeuicc.R import im.angry.openeuicc.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import net.typeblog.lpac_jni.LocalProfileInfo class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { @@ -16,14 +14,23 @@ class PrivilegedEuiccManagementFragment: EuiccManagementFragment() { newInstanceEuicc(PrivilegedEuiccManagementFragment::class.java, slotId, portId) } + private var isMEP = false + private var isRemovable = false + + override suspend fun doRefresh() { + super.doRefresh() + withEuiccChannel { channel -> + isMEP = channel.isMEP + isRemovable = channel.removable + } + } + override suspend fun onCreateFooterViews( parent: ViewGroup, profiles: List ): List = super.onCreateFooterViews(parent, profiles).let { footers -> - // isMEP can map to a slow operation (UiccCardInfo.isMultipleEnabledProfilesSupported()) - // so let's do it in the IO context - if (withContext(Dispatchers.IO) { channel.isMEP }) { + if (isMEP) { val view = layoutInflater.inflate(R.layout.footer_mep, parent, false) view.requireViewById