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 b715ca0..cd62fca 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 @@ -45,8 +45,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : matchingId: String?, imei: String?, confirmationCode: String?, + preview: Boolean, callback: ProfileDownloadCallback - ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) + ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, preview, callback) override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber) 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 760f1af..07c8bd4 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 @@ -373,6 +373,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { smdp: String, matchingId: String?, confirmationCode: String?, + preview: Boolean, imei: String? ): ForegroundTaskSubscriberFlow = launchForegroundTask( @@ -387,7 +388,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { matchingId, imei, confirmationCode, + preview, object : ProfileDownloadCallback { + override fun onMetadataReceived(profileName: String, iccid: String, appCerts: String) { + } override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { if (state.progress == 0) return foregroundTaskState.value = diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index e342dee..11503ac 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -29,6 +29,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { var smdp: String, var matchingId: String?, var confirmationCode: String?, + var preview: Boolean, var imei: String?, var downloadStarted: Boolean, var downloadTaskID: Long, @@ -66,6 +67,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { "", null, null, + false, null, false, -1, diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index 1b816d4..9fd7c21 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -160,6 +160,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep state.smdp, state.matchingId, state.confirmationCode, + state.preview, state.imei ) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cae19d3..4f5083e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,6 +53,18 @@ + + + + + + + + + + channel.lpa.downloadProfile( + smdp = smdpAddress, + matchingId = matchingId, + imei = telephonyManager.imei, + confirmationCode = subscription.confirmationCode, + preview = false, + callback = object : ProfileDownloadCallback { + override fun onMetadataReceived(profileName: String, iccid: String, appCerts: String) { + Log.i(TAG, "Profile metadata received: profileName=$profileName iccid=$iccid") + profileIccid = iccid + } + + override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { + Log.i(TAG, "Profile download state update: progress=${state.progress}") + } + } + ) + + if (switchAfterDownload) { + onSwitchToSubscriptionWithPort(slotIndex, portId, profileIccid, false) + } + + DownloadSubscriptionResult( + RESULT_OK, + 0, + channel.cardId + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "Error during profile download: ${e.message}", e) + DownloadSubscriptionResult( + RESULT_FIRST_USER, + 0, + -1 + ) + } + } + override fun onGetDownloadableSubscriptionMetadata( slotId: Int, subscription: DownloadableSubscription?, forceDeactivateSim: Boolean ): GetDownloadableSubscriptionMetadataResult { - // Stub: return as-is and do not fetch anything - // This is incompatible with carrier eSIM apps; should we make it compatible? - return GetDownloadableSubscriptionMetadataResult(RESULT_OK, subscription) + Log.i(TAG, "onGetDownloadableSubscriptionMetadata slotId=$slotId") + + if (subscription == null) { + return GetDownloadableSubscriptionMetadataResult( + RESULT_FIRST_USER, + null + ) + } + + Log.i(TAG, "onGetDownloadableSubscriptionMetadata getEncodedActivationCode()=${subscription.getEncodedActivationCode()}") + + // Extract SMDP address and matching ID + val activationParts = subscription.getEncodedActivationCode()?.split("$") + val smdpAddress = activationParts?.getOrNull(1) + val matchingId = activationParts?.getOrNull(2) + + if (smdpAddress.isNullOrBlank() || matchingId.isNullOrBlank()) { + Log.e(TAG, "SMDP address or activation code is not valid.") + return GetDownloadableSubscriptionMetadataResult( + RESULT_FIRST_USER, + null + ) + } + + Log.i(TAG, "onGetDownloadableSubscriptionMetadata smdpAddress=$smdpAddress") + Log.i(TAG, "onGetDownloadableSubscriptionMetadata matchingId=$matchingId") + + val portId = withEuiccChannelManager { + euiccChannelManager.findFirstAvailablePort(slotId) + } + + if (portId < 0) { + Log.e(TAG, "No available port for slotId=$slotId") + return GetDownloadableSubscriptionMetadataResult( + RESULT_FIRST_USER, + null + ) + } + + var updatedSubscription: DownloadableSubscription? = null + + return try { + withEuiccChannelManager { + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + channel.lpa.downloadProfile( + smdp = smdpAddress, + matchingId = matchingId, + imei = telephonyManager.imei, + confirmationCode = subscription.confirmationCode, + preview = true, + callback = object : ProfileDownloadCallback { + override fun onMetadataReceived(profileName: String, iccid: String, appCerts: String) { + Log.i(TAG, "App certificates: appCerts=$appCerts") + Log.i(TAG, "Profile metadata received: profileName=$profileName") + + val accessRules = appCerts.split(",").mapNotNull { certHex -> + try { + val certBytes = certHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() + UiccAccessRule(certBytes, null, 0L) + } catch (e: Exception) { + Log.e(TAG, "Failed to parse certificate hash: $certHex", e) + null + } + } + + updatedSubscription = DownloadableSubscription.Builder(subscription) + .setCarrierName(profileName) + .setAccessRules(accessRules) + .build() + } + + override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) { + Log.i(TAG, "Profile download state update: progress=${state.progress}") + } + } + ) + } + } + + if (updatedSubscription == null) { + throw IllegalStateException("Metadata not received from callback") + } + + GetDownloadableSubscriptionMetadataResult( + RESULT_OK, + updatedSubscription + ) + } catch (e: Exception) { + Log.e(TAG, "Error during profile download: ${e.message}", e) + GetDownloadableSubscriptionMetadataResult( + RESULT_FIRST_USER, + null + ) + } } override fun onGetDefaultDownloadableSubscriptionList( diff --git a/app/src/main/java/im/angry/openeuicc/ui/UserConsentActivity.kt b/app/src/main/java/im/angry/openeuicc/ui/UserConsentActivity.kt new file mode 100644 index 0000000..d425a3b --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/UserConsentActivity.kt @@ -0,0 +1,94 @@ +package im.angry.openeuicc.ui + +import android.app.Activity +import android.app.AlertDialog +import android.content.pm.PackageManager +import android.os.Bundle +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import android.text.TextUtils +import android.util.Log + +import im.angry.openeuicc.R + +class UserConsentActivity : Activity() { + + private lateinit var euiccManager: EuiccManager + private lateinit var subscriptionManager: SubscriptionManager + private lateinit var telephonyManager: TelephonyManager + private var mCardId: Int = -1 + private var mUsePortIndex: Boolean = false + private var mPortIndex: Int = -1 + private var mSubscriptionId: Int = -1 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + euiccManager = getSystemService(EuiccManager::class.java) as EuiccManager + subscriptionManager = getSystemService(SubscriptionManager::class.java) as SubscriptionManager + telephonyManager = getSystemService(TelephonyManager::class.java) as TelephonyManager + + val callingPackage = intent.getStringExtra("android.service.euicc.extra.RESOLUTION_CALLING_PACKAGE") + if (TextUtils.isEmpty(callingPackage)) { + Log.e("UserConsentActivity", "Unknown calling package in resolution activity") + sendResultAndFinish(false) + return + } + + val extras = intent.extras + if (extras != null) { + mCardId = extras.getInt("android.service.euicc.extra.RESOLUTION_CARD_ID", -1) + mUsePortIndex = extras.getBoolean("android.service.euicc.extra.RESOLUTION_USE_PORT_INDEX", false) + mPortIndex = extras.getInt("android.service.euicc.extra.RESOLUTION_PORT_INDEX", -1) + mSubscriptionId = extras.getInt("android.service.euicc.extra.RESOLUTION_SUBSCRIPTION_ID", -1) + } + + try { + val packageManager = packageManager + val appLabel = packageManager.getApplicationLabel(packageManager.getApplicationInfo(callingPackage!!, 0)) + if (TextUtils.isEmpty(appLabel)) { + Log.e("UserConsentActivity", "Calling app doesn't have a label") + sendResultAndFinish(false) + return + } + } catch (e: PackageManager.NameNotFoundException) { + Log.e("UserConsentActivity", "Calling package doesn't exist", e) + sendResultAndFinish(false) + return + } + + showUserConsentDialog() + } + + fun sendResultAndFinish(consent: Boolean) { + val bundle = Bundle() + + bundle.putBoolean("android.service.euicc.extra.RESOLUTION_CONSENT", consent) + if (mUsePortIndex) { + bundle.putInt("android.service.euicc.extra.RESOLUTION_PORT_INDEX", mPortIndex); + } + + euiccManager.createForCardId(mCardId) + euiccManager.continueOperation(intent, bundle) + + setResult(if (consent) Activity.RESULT_OK else Activity.RESULT_CANCELED) + finish() + } + + fun showUserConsentDialog() { + AlertDialog.Builder(this) + .setTitle(getString(R.string.user_consent_title)) + .setMessage(getString(R.string.user_consent_message)) + .setPositiveButton(getString(R.string.user_consent_positive)) { _, _ -> + sendResultAndFinish(true) + } + .setNegativeButton(getString(R.string.user_consent_negative)) { _, _ -> + sendResultAndFinish(false) + } + .setOnCancelListener { + sendResultAndFinish(false) + } + .show() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47c88bd..041bf0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,4 +22,9 @@ Your device supports eSIMs. To connect to mobile network, download your eSIM issued by a carrier, or insert a physical SIM. Skip Download eSIM + + Allow your carrier to download SIM? + Your SIM will be used immediately after download + No thanks + Yes \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..54cdad2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,13 @@ + + + diff --git a/libs/hidden-apis-stub/src/main/java/android/service/euicc/EuiccService.java b/libs/hidden-apis-stub/src/main/java/android/service/euicc/EuiccService.java index a1cd43b..629b8d8 100644 --- a/libs/hidden-apis-stub/src/main/java/android/service/euicc/EuiccService.java +++ b/libs/hidden-apis-stub/src/main/java/android/service/euicc/EuiccService.java @@ -305,12 +305,45 @@ public abstract class EuiccService extends Service { * defined in {@code RESOLVABLE_ERROR_}. A subclass should override this method. Otherwise, * this method does nothing and returns null by default. * @see android.telephony.euicc.EuiccManager#downloadSubscription + * @deprecated prefer {@link #onDownloadSubscription(int, int, + * DownloadableSubscription, boolean, boolean, Bundle)} */ + @Deprecated public DownloadSubscriptionResult onDownloadSubscription(int slotId, - DownloadableSubscription subscription, boolean switchAfterDownload, - boolean forceDeactivateSim, Bundle resolvedBundle) { + @NonNull DownloadableSubscription subscription, boolean switchAfterDownload, + boolean forceDeactivateSim, @Nullable Bundle resolvedBundle) { return null; } + /** + * Download the given subscription. + * + * @param slotIndex Index of the SIM slot to use for the operation. + * @param portIndex Index of the port from the slot. portIndex is used when + * switchAfterDownload is set to {@code true}, otherwise download is port agnostic. + * @param subscription The subscription to download. + * @param switchAfterDownload If true, the subscription should be enabled upon successful + * download. + * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the + * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM} + * should be returned to allow the user to consent to this operation first. + * @param resolvedBundle The bundle containing information on resolved errors. It can contain + * a string of confirmation code for the key {@link #EXTRA_RESOLUTION_CONFIRMATION_CODE}, + * and a boolean for key {@link #EXTRA_RESOLUTION_ALLOW_POLICY_RULES} indicating whether + * the user allows profile policy rules or not. + * @return a DownloadSubscriptionResult instance including a result code, a resolvable errors + * bit map, and original the card Id. The result code may be one of the predefined + * {@code RESULT_} constants or any implementation-specific code starting with + * {@link #RESULT_FIRST_USER}. The resolvable error bit map can be either 0 or values + * defined in {@code RESOLVABLE_ERROR_}. + * @see android.telephony.euicc.EuiccManager#downloadSubscription + */ + @NonNull + public DownloadSubscriptionResult onDownloadSubscription(int slotIndex, int portIndex, + @NonNull DownloadableSubscription subscription, boolean switchAfterDownload, + boolean forceDeactivateSim, @NonNull Bundle resolvedBundle) { + // stub implementation, LPA needs to implement this + throw new UnsupportedOperationException("LPA must override onDownloadSubscription"); + } /** * Download the given subscription. * @@ -329,8 +362,8 @@ public abstract class EuiccService extends Service { * {@link #onDownloadSubscription(int, DownloadableSubscription, boolean, boolean, Bundle)}. The * default return value for this one is Integer.MIN_VALUE. */ - @Deprecated public int onDownloadSubscription(int slotId, - DownloadableSubscription subscription, boolean switchAfterDownload, + @Deprecated public @Result int onDownloadSubscription(int slotId, + @NonNull DownloadableSubscription subscription, boolean switchAfterDownload, boolean forceDeactivateSim) { return Integer.MIN_VALUE; } 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 48ab1c5..5056da3 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 @@ -37,7 +37,7 @@ interface LocalProfileAssistant { fun deleteProfile(iccid: String): Boolean fun downloadProfile(smdp: String, matchingId: String?, imei: String?, - confirmationCode: String?, callback: ProfileDownloadCallback) + confirmationCode: String?, preview: Boolean, callback: ProfileDownloadCallback) fun deleteNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): 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 370fcab..956736e 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 @@ -28,7 +28,7 @@ internal object LpacJni { // es9p + es10b // We do not expose all of the functions because of tediousness :) external fun downloadProfile(handle: Long, smdp: String, matchingId: String?, imei: String?, - confirmationCode: String?, callback: ProfileDownloadCallback): Int + confirmationCode: String?, preview: Boolean, callback: ProfileDownloadCallback): Int external fun downloadErrCodeToString(code: Int): String external fun handleNotification(handle: Long, seqNumber: Long): Int // Cancel any ongoing es9p and/or es10b sessions diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt index 289ddf6..aa37540 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt @@ -21,5 +21,6 @@ interface ProfileDownloadCallback { Finalizing(80), // load bpp } + fun onMetadataReceived(profileName: String, iccid: String, appCerts: String) fun onStateUpdate(state: DownloadState) } \ 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 7310acd..ff1a9b7 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 @@ -202,13 +202,14 @@ class LocalProfileAssistantImpl( @Synchronized override fun downloadProfile(smdp: String, matchingId: String?, imei: String?, - confirmationCode: String?, callback: ProfileDownloadCallback) { + confirmationCode: String?, preview: Boolean, callback: ProfileDownloadCallback) { val res = LpacJni.downloadProfile( contextHandle, smdp, matchingId, imei, confirmationCode, + preview, callback ) 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 028e30d..04bc331 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 @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include #include @@ -60,7 +62,7 @@ JNIEXPORT jint JNICALL Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, jlong handle, jstring smdp, jstring matching_id, jstring imei, jstring confirmation_code, - jobject callback) { + jboolean preview, jobject callback) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; struct es10b_load_bound_profile_package_result es10b_load_bound_profile_package_result; const char *_confirmation_code = NULL; @@ -109,6 +111,63 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadProfile(JNIEnv *env, jobject thiz, j goto out; } + if (ctx->http._internal.prepare_download_param->b64_profileMetadata) { + struct es8p_metadata *profile_metadata = NULL; + ret = es8p_metadata_parse(&profile_metadata, ctx->http._internal.prepare_download_param->b64_profileMetadata); + + if (ret == 0) { + const char *profile_name = profile_metadata->serviceProviderName ? profile_metadata->serviceProviderName : "Unknown"; + const char *iccid = (profile_metadata->iccid[0] != '\0') ? profile_metadata->iccid : "Unknown"; + + size_t totalLength = 0; + for (int i = 0; i < profile_metadata->numRefArDo; i++) { + if (profile_metadata->refArDoList[i].deviceAppIdRefDo) { + totalLength += (profile_metadata->refArDoList[i].deviceAppIdLength * 2) + 1; + } + } + + char *certSignatures = malloc(totalLength + 1); + if (certSignatures) { + certSignatures[0] = '\0'; + for (int i = 0; i < profile_metadata->numRefArDo; i++) { + if (profile_metadata->refArDoList[i].deviceAppIdRefDo) { + char deviceAppIdHex[profile_metadata->refArDoList[i].deviceAppIdLength * 2 + 1]; + euicc_hexutil_bin2hex(deviceAppIdHex, sizeof(deviceAppIdHex), + profile_metadata->refArDoList[i].deviceAppIdRefDo, + profile_metadata->refArDoList[i].deviceAppIdLength); + strcat(certSignatures, deviceAppIdHex); + strcat(certSignatures, ","); + } + } + if (strlen(certSignatures) > 0 && certSignatures[strlen(certSignatures) - 1] == ',') { + certSignatures[strlen(certSignatures) - 1] = '\0'; + } + } + + jclass callbackClass = (*env)->GetObjectClass(env, callback); + jmethodID onMetadataReceived = (*env)->GetMethodID(env, callbackClass, "onMetadataReceived", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + + if (onMetadataReceived) { + jstring jProfileName = (*env)->NewStringUTF(env, profile_name); + jstring jIccid = (*env)->NewStringUTF(env, iccid); + jstring jCertSignatures = (*env)->NewStringUTF(env, certSignatures ? certSignatures : ""); + + (*env)->CallVoidMethod(env, callback, onMetadataReceived, jProfileName, jIccid, jCertSignatures); + + (*env)->DeleteLocalRef(env, jProfileName); + (*env)->DeleteLocalRef(env, jIccid); + (*env)->DeleteLocalRef(env, jCertSignatures); + } + + es8p_metadata_free(&profile_metadata); + } + + if (preview) { + 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); @@ -178,4 +237,4 @@ Java_net_typeblog_lpac_1jni_LpacJni_downloadErrCodeToString(JNIEnv *env, jobject default: return toJString(env, "ES10B_ERROR_REASON_UNDEFINED"); } -} \ No newline at end of file +}