瀏覽代碼

lpac-jni: Export profile metadata during download and allow cancellation at that point

(Shoutout to Mark Gallagher <mark@fts.scot> who in #313 introduced
a similar feature. Note that we still have not exposed profile
installation result here, but what's really useful would only be the
sequence number for us)

This leaves a TODO to actually hook up the UI to allow cancellation
after metadata is displayed. But most of this is intended to support
EuiccService APIs to allow apps to download profiles directly, which
requires the use of profilePolicyRules (not done yet; lpac pending) in
the preview metadata.
Peter Cai 2 周之前
父節點
當前提交
d4855f130c

+ 20 - 4
app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt

@@ -37,7 +37,10 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeoutOrNull
 import kotlinx.coroutines.yield
+import net.typeblog.lpac_jni.ProfileDownloadCallback
 import net.typeblog.lpac_jni.ProfileDownloadInput
+import net.typeblog.lpac_jni.ProfileDownloadState
+import net.typeblog.lpac_jni.RemoteProfileInfo
 
 /**
  * An Android Service wrapper for EuiccChannelManager.
@@ -389,10 +392,23 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
         ) {
             euiccChannelManager.beginTrackedOperation(slotId, portId, seId) {
                 euiccChannelManager.withEuiccChannel(slotId, portId, seId) { channel ->
-                    channel.lpa.downloadProfile(input) { state ->
-                        if (state.progress == 0) return@downloadProfile
-                        foregroundTaskState.value = ForegroundTaskState.InProgress(state.progress)
-                    }
+                    channel.lpa.downloadProfile(input, object : ProfileDownloadCallback {
+                        override fun onStateUpdate(state: ProfileDownloadState) {
+                            if (state.progress == 0) return
+                            foregroundTaskState.value = ForegroundTaskState.InProgress(state.progress)
+                        }
+
+                        override fun onConfirmMetadata(metadata: RemoteProfileInfo?): Boolean {
+                            // TODO: Actually do something here and not just logging?
+                            if (metadata != null) {
+                                Log.i(
+                                    TAG,
+                                    "Downloading profile provider=${metadata.providerName} name=${metadata.name}"
+                                )
+                            }
+                            return true
+                        }
+                    })
                 }
 
                 preferenceRepository.notificationDownloadFlow.first()

+ 4 - 3
app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import net.typeblog.lpac_jni.LocalProfileInfo
+import net.typeblog.lpac_jni.ProfileClass
 
 open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
     EuiccChannelFragmentMarker {
@@ -395,9 +396,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
             profileClass.isVisible = unfilteredProfileListFlow.value
             profileClass.setText(
                 when (profile.profileClass) {
-                    LocalProfileInfo.Clazz.Testing -> R.string.profile_class_testing
-                    LocalProfileInfo.Clazz.Provisioning -> R.string.profile_class_provisioning
-                    LocalProfileInfo.Clazz.Operational -> R.string.profile_class_operational
+                    ProfileClass.Testing -> R.string.profile_class_testing
+                    ProfileClass.Provisioning -> R.string.profile_class_provisioning
+                    ProfileClass.Operational -> R.string.profile_class_operational
                 }
             )
             iccid.text = profile.iccid

+ 3 - 2
app-common/src/main/java/im/angry/openeuicc/util/LPAUtils.kt

@@ -5,6 +5,7 @@ import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannelManager
 import net.typeblog.lpac_jni.LocalProfileAssistant
 import net.typeblog.lpac_jni.LocalProfileInfo
+import net.typeblog.lpac_jni.ProfileClass
 
 const val TAG = "LPAUtils"
 
@@ -16,7 +17,7 @@ val LocalProfileInfo.isEnabled: Boolean
     get() = state == LocalProfileInfo.State.Enabled
 
 val List<LocalProfileInfo>.operational: List<LocalProfileInfo>
-    get() = filter { it.profileClass == LocalProfileInfo.Clazz.Operational || it.isEnabled }
+    get() = filter { it.profileClass == ProfileClass.Operational || it.isEnabled }
 
 val List<LocalProfileInfo>.enabled: LocalProfileInfo?
     get() = find { it.isEnabled }
@@ -104,4 +105,4 @@ suspend inline fun EuiccChannelManager.beginTrackedOperation(
         }
     }
     Log.d(TAG, "Operation complete")
-}
+}

+ 4 - 3
app/src/main/java/im/angry/openeuicc/service/OpenEuiccService.kt

@@ -19,6 +19,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.runBlocking
 import net.typeblog.lpac_jni.LocalProfileInfo
+import net.typeblog.lpac_jni.ProfileClass
 
 class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
     companion object {
@@ -210,9 +211,9 @@ class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
                             )
                             setProfileClass(
                                 when (it.profileClass) {
-                                    LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
-                                    LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
-                                    LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
+                                    ProfileClass.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
+                                    ProfileClass.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
+                                    ProfileClass.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
                                 }
                             )
                         }.build()

+ 2 - 18
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileInfo.kt

@@ -7,7 +7,7 @@ data class LocalProfileInfo(
     val nickName: String,
     val providerName: String,
     val isdpAID: String,
-    val profileClass: Clazz
+    val profileClass: ProfileClass
 ) {
     enum class State {
         Enabled,
@@ -24,20 +24,4 @@ data class LocalProfileInfo(
         }
     }
 
-    enum class Clazz {
-        Testing,
-        Provisioning,
-        Operational;
-
-        companion object {
-            @JvmStatic
-            fun fromString(str: String?) =
-                when (str?.lowercase()) {
-                    "test" -> Testing
-                    "provisioning" -> Provisioning
-                    "operational" -> Operational
-                    else -> Operational
-                }
-        }
-    }
-}
+}

+ 18 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileClass.kt

@@ -0,0 +1,18 @@
+package net.typeblog.lpac_jni
+
+enum class ProfileClass {
+    Testing,
+    Provisioning,
+    Operational;
+
+    companion object {
+        @JvmStatic
+        fun fromString(str: String?) =
+            when (str?.lowercase()) {
+                "test" -> Testing
+                "provisioning" -> Provisioning
+                "operational" -> Operational
+                else -> Operational
+            }
+    }
+}

+ 6 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt

@@ -2,4 +2,10 @@ package net.typeblog.lpac_jni
 
 fun interface ProfileDownloadCallback {
     fun onStateUpdate(state: ProfileDownloadState)
+
+    /**
+     * Optionally override this to abort / continue a download based on metadata acquired in the process
+     * Note that not all ES9P servers may return metadata.
+     */
+    fun onConfirmMetadata(metadata: RemoteProfileInfo?): Boolean = true
 }

