/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ons; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PersistableBundle; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.euicc.DownloadableSubscription; import android.telephony.euicc.EuiccManager; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import java.util.Stack; public class ONSProfileDownloader { interface IONSProfileDownloaderListener { void onDownloadComplete(int primarySubId); void onDownloadError(int pSIMSubId, DownloadRetryResultCode resultCode, int detailErrCode); } private static final String TAG = ONSProfileDownloader.class.getName(); public static final String ACTION_ONS_ESIM_DOWNLOAD = "com.android.ons.action.ESIM_DOWNLOAD"; @VisibleForTesting protected static final String PARAM_PRIMARY_SUBID = "PrimarySubscriptionID"; @VisibleForTesting protected static final String PARAM_REQUEST_TYPE = "REQUEST"; @VisibleForTesting protected static final int REQUEST_CODE_DOWNLOAD_SUB = 1; private final Handler mHandler; private final Context mContext; private final CarrierConfigManager mCarrierConfigManager; private final EuiccManager mEuiccManager; private final SubscriptionManager mSubManager; private final ONSProfileConfigurator mONSProfileConfig; private IONSProfileDownloaderListener mListener; // Subscription Id of the CBRS PSIM for which opportunistic eSIM is being downloaded. Used to // ignore duplicate download requests when download is in progress. private int mDownloadingPSimSubId; protected enum DownloadRetryResultCode { DOWNLOAD_SUCCESSFUL, ERR_UNRESOLVABLE, ERR_MEMORY_FULL, ERR_INSTALL_ESIM_PROFILE_FAILED, ERR_RETRY_DOWNLOAD }; public ONSProfileDownloader(Context context, CarrierConfigManager carrierConfigManager, EuiccManager euiccManager, SubscriptionManager subManager, ONSProfileConfigurator onsProfileConfigurator, IONSProfileDownloaderListener listener) { mContext = context; mListener = listener; mEuiccManager = euiccManager; mSubManager = subManager; mONSProfileConfig = onsProfileConfigurator; mCarrierConfigManager = carrierConfigManager; mHandler = new DownloadHandler(); } class DownloadHandler extends Handler { DownloadHandler() { super(Looper.myLooper()); } @Override public void handleMessage(Message msg) { switch (msg.what) { // Received Response for download request. REQUEST_CODE_DOWNLOAD_SUB was sent to LPA // as part of request intent. case REQUEST_CODE_DOWNLOAD_SUB: { Log.d(TAG, "REQUEST_CODE_DOWNLOAD_SUB callback received"); //Clear downloading subscription flag. Indicates no download in progress. synchronized (this) { mDownloadingPSimSubId = -1; } int pSIMSubId = ((Intent) msg.obj).getIntExtra(PARAM_PRIMARY_SUBID, 0); int detailedErrCode = ((Intent) msg.obj).getIntExtra( EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0); int operationCode = ((Intent) msg.obj).getIntExtra( EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_OPERATION_CODE, 0); int errorCode = ((Intent) msg.obj).getIntExtra( EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_ERROR_CODE, 0); Log.d(TAG, "Result Code : " + detailedErrCode); Log.d(TAG, "Operation Code : " + operationCode); Log.d(TAG, "Error Code : " + errorCode); DownloadRetryResultCode resultCode = mapDownloaderErrorCode(msg.arg1, detailedErrCode, operationCode, errorCode); Log.d(TAG, "DownloadRetryResultCode: " + resultCode); switch (resultCode) { case DOWNLOAD_SUCCESSFUL: mListener.onDownloadComplete(pSIMSubId); break; case ERR_UNRESOLVABLE: mListener.onDownloadError(pSIMSubId, resultCode, detailedErrCode); Log.e(TAG, "Unresolvable download error: " + getUnresolvableErrorDescription(errorCode)); break; default: mListener.onDownloadError(pSIMSubId, resultCode, detailedErrCode); break; } } break; } } @VisibleForTesting protected DownloadRetryResultCode mapDownloaderErrorCode(int resultCode, int detailedErrCode, int operationCode, int errorCode) { if (operationCode == EuiccManager.OPERATION_SMDX_SUBJECT_REASON_CODE) { //SMDP Error codes handling Pair errCode = decodeSmdxSubjectAndReasonCode(detailedErrCode); Log.e(TAG, " Subject Code: " + errCode.first + " Reason Code: " + errCode.second); //8.1 - eUICC, 4.8 - Insufficient Memory // eUICC does not have sufficient space for this Profile. if (errCode.equals(Pair.create("8.1.0", "4.8"))) { return DownloadRetryResultCode.ERR_MEMORY_FULL; } //8.8.5 - Download order, 4.10 - Time to Live Expired //The Download order has expired if (errCode.equals(Pair.create("8.8.5", "4.10"))) { return DownloadRetryResultCode.ERR_RETRY_DOWNLOAD; } //All other errors are unresolvable or retry after SIM State Change return DownloadRetryResultCode.ERR_UNRESOLVABLE; } switch (errorCode) { //Success Cases case EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK: { return DownloadRetryResultCode.DOWNLOAD_SUCCESSFUL; } //Low eUICC memory cases case EuiccManager.ERROR_EUICC_INSUFFICIENT_MEMORY: { Log.d(TAG, "Download ERR: EUICC_INSUFFICIENT_MEMORY"); return DownloadRetryResultCode.ERR_MEMORY_FULL; } //Temporary download error cases case EuiccManager.ERROR_TIME_OUT: case EuiccManager.ERROR_CONNECTION_ERROR: case EuiccManager.ERROR_OPERATION_BUSY: { return DownloadRetryResultCode.ERR_RETRY_DOWNLOAD; } //Profile installation failure cases case EuiccManager.ERROR_INSTALL_PROFILE: { return DownloadRetryResultCode.ERR_INSTALL_ESIM_PROFILE_FAILED; } default: { return DownloadRetryResultCode.ERR_UNRESOLVABLE; } } } } private String getUnresolvableErrorDescription(int errorCode) { switch (errorCode) { case EuiccManager.ERROR_INVALID_ACTIVATION_CODE: return "ERROR_INVALID_ACTIVATION_CODE"; case EuiccManager.ERROR_UNSUPPORTED_VERSION: return "ERROR_UNSUPPORTED_VERSION"; case EuiccManager.ERROR_INSTALL_PROFILE: return "ERROR_INSTALL_PROFILE"; case EuiccManager.ERROR_SIM_MISSING: return "ERROR_SIM_MISSING"; case EuiccManager.ERROR_ADDRESS_MISSING: return "ERROR_ADDRESS_MISSING"; case EuiccManager.ERROR_CERTIFICATE_ERROR: return "ERROR_CERTIFICATE_ERROR"; case EuiccManager.ERROR_NO_PROFILES_AVAILABLE: return "ERROR_NO_PROFILES_AVAILABLE"; case EuiccManager.ERROR_CARRIER_LOCKED: return "ERROR_CARRIER_LOCKED"; } return "Unknown"; } protected enum DownloadProfileResult { SUCCESS, DUPLICATE_REQUEST, INVALID_SMDP_ADDRESS } protected DownloadProfileResult downloadProfile(int primarySubId) { Log.d(TAG, "downloadProfile"); //Get SMDP address from carrier configuration. String smdpAddress = getSMDPServerAddress(primarySubId); if (smdpAddress == null || smdpAddress.length() <= 0) { return DownloadProfileResult.INVALID_SMDP_ADDRESS; } synchronized (this) { if (mDownloadingPSimSubId == primarySubId) { Log.d(TAG, "Download already in progress."); return DownloadProfileResult.DUPLICATE_REQUEST; } mDownloadingPSimSubId = primarySubId; } //Generate Activation code 1${SM-DP+ FQDN}$ String activationCode = "1$" + smdpAddress + "$"; Intent intent = new Intent(mContext, ONSProfileResultReceiver.class); intent.setAction(ACTION_ONS_ESIM_DOWNLOAD); intent.putExtra(PARAM_REQUEST_TYPE, REQUEST_CODE_DOWNLOAD_SUB); intent.putExtra(PARAM_PRIMARY_SUBID, primarySubId); PendingIntent callbackIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE_DOWNLOAD_SUB, intent, PendingIntent.FLAG_MUTABLE); Log.d(TAG, "Download Request sent to EUICC Manager"); // TODO: Check with euicc team if card specific EUICC Manager should be used to download. // Once the eSIM profile is downloaded, port is auto decided by eUICC to activate the // subscription. If there is no available port, an // {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} will be returned in the callback // intent to prompt the user to disable an already-active subscription. However, ONS will // not show any prompt to the user and silently fails to activate the subscription. // ONS will try to provision again when carrier configuration change event is received. SubscriptionInfo primarySubInfo = mSubManager.getActiveSubscriptionInfo(primarySubId); if (primarySubInfo != null && primarySubInfo.isEmbedded()) { EuiccManager euiccManager = mEuiccManager.createForCardId(primarySubInfo.getCardId()); euiccManager.downloadSubscription(DownloadableSubscription.forActivationCode( activationCode), true, callbackIntent); } else { mEuiccManager.downloadSubscription(DownloadableSubscription.forActivationCode( activationCode), true, callbackIntent); } return DownloadProfileResult.SUCCESS; } /** * Retrieves SMDP+ server address of the given subscription from carrier configuration. * * @param subscriptionId subscription Id of the primary SIM. * @return FQDN of SMDP+ server. */ private String getSMDPServerAddress(int subscriptionId) { PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subscriptionId); if (config != null) { return config.getString(CarrierConfigManager.KEY_SMDP_SERVER_ADDRESS_STRING); } else { return null; } } /** * Given encoded error code described in * {@link android.telephony.euicc.EuiccManager#OPERATION_SMDX_SUBJECT_REASON_CODE} decode it * into SubjectCode[5.2.6.1] and ReasonCode[5.2.6.2] from GSMA (SGP.22 v2.2) * * @param resultCode from * {@link android.telephony.euicc.EuiccManager#OPERATION_SMDX_SUBJECT_REASON_CODE} * * @return a pair containing SubjectCode[5.2.6.1] and ReasonCode[5.2.6.2] from GSMA (SGP.22 * v2.2) */ @VisibleForTesting protected static Pair decodeSmdxSubjectAndReasonCode(int resultCode) { final int numOfSections = 6; final int bitsPerSection = 4; final int sectionMask = 0xF; final Stack sections = new Stack<>(); // Extracting each section of digits backwards. for (int i = 0; i < numOfSections; ++i) { int sectionDigit = resultCode & sectionMask; sections.push(sectionDigit); resultCode = resultCode >>> bitsPerSection; } String subjectCode = sections.pop() + "." + sections.pop() + "." + sections.pop(); String reasonCode = sections.pop() + "." + sections.pop() + "." + sections.pop(); // drop the leading zeros, e.g. 0.1 -> 1, 0.0.3 -> 3, 0.5.1 -> 5.1 subjectCode = subjectCode.replaceAll("^(0\\.)*", ""); reasonCode = reasonCode.replaceAll("^(0\\.)*", ""); return Pair.create(subjectCode, reasonCode); } /** * Callback to receive result for subscription activate request and process the same. * @param intent * @param resultCode */ public void onCallbackIntentReceived(Intent intent, int resultCode) { Message msg = new Message(); msg.what = REQUEST_CODE_DOWNLOAD_SUB; msg.arg1 = resultCode; msg.obj = intent; mHandler.sendMessage(msg); } }