+ 9 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/RemoteProfileInfo.kt

@@ -0,0 +1,9 @@
+package net.typeblog.lpac_jni
+
+// TODO: We need to export profilePolicyRules here as well (currently unsupported by lpac)
+data class RemoteProfileInfo(
+    val iccid: String,
+    val name: String,
+    val providerName: String,
+    val profileClass: ProfileClass,
+)

+ 2 - 1
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt

@@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
 import net.typeblog.lpac_jni.LocalProfileInfo
 import net.typeblog.lpac_jni.LocalProfileNotification
 import net.typeblog.lpac_jni.LpacJni
+import net.typeblog.lpac_jni.ProfileClass
 import net.typeblog.lpac_jni.ProfileDownloadCallback
 import net.typeblog.lpac_jni.Version
 import java.util.concurrent.locks.ReentrantLock
@@ -118,7 +119,7 @@ class LocalProfileAssistantImpl(
             val ret = mutableListOf<LocalProfileInfo>()
             while (curr != 0L) {
                 val state = LocalProfileInfo.State.fromString(LpacJni.profileGetStateString(curr))
-                val clazz = LocalProfileInfo.Clazz.fromString(LpacJni.profileGetClassString(curr))
+                val clazz = ProfileClass.fromString(LpacJni.profileGetClassString(curr))
                 ret.add(
                     LocalProfileInfo(
                         LpacJni.profileGetIccid(curr),

+ 113 - 0
libs/lpac-jni/src/main/jni/lpac-jni/lpac-download.c

@@ -1,5 +1,6 @@
 #include <euicc/es9p.h>
 #include <euicc/es10b.h>
+#include <euicc/es8p.h>
 #include <stdlib.h>
 #include <string.h>
 #include <syslog.h>
@@ -12,6 +13,12 @@ jobject download_state_downloading;
 jobject download_state_finalizing;
 
 jmethodID on_state_update;
+jmethodID on_confirm_metadata;
+jclass remote_profile_info_class;
+jmethodID remote_profile_info_constructor;
+jobject profile_class_testing;
+jobject profile_class_provisioning;
+jobject profile_class_operational;
 
 void lpac_download_init() {
     LPAC_JNI_SETUP_ENV;
@@ -54,6 +61,77 @@ void lpac_download_init() {
                                                        "net/typeblog/lpac_jni/ProfileDownloadCallback");
     on_state_update = (*env)->GetMethodID(env, download_callback_class, "onStateUpdate",
                                           "(Lnet/typeblog/lpac_jni/ProfileDownloadState;)V");
+    on_confirm_metadata = (*env)->GetMethodID(env, download_callback_class, "onConfirmMetadata",
+                                              "(Lnet/typeblog/lpac_jni/RemoteProfileInfo;)Z");
+
+    jclass profile_class_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/ProfileClass");
+    jfieldID profile_class_testing_field = (*env)->GetStaticFieldID(env, profile_class_class,
+                                                                    "Testing",
+                                                                    "Lnet/typeblog/lpac_jni/ProfileClass;");
+    profile_class_testing = (*env)->GetStaticObjectField(env, profile_class_class,
+                                                         profile_class_testing_field);
+    profile_class_testing = (*env)->NewGlobalRef(env, profile_class_testing);
+    jfieldID profile_class_provisioning_field = (*env)->GetStaticFieldID(env, profile_class_class,
+                                                                         "Provisioning",
+                                                                         "Lnet/typeblog/lpac_jni/ProfileClass;");
+    profile_class_provisioning = (*env)->GetStaticObjectField(env, profile_class_class,
+                                                              profile_class_provisioning_field);
+    profile_class_provisioning = (*env)->NewGlobalRef(env, profile_class_provisioning);
+    jfieldID profile_class_operational_field = (*env)->GetStaticFieldID(env, profile_class_class,
+                                                                        "Operational",
+                                                                        "Lnet/typeblog/lpac_jni/ProfileClass;");
+    profile_class_operational = (*env)->GetStaticObjectField(env, profile_class_class,
+                                                             profile_class_operational_field);
+    profile_class_operational = (*env)->NewGlobalRef(env, profile_class_operational);
+
+    jclass _remote_profile_info_class = (*env)->FindClass(env,
+                                                          "net/typeblog/lpac_jni/RemoteProfileInfo");
+    remote_profile_info_class = (*env)->NewGlobalRef(env, _remote_profile_info_class);
+    remote_profile_info_constructor = (*env)->GetMethodID(env, remote_profile_info_class, "<init>",
+                                                          "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lnet/typeblog/lpac_jni/ProfileClass;)V");
+}
+
+static jobject profile_class_from_es10c_profile_class(enum es10c_profile_class profile_class) {
+    switch (profile_class) {
+        case ES10C_PROFILE_CLASS_TEST:
+            return profile_class_testing;
+        case ES10C_PROFILE_CLASS_PROVISIONING:
+            return profile_class_provisioning;
+        case ES10C_PROFILE_CLASS_OPERATIONAL:
+        default:
+            // In es10c profiles are considered operational if the field is missing (null).
+            return profile_class_operational;
+    }
+}
+
+static jobject create_remote_profile_info(JNIEnv *env,
+                                          struct es8p_metadata *profile_metadata) {
+    jobject profile_class = NULL;
+    jstring metadata_iccid = NULL;
+    jstring metadata_profile_name = NULL;
+    jstring metadata_provider_name = NULL;
+    jobject remote_profile_info = NULL;
+
+    metadata_iccid = toJString(env, profile_metadata->iccid);
+    metadata_profile_name = toJString(env, profile_metadata->profileName);
+    metadata_provider_name = toJString(env, profile_metadata->serviceProviderName);
+    profile_class = profile_class_from_es10c_profile_class(profile_metadata->profileClass);
+
+    remote_profile_info = (*env)->NewObject(env, remote_profile_info_class,
+                                            remote_profile_info_constructor,
+                                            metadata_iccid,
+                                            metadata_profile_name,
+                                            metadata_provider_name,
+                                            profile_class);
+
+    if (metadata_iccid != NULL)
+        (*env)->DeleteLocalRef(env, metadata_iccid);
+    if (metadata_profile_name != NULL)
+        (*env)->DeleteLocalRef(env, metadata_profile_name);
+    if (metadata_provider_name != NULL)
+        (*env)->DeleteLocalRef(env, metadata_provider_name);
+
+    return remote_profile_info;
 }
 
 JNIEXPORT jint JNICALL
@@ -62,11 +140,14 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
                                                     jstring imei, jstring confirmation_code,
                                                     jobject callback) {
     struct euicc_ctx *ctx = (struct euicc_ctx *) handle;
+    struct es8p_metadata *profile_metadata = NULL;
     struct es10b_load_bound_profile_package_result es10b_load_bound_profile_package_result;
     const char *_confirmation_code = NULL;
     const char *_matching_id = NULL;
     const char *_smdp = NULL;
     const char *_imei = NULL;
+    jobject remote_profile_info = NULL;
+    jboolean confirmed = JNI_FALSE;
     int ret;
 
     if (confirmation_code != NULL)
@@ -109,6 +190,34 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
         goto out;
     }
 
+    if (ctx->http._internal.prepare_download_param != NULL &&
+        ctx->http._internal.prepare_download_param->b64_profileMetadata != NULL) {
+        ret = es8p_metadata_parse(&profile_metadata,
+                                  ctx->http._internal.prepare_download_param->b64_profileMetadata);
+        if (ret < 0) {
+            ret = -ES10B_ERROR_REASON_UNDEFINED;
+            goto out;
+        }
+
+        remote_profile_info = create_remote_profile_info(env, profile_metadata);
+        if (remote_profile_info == NULL) {
+            ret = -ES10B_ERROR_REASON_UNDEFINED;
+            goto out;
+        }
+    }
+
+    confirmed = (*env)->CallBooleanMethod(env, callback, on_confirm_metadata, remote_profile_info);
+
+    if (remote_profile_info != NULL) {
+        (*env)->DeleteLocalRef(env, remote_profile_info);
+        remote_profile_info = NULL;
+    }
+
+    if (!confirmed) {
+        ret = -ES10B_ERROR_REASON_UNDEFINED;
+        goto out;
+    }
+
     (*env)->CallVoidMethod(env, callback, on_state_update, download_state_downloading);
     ret = es10b_prepare_download(ctx, _confirmation_code);
     syslog(LOG_INFO, "es10b_prepare_download %d", ret);
@@ -141,6 +250,10 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j
     (*env)->ReleaseStringUTFChars(env, smdp, _smdp);
     if (_imei != NULL)
         (*env)->ReleaseStringUTFChars(env, imei, _imei);
+    if (remote_profile_info != NULL)
+        (*env)->DeleteLocalRef(env, remote_profile_info);
+    if (profile_metadata != NULL)
+        es8p_metadata_free(&profile_metadata);
     return ret;
 }