diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-05-10 15:44:57 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-05-10 15:44:57 +0000 |
commit | 70f20a4192a48accf1b3c210a932c06466cda80d (patch) | |
tree | 96805df0d6e5ebd69702d50d5c50b7b6f8456353 | |
parent | 8e355def9a181678491c8e8cdb7e007cd7001d33 (diff) | |
parent | 7ec98147c9ecc867ab512ef61da4a6c0d3f0c084 (diff) | |
download | service_entitlement-busytown-mac-infra-release.tar.gz |
Snap for 11819167 from 7ec98147c9ecc867ab512ef61da4a6c0d3f0c084 to busytown-mac-infra-releasebusytown-mac-infra-release
Change-Id: I64b2bfe518668c5a5f86928b4d44e9aa57f4c3d6
33 files changed, 7178 insertions, 460 deletions
@@ -22,6 +22,7 @@ java_defaults { libs: [ "androidx.annotation_annotation", "auto_value_annotations", + "error_prone_annotations", ], plugins: ["auto_value_plugin"], sdk_version: "system_current", @@ -37,7 +38,6 @@ java_library { srcs: [ "java/com/android/libraries/entitlement/eapaka/*.java", "java/com/android/libraries/entitlement/http/*.java", - "java/com/android/libraries/entitlement/utils/*.java", ], static_libs: [ "guava", @@ -71,6 +71,8 @@ java_library { srcs: [ "java/com/android/libraries/entitlement/ServiceEntitlement.java", "java/com/android/libraries/entitlement/EapAkaHelper.java", + "java/com/android/libraries/entitlement/Ts43Authentication.java", + "java/com/android/libraries/entitlement/Ts43Operation.java", ], static_libs: [ "guava", @@ -95,6 +97,8 @@ java_library { "java/com/android/libraries/entitlement/EsimOdsaOperation.java", "java/com/android/libraries/entitlement/ServiceEntitlementException.java", "java/com/android/libraries/entitlement/ServiceEntitlementRequest.java", + "java/com/android/libraries/entitlement/odsa/*.java", + "java/com/android/libraries/entitlement/utils/*.java", ], static_libs: [ "guava", diff --git a/java/com/android/libraries/entitlement/CarrierConfig.java b/java/com/android/libraries/entitlement/CarrierConfig.java index 44a4170..d0a63f8 100644 --- a/java/com/android/libraries/entitlement/CarrierConfig.java +++ b/java/com/android/libraries/entitlement/CarrierConfig.java @@ -32,9 +32,23 @@ public abstract class CarrierConfig { /** Default value of {@link #timeoutInSec} if not set. */ public static final int DEFAULT_TIMEOUT_IN_SEC = 30; + public static final String CLIENT_TS_43_IMS_ENTITLEMENT = "IMS-Entitlement"; + public static final String CLIENT_TS_43_COMPANION_ODSA = "Companion-ODSA"; + public static final String CLIENT_TS_43_PRIMARY_ODSA = "Primary-ODSA"; + public static final String CLIENT_TS_43_SERVER_ODSA = "Server-ODSA"; + /** The carrier's entitlement server URL. See {@link Builder#setServerUrl}. */ public abstract String serverUrl(); + /** + * Client-ts43 attribute. Used to set the User-Agent header in HTTP requests as defined in TS.43 + * section 2.2. + */ + public abstract String clientTs43(); + + /** Returns {@code true} if HTTP POST, instead of GET, should be used for TS.43 requests. */ + public abstract boolean useHttpPost(); + /** Client side timeout for HTTP connection. See {@link Builder#setTimeoutInSec}. */ public abstract int timeoutInSec(); @@ -46,6 +60,8 @@ public abstract class CarrierConfig { public static Builder builder() { return new AutoValue_CarrierConfig.Builder() .setServerUrl("") + .setClientTs43("") + .setUseHttpPost(false) .setTimeoutInSec(DEFAULT_TIMEOUT_IN_SEC); } @@ -60,6 +76,12 @@ public abstract class CarrierConfig { */ public abstract Builder setServerUrl(String url); + /** Sets the Client-ts43 attribute. Used to set the User-Agent header in HTTP requests. */ + public abstract Builder setClientTs43(String clientTs43); + + /** Set to {@code true} to use HTTP POST instead of GET for TS.43 requests. */ + public abstract Builder setUseHttpPost(boolean useHttpPost); + /** * Sets the client side timeout for HTTP connection. Default to * {@link DEFAULT_TIMEOUT_IN_SEC}. diff --git a/java/com/android/libraries/entitlement/EapAkaHelper.java b/java/com/android/libraries/entitlement/EapAkaHelper.java index e5af73e..f29cb0f 100644 --- a/java/com/android/libraries/entitlement/EapAkaHelper.java +++ b/java/com/android/libraries/entitlement/EapAkaHelper.java @@ -87,7 +87,7 @@ public class EapAkaHelper { EapAkaResponse eapAkaResponse = getEapAkaResponse(challenge); return (eapAkaResponse == null) ? null - : eapAkaResponse.response(); // Would be null on synchrinization failure + : eapAkaResponse.response(); // Would be null on synchronization failure } /** diff --git a/java/com/android/libraries/entitlement/EsimOdsaOperation.java b/java/com/android/libraries/entitlement/EsimOdsaOperation.java index 9a3eae6..e7b28f6 100644 --- a/java/com/android/libraries/entitlement/EsimOdsaOperation.java +++ b/java/com/android/libraries/entitlement/EsimOdsaOperation.java @@ -16,189 +16,312 @@ package com.android.libraries.entitlement; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.StringDef; + import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** - * HTTP request parameters specific to on device service actiavation (ODSA). See GSMA spec TS.43 + * HTTP request parameters specific to on device service activation (ODSA). See GSMA spec TS.43 * section 6.2. */ @AutoValue public abstract class EsimOdsaOperation { - /** - * OSDA operation: CheckEligibility. - */ + /** ODSA operation unknown. For initialization only. */ + public static final String OPERATION_UNKNOWN = ""; + + /** ODSA operation: CheckEligibility. */ public static final String OPERATION_CHECK_ELIGIBILITY = "CheckEligibility"; - /** - * OSDA operation: ManageSubscription. - */ + + /** ODSA operation: ManageSubscription. */ public static final String OPERATION_MANAGE_SUBSCRIPTION = "ManageSubscription"; - /** - * OSDA operation: ManageService. - */ + + /** ODSA operation: ManageService. */ public static final String OPERATION_MANAGE_SERVICE = "ManageService"; - /** - * OSDA operation: AcquireConfiguration. - */ + + /** ODSA operation: AcquireConfiguration. */ public static final String OPERATION_ACQUIRE_CONFIGURATION = "AcquireConfiguration"; - /** - * OSDA operation: AcquireTemporaryToken. - */ + + /** ODSA operation: AcquireTemporaryToken. */ public static final String OPERATION_ACQUIRE_TEMPORARY_TOKEN = "AcquireTemporaryToken"; - /** - * Indicates that operation_type is not set. - */ + /** ODSA operation: GetPhoneNumber */ + public static final String OPERATION_GET_PHONE_NUMBER = "GetPhoneNumber"; + + /** ODSA operation: AcquirePlan */ + public static final String OPERATION_ACQUIRE_PLAN = "AcquirePlan"; + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + OPERATION_UNKNOWN, + OPERATION_CHECK_ELIGIBILITY, + OPERATION_MANAGE_SUBSCRIPTION, + OPERATION_MANAGE_SERVICE, + OPERATION_ACQUIRE_CONFIGURATION, + OPERATION_ACQUIRE_PLAN, + OPERATION_ACQUIRE_TEMPORARY_TOKEN, + OPERATION_GET_PHONE_NUMBER + }) + public @interface OdsaOperation { + } + + /** eSIM device’s service is unknown. */ + public static final int SERVICE_STATUS_UNKNOWN = -1; + + /** eSIM device’s service is activated. */ + public static final int SERVICE_STATUS_ACTIVATED = 1; + + /** eSIM device’s service is being activated. */ + public static final int SERVICE_STATUS_ACTIVATING = 2; + + /** eSIM device’s service is not activated. */ + public static final int SERVICE_STATUS_DEACTIVATED = 3; + + /** eSIM device’s service is not activated and the associated ICCID should not be reused. */ + public static final int SERVICE_STATUS_DEACTIVATED_NO_REUSE = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SERVICE_STATUS_UNKNOWN, + SERVICE_STATUS_ACTIVATED, + SERVICE_STATUS_ACTIVATING, + SERVICE_STATUS_DEACTIVATED, + SERVICE_STATUS_DEACTIVATED_NO_REUSE + }) + public @interface OdsaServiceStatus { + } + + /** Indicates that operation_type is not set. */ public static final int OPERATION_TYPE_NOT_SET = -1; - /** - * To activate a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. - */ + + /** To activate a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ public static final int OPERATION_TYPE_SUBSCRIBE = 0; - /** - * To cancel a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. - */ + + /** To cancel a subscription, used by {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ public static final int OPERATION_TYPE_UNSUBSCRIBE = 1; - /** - * To manage an existing subscription, for {@link #OPERATION_MANAGE_SUBSCRIPTION}. - */ + + /** To manage an existing subscription, for {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ public static final int OPERATION_TYPE_CHANGE_SUBSCRIPTION = 2; + /** * To transfer a subscription from an existing device, used by {@link * #OPERATION_MANAGE_SUBSCRIPTION}. */ public static final int OPERATION_TYPE_TRANSFER_SUBSCRIPTION = 3; + /** * To inform the network of a subscription update, used by * {@link #OPERATION_MANAGE_SUBSCRIPTION}. */ public static final int OPERATION_TYPE_UPDATE_SUBSCRIPTION = 4; - /** - * To activate a service, used by {@link #OPERATION_MANAGE_SERVICE}. - */ + + /** To activate a service, used by {@link #OPERATION_MANAGE_SERVICE}. */ public static final int OPERATION_TYPE_ACTIVATE_SERVICE = 10; - /** - * To deactivate a service, used by {@link #OPERATION_MANAGE_SERVICE}. - */ + + /** To deactivate a service, used by {@link #OPERATION_MANAGE_SERVICE}. */ public static final int OPERATION_TYPE_DEACTIVATE_SERVICE = 11; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + OPERATION_TYPE_NOT_SET, + OPERATION_TYPE_SUBSCRIBE, + OPERATION_TYPE_UNSUBSCRIBE, + OPERATION_TYPE_CHANGE_SUBSCRIPTION, + OPERATION_TYPE_TRANSFER_SUBSCRIPTION, + OPERATION_TYPE_UPDATE_SUBSCRIPTION, + OPERATION_TYPE_ACTIVATE_SERVICE, + OPERATION_TYPE_DEACTIVATE_SERVICE + }) + public @interface OdsaOperationType { + } + + /** Operation result unknown. */ + public static final int OPERATION_RESULT_UNKNOWN = -1; + + /** Operation was a success. */ + public static final int OPERATION_RESULT_SUCCESS = 1; + + /** There was a general error during processing. */ + public static final int OPERATION_RESULT_ERROR_GENERAL = 100; + + /** An invalid operation value was provided in request. */ + public static final int OPERATION_RESULT_ERROR_INVALID_OPERATION = 101; + + /** An invalid parameter name or value was provided in request. */ + public static final int OPERATION_RESULT_ERROR_INVALID_PARAMETER = 102; + /** - * Indicates the companion device carries the same MSISDN as the primary device. - */ - public static final String COMPANION_SERVICE_SHAERED_NUMBER = "SharedNumber"; - /** - * Indicates the companion device carries a different MSISDN as the primary device. + * The optional operation is not supported by the carrier. Device should continue with the flow. + * This error only applies to optional operations (for example ManageService). */ + public static final int OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION = 103; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + OPERATION_RESULT_UNKNOWN, + OPERATION_RESULT_SUCCESS, + OPERATION_RESULT_ERROR_GENERAL, + OPERATION_RESULT_ERROR_INVALID_OPERATION, + OPERATION_RESULT_ERROR_INVALID_PARAMETER, + OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION + }) + public @interface OdsaOperationResult { + } + + /** Companion service unknown. For initialization only. */ + public static final String COMPANION_SERVICE_UNKNOWN = ""; + + /** Indicates the companion device carries the same MSISDN as the primary device. */ + public static final String COMPANION_SERVICE_SHARED_NUMBER = "SharedNumber"; + + /** Indicates the companion device carries a different MSISDN as the primary device. */ public static final String COMPANION_SERVICE_DIFFERENT_NUMBER = "DiffNumber"; - /** - * Returns the eSIM ODSA operation. Used by HTTP parameter "operation". - */ + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + COMPANION_SERVICE_UNKNOWN, + COMPANION_SERVICE_SHARED_NUMBER, + COMPANION_SERVICE_DIFFERENT_NUMBER + }) + public @interface CompanionService { + } + + /** Returns the ODSA operation. Used by HTTP parameter {@code operation}. */ public abstract String operation(); /** - * Returns the detiled type of the eSIM ODSA operation. Used by HTTP parameter - * "operation_type". + * Returns the detailed type of the ODSA operation. Used by HTTP parameter + * {@code operation_type}. */ public abstract int operationType(); /** * Returns the comma separated list of operation targets used with temporary token from - * AcquireTemporaryToken operation. Used by HTTP parameter "operation_targets". + * AcquireTemporaryToken operation. Used by HTTP parameter {@code operation_targets}. */ public abstract ImmutableList<String> operationTargets(); /** * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter - * "companion_terminal_id". + * {@code + * companion_terminal_id}. */ public abstract String companionTerminalId(); /** - * Returns the OEM of the companion device. Used by HTTP parameter "companion_terminal_vendor". + * Returns the OEM of the companion device. Used by HTTP parameter {@code + * companion_terminal_vendor}. */ public abstract String companionTerminalVendor(); /** - * Returns the model of the companion device. Used by HTTP parameter - * "companion_terminal_model". + * Returns the model of the companion device. Used by HTTP parameter {@code + * companion_terminal_model}. */ public abstract String companionTerminalModel(); /** - * Returns the software version of the companion device. Used by HTTP parameter - * "companion_terminal_sw_version". + * Returns the software version of the companion device. Used by HTTP parameter {@code + * companion_terminal_sw_version}. */ public abstract String companionTerminalSoftwareVersion(); /** - * Returns the user-friendly version of the companion device. Used by HTTP parameter - * "companion_terminal_friendly_name". + * Returns the user-friendly version of the companion device. Used by HTTP parameter {@code + * companion_terminal_friendly_name}. */ public abstract String companionTerminalFriendlyName(); /** * Returns the service type of the companion device, e.g. if the MSISDN is same as the primary - * device. Used by HTTP parameter "companion_terminal_service". + * device. Used by HTTP parameter {@code companion_terminal_service}. */ public abstract String companionTerminalService(); /** - * Returns the ICCID of the companion device. Used by HTTP parameter - * "companion_terminal_iccid". + * Returns the ICCID of the companion device. Used by HTTP parameter {@code + * companion_terminal_iccid}. */ public abstract String companionTerminalIccid(); /** - * Returns the EID of the companion device. Used by HTTP parameter "companion_terminal_eid". + * Returns the EID of the companion device. Used by HTTP parameter + * {@code companion_terminal_eid}. */ public abstract String companionTerminalEid(); /** - * Returns the ICCID of the primary device eSIM. Used by HTTP parameter "terminal_iccid". + * Returns the ICCID of the primary device eSIM. Used by HTTP parameter {@code terminal_iccid}. */ public abstract String terminalIccid(); /** - * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter - * "terminal_eid". + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter {@code + * terminal_eid}. */ public abstract String terminalEid(); /** * Returns the unique identifier of the primary device eSIM, like the IMEI associated with the - * eSIM. Used by HTTP parameter "target_terminal_id". + * eSIM. Used by HTTP parameter {@code target_terminal_id}. */ public abstract String targetTerminalId(); /** - * Returns the ICCID primary device eSIM. Used by HTTP parameter "target_terminal_iccid". + * Returns the unique identifiers of the primary device eSIM if more than one, like the IMEIs on + * dual-SIM devices. Used by HTTP parameter {@code target_terminal_imeis}. + * + * <p>This is a non-standard params required by some carriers. + */ + @NonNull + public abstract ImmutableList<String> targetTerminalIds(); + + /** + * Returns the ICCID primary device eSIM. Used by HTTP parameter {@code target_terminal_iccid}. */ public abstract String targetTerminalIccid(); /** - * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter - * "target_terminal_eid". + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter {@code + * target_terminal_eid}. */ public abstract String targetTerminalEid(); - /** - * Returns the unique identifier of the old device eSIM, like the IMEI associated with the - * eSIM. Used by HTTP parameter "old_terminal_id". + * Returns the serial number of primary device. Used by HTTP parameter + * {@code target_terminal_sn}. + * + * <p>This is a non-standard params required by some carriers. */ - public abstract String oldTerminalId(); + @NonNull + public abstract String targetTerminalSerialNumber(); /** - * Returns the ICCID of old device eSIM. Used by HTTP parameter "old_terminal_iccid". + * Returns the model of primary device. Used by HTTP parameter {@code target_terminal_model}. + * + * <p>This is a non-standard params required by some carriers. */ - public abstract String oldTerminalIccid(); + @NonNull + public abstract String targetTerminalModel(); /** - * Returns a new {@link Builder} object. + * Returns the unique identifier of the old device eSIM, like the IMEI associated with the eSIM. + * Used by HTTP parameter {@code old_terminal_id}. */ + public abstract String oldTerminalId(); + + /** Returns the ICCID of old device eSIM. Used by HTTP parameter {@code old_terminal_iccid}. */ + public abstract String oldTerminalIccid(); + + /** Returns a new {@link Builder} object. */ public static Builder builder() { return new AutoValue_EsimOdsaOperation.Builder() - .setOperation("") + .setOperation(OPERATION_UNKNOWN) .setOperationType(OPERATION_TYPE_NOT_SET) .setOperationTargets(ImmutableList.of()) .setCompanionTerminalId("") @@ -206,14 +329,17 @@ public abstract class EsimOdsaOperation { .setCompanionTerminalModel("") .setCompanionTerminalSoftwareVersion("") .setCompanionTerminalFriendlyName("") - .setCompanionTerminalService("") + .setCompanionTerminalService(COMPANION_SERVICE_UNKNOWN) .setCompanionTerminalIccid("") .setCompanionTerminalEid("") .setTerminalIccid("") .setTerminalEid("") .setTargetTerminalId("") + .setTargetTerminalIds(ImmutableList.of()) .setTargetTerminalIccid("") .setTargetTerminalEid("") + .setTargetTerminalSerialNumber("") + .setTargetTerminalModel("") .setOldTerminalId("") .setOldTerminalIccid(""); } @@ -222,26 +348,33 @@ public abstract class EsimOdsaOperation { * Builder. * * <p>For ODSA, the rule of which parameters are required varies or each - * operation/opeation_type. The Javadoc below gives high-level description, but please refer to - * GMSA spec TS.43 section 6.2 for details. + * operation/operation_type. + * The Javadoc below gives high-level description, but please refer to GSMA spec TS.43 section + * 6.2 + * for details. */ @AutoValue.Builder public abstract static class Builder { /** - * Sets the eSIM ODSA operation. Used by HTTP parameter "operation". - * - * <p>Required. + * Sets the eSIM ODSA operation. Used by HTTP parameter {@code operation}. * + * @param operation ODSA operation. + * @return The builder. * @see #OPERATION_CHECK_ELIGIBILITY * @see #OPERATION_MANAGE_SUBSCRIPTION * @see #OPERATION_MANAGE_SERVICE * @see #OPERATION_ACQUIRE_CONFIGURATION + * @see #OPERATION_ACQUIRE_TEMPORARY_TOKEN + * @see #OPERATION_GET_PHONE_NUMBER + * @see #OPERATION_ACQUIRE_PLAN */ - public abstract Builder setOperation(String value); + @NonNull + public abstract Builder setOperation(@NonNull @OdsaOperation String operation); /** - * Sets the detiled type of the eSIM ODSA operation. Used by HTTP parameter "operation_type" - * if set. + * Sets the detailed type of the eSIM ODSA operation. Used by HTTP parameter + * "operation_type" if + * set. * * <p>Required by some operation. * @@ -253,136 +386,252 @@ public abstract class EsimOdsaOperation { * @see #OPERATION_TYPE_ACTIVATE_SERVICE * @see #OPERATION_TYPE_DEACTIVATE_SERVICE */ - public abstract Builder setOperationType(int value); + @NonNull + public abstract Builder setOperationType(@OdsaOperationType int operationType); /** * Sets the operation targets to be used with temporary token from AcquireTemporaryToken - * operation. Used by HTTP parameter "operation_targets" if set. + * operation. Used by HTTP parameter {@code operation_targets} if set. */ - public abstract Builder setOperationTargets(ImmutableList<String> value); + @NonNull + public abstract Builder setOperationTargets( + @NonNull @OdsaOperation ImmutableList<String> operationTargets); /** * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter - * "companion_terminal_id" if set. + * {@code + * companion_terminal_id} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalId The unique identifier of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalId(String value); + @NonNull + public abstract Builder setCompanionTerminalId(@NonNull String companionTerminalId); /** - * Sets the OEM of the companion device. Used by HTTP parameter "companion_terminal_vendor" - * if set. + * Sets the OEM of the companion device. Used by HTTP parameter {@code + * companion_terminal_vendor} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalVendor The OEM of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalVendor(String value); + @NonNull + public abstract Builder setCompanionTerminalVendor(@NonNull String companionTerminalVendor); /** - * Sets the model of the companion device. Used by HTTP parameter "companion_terminal_model" - * if set. + * Sets the model of the companion device. Used by HTTP parameter {@code + * companion_terminal_model} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalModel The model of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalModel(String value); + @NonNull + public abstract Builder setCompanionTerminalModel(@NonNull String companionTerminalModel); /** - * Sets the software version of the companion device. Used by HTTP parameter - * "companion_terminal_sw_version" if set. + * Sets the software version of the companion device. Used by HTTP parameter {@code + * companion_terminal_sw_version} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalSoftwareVersion The software version of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalSoftwareVersion(String value); + @NonNull + public abstract Builder setCompanionTerminalSoftwareVersion( + @NonNull String companionTerminalSoftwareVersion); /** - * Sets the user-friendly version of the companion device. Used by HTTP parameter - * "companion_terminal_friendly_name" if set. + * Sets the user-friendly version of the companion device. Used by HTTP parameter {@code + * companion_terminal_friendly_name} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalFriendlyName The user-friendly version of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalFriendlyName(String value); + @NonNull + public abstract Builder setCompanionTerminalFriendlyName( + @NonNull String companionTerminalFriendlyName); /** * Sets the service type of the companion device, e.g. if the MSISDN is same as the primary - * device. Used by HTTP parameter "companion_terminal_service" if set. + * device. Used by HTTP parameter {@code companion_terminal_service} if set. * * <p>Used by companion device ODSA operation. * - * @see #COMPANION_SERVICE_SHAERED_NUMBER + * @param companionTerminalService The service type of the companion device. + * @return The builder. + * @see #COMPANION_SERVICE_SHARED_NUMBER * @see #COMPANION_SERVICE_DIFFERENT_NUMBER */ - public abstract Builder setCompanionTerminalService(String value); + @NonNull + public abstract Builder setCompanionTerminalService( + @NonNull @CompanionService String companionTerminalService); /** - * Sets the ICCID of the companion device. Used by HTTP parameter "companion_terminal_iccid" - * if set. + * Sets the ICCID of the companion device. Used by HTTP parameter {@code + * companion_terminal_iccid} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalIccid The ICCID of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalIccid(String value); + @NonNull + public abstract Builder setCompanionTerminalIccid(@NonNull String companionTerminalIccid); /** - * Sets the eUICC identifier (EID) of the companion device. Used by HTTP parameter - * "companion_terminal_eid" if set. + * Sets the eUICC identifier (EID) of the companion device. Used by HTTP parameter {@code + * companion_terminal_eid} if set. * * <p>Used by companion device ODSA operation. + * + * @param companionTerminalEid The eUICC identifier (EID) of the companion device. + * @return The builder. */ - public abstract Builder setCompanionTerminalEid(String value); + @NonNull + public abstract Builder setCompanionTerminalEid(@NonNull String companionTerminalEid); /** * Sets the ICCID of the primary device eSIM in case of primary SIM not present. Used by - * HTTP parameter "terminal_eid" if set. + * HTTP + * parameter {@code terminal_eid} if set. * * <p>Used by primary device ODSA operation. + * + * @param terminalIccid The ICCID of the primary device eSIM in case of primary SIM not + * present. + * @return The builder. */ - public abstract Builder setTerminalIccid(String value); + @NonNull + public abstract Builder setTerminalIccid(@NonNull String terminalIccid); /** * Sets the eUICC identifier (EID) of the primary device eSIM in case of primary SIM not - * present. Used by HTTP parameter "terminal_eid" if set. + * present. Used by HTTP parameter {@code terminal_eid} if set. * * <p>Used by primary device ODSA operation. + * + * @param terminalEid The eUICC identifier (EID) of the primary device eSIM in case of + * primary + * SIM not present. + * @return The builder. */ - public abstract Builder setTerminalEid(String value); + @NonNull + public abstract Builder setTerminalEid(@NonNull String terminalEid); /** * Sets the unique identifier of the primary device eSIM in case of multiple SIM, like the - * IMEI associated with the eSIM. Used by HTTP parameter "target_terminal_id" if set. + * IMEI + * associated with the eSIM. Used by HTTP parameter {@code target_terminal_id} if set. * * <p>Used by primary device ODSA operation. + * + * @param targetTerminalId The unique identifier of the primary device eSIM in case of + * multiple + * SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalId(@NonNull String targetTerminalId); + + /** + * Sets the unique identifiers of the primary device eSIM if more than one, like the IMEIs + * on + * dual-SIM devices. Used by HTTP parameter {@code target_terminal_imeis}. + * + * <p>This is a non-standard params required by some carriers. + * + * @param targetTerminalIds The unique identifiers of the primary device eSIM if more than + * one. + * @return The builder. */ - public abstract Builder setTargetTerminalId(String value); + public abstract Builder setTargetTerminalIds( + @NonNull ImmutableList<String> targetTerminalIds); /** - * Sets the ICCID primary device eSIM in case of multiple SIM. Used by HTTP parameter - * "target_terminal_iccid" if set. + * Sets the ICCID primary device eSIM in case of multiple SIM. Used by HTTP parameter {@code + * target_terminal_iccid} if set. * * <p>Used by primary device ODSA operation. + * + * @param targetTerminalIccid The ICCID primary device eSIM in case of multiple SIM. + * @return The builder. */ - public abstract Builder setTargetTerminalIccid(String value); + @NonNull + public abstract Builder setTargetTerminalIccid(@NonNull String targetTerminalIccid); /** * Sets the eUICC identifier (EID) of the primary device eSIM in case of multiple SIM. Used - * by HTTP parameter "target_terminal_eid" if set. + * by + * HTTP parameter {@code target_terminal_eid} if set. * * <p>Used by primary device ODSA operation. + * + * @param terminalEid The eUICC identifier (EID) of the primary device eSIM in case of + * multiple + * SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalEid(@NonNull String terminalEid); + + /** + * Sets the serial number of primary device. Used by HTTP parameter + * {@code target_terminal_sn}. + * + * @param targetTerminalSerialNumber The serial number of primary device. + * <p>This is a non-standard params required by some + * carriers. + * @return The builder. */ - public abstract Builder setTargetTerminalEid(String value); + @NonNull + public abstract Builder setTargetTerminalSerialNumber( + @NonNull String targetTerminalSerialNumber); + + /** + * Sets the model of primary device. Used by HTTP parameter {@code target_terminal_model}. + * + * @param targetTerminalModel The model of primary device. + * <p>This is a non-standard params required by some carriers. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalModel(@NonNull String targetTerminalModel); /** * Sets the unique identifier of the old device eSIM, like the IMEI associated with the - * eSIM. Used by HTTP parameter "old_terminal_id" if set. + * eSIM. + * Used by HTTP parameter {@code old_terminal_id} if set. * * <p>Used by primary device ODSA operation. + * + * @param oldTerminalId The unique identifier of the old device eSIM. + * @return The builder. */ - public abstract Builder setOldTerminalId(String value); + @NonNull + public abstract Builder setOldTerminalId(@NonNull String oldTerminalId); /** - * Sets the ICCID old device eSIM. Used by HTTP parameter "old_terminal_iccid" if set. + * Sets the ICCID old device eSIM. Used by HTTP parameter {@code old_terminal_iccid} if set. * * <p>Used by primary device ODSA operation. + * + * @param oldTerminalIccid The ICCID old device eSIM. + * @return The builder. */ - public abstract Builder setOldTerminalIccid(String value); + @NonNull + public abstract Builder setOldTerminalIccid(@NonNull String oldTerminalIccid); + /** Returns the {@link EsimOdsaOperation} object. */ + @NonNull public abstract EsimOdsaOperation build(); } } diff --git a/java/com/android/libraries/entitlement/ServiceEntitlement.java b/java/com/android/libraries/entitlement/ServiceEntitlement.java index e050cd3..af30de6 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlement.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlement.java @@ -18,67 +18,93 @@ package com.android.libraries.entitlement; import android.content.Context; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.libraries.entitlement.eapaka.EapAkaApi; +import com.android.libraries.entitlement.http.HttpResponse; +import com.android.libraries.entitlement.utils.DebugUtils; +import com.android.libraries.entitlement.utils.Ts43Constants; import com.google.common.collect.ImmutableList; import java.util.List; /** - * Implemnets protocol for carrier service entitlement configuration query and operation, based on + * Implements protocol for carrier service entitlement configuration query and operation, based on * GSMA TS.43 spec. */ public class ServiceEntitlement { /** * App ID for Voice-Over-LTE entitlement. */ - public static final String APP_VOLTE = "ap2003"; + public static final String APP_VOLTE = Ts43Constants.APP_VOLTE; /** * App ID for Voice-Over-WiFi entitlement. */ - public static final String APP_VOWIFI = "ap2004"; + public static final String APP_VOWIFI = Ts43Constants.APP_VOWIFI; /** * App ID for SMS-Over-IP entitlement. */ - public static final String APP_SMSOIP = "ap2005"; + public static final String APP_SMSOIP = Ts43Constants.APP_SMSOIP; /** - * App ID for on device service activation (OSDA) for companion device. + * App ID for on device service activation (ODSA) for companion device. */ - public static final String APP_ODSA_COMPANION = "ap2006"; + public static final String APP_ODSA_COMPANION = Ts43Constants.APP_ODSA_COMPANION; /** - * App ID for on device service activation (OSDA) for primary device. + * App ID for on device service activation (ODSA) for primary device. */ - public static final String APP_ODSA_PRIMARY = "ap2009"; + public static final String APP_ODSA_PRIMARY = Ts43Constants.APP_ODSA_PRIMARY; /** - * App ID for premium network slice entitlement + * App ID for data plan information entitlement. */ - public static final String APP_PREMIUM_NETWORK_SLICE = "ap2012"; + public static final String APP_DATA_PLAN_BOOST = Ts43Constants.APP_DATA_PLAN_BOOST; + + /** + * App ID for server initiated requests, entitlement and activation. + */ + public static final String APP_ODSA_SERVER_INITIATED_REQUESTS = + Ts43Constants.APP_ODSA_SERVER_INITIATED_REQUESTS; + + /** + * App ID for direct carrier billing. + */ + public static final String APP_DIRECT_CARRIER_BILLING = + Ts43Constants.APP_DIRECT_CARRIER_BILLING; + + /** + * App ID for private user identity. + */ + public static final String APP_PRIVATE_USER_IDENTITY = Ts43Constants.APP_PRIVATE_USER_IDENTITY; + + /** + * App ID for phone number information. + */ + public static final String APP_PHONE_NUMBER_INFORMATION = + Ts43Constants.APP_PHONE_NUMBER_INFORMATION; + + /** + * App ID for satellite entitlement. + */ + public static final String APP_SATELLITE_ENTITLEMENT = Ts43Constants.APP_SATELLITE_ENTITLEMENT; private final CarrierConfig carrierConfig; private final EapAkaApi eapAkaApi; - + private ServiceEntitlementRequest mOidcRequest; /** * Creates an instance for service entitlement configuration query and operation for the * carrier. * * @param context context of application * @param carrierConfig carrier specific configs used in the queries and operations. - * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates + * @param simSubscriptionId the subscription ID of the carrier's SIM on device. This indicates * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA * authentication with. See * {@link android.telephony.SubscriptionManager} - * for how to get the subscroption ID. + * for how to get the subscription ID. */ public ServiceEntitlement(Context context, CarrierConfig carrierConfig, int simSubscriptionId) { - this( - context, - carrierConfig, - simSubscriptionId, - /* saveHttpHistory= */ false, - /* bypassEapAkaResponse= */ ""); + this(context, carrierConfig, simSubscriptionId, /* saveHttpHistory= */ false); } /** @@ -87,9 +113,9 @@ public class ServiceEntitlement { * * @param context context of application * @param carrierConfig carrier specific configs used in the queries and operations. - * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates + * @param simSubscriptionId the subscription ID of the carrier's SIM on device. This indicates * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA authentication with. See {@link - * android.telephony.SubscriptionManager} for how to get the subscroption ID. + * android.telephony.SubscriptionManager} for how to get the subscription ID. * @param saveHttpHistory set to {@code true} to save the history of request and response which * can later be retrieved by {@code getHistory()}. Intended for debugging. */ @@ -103,7 +129,7 @@ public class ServiceEntitlement { carrierConfig, simSubscriptionId, saveHttpHistory, - /* bypassEapAkaResponse= */ ""); + DebugUtils.getBypassEapAkaResponse()); } /** @@ -112,9 +138,9 @@ public class ServiceEntitlement { * * @param context context of application * @param carrierConfig carrier specific configs used in the queries and operations. - * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates + * @param simSubscriptionId the subscription ID of the carrier's SIM on device. This indicates * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA authentication with. See {@link - * android.telephony.SubscriptionManager} for how to get the subscroption ID. + * android.telephony.SubscriptionManager} for how to get the subscription ID. * @param saveHttpHistory set to {@code true} to save the history of request and response which * can later be retrieved by {@code getHistory()}. Intended for debugging. * @param bypassEapAkaResponse set to non empty string to bypass EAP-AKA authentication. @@ -155,14 +181,14 @@ public class ServiceEntitlement { * <li>"token": not set, or {@code request.authenticationToken()} if it's not empty. * <li>"IMSI": if "token" is set, set to {@link android.telephony.TelephonyManager#getImei}. * <li>"EAP_ID": if "token" is not set, set this parameter to trigger embedded EAP-AKA - * authentication as decribed in TS.43 section 2.6.1. Its value is derived from IMSI as per + * authentication as described in TS.43 section 2.6.1. Its value is derived from IMSI as per * GSMA spec RCC.14 section C.2. * <li>"terminal_id": IMEI, or {@code request.terminalId()} if it's not empty. * <li>"terminal_vendor": {@link android.os.Build#MANUFACTURER}, or {@code * request.terminalVendor()} if it's not empty. * <li>"terminal_model": {@link android.os.Build#MODEL}, or {@code request.terminalModel()} if * it's not empty. - * <li>"terminal_sw_version": {@llink android.os.Build.VERSION#BASE_OS}, or {@code + * <li>"terminal_sw_version": {@link android.os.Build.VERSION#BASE_OS}, or {@code * request.terminalSoftwareVersion()} if it's not empty. * <li>"app_name": not set, or {@code request.appName()} if it's not empty. * <li>"app_version": not set, or {@code request.appVersion()} if it's not empty. @@ -176,10 +202,10 @@ public class ServiceEntitlement { * @param appId an app ID string defined in TS.43 section 2.2, e.g. {@link #APP_VOWIFI}. * @param request contains parameters that can be used in the HTTP request. */ - @Nullable + @NonNull public String queryEntitlementStatus(String appId, ServiceEntitlementRequest request) throws ServiceEntitlementException { - return eapAkaApi.queryEntitlementStatus(ImmutableList.of(appId), carrierConfig, request); + return queryEntitlementStatus(ImmutableList.of(appId), request); } /** @@ -187,13 +213,29 @@ public class ServiceEntitlement { * request/response. For on device service activation (ODSA) of eSIM for companion/primary * devices, use {@link #performEsimOdsa} instead. * - * <p>Same with {@link #queryEntitlementStatus(String, ServiceEntitlementRequest)} except that + * <p>Same as {@link #queryEntitlementStatus(String, ServiceEntitlementRequest)} except that * multiple "app" parameters will be set in the HTTP request, in the order as they appear in * parameter {@code appIds}. */ + @NonNull public String queryEntitlementStatus(ImmutableList<String> appIds, ServiceEntitlementRequest request) throws ServiceEntitlementException { + return getEntitlementStatusResponse(appIds, request).body(); + } + + /** + * Retrieves service entitlement configurations for multiple app IDs in one HTTP + * request/response. For on device service activation (ODSA) of eSIM for companion/primary + * devices, use {@link #performEsimOdsa} instead. + * + * <p>Same as {@link #queryEntitlementStatus(ImmutableList, ServiceEntitlementRequest)} + * except that it returns the full HTTP response instead of just the body. + */ + @NonNull + public HttpResponse getEntitlementStatusResponse(ImmutableList<String> appIds, + ServiceEntitlementRequest request) + throws ServiceEntitlementException { return eapAkaApi.queryEntitlementStatus(appIds, carrierConfig, request); } @@ -207,16 +249,79 @@ public class ServiceEntitlement { * needed, and returns the raw configuration doc as a string. Additional parameters from {@code * operation} are set to the HTTP request. See {@link EsimOdsaOperation} for details. */ + @NonNull public String performEsimOdsa( String appId, ServiceEntitlementRequest request, EsimOdsaOperation operation) throws ServiceEntitlementException { + return getEsimOdsaResponse(appId, request, operation).body(); + } + + /** + * Retrieves the HTTP response after performing on device service activation (ODSA) of eSIM for + * companion/primary devices. + * + * <p>Same as {@link #performEsimOdsa(String, ServiceEntitlementRequest, EsimOdsaOperation)} + * except that it returns the full HTTP response instead of just the body. + */ + @NonNull + public HttpResponse getEsimOdsaResponse( + String appId, ServiceEntitlementRequest request, EsimOdsaOperation operation) + throws ServiceEntitlementException { return eapAkaApi.performEsimOdsaOperation(appId, carrierConfig, request, operation); } /** + * Retrieves the endpoint for OpenID Connect(OIDC) authentication. + * + * <p>Implementation based on section 2.8.2 of TS.43 + * + * <p>The user should call {@link #queryEntitlementStatusFromOidc(String url)} with the + * authentication result to retrieve the service entitlement configuration. + * + * @param appId an app ID string defined in TS.43 section 2.2 + * @param request contains parameters that can be used in the HTTP request + */ + @NonNull + public String acquireOidcAuthenticationEndpoint(String appId, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + mOidcRequest = request; + return eapAkaApi.acquireOidcAuthenticationEndpoint(appId, carrierConfig, request); + } + + /** + * Retrieves the service entitlement configuration from OIDC authentication result. + * + * <p>Implementation based on section 2.8.2 of TS.43. + * + * <p>{@link #acquireOidcAuthenticationEndpoint} must be called before calling this method. + * + * @param url the redirect url from OIDC authentication result. + */ + @NonNull + public String queryEntitlementStatusFromOidc(String url) throws ServiceEntitlementException { + return getEntitlementStatusResponseFromOidc(url).body(); + } + + /** + * Retrieves the HTTP response containing the service entitlement configuration from + * OIDC authentication result. + * + * <p>Same as {@link #queryEntitlementStatusFromOidc(String)} except that it returns the + * full HTTP response instead of just the body. + * + * @param url the redirect url from OIDC authentication result. + */ + @NonNull + public HttpResponse getEntitlementStatusResponseFromOidc(String url) + throws ServiceEntitlementException { + return eapAkaApi.queryEntitlementStatusFromOidc(url, carrierConfig, mOidcRequest); + } + + /** * Retrieves the history of past HTTP request and responses if {@code saveHttpHistory} was set * in constructor. */ + @NonNull public List<String> getHistory() { return eapAkaApi.getHistory(); } diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementException.java b/java/com/android/libraries/entitlement/ServiceEntitlementException.java index 45b1b9b..612e2e7 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlementException.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlementException.java @@ -16,6 +16,11 @@ package com.android.libraries.entitlement; +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Indicates errors happened in retrieving service entitlement configuration. */ @@ -25,28 +30,38 @@ public class ServiceEntitlementException extends Exception { */ public static final int ERROR_UNKNOWN = 0; + /** + * Failure to compose JSON when making POST requests. + */ + public static final int ERROR_JSON_COMPOSE_FAILURE = 1; + // Android telephony related failures /** * Android telephony is unable to provide info like IMSI, e.g. when modem crashed. */ public static final int ERROR_PHONE_NOT_AVAILABLE = 10; - // EAP-AKA authentication related falures + // EAP-AKA authentication related failures /** * SIM not returning a response to the EAP-AKA challenge, e.g. when the challenge is invalid. - * This can happen only when an embedded EAP-AKA challange is conducted, as per GMSA spec TS.43 + * This can happen only when an embedded EAP-AKA challenge is conducted, as per GSMA spec TS.43 * section 2.6.1. */ public static final int ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE = 20; /** - * EAP-AKA synchronization failure that cannot be recoverd even after the "Sequence number + * EAP-AKA synchronization failure that cannot be recovered even after the "Sequence number * synchronization" procedure as defined in RFC 4187. */ public static final int ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE = 21; + /** + * EAP-AKA failure that happens when the client fails to authenticate within the maximum number + * of attempts + */ + public static final int ERROR_EAP_AKA_FAILURE = 22; // HTTP related failures /** - * Cannot connect to the entitlment server, e.g. due to weak mobile network and Wi-Fi + * Cannot connect to the entitlement server, e.g. due to weak mobile network and Wi-Fi * connection. */ public static final int ERROR_SERVER_NOT_CONNECTABLE = 30; @@ -62,19 +77,40 @@ public class ServiceEntitlementException extends Exception { */ public static final int ERROR_MALFORMED_HTTP_RESPONSE = 32; + // ODSA errors + /** + * HTTP response does not contain the authentication token. + */ + public static final int ERROR_TOKEN_NOT_AVAILABLE = 60; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ERROR_UNKNOWN, + ERROR_PHONE_NOT_AVAILABLE, + ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE, + ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE, + ERROR_EAP_AKA_FAILURE, + ERROR_SERVER_NOT_CONNECTABLE, + ERROR_HTTP_STATUS_NOT_SUCCESS, + ERROR_MALFORMED_HTTP_RESPONSE, + ERROR_TOKEN_NOT_AVAILABLE + }) + public @interface ErrorCode {} + /** * Default HTTP status if not been specified. */ - private static final int HTTP_STATUS_UNSPECIFIED = 0; + public static final int HTTP_STATUS_UNSPECIFIED = 0; /** * An empty string if Retry-After header in HTTP response not been specified. */ - private static final String RETRY_AFTER_UNSPECIFIED = ""; + public static final String RETRY_AFTER_UNSPECIFIED = ""; - private int mError; - private int mHttpStatus; - private String mRetryAfter; + @ErrorCode + private final int mError; + private final int mHttpStatus; + private final String mRetryAfter; public ServiceEntitlementException(int error, String message) { this(error, HTTP_STATUS_UNSPECIFIED, RETRY_AFTER_UNSPECIFIED, message); @@ -109,8 +145,9 @@ public class ServiceEntitlementException extends Exception { } /** - * Returns the error code, see {@link #ERROR_*}. {@link #ERROR_UNKNOWN} if not been specified. + * Returns the error code. {@link #ERROR_UNKNOWN} if not been specified. */ + @ErrorCode public int getErrorCode() { return mError; } @@ -127,8 +164,8 @@ public class ServiceEntitlementException extends Exception { * Returns the "Retry-After" header in HTTP response, often set with HTTP status code 503; an * empty string if unavailable. * - * @return the HTTP-date or a number of seconds to delay, as defiend in RFC 7231: - * https://tools.ietf.org/html/rfc7231#section-7.1.3 + * @return the HTTP-date or a number of seconds to delay, as defined in RFC 7231: + * <a href="https://tools.ietf.org/html/rfc7231#section-7.1.3">...</a> */ public String getRetryAfter() { return mRetryAfter; diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java index 59e70ef..38206de 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java @@ -19,30 +19,24 @@ package com.android.libraries.entitlement; import android.os.Build; import android.os.Build.VERSION; +import com.android.libraries.entitlement.utils.Ts43Constants; + import com.google.auto.value.AutoValue; /** - * Service entitlement HTTP request parameters, as defiend in GSMA spec TS.43 section 2.2. + * Service entitlement HTTP request parameters, as defined in GSMA spec TS.43 section 2.2. */ @AutoValue public abstract class ServiceEntitlementRequest { - /** Disables notification token. */ - public static final int NOTICATION_ACTION_DISABLE = 0; - /** Enables FCM notification token. */ - public static final int NOTICATION_ACTION_ENABLE_FCM = 2; /** Accepts the content type in XML format. */ public static final String ACCEPT_CONTENT_TYPE_XML = "text/vnd.wap.connectivity-xml"; /** Accepts the content type in JSON format. */ public static final String ACCEPT_CONTENT_TYPE_JSON = - "application/vnd.gsma.eap-relay.v1.0+json"; - /** Accepts the content type in JSON or XML format. */ + "application/json"; public static final String ACCEPT_CONTENT_TYPE_JSON_AND_XML = - "application/vnd.gsma.eap-relay.v1.0+json, text/vnd.wap.connectivity-xml"; + "application/json, text/vnd.wap.connectivity-xml"; /** Default value of configuration version. */ public static final int DEFAULT_CONFIGURATION_VERSION = 0; - /** Default value of entitlement version. */ - public static final String DEFAULT_ENTITLEMENT_VERSION = "2.0"; - /** * Returns the version of configuration currently stored on the client. Used by HTTP parameter @@ -107,10 +101,8 @@ public abstract class ServiceEntitlementRequest { /** * Returns the action associated with the FCM registration token. Used by HTTP parameter * "notif_action". - * - * @see #NOTICATION_ACTION_ENABLE_FCM - * @see #NOTICATION_ACTION_DISABLE */ + @Ts43Constants.NotificationAction public abstract int notificationAction(); /** @@ -123,10 +115,9 @@ public abstract class ServiceEntitlementRequest { public abstract String acceptContentType(); /** - * Returns the network identifier for premium network. Used for premium network slice - * entitlement. + * Returns the boost type for premium network. Used for premium network slice entitlement. */ - public abstract String networkIdentifier(); + public abstract String boostType(); /** * Returns a new {@link Builder} object. @@ -134,19 +125,19 @@ public abstract class ServiceEntitlementRequest { public static Builder builder() { return new AutoValue_ServiceEntitlementRequest.Builder() .setConfigurationVersion(DEFAULT_CONFIGURATION_VERSION) - .setEntitlementVersion(DEFAULT_ENTITLEMENT_VERSION) + .setEntitlementVersion(Ts43Constants.DEFAULT_ENTITLEMENT_VERSION) .setAuthenticationToken("") .setTemporaryToken("") .setTerminalId("") .setTerminalVendor(Build.MANUFACTURER) .setTerminalModel(Build.MODEL) - .setTerminalSoftwareVersion(VERSION.BASE_OS) + .setTerminalSoftwareVersion(VERSION.RELEASE) .setAppName("") .setAppVersion("") .setNotificationToken("") - .setNotificationAction(NOTICATION_ACTION_ENABLE_FCM) + .setNotificationAction(Ts43Constants.NOTIFICATION_ACTION_ENABLE_FCM) .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML) - .setNetworkIdentifier(""); + .setBoostType(""); } /** @@ -167,7 +158,8 @@ public abstract class ServiceEntitlementRequest { * Sets the current version of the entitlement specification. Used by HTTP parameter * "entitlement_version". * - * <p>If not set, default to {@link #DEFAULT_ENTITLEMENT_VERSION} base on TS.43-v5.0. + * <p>If not set, default to {@link Ts43Constants#DEFAULT_ENTITLEMENT_VERSION} base on + * TS.43-v5.0. */ public abstract Builder setEntitlementVersion(String value); @@ -243,13 +235,10 @@ public abstract class ServiceEntitlementRequest { * Sets the action associated with the FCM registration token. Used by HTTP parameter * "notif_action". * - * <p>Required if a token is set with {@link #setNotificationToken}, and default to {@link - * #NOTICATION_ACTION_ENABLE_FCM}; otherwise ignored. - * - * @see #NOTICATION_ACTION_ENABLE_FCM - * @see #NOTICATION_ACTION_DISABLE + * <p>Required if a token is set with {@link #setNotificationToken}, and default to + * {@link Ts43Constants#NOTIFICATION_ACTION_ENABLE_FCM}; otherwise ignored. */ - public abstract Builder setNotificationAction(int value); + public abstract Builder setNotificationAction(@Ts43Constants.NotificationAction int value); /** * Sets the configuration document format the caller accepts, e.g. XML or JSON. Used by HTTP @@ -264,12 +253,12 @@ public abstract class ServiceEntitlementRequest { public abstract Builder setAcceptContentType(String contentType); /** - * Sets the network identifier for premium network. Used by HTTP parameter - * "network_identifier" in case of premium network slice entitlement. + * Sets the boost type for premium network. Used by HTTP parameter + * "boost_type" in case of premium network slice entitlement. * * <p>Optional. */ - public abstract Builder setNetworkIdentifier(String value); + public abstract Builder setBoostType(String value); public abstract ServiceEntitlementRequest build(); } diff --git a/java/com/android/libraries/entitlement/Ts43Authentication.java b/java/com/android/libraries/entitlement/Ts43Authentication.java new file mode 100644 index 0000000..29d0cb9 --- /dev/null +++ b/java/com/android/libraries/entitlement/Ts43Authentication.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement; + +import static com.google.common.base.Strings.nullToEmpty; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.libraries.entitlement.http.HttpResponse; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43Constants.AppId; +import com.android.libraries.entitlement.utils.Ts43XmlDoc; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import java.net.URL; +import java.util.Objects; + +/** + * The class responsible for TS.43 authentication process. + */ +public class Ts43Authentication { + private static final String TAG = "Ts43Auth"; + + /** + * The authentication token for TS.43 operation. + */ + @AutoValue + public abstract static class Ts43AuthToken { + /** + * Indicating the validity of token is not available. + */ + public static long VALIDITY_NOT_AVAILABLE = -1; + + /** + * The authentication token for TS.43 operations. + */ + @NonNull + public abstract String token(); + + /** + * The list of cookies from the {@code Set-Cookie} header of the TS.43 response. + */ + @NonNull + public abstract ImmutableList<String> cookies(); + + /** + * Indicates the validity of the token. Note this value is server dependent. The client is + * expected to interpret this value itself. + */ + public abstract long validity(); + + /** + * Create the {@link Ts43AuthToken} object. + * + * @param token The authentication token for TS.43 operations. + * @param cookie The list of cookies from the {@code Set-Cookie} header. + * @param validity Indicates the validity of the token. Note this value is server + * dependent. If not available, set to {@link #VALIDITY_NOT_AVAILABLE}. + * + * @return The {@link Ts43AuthToken} object. + */ + public static Ts43AuthToken create(@NonNull String token, + @NonNull ImmutableList<String> cookie, long validity) { + return new AutoValue_Ts43Authentication_Ts43AuthToken(token, cookie, validity); + } + } + + /** + * The application context. + */ + @NonNull + private final Context mContext; + + /** + * The entitlement server address. + */ + @NonNull + private final URL mEntitlementServerAddress; + + /** + * The TS.43 entitlement version to use. For example, {@code "9.0"}. + */ + @NonNull + private final String mEntitlementVersion; + + /** + * For test mocking only. + */ + @VisibleForTesting + private ServiceEntitlement mServiceEntitlement; + + /** + * Ts43Authentication constructor. + * + * @param context The application context. + * @param entitlementServerAddress The entitlement server address. + * @param entitlementVersion The TS.43 entitlement version to use. For example, {@code "9.0"}. + * If {@code null}, version {@code "2.0"} will be used by default. + * + * @throws NullPointerException wWhen {@code context} or {@code entitlementServerAddress} is + * {@code null}. + */ + public Ts43Authentication(@NonNull Context context, @NonNull URL entitlementServerAddress, + @Nullable String entitlementVersion) { + Objects.requireNonNull(context, "context is null"); + Objects.requireNonNull(entitlementServerAddress, "entitlementServerAddress is null."); + + mContext = context; + mEntitlementServerAddress = entitlementServerAddress; + + if (entitlementVersion != null) { + mEntitlementVersion = entitlementVersion; + } else { + mEntitlementVersion = Ts43Constants.DEFAULT_ENTITLEMENT_VERSION; + } + } + + /** + * Get the authentication token for TS.43 operations with EAP-AKA described in TS.43 + * Service Entitlement Configuration section 2.8.1. + * + * @param slotIndex The logical SIM slot index involved in ODSA operation. + * See {@link SubscriptionInfo#getSubscriptionId()}. + + * @param appId Application id. For example, {@link Ts43Constants#APP_VOWIFI} for VoWifi, + * {@link Ts43Constants#APP_ODSA_PRIMARY} for ODSA primary device. Refer GSMA to Service + * Entitlement Configuration section 2.3. + * @param appName The calling client's package name. Used for {@code app_name} in HTTP GET + * request in GSMA TS.43 Service Entitlement Configuration section 2.3. + * @param appVersion The calling client's version. Used for {@code app_version} in HTTP GET + * request in GSMA TS.43 Service Entitlement Configuration section 2.3. + * + * @return The authentication token. + * + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()}. + * @throws IllegalArgumentException when {@code slotIndex} or {@code appId} is invalid. + * @throws NullPointerException when {@code context}, {@code entitlementServerAddress}, or + * {@code appId} is {@code null}. + */ + @NonNull + public Ts43AuthToken getAuthToken(int slotIndex, @NonNull @AppId String appId, + @Nullable String appName, @Nullable String appVersion) + throws ServiceEntitlementException { + Objects.requireNonNull(appId, "appId is null"); + + if (!Ts43Constants.isValidAppId(appId)) { + throw new IllegalArgumentException("getAuthToken: invalid app id " + appId); + } + + String imei = null; + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + if (telephonyManager != null) { + if (slotIndex < 0 || slotIndex >= telephonyManager.getActiveModemCount()) { + throw new IllegalArgumentException("getAuthToken: invalid slot index " + slotIndex); + } + imei = telephonyManager.getImei(slotIndex); + } + + // Build the HTTP request. The default params are specified in + // ServiceEntitlementRequest.builder() already. + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion) + .setTerminalId(imei) + .setAppName(appName) + .setAppVersion(appVersion) + .build(); + CarrierConfig carrierConfig = CarrierConfig.builder() + .setServerUrl(mEntitlementServerAddress.toString()) + .build(); + + if (mServiceEntitlement == null) { + mServiceEntitlement = new ServiceEntitlement(mContext, carrierConfig, + SubscriptionManager.getSubscriptionId(slotIndex)); + } + + // Get the full HTTP response instead of just the body so we can reuse the same cookies. + HttpResponse response; + String rawXml; + try { + response = mServiceEntitlement.getEntitlementStatusResponse( + ImmutableList.of(appId), request); + rawXml = response == null ? null : response.body(); + Log.d(TAG, "getAuthToken: rawXml=" + rawXml); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "Failed to get authentication token. e=" + e); + throw e; + } + + ImmutableList<String> cookies = response == null ? ImmutableList.of() : response.cookies(); + + Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml); + String authToken = ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.TOKEN), Ts43XmlDoc.Parm.TOKEN); + if (TextUtils.isEmpty(authToken)) { + Log.w(TAG, "Failed to parse authentication token"); + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_TOKEN_NOT_AVAILABLE, + "Failed to parse authentication token"); + } + + String validityString = nullToEmpty(ts43XmlDoc.get(ImmutableList.of( + Ts43XmlDoc.CharacteristicType.TOKEN), Ts43XmlDoc.Parm.VALIDITY)); + long validity; + try { + validity = Long.parseLong(validityString); + } catch (NumberFormatException e) { + validity = Ts43AuthToken.VALIDITY_NOT_AVAILABLE; + } + + return Ts43AuthToken.create(authToken, cookies, validity); + } + + /** + * Get the URL of OIDC (OpenID Connect) server as described in TS.43 Service Entitlement + * Configuration section 2.8.2. + * + * The caller is expected to present the content of the URL to the user to proceed the + * authentication process. After that the caller can call {@link #getAuthToken(URL)} + * to get the authentication token. + * + * @param slotIndex The logical SIM slot index involved in ODSA operation. + * @param entitlementServerAddress The entitlement server address. + * @param entitlementVersion The TS.43 entitlement version to use. For example, {@code "9.0"}. + * @param appId Application id. For example, {@link Ts43Constants#APP_VOWIFI} for VoWifi, + * {@link Ts43Constants#APP_ODSA_PRIMARY} for ODSA primary device. Refer GSMA to Service + * Entitlement Configuration section 2.3. + * @param appName The calling client's package name. Used for {@code app_name} in HTTP GET + * request in GSMA TS.43 Service Entitlement Configuration section 2.3. + * @param appVersion The calling client's version. Used for {@code app_version} in HTTP GET + * request in GSMA TS.43 Service Entitlement Configuration section 2.3. + * + * @return The URL of OIDC server with all the required parameters for client to launch a + * user interface for users to interact with the authentication process. The parameters in URL + * include {@code client_id}, {@code redirect_uri}, {@code state}, and {@code nonce}. + * + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public URL getOidcAuthServer(@NonNull Context context, int slotIndex, + @NonNull URL entitlementServerAddress, @Nullable String entitlementVersion, + @NonNull @AppId String appId, @Nullable String appName, @Nullable String appVersion) + throws ServiceEntitlementException { + return null; + } + + /** + * Get the authentication token for TS.43 operations with OIDC (OpenID Connect) described in + * TS.43 Service Entitlement Configuration section 2.8.2. + * + * @param aesUrl The AES URL used to retrieve auth token. The parameters in the URL include + * the OIDC auth code {@code code} and {@code state}. + * + * @return The authentication token. + * + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public Ts43AuthToken getAuthToken(@NonNull URL aesUrl) + throws ServiceEntitlementException { + return null; + } +} diff --git a/java/com/android/libraries/entitlement/Ts43Operation.java b/java/com/android/libraries/entitlement/Ts43Operation.java new file mode 100644 index 0000000..e603e86 --- /dev/null +++ b/java/com/android/libraries/entitlement/Ts43Operation.java @@ -0,0 +1,1076 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement; + +import android.content.Context; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaServiceStatus; +import com.android.libraries.entitlement.http.HttpConstants; +import com.android.libraries.entitlement.odsa.AcquireConfigurationOperation.AcquireConfigurationRequest; +import com.android.libraries.entitlement.odsa.AcquireConfigurationOperation.AcquireConfigurationResponse; +import com.android.libraries.entitlement.odsa.AcquireTemporaryTokenOperation.AcquireTemporaryTokenRequest; +import com.android.libraries.entitlement.odsa.AcquireTemporaryTokenOperation.AcquireTemporaryTokenResponse; +import com.android.libraries.entitlement.odsa.CheckEligibilityOperation; +import com.android.libraries.entitlement.odsa.CheckEligibilityOperation.CheckEligibilityRequest; +import com.android.libraries.entitlement.odsa.CheckEligibilityOperation.CheckEligibilityResponse; +import com.android.libraries.entitlement.odsa.DownloadInfo; +import com.android.libraries.entitlement.odsa.GetPhoneNumberOperation.GetPhoneNumberRequest; +import com.android.libraries.entitlement.odsa.GetPhoneNumberOperation.GetPhoneNumberResponse; +import com.android.libraries.entitlement.odsa.ManageServiceOperation.ManageServiceRequest; +import com.android.libraries.entitlement.odsa.ManageServiceOperation.ManageServiceResponse; +import com.android.libraries.entitlement.odsa.ManageSubscriptionOperation.ManageSubscriptionRequest; +import com.android.libraries.entitlement.odsa.ManageSubscriptionOperation.ManageSubscriptionResponse; +import com.android.libraries.entitlement.odsa.OdsaResponse; +import com.android.libraries.entitlement.odsa.PlanOffer; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43XmlDoc; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** TS43 operations described in GSMA Service Entitlement Configuration spec. */ +public class Ts43Operation { + private static final String TAG = "Ts43"; + + /** + * The normal token retrieved via {@link Ts43Authentication#getAuthToken(int, String, String, + * String)} or {@link Ts43Authentication#getAuthToken(URL)}. + */ + public static final int TOKEN_TYPE_NORMAL = 1; + + /** + * The temporary token retrieved via {@link + * Ts43Operation#acquireTemporaryToken(AcquireTemporaryTokenRequest)}. + */ + public static final int TOKEN_TYPE_TEMPORARY = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({TOKEN_TYPE_NORMAL, TOKEN_TYPE_TEMPORARY}) + public @interface TokenType { + } + + /** The application context. */ + @NonNull + private final Context mContext; + + /** + * The TS.43 entitlement version to use. For example, {@code "9.0"}. If {@code null}, version + * {@code "2.0"} will be used by default. + */ + @NonNull + private final String mEntitlementVersion; + + /** The entitlement server address. */ + @NonNull + private final URL mEntitlementServerAddress; + + /** + * The authentication token used for TS.43 operation. This token could be automatically updated + * after each TS.43 operation if the server provides the new token in the operation's HTTP + * response. + */ + @Nullable + private String mAuthToken; + + /** + * The temporary token retrieved from {@link + * #acquireTemporaryToken(AcquireTemporaryTokenRequest)}. + */ + @Nullable + private String mTemporaryToken; + + /** + * Token type. When token type is {@link #TOKEN_TYPE_NORMAL}, {@link #mAuthToken} is used. When + * toke type is {@link #TOKEN_TYPE_TEMPORARY}, {@link #mTemporaryToken} is used. + */ + @TokenType + private int mTokenType; + + private final ServiceEntitlement mServiceEntitlement; + + /** IMEI of the device. */ + private final String mImei; + + /** + * Constructor of Ts43Operation. + * + * @param slotIndex The logical SIM slot index involved in ODSA operation. + * @param entitlementServerAddress The entitlement server address. + * @param entitlementVersion The TS.43 entitlement version to use. For example, + * {@code "9.0"}. If {@code null}, version {@code "2.0"} will be used + * by default. + * @param authToken The authentication token. + * @param tokenType The token type. Can be {@link #TOKEN_TYPE_NORMAL} or + * {@link #TOKEN_TYPE_TEMPORARY}. + */ + public Ts43Operation( + @NonNull Context context, + int slotIndex, + @NonNull URL entitlementServerAddress, + @Nullable String entitlementVersion, + @NonNull String authToken, + @TokenType int tokenType) { + mContext = context; + mEntitlementServerAddress = entitlementServerAddress; + if (entitlementVersion != null) { + mEntitlementVersion = entitlementVersion; + } else { + mEntitlementVersion = Ts43Constants.DEFAULT_ENTITLEMENT_VERSION; + } + + if (tokenType == TOKEN_TYPE_NORMAL) { + mAuthToken = authToken; + } else if (tokenType == TOKEN_TYPE_TEMPORARY) { + mTemporaryToken = authToken; + } else { + throw new IllegalArgumentException("Invalid token type " + tokenType); + } + mTokenType = tokenType; + + CarrierConfig carrierConfig = + CarrierConfig.builder().setServerUrl(mEntitlementServerAddress.toString()).build(); + + mServiceEntitlement = + new ServiceEntitlement( + mContext, carrierConfig, SubscriptionManager.getSubscriptionId(slotIndex)); + + String imei = null; + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + if (telephonyManager != null) { + if (slotIndex < 0 || slotIndex >= telephonyManager.getActiveModemCount()) { + throw new IllegalArgumentException("getAuthToken: invalid slot index " + slotIndex); + } + imei = telephonyManager.getImei(slotIndex); + } + mImei = Strings.nullToEmpty(imei); + } + + /** + * To verify if end-user is allowed to invoke the ODSA application as described in GSMA Service + * Entitlement Configuration section 6.2 and 6.5.2. + * + * @return {@code true} if the end-user is allowed to perform ODSA operation. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public CheckEligibilityResponse checkEligibility( + @NonNull CheckEligibilityRequest checkEligibilityRequest) + throws ServiceEntitlementException { + Objects.requireNonNull(checkEligibilityRequest); + + ServiceEntitlementRequest.Builder builder = + ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion) + .setTerminalId(mImei); + + if (mTokenType == TOKEN_TYPE_NORMAL) { + builder.setAuthenticationToken(mAuthToken); + } else if (mTokenType == TOKEN_TYPE_TEMPORARY) { + builder.setTemporaryToken(mTemporaryToken); + } + + String notificationToken = checkEligibilityRequest.notificationToken(); + if (!TextUtils.isEmpty(notificationToken)) { + builder.setNotificationToken(notificationToken); + } + int notificationAction = checkEligibilityRequest.notificationAction(); + if (Ts43Constants.isValidNotificationAction(notificationAction)) { + builder.setNotificationAction(notificationAction); + } + + ServiceEntitlementRequest request = builder.build(); + + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_CHECK_ELIGIBILITY) + .setCompanionTerminalId(checkEligibilityRequest.companionTerminalId()) + .setCompanionTerminalVendor( + checkEligibilityRequest.companionTerminalVendor()) + .setCompanionTerminalModel(checkEligibilityRequest.companionTerminalModel()) + .setCompanionTerminalSoftwareVersion( + checkEligibilityRequest.companionTerminalSoftwareVersion()) + .setCompanionTerminalFriendlyName( + checkEligibilityRequest.companionTerminalFriendlyName()) + .build(); + + String rawXml; + try { + rawXml = + mServiceEntitlement.performEsimOdsa(checkEligibilityRequest.appId(), request, + operation); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "manageSubscription: Failed to perform ODSA operation. e=" + e); + throw e; + } + + // Build the response of check eligibility operation. Refer to GSMA Service Entitlement + // Configuration section 6.5.2. + CheckEligibilityResponse.Builder responseBuilder = CheckEligibilityResponse.builder(); + + Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml); + + try { + processGeneralResult(ts43XmlDoc, responseBuilder); + } catch (MalformedURLException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE, + "checkEligibility: Malformed URL " + rawXml); + } + + // Parse the eligibility + String eligibilityString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.PRIMARY_APP_ELIGIBILITY); + if (TextUtils.isEmpty(eligibilityString)) { + eligibilityString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.COMPANION_APP_ELIGIBILITY); + } + + int eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_UNKNOWN; + if (!TextUtils.isEmpty(eligibilityString)) { + switch (eligibilityString) { + case Ts43XmlDoc.ParmValues.DISABLED: + eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_DISABLED; + break; + case Ts43XmlDoc.ParmValues.ENABLED: + eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_ENABLED; + break; + case Ts43XmlDoc.ParmValues.INCOMPATIBLE: + eligibility = CheckEligibilityOperation.ELIGIBILITY_RESULT_INCOMPATIBLE; + break; + } + } + responseBuilder.setAppEligibility(eligibility); + + // Parse companion device services + String companionDeviceServices = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.COMPANION_DEVICE_SERVICES); + + if (!TextUtils.isEmpty(companionDeviceServices)) { + List<String> companionDeviceServicesList = + Arrays.asList(companionDeviceServices.split("\\s*,\\s*")); + responseBuilder.setCompanionDeviceServices( + ImmutableList.copyOf(companionDeviceServicesList)); + } + + // Parse notEnabledURL + URL notEnabledURL = null; + String notEnabledURLString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.NOT_ENABLED_URL); + + try { + notEnabledURL = new URL(notEnabledURLString); + responseBuilder.setNotEnabledUrl(notEnabledURL); + } catch (MalformedURLException e) { + Log.w(TAG, "checkEligibility: malformed URL " + notEnabledURLString); + } + + // Parse notEnabledUserData + String notEnabledUserData = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.NOT_ENABLED_USER_DATA); + + if (!TextUtils.isEmpty(notEnabledUserData)) { + responseBuilder.setNotEnabledUserData(notEnabledUserData); + } + + // Parse notEnabledContentsType + String notEnabledContentsTypeString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.NOT_ENABLED_CONTENTS_TYPE); + + int notEnabledContentsType = HttpConstants.ContentType.UNKNOWN; + if (!TextUtils.isEmpty(notEnabledContentsTypeString)) { + switch (notEnabledContentsTypeString) { + case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_XML: + notEnabledContentsType = HttpConstants.ContentType.XML; + break; + case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_JSON: + notEnabledContentsType = HttpConstants.ContentType.JSON; + break; + } + } + responseBuilder.setNotEnabledContentsType(notEnabledContentsType); + + return responseBuilder.build(); + } + + /** + * To request for subscription-related action on a primary or companion device as described in + * GSMA Service Entitlement Configuration section 6.2 and 6.5.3. + * + * @param manageSubscriptionRequest The manage subscription request. + * @return The response of manage subscription request. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public ManageSubscriptionResponse manageSubscription( + @NonNull ManageSubscriptionRequest manageSubscriptionRequest) + throws ServiceEntitlementException { + Objects.requireNonNull(manageSubscriptionRequest); + + ServiceEntitlementRequest.Builder builder = + ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion) + .setTerminalId(mImei) + .setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML); + + if (mTokenType == TOKEN_TYPE_NORMAL) { + builder.setAuthenticationToken(mAuthToken); + } else if (mTokenType == TOKEN_TYPE_TEMPORARY) { + builder.setTemporaryToken(mTemporaryToken); + } + + String notificationToken = manageSubscriptionRequest.notificationToken(); + if (!TextUtils.isEmpty(notificationToken)) { + builder.setNotificationToken(notificationToken); + } + int notificationAction = manageSubscriptionRequest.notificationAction(); + if (Ts43Constants.isValidNotificationAction(notificationAction)) { + builder.setNotificationAction(notificationAction); + } + + ServiceEntitlementRequest request = builder.build(); + + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION) + .setOperationType(manageSubscriptionRequest.operationType()) + .setCompanionTerminalId(manageSubscriptionRequest.companionTerminalId()) + .setCompanionTerminalVendor( + manageSubscriptionRequest.companionTerminalVendor()) + .setCompanionTerminalModel( + manageSubscriptionRequest.companionTerminalModel()) + .setCompanionTerminalSoftwareVersion( + manageSubscriptionRequest.companionTerminalSoftwareVersion()) + .setCompanionTerminalFriendlyName( + manageSubscriptionRequest.companionTerminalFriendlyName()) + .setCompanionTerminalService( + manageSubscriptionRequest.companionTerminalService()) + .setCompanionTerminalIccid( + manageSubscriptionRequest.companionTerminalIccid()) + .setCompanionTerminalEid(manageSubscriptionRequest.companionTerminalEid()) + .setTerminalIccid(manageSubscriptionRequest.terminalIccid()) + .setTerminalEid(manageSubscriptionRequest.terminalEid()) + .setTargetTerminalId(manageSubscriptionRequest.targetTerminalId()) + // non TS.43 standard support + .setTargetTerminalIds(manageSubscriptionRequest.targetTerminalIds()) + .setTargetTerminalIccid(manageSubscriptionRequest.targetTerminalIccid()) + .setTargetTerminalEid(manageSubscriptionRequest.targetTerminalEid()) + // non TS.43 standard support + .setTargetTerminalSerialNumber( + manageSubscriptionRequest.targetTerminalSerialNumber()) + // non TS.43 standard support + .setTargetTerminalModel(manageSubscriptionRequest.targetTerminalModel()) + .setOldTerminalId(manageSubscriptionRequest.oldTerminalId()) + .setOldTerminalIccid(manageSubscriptionRequest.oldTerminalIccid()) + .build(); + + String rawXml; + try { + rawXml = + mServiceEntitlement.performEsimOdsa( + manageSubscriptionRequest.appId(), request, operation); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "manageSubscription: Failed to perform ODSA operation. e=" + e); + throw e; + } + + // Build the response of manage subscription operation. Refer to GSMA Service Entitlement + // Configuration section 6.5.3. + ManageSubscriptionResponse.Builder responseBuilder = ManageSubscriptionResponse.builder(); + + Ts43XmlDoc ts43XmlDoc; + try { + ts43XmlDoc = new Ts43XmlDoc(rawXml); + processGeneralResult(ts43XmlDoc, responseBuilder); + } catch (MalformedURLException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE, + "manageSubscription: Malformed URL " + rawXml); + } + + int subscriptionResult = ManageSubscriptionResponse.SUBSCRIPTION_RESULT_UNKNOWN; + + // Parse subscription result. + String subscriptionResultString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.SUBSCRIPTION_RESULT); + + if (!TextUtils.isEmpty(subscriptionResultString)) { + switch (subscriptionResultString) { + case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET: + subscriptionResult = + ManageSubscriptionResponse.SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET; + + String subscriptionServiceURLString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.SUBSCRIPTION_SERVICE_URL); + + if (!TextUtils.isEmpty(subscriptionServiceURLString)) { + try { + responseBuilder.setSubscriptionServiceUrl( + new URL(subscriptionServiceURLString)); + + String subscriptionServiceUserDataString = + ts43XmlDoc.get( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.SUBSCRIPTION_SERVICE_USER_DATA); + if (!TextUtils.isEmpty(subscriptionServiceUserDataString)) { + responseBuilder.setSubscriptionServiceUserData( + subscriptionServiceUserDataString); + } + + String subscriptionServiceContentsTypeString = + ts43XmlDoc.get( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.SUBSCRIPTION_SERVICE_CONTENTS_TYPE); + if (!TextUtils.isEmpty(subscriptionServiceContentsTypeString)) { + int contentsType = HttpConstants.ContentType.UNKNOWN; + switch (subscriptionServiceContentsTypeString) { + case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_XML: + contentsType = HttpConstants.ContentType.XML; + break; + case Ts43XmlDoc.ParmValues.CONTENTS_TYPE_JSON: + contentsType = HttpConstants.ContentType.JSON; + break; + } + responseBuilder.setSubscriptionServiceContentsType(contentsType); + } + } catch (MalformedURLException e) { + Log.w(TAG, "Malformed URL received. " + subscriptionServiceURLString); + } + } + break; + case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE: + subscriptionResult = + ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE; + DownloadInfo downloadInfo = + parseDownloadInfo( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION, + Ts43XmlDoc.CharacteristicType.DOWNLOAD_INFO), + ts43XmlDoc); + if (downloadInfo != null) { + responseBuilder.setDownloadInfo(downloadInfo); + } + break; + case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DONE: + subscriptionResult = ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DONE; + break; + case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD: + subscriptionResult = + ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD; + break; + case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DISMISS: + subscriptionResult = ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DISMISS; + break; + case Ts43XmlDoc.ParmValues.SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE: + subscriptionResult = + ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE; + break; + } + } + + responseBuilder.setSubscriptionResult(subscriptionResult); + return responseBuilder.build(); + } + + /** + * To activate/deactivate the service on the primary or companion device as described in GSMA + * Service Entitlement Configuration section 6.2 and 6.5.4. This is an optional operation. + * + * @param manageServiceRequest The manage service request. + * @return The response of manage service request. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public ManageServiceResponse manageService(@NonNull ManageServiceRequest manageServiceRequest) + throws ServiceEntitlementException { + Objects.requireNonNull(manageServiceRequest); + + ServiceEntitlementRequest.Builder builder = + ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion) + .setTerminalId(mImei); + + if (mTokenType == TOKEN_TYPE_NORMAL) { + builder.setAuthenticationToken(mAuthToken); + } else if (mTokenType == TOKEN_TYPE_TEMPORARY) { + builder.setTemporaryToken(mTemporaryToken); + } + + ServiceEntitlementRequest request = builder.build(); + + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SERVICE) + .setOperationType(manageServiceRequest.operationType()) + .setCompanionTerminalId(manageServiceRequest.companionTerminalId()) + .setCompanionTerminalVendor(manageServiceRequest.companionTerminalVendor()) + .setCompanionTerminalModel(manageServiceRequest.companionTerminalModel()) + .setCompanionTerminalSoftwareVersion( + manageServiceRequest.companionTerminalSoftwareVersion()) + .setCompanionTerminalFriendlyName( + manageServiceRequest.companionTerminalFriendlyName()) + .setCompanionTerminalService( + manageServiceRequest.companionTerminalService()) + .setCompanionTerminalIccid(manageServiceRequest.companionTerminalIccid()) + .build(); + + String rawXml; + try { + rawXml = + mServiceEntitlement.performEsimOdsa(manageServiceRequest.appId(), request, + operation); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "manageService: Failed to perform ODSA operation. e=" + e); + throw e; + } + + // Build the response of manage service operation. Refer to GSMA Service Entitlement + // Configuration section 6.5.4. + ManageServiceResponse.Builder responseBuilder = ManageServiceResponse.builder(); + + Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml); + + try { + processGeneralResult(ts43XmlDoc, responseBuilder); + } catch (MalformedURLException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE, + "manageService: Malformed URL " + rawXml); + } + + // Parse service status. + String serviceStatusString = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.SERVICE_STATUS); + + if (!TextUtils.isEmpty(serviceStatusString)) { + responseBuilder.setServiceStatus(getServiceStatusFromString(serviceStatusString)); + } + + return responseBuilder.build(); + } + + /** + * To provide service related data about a primary or companion device as described in GSMA + * Service Entitlement Configuration section 6.2 and 6.5.5. + * + * @param acquireConfigurationRequest The acquire configuration request. + * @return The response of acquire configuration request. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public AcquireConfigurationResponse acquireConfiguration( + @NonNull AcquireConfigurationRequest acquireConfigurationRequest) + throws ServiceEntitlementException { + Objects.requireNonNull(acquireConfigurationRequest); + + ServiceEntitlementRequest.Builder builder = ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion) + .setTerminalId(mImei) + .setAuthenticationToken(mAuthToken); + + String notificationToken = acquireConfigurationRequest.notificationToken(); + if (!TextUtils.isEmpty(notificationToken)) { + builder.setNotificationToken(notificationToken); + } + int notificationAction = acquireConfigurationRequest.notificationAction(); + if (Ts43Constants.isValidNotificationAction(notificationAction)) { + builder.setNotificationAction(notificationAction); + } + + ServiceEntitlementRequest request = builder.build(); + + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_ACQUIRE_CONFIGURATION) + .setCompanionTerminalId(acquireConfigurationRequest.companionTerminalId()) + .setCompanionTerminalIccid( + acquireConfigurationRequest.companionTerminalIccid()) + .setCompanionTerminalEid(acquireConfigurationRequest.companionTerminalEid()) + .setTerminalIccid(acquireConfigurationRequest.terminalIccid()) + .setTerminalEid(acquireConfigurationRequest.terminalEid()) + .setTargetTerminalId(acquireConfigurationRequest.targetTerminalId()) + .setTargetTerminalIccid(acquireConfigurationRequest.targetTerminalIccid()) + .setTargetTerminalEid(acquireConfigurationRequest.targetTerminalEid()) + .build(); + + String rawXml; + try { + rawXml = + mServiceEntitlement.performEsimOdsa( + acquireConfigurationRequest.appId(), request, operation); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "acquireConfiguration: Failed to perform ODSA operation. e=" + e); + throw e; + } + + AcquireConfigurationResponse.Builder responseBuilder = + AcquireConfigurationResponse.builder(); + AcquireConfigurationResponse.Configuration.Builder configBuilder = + AcquireConfigurationResponse.Configuration.builder(); + + Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml); + + try { + processGeneralResult(ts43XmlDoc, responseBuilder); + } catch (MalformedURLException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE, + "manageSubscription: Malformed URL " + rawXml); + } + + // Parse service status. + String serviceStatusString = + ts43XmlDoc.get( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION, + Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION), + Ts43XmlDoc.Parm.SERVICE_STATUS); + + if (!TextUtils.isEmpty(serviceStatusString)) { + configBuilder.setServiceStatus(getServiceStatusFromString(serviceStatusString)); + } + + // Parse ICCID + String iccIdString = + ts43XmlDoc.get( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION, + Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION), + Ts43XmlDoc.Parm.ICCID); + + if (!TextUtils.isEmpty(iccIdString)) { + configBuilder.setIccid(iccIdString); + } + + // Parse polling interval + String pollingIntervalString = + ts43XmlDoc.get( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION, + Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION), + Ts43XmlDoc.Parm.POLLING_INTERVAL); + + if (!TextUtils.isEmpty(pollingIntervalString)) { + try { + configBuilder.setPollingInterval(Integer.parseInt(pollingIntervalString)); + } catch (NumberFormatException e) { + Log.w( + TAG, "acquireConfiguration: Failed to parse polling interval " + + pollingIntervalString); + } + } + + // Parse download info + DownloadInfo downloadInfo = + parseDownloadInfo( + ImmutableList.of( + Ts43XmlDoc.CharacteristicType.APPLICATION, + Ts43XmlDoc.CharacteristicType.PRIMARY_CONFIGURATION, + Ts43XmlDoc.CharacteristicType.DOWNLOAD_INFO), + ts43XmlDoc); + if (downloadInfo != null) { + configBuilder.setDownloadInfo(downloadInfo); + } + + // TODO: Support different type of configuration. + configBuilder.setType( + AcquireConfigurationResponse.Configuration.CONFIGURATION_TYPE_PRIMARY); + + // TODO: Support multiple configurations. + return responseBuilder.setConfigurations(ImmutableList.of(configBuilder.build())).build(); + } + + /** + * Acquire available mobile plans to be offered by the MNO to a specific user or MDM as + * described in GSMA Service Entitlement Configuration section 6.2 and 6.5.6. + * + * @return List of mobile plans. Empty list if not available. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public List<PlanOffer> acquirePlans() throws ServiceEntitlementException { + return Collections.emptyList(); + } + + /** + * To request a temporary token used to establish trust between ECS and the client as described + * in GSMA Service Entitlement Configuration section 6.2 and 6.5.7. + * + * @param acquireTemporaryTokenRequest The acquire temporary token request. + * @return The temporary token response. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + @SuppressWarnings("AndroidJdkLibsChecker") // java.time.Instant + public AcquireTemporaryTokenResponse acquireTemporaryToken( + @NonNull AcquireTemporaryTokenRequest acquireTemporaryTokenRequest) + throws ServiceEntitlementException { + Objects.requireNonNull(acquireTemporaryTokenRequest); + + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion) + .setTerminalId(mImei) + .setAuthenticationToken(mAuthToken) + .build(); + + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_ACQUIRE_TEMPORARY_TOKEN) + .setOperationTargets(acquireTemporaryTokenRequest.operationTargets()) + .setCompanionTerminalId(acquireTemporaryTokenRequest.companionTerminalId()) + .build(); + + String rawXml; + try { + rawXml = + mServiceEntitlement.performEsimOdsa( + acquireTemporaryTokenRequest.appId(), request, operation); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "acquireTemporaryToken: Failed to perform ODSA operation. e=" + e); + throw e; + } + + Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml); + AcquireTemporaryTokenResponse.Builder responseBuilder = + AcquireTemporaryTokenResponse.builder(); + + try { + processGeneralResult(ts43XmlDoc, responseBuilder); + } catch (MalformedURLException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE, + "AcquireTemporaryTokenResponse: Malformed URL " + rawXml); + } + + // Parse the operation targets. + String operationTargets = + Strings.nullToEmpty( + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.OPERATION_TARGETS)); + + if (operationTargets != null) { + List<String> operationTargetsList = Arrays.asList(operationTargets.split("\\s*,\\s*")); + responseBuilder.setOperationTargets(ImmutableList.copyOf(operationTargetsList)); + } + + // Parse the temporary token + String temporaryToken = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.TEMPORARY_TOKEN); + + if (temporaryToken == null) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_TOKEN_NOT_AVAILABLE, + "temporary token is not available."); + } + + responseBuilder.setTemporaryToken(temporaryToken); + + String temporaryTokenExpiry = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.TEMPORARY_TOKEN_EXPIRY); + + // Parse the token expiration time. + Instant expiry; + try { + expiry = OffsetDateTime.parse(temporaryTokenExpiry).toInstant(); + responseBuilder.setTemporaryTokenExpiry(expiry); + } catch (DateTimeParseException e) { + Log.w(TAG, "Failed to parse temporaryTokenExpiry: " + temporaryTokenExpiry); + } + + return responseBuilder.build(); + } + + /** + * Get the phone number as described in GSMA Service Entitlement Configuration section 6.2 and + * 6.5.8. + * + * @param getPhoneNumberRequest The get phone number request. + * @return The phone number response from the network. + * @throws ServiceEntitlementException The exception for error case. If it's an HTTP response + * error from the server, the error code can be retrieved by + * {@link ServiceEntitlementException#getHttpStatus()} + */ + @NonNull + public GetPhoneNumberResponse getPhoneNumber( + @NonNull GetPhoneNumberRequest getPhoneNumberRequest) + throws ServiceEntitlementException { + ServiceEntitlementRequest.Builder builder = + ServiceEntitlementRequest.builder() + .setEntitlementVersion(mEntitlementVersion); + + if (!TextUtils.isEmpty(getPhoneNumberRequest.terminalId())) { + builder.setTerminalId(getPhoneNumberRequest.terminalId()); + } else { + builder.setTerminalId(mImei); + } + + if (mTokenType == TOKEN_TYPE_NORMAL) { + builder.setAuthenticationToken(mAuthToken); + } else if (mTokenType == TOKEN_TYPE_TEMPORARY) { + builder.setTemporaryToken(mTemporaryToken); + } + + ServiceEntitlementRequest request = builder.build(); + + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_GET_PHONE_NUMBER) + .build(); + + String rawXml; + try { + rawXml = + mServiceEntitlement.performEsimOdsa( + Ts43Constants.APP_PHONE_NUMBER_INFORMATION, request, operation); + } catch (ServiceEntitlementException e) { + Log.w(TAG, "getPhoneNumber: Failed to perform ODSA operation. e=" + e); + throw e; + } + + // Build the response of get phone number operation. Refer to GSMA Service Entitlement + // Configuration section 6.5.8. + GetPhoneNumberResponse.Builder responseBuilder = GetPhoneNumberResponse.builder(); + + Ts43XmlDoc ts43XmlDoc = new Ts43XmlDoc(rawXml); + + try { + processGeneralResult(ts43XmlDoc, responseBuilder); + } catch (MalformedURLException e) { + throw new ServiceEntitlementException( + ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE, + "getPhoneNumber: Malformed URL " + rawXml); + } + + // Parse msisdn. + String msisdn = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.MSISDN); + + if (!TextUtils.isEmpty(msisdn)) { + responseBuilder.setMsisdn(msisdn); + } + + return responseBuilder.build(); + } + + /** + * Parse the download info from {@link ManageSubscriptionResponse}. + * + * @param characteristics The XML nodes to search activation code. + * @param ts43XmlDoc The XML format http response. + * @return The download info. + */ + @Nullable + @SuppressWarnings("AndroidJdkLibsChecker") // java.util.Base64 + private DownloadInfo parseDownloadInfo( + @NonNull ImmutableList<String> characteristics, @NonNull Ts43XmlDoc ts43XmlDoc) { + String activationCode = + Strings.nullToEmpty( + ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.PROFILE_ACTIVATION_CODE)); + String smdpAddress = + Strings.nullToEmpty( + ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.PROFILE_SMDP_ADDRESS)); + String iccid = + Strings.nullToEmpty(ts43XmlDoc.get(characteristics, Ts43XmlDoc.Parm.PROFILE_ICCID)); + + // DownloadInfo should contain either activationCode or smdpAddress + iccid + if (!activationCode.isEmpty()) { + // decode the activation code, which is in base64 format + try { + activationCode = new String(Base64.getDecoder().decode(activationCode)); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Failed to decode the activation code " + activationCode); + return null; + } + return DownloadInfo.builder() + .setProfileActivationCode(activationCode) + .setProfileIccid(iccid) + .build(); + } else if (!smdpAddress.isEmpty() && !iccid.isEmpty()) { + return DownloadInfo.builder() + .setProfileIccid(iccid) + .setProfileSmdpAddresses( + ImmutableList.copyOf(Arrays.asList(smdpAddress.split("\\s*,\\s*")))) + .build(); + } else { + Log.w( + TAG, + "Failed to parse download info. activationCode=" + + activationCode + + ", smdpAddress=" + + smdpAddress + + ", iccid=" + + iccid); + return null; + } + } + + /** + * Process the common ODSA result from HTTP response. + * + * @param ts43XmlDoc The TS.43 ODSA operation response in XLM format. + * @param builder The response builder. + * @throws MalformedURLException when HTTP response is not well formatted. + */ + private void processGeneralResult( + @NonNull Ts43XmlDoc ts43XmlDoc, @NonNull OdsaResponse.Builder builder) + throws MalformedURLException { + // Now start to parse the result from HTTP response. + // Parse the operation result. + String operationResult = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.OPERATION_RESULT); + + builder.setOperationResult(EsimOdsaOperation.OPERATION_RESULT_UNKNOWN); + if (!TextUtils.isEmpty(operationResult)) { + switch (operationResult) { + case Ts43XmlDoc.ParmValues.OPERATION_RESULT_SUCCESS: + builder.setOperationResult(EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + break; + case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_GENERAL: + builder.setOperationResult(EsimOdsaOperation.OPERATION_RESULT_ERROR_GENERAL); + break; + case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_INVALID_OPERATION: + builder.setOperationResult( + EsimOdsaOperation.OPERATION_RESULT_ERROR_INVALID_OPERATION); + break; + case Ts43XmlDoc.ParmValues.OPERATION_RESULT_ERROR_INVALID_PARAMETER: + builder.setOperationResult( + EsimOdsaOperation.OPERATION_RESULT_ERROR_INVALID_PARAMETER); + break; + case Ts43XmlDoc.ParmValues.OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION: + builder.setOperationResult( + EsimOdsaOperation.OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION); + break; + } + } + + // Parse the general error URL + String generalErrorUrl = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.GENERAL_ERROR_URL); + if (!TextUtils.isEmpty(generalErrorUrl)) { + builder.setGeneralErrorUrl(new URL(generalErrorUrl)); + } + + // Parse the general error URL user data + String generalErrorUserData = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.APPLICATION), + Ts43XmlDoc.Parm.GENERAL_ERROR_USER_DATA); + if (!TextUtils.isEmpty(generalErrorUserData)) { + builder.setGeneralErrorUserData(generalErrorUserData); + } + + // Parse the token for next operation. + String token = + ts43XmlDoc.get( + ImmutableList.of(Ts43XmlDoc.CharacteristicType.TOKEN), + Ts43XmlDoc.Parm.TOKEN); + if (!TextUtils.isEmpty(token)) { + // Some servers issue the new token in operation result for next operation to use. + // We need to save it. + mAuthToken = token; + Log.d(TAG, "processGeneralResult: Token replaced."); + } + } + + /** + * Get the service status from string as described in GSMA Service Entitlement Configuration + * section 6.5.4. + * + * @param serviceStatusString Service status in string format defined in GSMA Service + * Entitlement Configuration section 6.5.4. + * @return The converted service status. {@link EsimOdsaOperation#SERVICE_STATUS_UNKNOWN} if not + * able to convert. + */ + @OdsaServiceStatus + private int getServiceStatusFromString(@NonNull String serviceStatusString) { + switch (serviceStatusString) { + case Ts43XmlDoc.ParmValues.SERVICE_STATUS_ACTIVATED: + return EsimOdsaOperation.SERVICE_STATUS_ACTIVATED; + case Ts43XmlDoc.ParmValues.SERVICE_STATUS_ACTIVATING: + return EsimOdsaOperation.SERVICE_STATUS_ACTIVATING; + case Ts43XmlDoc.ParmValues.SERVICE_STATUS_DEACTIVATED: + return EsimOdsaOperation.SERVICE_STATUS_DEACTIVATED; + case Ts43XmlDoc.ParmValues.SERVICE_STATUS_DEACTIVATED_NO_REUSE: + return EsimOdsaOperation.SERVICE_STATUS_DEACTIVATED_NO_REUSE; + } + return EsimOdsaOperation.SERVICE_STATUS_UNKNOWN; + } +}
\ No newline at end of file diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java index 2847c16..3029200 100644 --- a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java @@ -16,15 +16,19 @@ package com.android.libraries.entitlement.eapaka; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_FAILURE; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_JSON_COMPOSE_FAILURE; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE; import android.content.Context; +import android.content.pm.PackageInfo; import android.net.Uri; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -33,6 +37,7 @@ import com.android.libraries.entitlement.EsimOdsaOperation; import com.android.libraries.entitlement.ServiceEntitlementException; import com.android.libraries.entitlement.ServiceEntitlementRequest; import com.android.libraries.entitlement.http.HttpClient; +import com.android.libraries.entitlement.http.HttpConstants.ContentType; import com.android.libraries.entitlement.http.HttpConstants.RequestMethod; import com.android.libraries.entitlement.http.HttpRequest; import com.android.libraries.entitlement.http.HttpResponse; @@ -49,6 +54,8 @@ public class EapAkaApi { private static final String TAG = "ServiceEntitlement"; public static final String EAP_CHALLENGE_RESPONSE = "eap-relay-packet"; + private static final String CONTENT_TYPE_EAP_RELAY_JSON = + "application/vnd.gsma.eap-relay.v1.0+json"; private static final String VERS = "vers"; private static final String ENTITLEMENT_VERSION = "entitlement_version"; @@ -83,21 +90,35 @@ public class EapAkaApi { private static final String TERMINAL_EID = "terminal_eid"; private static final String TARGET_TERMINAL_ID = "target_terminal_id"; + // Non-standard params for Korean carriers + private static final String TARGET_TERMINAL_IDS = "target_terminal_imeis"; private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid"; private static final String TARGET_TERMINAL_EID = "target_terminal_eid"; + // Non-standard params for Korean carriers + private static final String TARGET_TERMINAL_SERIAL_NUMBER = "target_terminal_sn"; + // Non-standard params for Korean carriers + private static final String TARGET_TERMINAL_MODEL = "target_terminal_model"; private static final String OLD_TERMINAL_ID = "old_terminal_id"; private static final String OLD_TERMINAL_ICCID = "old_terminal_iccid"; - private static final String NETWORK_IDENTIFIER = "network_identifier"; + private static final String BOOST_TYPE = "boost_type"; - // In case of EAP-AKA synchronization failure, we try to recover for at most two times. - private static final int FOLLOW_SYNC_FAILURE_MAX_COUNT = 2; + // In case of EAP-AKA synchronization failure or another challenge, we try to authenticate for + // at most three times. + private static final int MAX_EAP_AKA_ATTEMPTS = 3; + + // Max TERMINAL_* string length according to GSMA RCC.14 section 2.4 + private static final int MAX_TERMINAL_VENDOR_LENGTH = 4; + private static final int MAX_TERMINAL_MODEL_LENGTH = 10; + private static final int MAX_TERMINAL_SOFTWARE_VERSION_LENGTH = 20; private final Context mContext; private final int mSimSubscriptionId; private final HttpClient mHttpClient; private final String mBypassEapAkaResponse; + private final String mAppVersion; + private final TelephonyManager mTelephonyManager; public EapAkaApi( Context context, @@ -117,40 +138,89 @@ public class EapAkaApi { this.mSimSubscriptionId = simSubscriptionId; this.mHttpClient = httpClient; this.mBypassEapAkaResponse = bypassEapAkaResponse; + this.mAppVersion = getAppVersion(context); + this.mTelephonyManager = + mContext.getSystemService(TelephonyManager.class) + .createForSubscriptionId(mSimSubscriptionId); } /** - * Retrieves raw entitlement configuration doc though EAP-AKA authentication. + * Retrieves HTTP response with the entitlement configuration doc though EAP-AKA authentication. * * <p>Implementation based on GSMA TS.43-v5.0 2.6.1. * * @throws ServiceEntitlementException when getting an unexpected http response. */ - @Nullable - public String queryEntitlementStatus(ImmutableList<String> appIds, - CarrierConfig carrierConfig, ServiceEntitlementRequest request) + @NonNull + public HttpResponse queryEntitlementStatus( + ImmutableList<String> appIds, + CarrierConfig carrierConfig, + ServiceEntitlementRequest request) throws ServiceEntitlementException { - Uri.Builder urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); - appendParametersForServiceEntitlementRequest(urlBuilder, appIds, request); + Uri.Builder urlBuilder = null; + JSONObject postData = null; + if (carrierConfig.useHttpPost()) { + postData = new JSONObject(); + appendParametersForAuthentication(postData, request); + appendParametersForServiceEntitlementRequest(postData, appIds, request); + } else { + urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); + appendParametersForAuthentication(urlBuilder, request); + appendParametersForServiceEntitlementRequest(urlBuilder, appIds, request); + } + if (!TextUtils.isEmpty(request.authenticationToken())) { // Fast Re-Authentication flow with pre-existing auth token Log.d(TAG, "Fast Re-Authentication"); - return httpGet( - urlBuilder.toString(), carrierConfig, request.acceptContentType()).body(); + return carrierConfig.useHttpPost() + ? httpPost( + postData, + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()) + : httpGet( + urlBuilder.toString(), + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); } else { // Full Authentication flow Log.d(TAG, "Full Authentication"); HttpResponse challengeResponse = - httpGet( - urlBuilder.toString(), - carrierConfig, - ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); + carrierConfig.useHttpPost() + ? httpPost( + postData, + carrierConfig, + CONTENT_TYPE_EAP_RELAY_JSON, + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()) + : httpGet( + urlBuilder.toString(), + carrierConfig, + CONTENT_TYPE_EAP_RELAY_JSON, + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); + String eapAkaChallenge = getEapAkaChallenge(challengeResponse); + if (eapAkaChallenge == null) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse EAP-AKA challenge: " + challengeResponse.body()); + } return respondToEapAkaChallenge( carrierConfig, - challengeResponse, - FOLLOW_SYNC_FAILURE_MAX_COUNT, - request.acceptContentType()) - .body(); + eapAkaChallenge, + challengeResponse.cookies(), + MAX_EAP_AKA_ATTEMPTS, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); } } @@ -158,62 +228,105 @@ public class EapAkaApi { * Sends a follow-up HTTP request to the HTTP {@code response} using the same cookie, and * returns the follow-up HTTP response. * - * <p>The {@code response} should contain a EAP-AKA challenge from server, and the - * follow-up request could contain: + * <p>The {@code eapAkaChallenge} should be the EAP-AKA challenge from server, and the follow-up + * request could contain: * * <ul> - * <li>The EAP-AKA response message, and the follow-up response should contain the - * service entitlement configuration; or, - * <li>The EAP-AKA synchronization failure message, and the follow-up response should - * contain the new EAP-AKA challenge. Then this method calls itself to follow-up - * the new challenge and return a new response, if {@code followSyncFailureCount} - * is greater than zero. When this method call itself {@code followSyncFailureCount} is - * reduced by one to prevent infinite loop (unlikely in practice, but just in case). + * <li>The EAP-AKA response message, and the follow-up response should contain the service + * entitlement configuration, or another EAP-AKA challenge in which case the method calls + * if {@code remainingAttempts} is greater than zero (If {@code remainingAttempts} reaches + * 0, the method will throw ServiceEntitlementException) ; or + * <li>The EAP-AKA synchronization failure message, and the follow-up response should contain + * the new EAP-AKA challenge. Then this method calls itself to follow-up the new challenge + * and return a new response, as long as {@code remainingAttempts} is greater than zero. * </ul> * - * @param response Challenge response from server which its content type is JSON + * @return Challenge response from server whose content type is JSON */ + @NonNull private HttpResponse respondToEapAkaChallenge( CarrierConfig carrierConfig, - HttpResponse response, - int followSyncFailureCount, - String contentType) + String eapAkaChallenge, + ImmutableList<String> cookies, + int remainingAttempts, + String acceptContentType, + String terminalVendor, + String terminalModel, + String terminalSoftwareVersion) throws ServiceEntitlementException { - String eapAkaChallenge; - try { - eapAkaChallenge = new JSONObject(response.body()).getString(EAP_CHALLENGE_RESPONSE); - } catch (JSONException jsonException) { - throw new ServiceEntitlementException( - ERROR_MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException); - } if (!mBypassEapAkaResponse.isEmpty()) { return challengeResponse( - mBypassEapAkaResponse, - carrierConfig, - response.cookies(), - contentType); + mBypassEapAkaResponse, + carrierConfig, + cookies, + CONTENT_TYPE_EAP_RELAY_JSON + ", " + acceptContentType, + terminalVendor, + terminalModel, + terminalSoftwareVersion); } + EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge); EapAkaResponse eapAkaResponse = EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge); - // This could be a successful authentication, or synchronization failure. - if (eapAkaResponse.response() != null) { // successful authentication - return challengeResponse( + // This could be a successful authentication, another challenge, or synchronization failure. + if (eapAkaResponse.response() != null) { + HttpResponse response = + challengeResponse( eapAkaResponse.response(), carrierConfig, - response.cookies(), - contentType); + cookies, + CONTENT_TYPE_EAP_RELAY_JSON + ", " + acceptContentType, + terminalVendor, + terminalModel, + terminalSoftwareVersion); + String nextEapAkaChallenge = getEapAkaChallenge(response); + // successful authentication + if (nextEapAkaChallenge == null) { + return response; + } + // another challenge + Log.d(TAG, "Received another challenge"); + if (remainingAttempts > 0) { + return respondToEapAkaChallenge( + carrierConfig, + nextEapAkaChallenge, + cookies, + remainingAttempts - 1, + acceptContentType, + terminalVendor, + terminalModel, + terminalSoftwareVersion); + } else { + throw new ServiceEntitlementException( + ERROR_EAP_AKA_FAILURE, "Unable to EAP-AKA authenticate"); + } } else if (eapAkaResponse.synchronizationFailureResponse() != null) { Log.d(TAG, "synchronization failure"); HttpResponse newChallenge = challengeResponse( eapAkaResponse.synchronizationFailureResponse(), carrierConfig, - response.cookies(), - ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); - if (followSyncFailureCount > 0) { + cookies, + CONTENT_TYPE_EAP_RELAY_JSON, + terminalVendor, + terminalModel, + terminalSoftwareVersion); + String nextEapAkaChallenge = getEapAkaChallenge(newChallenge); + if (nextEapAkaChallenge == null) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse EAP-AKA challenge: " + newChallenge.body()); + } + if (remainingAttempts > 0) { return respondToEapAkaChallenge( - carrierConfig, newChallenge, followSyncFailureCount - 1, contentType); + carrierConfig, + nextEapAkaChallenge, + cookies, + remainingAttempts - 1, + acceptContentType, + terminalVendor, + terminalModel, + terminalSoftwareVersion); } else { throw new ServiceEntitlementException( ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE, @@ -224,13 +337,16 @@ public class EapAkaApi { } } + @NonNull private HttpResponse challengeResponse( String eapAkaChallengeResponse, CarrierConfig carrierConfig, ImmutableList<String> cookies, - String contentType) + String acceptContentType, + String terminalVendor, + String terminalModel, + String terminalSoftwareVersion) throws ServiceEntitlementException { - Log.d(TAG, "challengeResponse"); JSONObject postData = new JSONObject(); try { postData.put(EAP_CHALLENGE_RESPONSE, eapAkaChallengeResponse); @@ -238,67 +354,174 @@ public class EapAkaApi { throw new ServiceEntitlementException( ERROR_MALFORMED_HTTP_RESPONSE, "Failed to put post data", jsonException); } - HttpRequest request = - HttpRequest.builder() - .setUrl(carrierConfig.serverUrl()) - .setRequestMethod(RequestMethod.POST) - .setPostData(postData) - .addRequestProperty(HttpHeaders.ACCEPT, contentType) - .addRequestProperty( - HttpHeaders.CONTENT_TYPE, - ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON) - .addRequestProperty(HttpHeaders.COOKIE, cookies) - .setTimeoutInSec(carrierConfig.timeoutInSec()) - .setNetwork(carrierConfig.network()) - .build(); - return mHttpClient.request(request); + return httpPost( + postData, + carrierConfig, + acceptContentType, + terminalVendor, + terminalModel, + terminalSoftwareVersion, + CONTENT_TYPE_EAP_RELAY_JSON, + cookies); } /** - * Retrieves raw doc of performing ODSA operations. For operation type, see {@link + * Retrieves HTTP response from performing ODSA operations. For operation type, see {@link * EsimOdsaOperation}. * * <p>Implementation based on GSMA TS.43-v5.0 6.1. */ - public String performEsimOdsaOperation(String appId, CarrierConfig carrierConfig, - ServiceEntitlementRequest request, EsimOdsaOperation odsaOperation) + @NonNull + public HttpResponse performEsimOdsaOperation( + String appId, + CarrierConfig carrierConfig, + ServiceEntitlementRequest request, + EsimOdsaOperation odsaOperation) throws ServiceEntitlementException { - Uri.Builder urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); - appendParametersForServiceEntitlementRequest(urlBuilder, ImmutableList.of(appId), request); - appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation); + Uri.Builder urlBuilder = null; + JSONObject postData = null; + if (carrierConfig.useHttpPost()) { + postData = new JSONObject(); + appendParametersForAuthentication(postData, request); + appendParametersForServiceEntitlementRequest( + postData, ImmutableList.of(appId), request); + appendParametersForEsimOdsaOperation(postData, odsaOperation); + } else { + urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); + appendParametersForAuthentication(urlBuilder, request); + appendParametersForServiceEntitlementRequest( + urlBuilder, ImmutableList.of(appId), request); + appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation); + } if (!TextUtils.isEmpty(request.authenticationToken()) || !TextUtils.isEmpty(request.temporaryToken())) { // Fast Re-Authentication flow with pre-existing auth token Log.d(TAG, "Fast Re-Authentication"); - return httpGet( - urlBuilder.toString(), carrierConfig, request.acceptContentType()).body(); + return carrierConfig.useHttpPost() + ? httpPost( + postData, + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()) + : httpGet( + urlBuilder.toString(), + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); } else { // Full Authentication flow Log.d(TAG, "Full Authentication"); HttpResponse challengeResponse = - httpGet( - urlBuilder.toString(), - carrierConfig, - ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); + carrierConfig.useHttpPost() + ? httpPost( + postData, + carrierConfig, + CONTENT_TYPE_EAP_RELAY_JSON, + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()) + : httpGet( + urlBuilder.toString(), + carrierConfig, + CONTENT_TYPE_EAP_RELAY_JSON, + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); + String eapAkaChallenge = getEapAkaChallenge(challengeResponse); + if (eapAkaChallenge == null) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse EAP-AKA challenge: " + challengeResponse.body()); + } return respondToEapAkaChallenge( carrierConfig, - challengeResponse, - FOLLOW_SYNC_FAILURE_MAX_COUNT, - request.acceptContentType()) - .body(); + eapAkaChallenge, + challengeResponse.cookies(), + MAX_EAP_AKA_ATTEMPTS, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); } } - private void appendParametersForServiceEntitlementRequest( - Uri.Builder urlBuilder, ImmutableList<String> appIds, - ServiceEntitlementRequest request) { - TelephonyManager telephonyManager = mContext.getSystemService( - TelephonyManager.class).createForSubscriptionId(mSimSubscriptionId); + /** + * Retrieves the endpoint for OpenID Connect(OIDC) authentication. + * + * <p>Implementation based on section 2.8.2 of TS.43 + * + * <p>The user should call {@link #queryEntitlementStatusFromOidc(String, CarrierConfig, + * String)} with the authentication result to retrieve the service entitlement configuration. + */ + @NonNull + public String acquireOidcAuthenticationEndpoint( + String appId, CarrierConfig carrierConfig, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + Uri.Builder urlBuilder = null; + JSONObject postData = null; + if (carrierConfig.useHttpPost()) { + postData = new JSONObject(); + appendParametersForServiceEntitlementRequest( + postData, ImmutableList.of(appId), request); + } else { + urlBuilder = Uri.parse(carrierConfig.serverUrl()).buildUpon(); + appendParametersForServiceEntitlementRequest( + urlBuilder, ImmutableList.of(appId), request); + } + + HttpResponse response = + carrierConfig.useHttpPost() + ? httpPost( + postData, + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()) + : httpGet( + urlBuilder.toString(), + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); + return response.location(); + } + + /** + * Retrieves the HTTP response with the service entitlement configuration from OIDC + * authentication result. + * + * <p>Implementation based on section 2.8.2 of TS.43. + * + * <p>{@link #acquireOidcAuthenticationEndpoint} must be called before calling this method. + */ + @NonNull + public HttpResponse queryEntitlementStatusFromOidc( + String url, CarrierConfig carrierConfig, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + Uri.Builder urlBuilder = Uri.parse(url).buildUpon(); + return httpGet( + urlBuilder.toString(), + carrierConfig, + request.acceptContentType(), + request.terminalVendor(), + request.terminalModel(), + request.terminalSoftwareVersion()); + } + + @SuppressWarnings("HardwareIds") + private void appendParametersForAuthentication( + Uri.Builder urlBuilder, ServiceEntitlementRequest request) { if (!TextUtils.isEmpty(request.authenticationToken())) { // IMSI and token required for fast AuthN. urlBuilder - .appendQueryParameter(IMSI, telephonyManager.getSubscriberId()) + .appendQueryParameter(IMSI, mTelephonyManager.getSubscriberId()) .appendQueryParameter(TOKEN, request.authenticationToken()); } else if (!TextUtils.isEmpty(request.temporaryToken())) { // temporary_token required for fast AuthN. @@ -307,20 +530,53 @@ public class EapAkaApi { // EAP_ID required for initial AuthN urlBuilder.appendQueryParameter( EAP_ID, - getImsiEap(telephonyManager.getSimOperator(), - telephonyManager.getSubscriberId())); + getImsiEap( + mTelephonyManager.getSimOperator(), + mTelephonyManager.getSubscriberId())); } + } + @SuppressWarnings("HardwareIds") + private void appendParametersForAuthentication( + JSONObject postData, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + try { + if (!TextUtils.isEmpty(request.authenticationToken())) { + // IMSI and token required for fast AuthN. + postData.put(IMSI, mTelephonyManager.getSubscriberId()); + postData.put(TOKEN, request.authenticationToken()); + } else if (!TextUtils.isEmpty(request.temporaryToken())) { + // temporary_token required for fast AuthN. + postData.put(TEMPORARY_TOKEN, request.temporaryToken()); + } else { + // EAP_ID required for initial AuthN + postData.put( + EAP_ID, + getImsiEap( + mTelephonyManager.getSimOperator(), + mTelephonyManager.getSubscriberId())); + } + } catch (JSONException jsonException) { + // Should never happen + throw new ServiceEntitlementException( + ERROR_JSON_COMPOSE_FAILURE, "Failed to compose JSON", jsonException); + } + } + + private void appendParametersForServiceEntitlementRequest( + Uri.Builder urlBuilder, + ImmutableList<String> appIds, + ServiceEntitlementRequest request) { if (!TextUtils.isEmpty(request.notificationToken())) { urlBuilder - .appendQueryParameter(NOTIF_ACTION, - Integer.toString(request.notificationAction())) + .appendQueryParameter( + NOTIF_ACTION, Integer.toString(request.notificationAction())) .appendQueryParameter(NOTIF_TOKEN, request.notificationToken()); } // Assign terminal ID with device IMEI if not set. if (TextUtils.isEmpty(request.terminalId())) { - urlBuilder.appendQueryParameter(TERMINAL_ID, telephonyManager.getImei()); + urlBuilder.appendQueryParameter(TERMINAL_ID, mTelephonyManager.getImei()); } else { urlBuilder.appendQueryParameter(TERMINAL_ID, request.terminalId()); } @@ -328,7 +584,7 @@ public class EapAkaApi { // Optional query parameters, append them if not empty appendOptionalQueryParameter(urlBuilder, APP_VERSION, request.appVersion()); appendOptionalQueryParameter(urlBuilder, APP_NAME, request.appName()); - appendOptionalQueryParameter(urlBuilder, NETWORK_IDENTIFIER, request.networkIdentifier()); + appendOptionalQueryParameter(urlBuilder, BOOST_TYPE, request.boostType()); for (String appId : appIds) { urlBuilder.appendQueryParameter(APP, appId); @@ -336,73 +592,354 @@ public class EapAkaApi { urlBuilder // Identity and Authentication parameters - .appendQueryParameter(TERMINAL_VENDOR, request.terminalVendor()) - .appendQueryParameter(TERMINAL_MODEL, request.terminalModel()) - .appendQueryParameter(TERMIAL_SW_VERSION, request.terminalSoftwareVersion()) + .appendQueryParameter( + TERMINAL_VENDOR, + trimString(request.terminalVendor(), MAX_TERMINAL_VENDOR_LENGTH)) + .appendQueryParameter( + TERMINAL_MODEL, + trimString(request.terminalModel(), MAX_TERMINAL_MODEL_LENGTH)) + .appendQueryParameter( + TERMIAL_SW_VERSION, + trimString( + request.terminalSoftwareVersion(), + MAX_TERMINAL_SOFTWARE_VERSION_LENGTH)) // General Service parameters .appendQueryParameter(VERS, Integer.toString(request.configurationVersion())) .appendQueryParameter(ENTITLEMENT_VERSION, request.entitlementVersion()); } + private void appendParametersForServiceEntitlementRequest( + JSONObject postData, ImmutableList<String> appIds, ServiceEntitlementRequest request) + throws ServiceEntitlementException { + try { + if (!TextUtils.isEmpty(request.notificationToken())) { + postData.put(NOTIF_ACTION, Integer.toString(request.notificationAction())); + postData.put(NOTIF_TOKEN, request.notificationToken()); + } + + // Assign terminal ID with device IMEI if not set. + if (TextUtils.isEmpty(request.terminalId())) { + postData.put(TERMINAL_ID, mTelephonyManager.getImei()); + } else { + postData.put(TERMINAL_ID, request.terminalId()); + } + + // Optional query parameters, append them if not empty + appendOptionalQueryParameter(postData, APP_VERSION, request.appVersion()); + appendOptionalQueryParameter(postData, APP_NAME, request.appName()); + appendOptionalQueryParameter(postData, BOOST_TYPE, request.boostType()); + + if (appIds.size() == 1) { + appendOptionalQueryParameter(postData, APP, appIds.get(0)); + } else { + appendOptionalQueryParameter( + postData, APP, "[" + TextUtils.join(",", appIds) + "]"); + } + + appendOptionalQueryParameter( + postData, + TERMINAL_VENDOR, + trimString(request.terminalVendor(), MAX_TERMINAL_VENDOR_LENGTH)); + appendOptionalQueryParameter( + postData, + TERMINAL_MODEL, + trimString(request.terminalModel(), MAX_TERMINAL_MODEL_LENGTH)); + appendOptionalQueryParameter( + postData, + TERMIAL_SW_VERSION, + trimString( + request.terminalSoftwareVersion(), + MAX_TERMINAL_SOFTWARE_VERSION_LENGTH)); + appendOptionalQueryParameter( + postData, VERS, Integer.toString(request.configurationVersion())); + appendOptionalQueryParameter( + postData, ENTITLEMENT_VERSION, request.entitlementVersion()); + } catch (JSONException jsonException) { + // Should never happen + throw new ServiceEntitlementException( + ERROR_JSON_COMPOSE_FAILURE, "Failed to compose JSON", jsonException); + } + } + private void appendParametersForEsimOdsaOperation( Uri.Builder urlBuilder, EsimOdsaOperation odsaOperation) { urlBuilder.appendQueryParameter(OPERATION, odsaOperation.operation()); if (odsaOperation.operationType() != EsimOdsaOperation.OPERATION_TYPE_NOT_SET) { - urlBuilder.appendQueryParameter(OPERATION_TYPE, - Integer.toString(odsaOperation.operationType())); + urlBuilder.appendQueryParameter( + OPERATION_TYPE, Integer.toString(odsaOperation.operationType())); } appendOptionalQueryParameter( urlBuilder, OPERATION_TARGETS, TextUtils.join(",", odsaOperation.operationTargets())); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ID, - odsaOperation.companionTerminalId()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_VENDOR, - odsaOperation.companionTerminalVendor()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_MODEL, - odsaOperation.companionTerminalModel()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_SW_VERSION, + appendOptionalQueryParameter( + urlBuilder, COMPANION_TERMINAL_ID, odsaOperation.companionTerminalId()); + appendOptionalQueryParameter( + urlBuilder, COMPANION_TERMINAL_VENDOR, odsaOperation.companionTerminalVendor()); + appendOptionalQueryParameter( + urlBuilder, COMPANION_TERMINAL_MODEL, odsaOperation.companionTerminalModel()); + appendOptionalQueryParameter( + urlBuilder, + COMPANION_TERMINAL_SW_VERSION, odsaOperation.companionTerminalSoftwareVersion()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_FRIENDLY_NAME, + appendOptionalQueryParameter( + urlBuilder, + COMPANION_TERMINAL_FRIENDLY_NAME, odsaOperation.companionTerminalFriendlyName()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_SERVICE, - odsaOperation.companionTerminalService()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ICCID, - odsaOperation.companionTerminalIccid()); - appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_EID, - odsaOperation.companionTerminalEid()); - appendOptionalQueryParameter(urlBuilder, TERMINAL_ICCID, - odsaOperation.terminalIccid()); + appendOptionalQueryParameter( + urlBuilder, COMPANION_TERMINAL_SERVICE, odsaOperation.companionTerminalService()); + appendOptionalQueryParameter( + urlBuilder, COMPANION_TERMINAL_ICCID, odsaOperation.companionTerminalIccid()); + appendOptionalQueryParameter( + urlBuilder, COMPANION_TERMINAL_EID, odsaOperation.companionTerminalEid()); + appendOptionalQueryParameter(urlBuilder, TERMINAL_ICCID, odsaOperation.terminalIccid()); appendOptionalQueryParameter(urlBuilder, TERMINAL_EID, odsaOperation.terminalEid()); - appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_ID, - odsaOperation.targetTerminalId()); - appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_ICCID, - odsaOperation.targetTerminalIccid()); - appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_EID, - odsaOperation.targetTerminalEid()); - appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ICCID, - odsaOperation.oldTerminalIccid()); - appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ID, - odsaOperation.oldTerminalId()); + appendOptionalQueryParameter( + urlBuilder, TARGET_TERMINAL_ID, odsaOperation.targetTerminalId()); + appendOptionalQueryParameter( + urlBuilder, TARGET_TERMINAL_IDS, odsaOperation.targetTerminalIds()); + appendOptionalQueryParameter( + urlBuilder, TARGET_TERMINAL_ICCID, odsaOperation.targetTerminalIccid()); + appendOptionalQueryParameter( + urlBuilder, TARGET_TERMINAL_EID, odsaOperation.targetTerminalEid()); + appendOptionalQueryParameter( + urlBuilder, + TARGET_TERMINAL_SERIAL_NUMBER, + odsaOperation.targetTerminalSerialNumber()); + appendOptionalQueryParameter( + urlBuilder, TARGET_TERMINAL_MODEL, odsaOperation.targetTerminalModel()); + appendOptionalQueryParameter( + urlBuilder, OLD_TERMINAL_ICCID, odsaOperation.oldTerminalIccid()); + appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ID, odsaOperation.oldTerminalId()); + } + + private void appendParametersForEsimOdsaOperation( + JSONObject postData, EsimOdsaOperation odsaOperation) + throws ServiceEntitlementException { + try { + postData.put(OPERATION, odsaOperation.operation()); + if (odsaOperation.operationType() != EsimOdsaOperation.OPERATION_TYPE_NOT_SET) { + postData.put(OPERATION_TYPE, Integer.toString(odsaOperation.operationType())); + } + appendOptionalQueryParameter( + postData, + OPERATION_TARGETS, + TextUtils.join(",", odsaOperation.operationTargets())); + appendOptionalQueryParameter( + postData, COMPANION_TERMINAL_ID, odsaOperation.companionTerminalId()); + appendOptionalQueryParameter( + postData, COMPANION_TERMINAL_VENDOR, odsaOperation.companionTerminalVendor()); + appendOptionalQueryParameter( + postData, COMPANION_TERMINAL_MODEL, odsaOperation.companionTerminalModel()); + appendOptionalQueryParameter( + postData, + COMPANION_TERMINAL_SW_VERSION, + odsaOperation.companionTerminalSoftwareVersion()); + appendOptionalQueryParameter( + postData, + COMPANION_TERMINAL_FRIENDLY_NAME, + odsaOperation.companionTerminalFriendlyName()); + appendOptionalQueryParameter( + postData, COMPANION_TERMINAL_SERVICE, odsaOperation.companionTerminalService()); + appendOptionalQueryParameter( + postData, COMPANION_TERMINAL_ICCID, odsaOperation.companionTerminalIccid()); + appendOptionalQueryParameter( + postData, COMPANION_TERMINAL_EID, odsaOperation.companionTerminalEid()); + appendOptionalQueryParameter(postData, TERMINAL_ICCID, odsaOperation.terminalIccid()); + appendOptionalQueryParameter(postData, TERMINAL_EID, odsaOperation.terminalEid()); + appendOptionalQueryParameter( + postData, TARGET_TERMINAL_ID, odsaOperation.targetTerminalId()); + appendOptionalQueryParameter( + postData, TARGET_TERMINAL_IDS, odsaOperation.targetTerminalIds()); + appendOptionalQueryParameter( + postData, TARGET_TERMINAL_ICCID, odsaOperation.targetTerminalIccid()); + appendOptionalQueryParameter( + postData, TARGET_TERMINAL_EID, odsaOperation.targetTerminalEid()); + appendOptionalQueryParameter( + postData, + TARGET_TERMINAL_SERIAL_NUMBER, + odsaOperation.targetTerminalSerialNumber()); + appendOptionalQueryParameter( + postData, TARGET_TERMINAL_MODEL, odsaOperation.targetTerminalModel()); + appendOptionalQueryParameter( + postData, OLD_TERMINAL_ICCID, odsaOperation.oldTerminalIccid()); + appendOptionalQueryParameter(postData, OLD_TERMINAL_ID, odsaOperation.oldTerminalId()); + } catch (JSONException jsonException) { + // Should never happen + throw new ServiceEntitlementException( + ERROR_JSON_COMPOSE_FAILURE, "Failed to compose JSON", jsonException); + } + } + + private void appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value) { + if (!TextUtils.isEmpty(value)) { + urlBuilder.appendQueryParameter(key, value); + } + } + + private void appendOptionalQueryParameter(JSONObject postData, String key, String value) + throws JSONException { + if (!TextUtils.isEmpty(value)) { + postData.put(key, value); + } + } + + private void appendOptionalQueryParameter( + Uri.Builder urlBuilder, String key, ImmutableList<String> values) { + for (String value : values) { + if (!TextUtils.isEmpty(value)) { + urlBuilder.appendQueryParameter(key, value); + } + } + } + + private void appendOptionalQueryParameter( + JSONObject postData, String key, ImmutableList<String> values) throws JSONException { + for (String value : values) { + if (!TextUtils.isEmpty(value)) { + postData.put(key, value); + } + } } - private HttpResponse httpGet(String url, CarrierConfig carrierConfig, String contentType) + @NonNull + private HttpResponse httpGet( + String url, + CarrierConfig carrierConfig, + String acceptContentType, + String terminalVendor, + String terminalModel, + String terminalSoftwareVersion) throws ServiceEntitlementException { - HttpRequest httpRequest = + HttpRequest.Builder builder = HttpRequest.builder() .setUrl(url) .setRequestMethod(RequestMethod.GET) - .addRequestProperty(HttpHeaders.ACCEPT, contentType) + .addRequestProperty(HttpHeaders.ACCEPT, acceptContentType) .setTimeoutInSec(carrierConfig.timeoutInSec()) - .setNetwork(carrierConfig.network()) - .build(); - return mHttpClient.request(httpRequest); + .setNetwork(carrierConfig.network()); + String userAgent = + getUserAgent( + carrierConfig.clientTs43(), + terminalVendor, + terminalModel, + terminalSoftwareVersion); + if (!TextUtils.isEmpty(userAgent)) { + builder.addRequestProperty(HttpHeaders.USER_AGENT, userAgent); + } + return mHttpClient.request(builder.build()); } - private void appendOptionalQueryParameter(Uri.Builder urlBuilder, String key, String value) { - if (!TextUtils.isEmpty(value)) { - urlBuilder.appendQueryParameter(key, value); + @NonNull + private HttpResponse httpPost( + JSONObject postData, + CarrierConfig carrierConfig, + String acceptContentType, + String terminalVendor, + String terminalModel, + String terminalSoftwareVersion) + throws ServiceEntitlementException { + return httpPost( + postData, + carrierConfig, + acceptContentType, + terminalVendor, + terminalModel, + terminalSoftwareVersion, + ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON, + ImmutableList.of()); + } + + @NonNull + private HttpResponse httpPost( + JSONObject postData, + CarrierConfig carrierConfig, + String acceptContentType, + String terminalVendor, + String terminalModel, + String terminalSoftwareVersion, + String contentType, + ImmutableList<String> cookies) + throws ServiceEntitlementException { + HttpRequest.Builder builder = + HttpRequest.builder() + .setUrl(carrierConfig.serverUrl()) + .setRequestMethod(RequestMethod.POST) + .setPostData(postData) + .addRequestProperty(HttpHeaders.ACCEPT, acceptContentType) + .addRequestProperty(HttpHeaders.CONTENT_TYPE, contentType) + .addRequestProperty(HttpHeaders.COOKIE, cookies) + .setTimeoutInSec(carrierConfig.timeoutInSec()) + .setNetwork(carrierConfig.network()); + String userAgent = + getUserAgent( + carrierConfig.clientTs43(), + terminalVendor, + terminalModel, + terminalSoftwareVersion); + if (!TextUtils.isEmpty(userAgent)) { + builder.addRequestProperty(HttpHeaders.USER_AGENT, userAgent); } + return mHttpClient.request(builder.build()); + } + + @Nullable + private String getEapAkaChallenge(HttpResponse response) throws ServiceEntitlementException { + String eapAkaChallenge = null; + String responseBody = response.body(); + if (response.contentType() == ContentType.JSON) { + try { + eapAkaChallenge = + new JSONObject(responseBody).optString(EAP_CHALLENGE_RESPONSE, null); + } catch (JSONException jsonException) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse json object", + jsonException); + } + } else if (response.contentType() == ContentType.XML) { + // EAP-AKA challenge is always in JSON format. + return null; + } else { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, "Unknown HTTP content type"); + } + return eapAkaChallenge; + } + + private String getAppVersion(Context context) { + try { + PackageInfo packageInfo = + context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionName; + } catch (Exception e) { + // should be impossible + } + return ""; + } + + private String getUserAgent( + String clientTs43, + String terminalVendor, + String terminalModel, + String terminalSoftwareVersion) { + if (!TextUtils.isEmpty(clientTs43) + && !TextUtils.isEmpty(terminalVendor) + && !TextUtils.isEmpty(terminalModel) + && !TextUtils.isEmpty(terminalSoftwareVersion)) { + return String.format( + "PRD-TS43 term-%s/%s %s/%s OS-Android/%s", + trimString(terminalVendor, MAX_TERMINAL_VENDOR_LENGTH), + trimString(terminalModel, MAX_TERMINAL_MODEL_LENGTH), + clientTs43, + mAppVersion, + trimString(terminalSoftwareVersion, MAX_TERMINAL_SOFTWARE_VERSION_LENGTH)); + } + return ""; + } + + private String trimString(String s, int maxLength) { + return s.substring(0, Math.min(s.length(), maxLength)); } /** @@ -425,16 +962,13 @@ public class EapAkaApi { return "0" + imsi + "@nai.epc.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org"; } - /** - * Retrieves the history of past HTTP request and responses. - */ + /** Retrieves the history of past HTTP request and responses. */ + @NonNull public List<String> getHistory() { return mHttpClient.getHistory(); } - /** - * Clears the history of past HTTP request and responses. - */ + /** Clears the history of past HTTP request and responses. */ public void clearHistory() { mHttpClient.clearHistory(); } diff --git a/java/com/android/libraries/entitlement/http/HttpClient.java b/java/com/android/libraries/entitlement/http/HttpClient.java index f2b394d..88ac5d5 100644 --- a/java/com/android/libraries/entitlement/http/HttpClient.java +++ b/java/com/android/libraries/entitlement/http/HttpClient.java @@ -125,7 +125,7 @@ public class HttpClient { } else { mConnection = (HttpURLConnection) network.openConnection(url); } - + mConnection.setInstanceFollowRedirects(false); // add HTTP headers for (Map.Entry<String, String> entry : request.requestProperties().entries()) { mConnection.addRequestProperty(entry.getKey(), entry.getValue()); @@ -158,13 +158,16 @@ public class HttpClient { try { int responseCode = connection.getResponseCode(); logPii("HttpClient.response headers: " + connection.getHeaderFields()); - if (responseCode != HttpURLConnection.HTTP_OK) { + if (responseCode != HttpURLConnection.HTTP_OK + && responseCode != HttpURLConnection.HTTP_MOVED_TEMP) { throw new ServiceEntitlementException(ERROR_HTTP_STATUS_NOT_SUCCESS, responseCode, connection.getHeaderField(HttpHeaders.RETRY_AFTER), - "Invalid connection response"); + "Invalid connection response: " + responseCode); } responseBuilder.setResponseCode(responseCode); responseBuilder.setResponseMessage(nullToEmpty(connection.getResponseMessage())); + responseBuilder.setLocation( + nullToEmpty(connection.getHeaderField(HttpHeaders.LOCATION))); } catch (IOException e) { throw new ServiceEntitlementException( ERROR_HTTP_STATUS_NOT_SUCCESS, "Read response code failed!", e); diff --git a/java/com/android/libraries/entitlement/http/HttpConstants.java b/java/com/android/libraries/entitlement/http/HttpConstants.java index 58f8c48..2c8fe83 100644 --- a/java/com/android/libraries/entitlement/http/HttpConstants.java +++ b/java/com/android/libraries/entitlement/http/HttpConstants.java @@ -41,9 +41,12 @@ public final class HttpConstants { private ContentType() { } - public static final int UNKNOWN = -1; - public static final int JSON = 0; - public static final int XML = 1; + public static final int UNKNOWN = + com.android.libraries.entitlement.utils.HttpConstants.UNKNOWN; + public static final int JSON = + com.android.libraries.entitlement.utils.HttpConstants.JSON; + public static final int XML = + com.android.libraries.entitlement.utils.HttpConstants.XML; public static final String NAME = "Content-Type"; } diff --git a/java/com/android/libraries/entitlement/http/HttpRequest.java b/java/com/android/libraries/entitlement/http/HttpRequest.java index b6cd771..ec5ca7a 100644 --- a/java/com/android/libraries/entitlement/http/HttpRequest.java +++ b/java/com/android/libraries/entitlement/http/HttpRequest.java @@ -16,6 +16,7 @@ package com.android.libraries.entitlement.http; +import android.content.res.Resources; import android.net.Network; import androidx.annotation.Nullable; @@ -24,6 +25,8 @@ import com.android.libraries.entitlement.CarrierConfig; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableListMultimap; +import com.google.common.net.HttpHeaders; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.json.JSONObject; @@ -72,6 +75,7 @@ public abstract class HttpRequest { abstract ImmutableListMultimap.Builder<String, String> requestPropertiesBuilder(); /** Adds an HTTP header field. */ + @CanIgnoreReturnValue public Builder addRequestProperty(String key, String value) { requestPropertiesBuilder().put(key, value); return this; @@ -82,6 +86,7 @@ public abstract class HttpRequest { * {@link #addRequestProperty(String, String)} multiple times with the same key and * one value at a time. */ + @CanIgnoreReturnValue public Builder addRequestProperty(String key, List<String> value) { requestPropertiesBuilder().putAll(key, value); return this; @@ -108,6 +113,13 @@ public abstract class HttpRequest { .setUrl("") .setRequestMethod("") .setPostData(new JSONObject()) - .setTimeoutInSec(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + .setTimeoutInSec(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC) + .addRequestProperty( + HttpHeaders.ACCEPT_LANGUAGE, + Resources.getSystem() + .getConfiguration() + .getLocales() + .get(0) + .toLanguageTag()); } } diff --git a/java/com/android/libraries/entitlement/http/HttpResponse.java b/java/com/android/libraries/entitlement/http/HttpResponse.java index 142639e..c2d1d2a 100644 --- a/java/com/android/libraries/entitlement/http/HttpResponse.java +++ b/java/com/android/libraries/entitlement/http/HttpResponse.java @@ -45,6 +45,11 @@ public abstract class HttpResponse { public abstract ImmutableList<String> cookies(); /** + * Content of the "Location" response header. + */ + public abstract String location(); + + /** * Builder of {@link HttpResponse}. */ @AutoValue.Builder @@ -63,6 +68,11 @@ public abstract class HttpResponse { * Sets the content of the "Set-Cookie" response headers. */ public abstract Builder setCookies(List<String> cookies); + + /** + * Sets the content of the "Location" response header. + */ + public abstract Builder setLocation(String location); } public static Builder builder() { @@ -71,7 +81,8 @@ public abstract class HttpResponse { .setBody("") .setResponseCode(0) .setResponseMessage("") - .setCookies(ImmutableList.of()); + .setCookies(ImmutableList.of()) + .setLocation(""); } /** @@ -91,7 +102,9 @@ public abstract class HttpResponse { .append(responseMessage()) .append(" cookies=[") .append(cookies().size()) - .append(" cookies]}") + .append(" cookies]") + .append(" location=") + .append(location()) .toString(); } } diff --git a/java/com/android/libraries/entitlement/odsa/AcquireConfigurationOperation.java b/java/com/android/libraries/entitlement/odsa/AcquireConfigurationOperation.java new file mode 100644 index 0000000..08a86b8 --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/AcquireConfigurationOperation.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.EsimOdsaOperation; +import com.android.libraries.entitlement.EsimOdsaOperation.CompanionService; +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaServiceStatus; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43Constants.AppId; +import com.android.libraries.entitlement.utils.Ts43Constants.NotificationAction; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Acquire configuration operation described in GSMA Service Entitlement Configuration section 6. + */ +public final class AcquireConfigurationOperation { + /** Indicating polling interval not available. */ + public static final int POLLING_INTERVAL_NOT_AVAILABLE = -1; + + /** + * HTTP request parameters specific to on device service activation (ODSA) acquire configuration + * operation. See GSMA spec TS.43 section 6.2. + */ + @AutoValue + public abstract static class AcquireConfigurationRequest { + /** + * Returns the application id. Can only be {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + */ + @AppId + public abstract String appId(); + + /** + * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id}. + */ + @NonNull + public abstract String companionTerminalId(); + + /** + * Returns the ICCID of the companion device. Used by HTTP parameter + * {@code companion_terminal_iccid}. + */ + @NonNull + public abstract String companionTerminalIccid(); + + /** + * Returns the EID of the companion device. Used by HTTP parameter + * {@code companion_terminal_eid}. + */ + @NonNull + public abstract String companionTerminalEid(); + + /** + * Returns the ICCID of the primary device eSIM. Used by HTTP parameter + * {@code terminal_iccid}. + */ + @NonNull + public abstract String terminalIccid(); + + /** + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * {@code terminal_eid}. + */ + @NonNull + public abstract String terminalEid(); + + /** + * Returns the unique identifier of the primary device eSIM, like the IMEI associated with + * the eSIM. Used by HTTP parameter {@code target_terminal_id}. + */ + @NonNull + public abstract String targetTerminalId(); + + /** + * Returns the ICCID primary device eSIM. Used by HTTP parameter + * {@code target_terminal_iccid}. + */ + @NonNull + public abstract String targetTerminalIccid(); + + /** + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * {@code target_terminal_eid}. + */ + @NonNull + public abstract String targetTerminalEid(); + + /** + * Returns the notification token used to register for entitlement configuration request + * from network. Used by HTTP parameter {@code notif_token}. + */ + @NonNull + public abstract String notificationToken(); + + /** + * Returns the action associated with the notification token. Used by HTTP parameter + * {@code notif_action}. + */ + @NotificationAction + public abstract int notificationAction(); + + /** Returns a new {@link Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_AcquireConfigurationOperation_AcquireConfigurationRequest + .Builder() + .setCompanionTerminalId("") + .setCompanionTerminalIccid("") + .setCompanionTerminalEid("") + .setTerminalIccid("") + .setTerminalEid("") + .setTargetTerminalId("") + .setTargetTerminalIccid("") + .setTargetTerminalEid("") + .setNotificationToken("") + .setNotificationAction(Ts43Constants.NOTIFICATION_ACTION_ENABLE_FCM); + } + + /** Builder */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the application id. + * + * @param appId The application id. Can only be + * {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + * @return The builder. + */ + @NonNull + public abstract Builder setAppId(@NonNull @AppId String appId); + + /** + * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalId The unique identifier of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalId(@NonNull String companionTerminalId); + + /** + * Sets the ICCID of the companion device. Used by HTTP parameter + * {@code companion_terminal_iccid} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalIccid The ICCID of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalIccid( + @NonNull String companionTerminalIccid); + + /** + * Sets the eUICC identifier (EID) of the companion device. Used by HTTP parameter + * {@code companion_terminal_eid} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalEid The eUICC identifier (EID) of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalEid(@NonNull String companionTerminalEid); + + /** + * Sets the ICCID of the primary device eSIM in case of primary SIM not present. Used by + * HTTP parameter {@code terminal_eid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param terminalIccid The ICCID of the primary device eSIM in case of primary SIM not + * present. + * @return The builder. + */ + @NonNull + public abstract Builder setTerminalIccid(@NonNull String terminalIccid); + + /** + * Sets the eUICC identifier (EID) of the primary device eSIM in case of primary SIM not + * present. Used by HTTP parameter {@code terminal_eid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param terminalEid The eUICC identifier (EID) of the primary device eSIM in case of + * primary SIM not present. + * @return The builder. + */ + @NonNull + public abstract Builder setTerminalEid(@NonNull String terminalEid); + + /** + * Sets the unique identifier of the primary device eSIM in case of multiple SIM, like + * the IMEI associated with the eSIM. Used by HTTP parameter {@code target_terminal_id} + * if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalId The unique identifier of the primary device eSIM in case of + * multiple SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalId(@NonNull String targetTerminalId); + + /** + * Sets the ICCID primary device eSIM in case of multiple SIM. Used by HTTP parameter + * {@code target_terminal_iccid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalIccid The ICCID primary device eSIM in case of multiple SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalIccid(@NonNull String targetTerminalIccid); + + /** + * Sets the eUICC identifier (EID) of the primary device eSIM in case of multiple SIM. + * Used by HTTP parameter {@code target_terminal_eid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalEid The eUICC identifier (EID) of the primary device eSIM in + * case of multiple SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalEid(@NonNull String targetTerminalEid); + + /** + * Sets the notification token used to register for entitlement configuration request + * from network. Used by HTTP parameter {@code notif_token} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param notificationToken The notification token used to register for entitlement + * configuration request from network. + * @return The builder. + */ + @NonNull + public abstract Builder setNotificationToken(@NonNull String notificationToken); + + /** + * Sets the action associated with the notification token. Used by HTTP parameter + * {@code notif_action} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param notificationAction The action associated with the notification token. + * @return The builder. + */ + @NonNull + public abstract Builder setNotificationAction( + @NotificationAction int notificationAction); + + /** Returns build the {@link AcquireConfigurationRequest} object. */ + @NonNull + public abstract AcquireConfigurationRequest build(); + } + } + + /** + * Acquire configuration response described in GSMA Service Entitlement Configuration section + * section 6.5.5 table 40. + */ + @AutoValue + public abstract static class AcquireConfigurationResponse extends OdsaResponse { + /** Configuration */ + @AutoValue + public abstract static class Configuration { + /** The configuration type is unknown. */ + public static final int CONFIGURATION_TYPE_UNKNOWN = -1; + + /** The configuration is for ODSA primary device. */ + public static final int CONFIGURATION_TYPE_PRIMARY = 1; + + /** The configuration is for companion device. */ + public static final int CONFIGURATION_TYPE_COMPANION = 2; + + /** The configuration is for server-initiated ODSA. */ + public static final int CONFIGURATION_TYPE_ENTERPRISE = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CONFIGURATION_TYPE_UNKNOWN, + CONFIGURATION_TYPE_PRIMARY, + CONFIGURATION_TYPE_COMPANION, + CONFIGURATION_TYPE_ENTERPRISE + }) + public @interface ConfigurationType { + } + + /** Indicates the configuration type. */ + @ConfigurationType + public abstract int type(); + + /** + * Integrated Circuit Card Identification - Identifier of the eSIM profile on the + * device’s eSIM. {@code null} if an eSIM profile does not exist for the device. + */ + @Nullable + public abstract String iccid(); + + /** + * Indicates the applicable companion device service. {@code null} if not for companion + * configuration. + */ + @Nullable + @CompanionService + public abstract String companionDeviceService(); + + /** + * Service status. + * + * @see EsimOdsaOperation#SERVICE_STATUS_UNKNOWN + * @see EsimOdsaOperation#SERVICE_STATUS_ACTIVATED + * @see EsimOdsaOperation#SERVICE_STATUS_ACTIVATING + * @see EsimOdsaOperation#SERVICE_STATUS_DEACTIVATED + * @see EsimOdsaOperation#SERVICE_STATUS_DEACTIVATED_NO_REUSE + */ + @OdsaServiceStatus + public abstract int serviceStatus(); + + /** + * Specifies the minimum interval (in minutes) with which the device application may + * poll the ECS to refresh the current {@link #serviceStatus()} using {@link + * AcquireConfigurationRequest}. This parameter will be present only when {@link + * #serviceStatus()} is {@link EsimOdsaOperation#SERVICE_STATUS_ACTIVATING}. If + * parameter is not present or value is 0, this polling procedure is not triggered and + * ODSA app will keep waiting for any external action to continue the flow. + * + * <p>The maximum number of {@link AcquireConfigurationRequest} before sending a {@link + * #serviceStatus()} with {@link EsimOdsaOperation#SERVICE_STATUS_DEACTIVATED_NO_REUSE} + * will be defined as an ECS configuration variable (MaxRefreshRequest). + * + * <p>{@link #POLLING_INTERVAL_NOT_AVAILABLE} when polling interval is not available. + */ + public abstract int pollingInterval(); + + /** + * Specifies how and where to download the eSIM profile associated with the device. + * Present in case the profile is to be downloaded at this stage. + */ + @Nullable + public abstract DownloadInfo downloadInfo(); + + /** Includes all information collected by the ES of the companion device. */ + @Nullable + public abstract CompanionDeviceInfo companionDeviceInfo(); + + /** Returns the builder. */ + @NonNull + public static Builder builder() { + return new AutoValue_AcquireConfigurationOperation_AcquireConfigurationResponse_Configuration + .Builder() + .setType(CONFIGURATION_TYPE_UNKNOWN) + .setIccid("") + .setServiceStatus(EsimOdsaOperation.SERVICE_STATUS_UNKNOWN) + .setPollingInterval(POLLING_INTERVAL_NOT_AVAILABLE); + } + + /** The builder of {@link Configuration} */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Set the configuration type. + * + * @param configType The configuration type. + * @return The builder. + */ + @NonNull + public abstract Builder setType(@ConfigurationType int configType); + + /** + * Set the iccid. + * + * @param iccid Integrated Circuit Card Identification - Identifier of the eSIM + * profile on the device’s eSIM. + * @return The builder. + */ + @NonNull + public abstract Builder setIccid(@NonNull String iccid); + + /** + * Set the applicable companion device service. + * + * @param companionDeviceService Indicates the applicable companion device service. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionDeviceService( + @NonNull @CompanionService String companionDeviceService); + + /** + * Set the service status. + * + * @param serviceStatus Service status. + * @return The builder. + */ + @NonNull + public abstract Builder setServiceStatus(@OdsaServiceStatus int serviceStatus); + + /** + * Set the polling interval. + * + * @param pollingInterval The minimum interval (in minutes) with which the device + * application may poll the ECS to refresh the current + * {@link #serviceStatus()} using + * {@link AcquireConfigurationRequest}. + * @return The builder. + */ + @NonNull + public abstract Builder setPollingInterval(int pollingInterval); + + /** + * Set the download information. + * + * @param downloadInfo Specifies how and where to download the eSIM profile + * associated with the device. + * @return The builder. + */ + @NonNull + public abstract Builder setDownloadInfo(@NonNull DownloadInfo downloadInfo); + + /** + * Set the companion device info. + * + * @param companionDeviceInfo Includes all information collected by the ES of the + * companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionDeviceInfo( + @NonNull CompanionDeviceInfo companionDeviceInfo); + + /** Returns build the {@link Configuration} object. */ + @NonNull + public abstract Configuration build(); + } + } + + /** + * Configurations defined in GSMA Service Entitlement Configuration section 6.5.5. Could be + * more than one if multiple companion device(s) associated with the requesting device that + * carry a configuration for ODSA. + */ + @NonNull + public abstract ImmutableList<Configuration> configurations(); + + /** Returns the builder. */ + @NonNull + public static Builder builder() { + return new AutoValue_AcquireConfigurationOperation_AcquireConfigurationResponse + .Builder() + .setConfigurations(ImmutableList.of()); + } + + /** The builder of {@link AcquireConfigurationResponse} */ + @AutoValue.Builder + public abstract static class Builder extends OdsaResponse.Builder { + /** + * Set the configurations + * + * @param configs Configurations defined in GSMA Service Entitlement Configuration + * section 6.5.5. Could be more than one if multiple companion device(s) + * associated with the requesting device that carry a configuration for + * ODSA. + * @return The builder. + */ + @NonNull + public abstract Builder setConfigurations( + @NonNull ImmutableList<Configuration> configs); + + /** Returns build the {@link AcquireConfigurationResponse} object. */ + @NonNull + public abstract AcquireConfigurationResponse build(); + } + } + + private AcquireConfigurationOperation() { + } +} diff --git a/java/com/android/libraries/entitlement/odsa/AcquireTemporaryTokenOperation.java b/java/com/android/libraries/entitlement/odsa/AcquireTemporaryTokenOperation.java new file mode 100644 index 0000000..4f814b9 --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/AcquireTemporaryTokenOperation.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaOperation; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43Constants.AppId; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import java.time.Instant; + +/** + * Acquire temporary token operation described in GSMA Service Entitlement Configuration section 6. + */ +public final class AcquireTemporaryTokenOperation { + /** + * Acquire temporary token request described in GSMA Service Entitlement Configuration section + * 6.2. + */ + @AutoValue + public abstract static class AcquireTemporaryTokenRequest { + /** + * Returns the application id. Can only be {@link Ts43Constants#APP_ODSA_COMPANION}, {@link + * Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + */ + @NonNull + @AppId + public abstract String appId(); + + /** + * Returns the comma separated list of operation targets used with temporary token from + * {@code AcquireTemporaryToken} operation. Used by HTTP parameter + * {@code operation_targets}. + */ + @NonNull + @OdsaOperation + public abstract ImmutableList<String> operationTargets(); + + /** + * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id}. + */ + @NonNull + public abstract String companionTerminalId(); + + /** Returns a new {@link Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_AcquireTemporaryTokenOperation_AcquireTemporaryTokenRequest + .Builder() + .setAppId(Ts43Constants.APP_UNKNOWN) + .setOperationTargets(ImmutableList.of()) + .setCompanionTerminalId(""); + } + + /** Builder. */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the application id. + * + * @param appId The application id. Can only be + * {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + * @return The builder. + */ + @NonNull + public abstract Builder setAppId(@NonNull @AppId String appId); + + /** + * Sets the operation targets to be used with temporary token from {@code + * AcquireTemporaryToken} operation. Used by HTTP parameter {@code operation_targets} if + * set. + * + * @param operationTargets The operation targets to be used with temporary token from + * {@code AcquireTemporaryToken} operation. + * @return The builder. + */ + @NonNull + public abstract Builder setOperationTargets( + @NonNull @OdsaOperation ImmutableList<String> operationTargets); + + /** + * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalId The unique identifier of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalId(@NonNull String companionTerminalId); + + /** Returns the {@link AcquireTemporaryTokenRequest} object. */ + @NonNull + public abstract AcquireTemporaryTokenRequest build(); + } + } + + /** + * Acquire temporary token response described in GSMA Service Entitlement Configuration section + * 6.5.7. + */ + @AutoValue + @AutoValue.CopyAnnotations + @SuppressWarnings("AndroidJdkLibsChecker") // java.time.Instant + public abstract static class AcquireTemporaryTokenResponse extends OdsaResponse { + /** The temporary token used to establish trust between ECS and the client. */ + @NonNull + public abstract String temporaryToken(); + + /** The expiration time (UTC time) of the token. {@code null} if not available. */ + @AutoValue.CopyAnnotations + @SuppressWarnings("AndroidJdkLibsChecker") // java.time.Instant + + @Nullable + public abstract Instant temporaryTokenExpiry(); + + /** The allowed ODSA operations requested using {@link #temporaryToken()}. */ + @NonNull + @OdsaOperation + public abstract ImmutableList<String> operationTargets(); + + /** Returns a new {@link AcquireTemporaryTokenRequest.Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_AcquireTemporaryTokenOperation_AcquireTemporaryTokenResponse + .Builder() + .setTemporaryToken("") + .setTemporaryTokenExpiry(null) + .setOperationTargets(ImmutableList.of()); + } + + /** Builder. */ + @AutoValue.Builder + @AutoValue.CopyAnnotations + @SuppressWarnings("AndroidJdkLibsChecker") // java.time.Instant + public abstract static class Builder extends OdsaResponse.Builder { + /** + * Sets the temporary token. + * + * @param token The temporary token used to establish trust between ECS and the client. + * @return The builder. + */ + @NonNull + public abstract Builder setTemporaryToken(@NonNull String token); + + /** + * Sets the expiration time of the token. + * + * @param expiry The expiration time (UTC time) of the token. + * @return The builder. + */ + @AutoValue.CopyAnnotations + @SuppressWarnings("AndroidJdkLibsChecker") // java.time.Instant + @NonNull + public abstract Builder setTemporaryTokenExpiry(@NonNull Instant expiry); + + /** + * Sets the allowed ODSA operations requested using {@link #temporaryToken()}. + * + * @param operationTargets The allowed ODSA operations requested using {@link + * #temporaryToken()}. + * @return The builder. + */ + @NonNull + public abstract Builder setOperationTargets( + @NonNull @OdsaOperation ImmutableList<String> operationTargets); + + /** Returns the {@link AcquireTemporaryTokenResponse} object. */ + @NonNull + public abstract AcquireTemporaryTokenResponse build(); + } + } + + private AcquireTemporaryTokenOperation() { + } +} diff --git a/java/com/android/libraries/entitlement/odsa/CheckEligibilityOperation.java b/java/com/android/libraries/entitlement/odsa/CheckEligibilityOperation.java new file mode 100644 index 0000000..8dc8d97 --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/CheckEligibilityOperation.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.EsimOdsaOperation.CompanionService; +import com.android.libraries.entitlement.utils.HttpConstants; +import com.android.libraries.entitlement.utils.HttpConstants.ContentType; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43Constants.AppId; +import com.android.libraries.entitlement.utils.Ts43Constants.NotificationAction; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.URL; + +/** Check eligibility operation described in GSMA Service Entitlement Configuration section 6. */ +public final class CheckEligibilityOperation { + /** ODSA app check eligibility result unknown. */ + public static final int ELIGIBILITY_RESULT_UNKNOWN = -1; + + /** ODSA app cannot be offered and invoked by the end-user. */ + public static final int ELIGIBILITY_RESULT_DISABLED = 0; + + /** ODSA app can be invoked by end-user or to activate a new subscription. */ + public static final int ELIGIBILITY_RESULT_ENABLED = 1; + + /** ODSA app is not compatible with the device or server. */ + public static final int ELIGIBILITY_RESULT_INCOMPATIBLE = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ELIGIBILITY_RESULT_UNKNOWN, + ELIGIBILITY_RESULT_DISABLED, + ELIGIBILITY_RESULT_ENABLED, + ELIGIBILITY_RESULT_INCOMPATIBLE + }) + public @interface EligibilityResult { + } + + /** + * HTTP request parameters specific to on device service activation (ODSA). + * See GSMA spec TS.43 section 6.2. + */ + @AutoValue + public abstract static class CheckEligibilityRequest { + /** + * Returns the application id. Can only be {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + */ + @AppId + public abstract String appId(); + + /** + * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id}. + */ + @NonNull + public abstract String companionTerminalId(); + + /** + * Returns the OEM of the companion device. Used by HTTP parameter + * {@code companion_terminal_vendor}. + */ + @NonNull + public abstract String companionTerminalVendor(); + + /** + * Returns the model of the companion device. Used by HTTP parameter + * {@code companion_terminal_model}. + */ + @NonNull + public abstract String companionTerminalModel(); + + /** + * Returns the software version of the companion device. Used by HTTP parameter + * {@code companion_terminal_sw_version}. + */ + @NonNull + public abstract String companionTerminalSoftwareVersion(); + + /** + * Returns the user-friendly version of the companion device. Used by HTTP parameter + * {@code companion_terminal_friendly_name}. + */ + @NonNull + public abstract String companionTerminalFriendlyName(); + + /** + * Returns the notification token used to register for entitlement configuration request + * from network. Used by HTTP parameter {@code notif_token}. + */ + @NonNull + public abstract String notificationToken(); + + /** + * Returns the action associated with the notification token. Used by HTTP parameter + * {@code notif_action}. + */ + @NotificationAction + public abstract int notificationAction(); + + /** Returns a new {@link Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_CheckEligibilityOperation_CheckEligibilityRequest.Builder() + .setAppId(Ts43Constants.APP_UNKNOWN) + .setCompanionTerminalId("") + .setCompanionTerminalVendor("") + .setCompanionTerminalModel("") + .setCompanionTerminalSoftwareVersion("") + .setCompanionTerminalFriendlyName("") + .setNotificationToken("") + .setNotificationAction(Ts43Constants.NOTIFICATION_ACTION_ENABLE_FCM); + } + + /** Builder */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the application id. + * + * @param appId The application id. Can only be + * {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or {@link + * Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + * @return The builder. + */ + @NonNull + public abstract Builder setAppId(@NonNull @AppId String appId); + + /** + * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalId The unique identifier of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalId(@NonNull String companionTerminalId); + + /** + * Sets the OEM of the companion device. Used by HTTP parameter + * {@code companion_terminal_vendor} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalVendor The OEM of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalVendor( + @NonNull String companionTerminalVendor); + + /** + * Sets the model of the companion device. Used by HTTP parameter + * {@code companion_terminal_model} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalModel The model of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalModel( + @NonNull String companionTerminalModel); + + /** + * Sets the software version of the companion device. Used by HTTP parameter + * {@code companion_terminal_sw_version} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalSoftwareVersion The software version of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalSoftwareVersion( + @NonNull String companionTerminalSoftwareVersion); + + /** + * Sets the user-friendly version of the companion device. Used by HTTP parameter + * {@code companion_terminal_friendly_name} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalFriendlyName The user-friendly version of the companion + * device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalFriendlyName( + @NonNull String companionTerminalFriendlyName); + + /** + * Sets the notification token used to register for entitlement configuration request + * from network. Used by HTTP parameter {@code notif_token} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param notificationToken The notification token used to register for entitlement + * configuration request from network. + * @return The builder. + */ + @NonNull + public abstract Builder setNotificationToken(@NonNull String notificationToken); + + /** + * Sets the action associated with the notification token. Used by HTTP parameter + * {@code notif_action} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param notificationAction The action associated with the notification token. + * @return The builder. + */ + @NonNull + public abstract Builder setNotificationAction( + @NotificationAction int notificationAction); + + /** Returns the {@link CheckEligibilityRequest} object. */ + @NonNull + public abstract CheckEligibilityRequest build(); + } + } + + /** + * Check eligibility response described in GSMA Service Entitlement Configuration section 6.5.2. + */ + @AutoValue + public abstract static class CheckEligibilityResponse extends OdsaResponse { + /** Returns the result of check eligibility request. */ + @EligibilityResult + public abstract int appEligibility(); + + /** Indicates the applicable companion device services. */ + @NonNull + @CompanionService + public abstract ImmutableList<String> companionDeviceServices(); + + /** + * The provided URL shall present a web view to user on the reason(s) why the ODSA app + * cannot be used/invoked. + */ + @Nullable + public abstract URL notEnabledUrl(); + + /** + * User data sent to the Service Provider when requesting the {@link #notEnabledUrl()} web + * view. It should contain user-specific attributes to improve user experience. The format + * must follow the {@link #notEnabledContentsType()} parameter. For content types of + * {@code JSON} and {@code XML}, it is possible to provide the base64 encoding of the value + * by preceding it with {@code encodedValue=}. + */ + @NonNull + public abstract String notEnabledUserData(); + + /** + * Specifies content and HTTP method to use when reaching out to the web server specified in + * {@link #notEnabledUrl()}. + */ + @ContentType + public abstract int notEnabledContentsType(); + + /** Returns the builder. */ + public static Builder builder() { + return new AutoValue_CheckEligibilityOperation_CheckEligibilityResponse.Builder() + .setAppEligibility(ELIGIBILITY_RESULT_UNKNOWN) + .setCompanionDeviceServices(ImmutableList.of()) + .setNotEnabledUserData("") + .setNotEnabledContentsType(HttpConstants.UNKNOWN); + } + + /** The builder. */ + @AutoValue.Builder + public abstract static class Builder extends OdsaResponse.Builder { + /** + * Set the eligibility. + * + * @param eligibility The result of check eligibility request. + * @return The builder. + */ + @NonNull + public abstract Builder setAppEligibility(@EligibilityResult int eligibility); + + /** + * Set the companion device services. + * + * @param companionDeviceServices The applicable companion device services. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionDeviceServices( + @NonNull @CompanionService ImmutableList<String> companionDeviceServices); + + /** + * Set the URL presenting a web view to user on the reason(s) why the ODSA app cannot be + * used/invoked. + * + * @param url The provided URL shall present a web view to user on the reason(s) why the + * ODSA app cannot be used/invoked. + * @return The builder. + */ + @NonNull + public abstract Builder setNotEnabledUrl(@NonNull URL url); + + /** + * Set the user data sent to the Service Provider when requesting the + * {@link #notEnabledUrl()} web view. + * + * @param notEnabledUserData User data sent to the Service Provider when requesting the + * {@link #notEnabledUrl()} web view. It should contain + * user-specific attributes to improve user experience. The + * format must follow the {@link #notEnabledContentsType()} + * parameter. For content types of {@link HttpConstants#JSON} + * and {@link HttpConstants#XML}, it is possible to provide + * the base64 encoding of the value by preceding it with + * {@code encodedValue=}. + * @return The builder. + */ + @NonNull + public abstract Builder setNotEnabledUserData(@NonNull String notEnabledUserData); + + /** + * Set the content and HTTP method to use when reaching out to the web server specified + * in {@link #notEnabledUrl()}. + * + * @param notEnabledContentsType Specifies content and HTTP method to use when reaching + * out to the web server specified in + * {@link #notEnabledUrl()}. + * @return The builder. + */ + @NonNull + public abstract Builder setNotEnabledContentsType( + @ContentType int notEnabledContentsType); + + /** Build the {@link CheckEligibilityResponse} object. */ + public abstract CheckEligibilityResponse build(); + } + } + + private CheckEligibilityOperation() { + } +} diff --git a/java/com/android/libraries/entitlement/odsa/CompanionDeviceInfo.java b/java/com/android/libraries/entitlement/odsa/CompanionDeviceInfo.java new file mode 100644 index 0000000..6762e79 --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/CompanionDeviceInfo.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +/** + * Companion device info described in GSMA Service Entitlement Configuration section 6.5.5 table 41. + */ +@AutoValue +public abstract class CompanionDeviceInfo { + /** + * User friendly identification for the companion device which can be used by the Service + * Provider in Web Views. + */ + @NonNull + public abstract String companionTerminalFriendlyName(); + + /** Manufacturer of the companion device. */ + @NonNull + public abstract String companionTerminalVendor(); + + /** Model of the companion device. */ + @Nullable + public abstract String companionTerminalModel(); + + /** eUICC identifier (EID) of the companion device being managed. */ + @Nullable + public abstract String companionTerminalEid(); + + /** Returns the builder of {@link CompanionDeviceInfo}. */ + @NonNull + public static Builder builder() { + return new AutoValue_CompanionDeviceInfo.Builder(); + } + + /** The builder. */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Set user friendly identification for the companion device. + * + * @param companionTerminalFriendlyName User friendly identification for the companion + * device which can be used by the Service Provider in + * Web Views. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalFriendlyName( + @NonNull String companionTerminalFriendlyName); + + /** + * Set manufacturer of the companion device. + * + * @param companionTerminalVendor manufacturer of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalVendor(@NonNull String companionTerminalVendor); + + /** + * Set model of the companion device. + * + * @param companionTerminalModel Model of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalModel(@NonNull String companionTerminalModel); + + /** + * Set EID of the companion device. + * + * @param companionTerminalEid eUICC identifier (EID) of the companion device being managed. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalEid(@NonNull String companionTerminalEid); + + /** Build the CompanionDeviceInfo object. */ + @NonNull + public abstract CompanionDeviceInfo build(); + } +} diff --git a/java/com/android/libraries/entitlement/odsa/DownloadInfo.java b/java/com/android/libraries/entitlement/odsa/DownloadInfo.java new file mode 100644 index 0000000..d087ec9 --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/DownloadInfo.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +/** + * Download information described in GSMA Service Entitlement Configuration section 6.5.3 table 38. + */ +@AutoValue +public abstract class DownloadInfo { + /** + * The ICCID of the eSIM profile to download from SM-DP+. This is not an empty string when + * {@link #profileSmdpAddresses()} is used to trigger the profile download. + */ + @NonNull + public abstract String profileIccid(); + + /** + * Address(es) of SM-DP+ to obtain eSIM profile. It is an empty list if {@link + * #profileActivationCode()} is not empty. + */ + @NonNull + public abstract ImmutableList<String> profileSmdpAddresses(); + + /** + * Activation code as defined in SGP.22 to permit the download of an eSIM profile from an + * SM-DP+. It is an empty string if {@link #profileSmdpAddresses()} is not empty. + */ + @NonNull + public abstract String profileActivationCode(); + + /** Returns builder of {@link DownloadInfo}. */ + @NonNull + public static Builder builder() { + return new AutoValue_DownloadInfo.Builder() + .setProfileActivationCode("") + .setProfileSmdpAddresses(ImmutableList.of()) + .setProfileIccid(""); + } + + /** Builder of DownloadInfo. */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Set the ICCID of the download profile. + * + * @param iccid The ICCID of the eSIM profile to download from SM-DP+. + * @return The builder. + */ + @NonNull + public abstract Builder setProfileIccid(@NonNull String iccid); + + /** + * Set the activation code. + * + * @param activationCode Activation code as defined in SGP.22 to permit the download of an + * eSIM profile from an SM-DP+. + * @return The builder. + */ + @NonNull + public abstract Builder setProfileActivationCode(@NonNull String activationCode); + + /** + * Set address(es) of SM-DP+ to obtain eSIM profile. + * + * @param smdpAddress Address(es) of SM-DP+ to obtain eSIM profile. + * @return The builder. + */ + @NonNull + public abstract Builder setProfileSmdpAddresses(@NonNull ImmutableList<String> smdpAddress); + + /** Build the DownloadInfo object. */ + @NonNull + public abstract DownloadInfo build(); + } +} diff --git a/java/com/android/libraries/entitlement/odsa/GetPhoneNumberOperation.java b/java/com/android/libraries/entitlement/odsa/GetPhoneNumberOperation.java new file mode 100644 index 0000000..3a30f0a --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/GetPhoneNumberOperation.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; + +import com.google.auto.value.AutoValue; + +/** + * Get phone number operation described in GSMA Service Entitlement Configuration section 6. + */ +public final class GetPhoneNumberOperation { + /** + * Get phone number request described in GSMA Service Entitlement Configuration section 6.4.8. + */ + @AutoValue + public abstract static class GetPhoneNumberRequest { + /** + * Returns the terminal id. + */ + @NonNull + public abstract String terminalId(); + + /** Returns a new {@link GetPhoneNumberRequest.Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_GetPhoneNumberOperation_GetPhoneNumberRequest + .Builder() + .setTerminalId(""); + } + + /** Builder. */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the terminal id. + * + * @param terminalId The terminal id. + * @return The builder. + */ + @NonNull + public abstract Builder setTerminalId(@NonNull String terminalId); + + /** Returns the {@link GetPhoneNumberRequest} object. */ + @NonNull + public abstract GetPhoneNumberRequest build(); + } + } + + /** + * Get phone number response described in GSMA Service Entitlement Configuration section + * 6.5.8. + */ + @AutoValue + public abstract static class GetPhoneNumberResponse extends OdsaResponse { + + /** The phone number of the subscriber in E.164 format. */ + public abstract String msisdn(); + + /** Returns a new {@link GetPhoneNumberResponse.Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_GetPhoneNumberOperation_GetPhoneNumberResponse + .Builder() + .setMsisdn(""); + } + + /** Builder. */ + @AutoValue.Builder + public abstract static class Builder extends OdsaResponse.Builder { + /** + * Sets the phone number of the subscriber. + * + * @param msisdn The phone number of the subscriber in E.164 format. + * @return The builder. + */ + @NonNull + public abstract Builder setMsisdn(@NonNull String msisdn); + + /** Returns the {@link GetPhoneNumberResponse} object. */ + @NonNull + public abstract GetPhoneNumberResponse build(); + } + } + + private GetPhoneNumberOperation() { + } +} diff --git a/java/com/android/libraries/entitlement/odsa/ManageServiceOperation.java b/java/com/android/libraries/entitlement/odsa/ManageServiceOperation.java new file mode 100644 index 0000000..34868a4 --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/ManageServiceOperation.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; + +import com.android.libraries.entitlement.EsimOdsaOperation; +import com.android.libraries.entitlement.EsimOdsaOperation.CompanionService; +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaOperationType; +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaServiceStatus; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43Constants.AppId; + +import com.google.auto.value.AutoValue; + +/** Manage service operation described in GSMA Service Entitlement Configuration section 6. */ +public final class ManageServiceOperation { + /** + * HTTP request parameters specific to on device service activation (ODSA) manage service + * request. See GSMA spec TS.43 section 6.2. + */ + @AutoValue + public abstract static class ManageServiceRequest { + /** + * Returns the application id. Can only be {@link Ts43Constants#APP_ODSA_COMPANION}, {@link + * Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + */ + @AppId + public abstract String appId(); + + /** + * Returns the detailed type of the eSIM ODSA operation. Used by HTTP parameter {@code + * operation_type}. + */ + @OdsaOperationType + public abstract int operationType(); + + /** + * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id}. + */ + @NonNull + public abstract String companionTerminalId(); + + /** + * Returns the OEM of the companion device. Used by HTTP parameter {@code + * companion_terminal_vendor}. + */ + @NonNull + public abstract String companionTerminalVendor(); + + /** + * Returns the model of the companion device. Used by HTTP parameter {@code + * companion_terminal_model}. + */ + @NonNull + public abstract String companionTerminalModel(); + + /** + * Returns the software version of the companion device. Used by HTTP parameter {@code + * companion_terminal_sw_version}. + */ + @NonNull + public abstract String companionTerminalSoftwareVersion(); + + /** + * Returns the user-friendly version of the companion device. Used by HTTP parameter {@code + * companion_terminal_friendly_name}. + */ + @NonNull + public abstract String companionTerminalFriendlyName(); + + /** + * Returns the service type of the companion device, e.g. if the MSISDN is same as the + * primary device. Used by HTTP parameter {@code companion_terminal_service}. + */ + @NonNull + @CompanionService + public abstract String companionTerminalService(); + + /** + * Returns the ICCID of the companion device. Used by HTTP parameter {@code + * companion_terminal_iccid}. + */ + @NonNull + public abstract String companionTerminalIccid(); + + /** Returns a new {@link Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_ManageServiceOperation_ManageServiceRequest.Builder() + .setAppId(Ts43Constants.APP_UNKNOWN) + .setOperationType(EsimOdsaOperation.OPERATION_TYPE_NOT_SET) + .setCompanionTerminalId("") + .setCompanionTerminalVendor("") + .setCompanionTerminalModel("") + .setCompanionTerminalSoftwareVersion("") + .setCompanionTerminalFriendlyName("") + .setCompanionTerminalService(EsimOdsaOperation.COMPANION_SERVICE_UNKNOWN) + .setCompanionTerminalIccid(""); + } + + /** Builder */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the application id. + * + * @param appId The application id. Can only be + * {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or {@link + * Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + * @return The builder. + */ + @NonNull + public abstract Builder setAppId(@NonNull @AppId String appId); + + /** + * Sets the detailed type of the eSIM ODSA operation. + * + * @param operationType Operation type. Only {@link + * EsimOdsaOperation#OPERATION_TYPE_ACTIVATE_SERVICE} and {@link + * EsimOdsaOperation#OPERATION_TYPE_DEACTIVATE_SERVICE} are + * allowed. + * @return The builder. + */ + @NonNull + public abstract Builder setOperationType(@OdsaOperationType int operationType); + + /** + * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id} if set. + * + * @param companionTerminalId The unique identifier of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalId(String companionTerminalId); + + /** + * Sets the OEM of the companion device. Used by HTTP parameter {@code + * companion_terminal_vendor} if set. + * + * @param companionTerminalVendor The OEM of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalVendor(String companionTerminalVendor); + + /** + * Sets the model of the companion device. Used by HTTP parameter {@code + * companion_terminal_model} if set. + * + * @param companionTerminalModel The model of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalModel( + @NonNull String companionTerminalModel); + + /** + * Sets the software version of the companion device. Used by HTTP parameter {@code + * companion_terminal_sw_version} if set. + * + * @param companionTerminalSoftwareVersion The software version of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalSoftwareVersion( + @NonNull String companionTerminalSoftwareVersion); + + /** + * Sets the user-friendly version of the companion device. Used by HTTP parameter {@code + * companion_terminal_friendly_name} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalFriendlyName The user-friendly version of the companion + * device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalFriendlyName( + @NonNull String companionTerminalFriendlyName); + + /** + * Sets the service type of the companion device, e.g. if the MSISDN is same as the + * primary device. Used by HTTP parameter {@code companion_terminal_service} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalService The service type of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalService( + @NonNull @CompanionService String companionTerminalService); + + /** + * Sets the ICCID of the companion device. Used by HTTP parameter {@code + * companion_terminal_iccid} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalIccid The ICCID of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalIccid( + @NonNull String companionTerminalIccid); + + /** Build the {@link ManageServiceRequest} object. */ + @NonNull + public abstract ManageServiceRequest build(); + } + } + + /** + * Manage service response described in GSMA Service Entitlement Configuration section 6.5.4 + * table 39. + */ + @AutoValue + public abstract static class ManageServiceResponse extends OdsaResponse { + /** Service status. */ + @OdsaServiceStatus + public abstract int serviceStatus(); + + /** Returns a new {@link ManageServiceResponse.Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_ManageServiceOperation_ManageServiceResponse.Builder() + .setServiceStatus(EsimOdsaOperation.SERVICE_STATUS_UNKNOWN); + } + + /** Builder */ + @AutoValue.Builder + public abstract static class Builder extends OdsaResponse.Builder { + /** + * Set the service status. + * + * @param serviceStatus Service status + * @return The builder. + */ + @NonNull + public abstract Builder setServiceStatus(@OdsaServiceStatus int serviceStatus); + + /** Build the {@link ManageServiceResponse} object. */ + @NonNull + public abstract ManageServiceResponse build(); + } + } + + private ManageServiceOperation() { + } +} diff --git a/java/com/android/libraries/entitlement/odsa/ManageSubscriptionOperation.java b/java/com/android/libraries/entitlement/odsa/ManageSubscriptionOperation.java new file mode 100644 index 0000000..8c06b5f --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/ManageSubscriptionOperation.java @@ -0,0 +1,732 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.EsimOdsaOperation; +import com.android.libraries.entitlement.EsimOdsaOperation.CompanionService; +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaOperationType; +import com.android.libraries.entitlement.utils.HttpConstants; +import com.android.libraries.entitlement.utils.HttpConstants.ContentType; +import com.android.libraries.entitlement.utils.Ts43Constants; +import com.android.libraries.entitlement.utils.Ts43Constants.AppId; +import com.android.libraries.entitlement.utils.Ts43Constants.NotificationAction; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.URL; + +/** + * Manage subscription operation described in GSMA Service Entitlement Configuration section 6.5.3. + */ +public final class ManageSubscriptionOperation { + /** + * HTTP request parameters specific to on device service activation (ODSA) manage subscription + * request. See GSMA spec TS.43 section 6.2. + */ + @AutoValue + public abstract static class ManageSubscriptionRequest { + /** + * Returns the application id. Can only be {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or + * {@link Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + */ + @NonNull + @AppId + public abstract String appId(); + + /** + * Returns the detailed type of the eSIM ODSA operation. Used by HTTP parameter + * {@code operation_type}. + */ + @OdsaOperationType + public abstract int operationType(); + + /** + * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id}. + */ + @NonNull + public abstract String companionTerminalId(); + + /** + * Returns the OEM of the companion device. Used by HTTP parameter + * {@code companion_terminal_vendor}. + */ + @NonNull + public abstract String companionTerminalVendor(); + + /** + * Returns the model of the companion device. Used by HTTP parameter + * {@code companion_terminal_model}. + */ + @NonNull + public abstract String companionTerminalModel(); + + /** + * Returns the software version of the companion device. Used by HTTP parameter + * {@code companion_terminal_sw_version}. + */ + @NonNull + public abstract String companionTerminalSoftwareVersion(); + + /** + * Returns the user-friendly version of the companion device. Used by HTTP parameter + * {@code companion_terminal_friendly_name}. + */ + @NonNull + public abstract String companionTerminalFriendlyName(); + + /** + * Returns the service type of the companion device, e.g. if the MSISDN is same as the + * primary device. Used by HTTP parameter {@code companion_terminal_service}. + */ + @NonNull + @CompanionService + public abstract String companionTerminalService(); + + /** + * Returns the ICCID of the companion device. Used by HTTP parameter + * {@code companion_terminal_iccid}. + */ + @NonNull + public abstract String companionTerminalIccid(); + + /** + * Returns the EID of the companion device. Used by HTTP parameter + * {@code companion_terminal_eid}. + */ + @NonNull + public abstract String companionTerminalEid(); + + /** + * Returns the ICCID of the primary device eSIM. Used by HTTP parameter + * {@code terminal_iccid}. + */ + @NonNull + public abstract String terminalIccid(); + + /** + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * {@code terminal_eid}. + */ + @NonNull + public abstract String terminalEid(); + + /** + * Returns the unique identifier of the primary device eSIM, like the IMEI associated with + * the eSIM. Used by HTTP parameter {@code target_terminal_id}. + */ + @NonNull + public abstract String targetTerminalId(); + + /** + * Returns the unique identifiers of the primary device eSIM if more than one, like the + * IMEIs on dual-SIM devices. Used by HTTP parameter {@code target_terminal_imeis}. + * + * <p>This is a non-standard params required by some carriers. + */ + @NonNull + public abstract ImmutableList<String> targetTerminalIds(); + + /** + * Returns the ICCID primary device eSIM. Used by HTTP parameter + * {@code target_terminal_iccid}. + */ + @NonNull + public abstract String targetTerminalIccid(); + + /** + * Returns the eUICC identifier (EID) of the primary device eSIM. Used by HTTP parameter + * {@code target_terminal_eid}. + */ + @NonNull + public abstract String targetTerminalEid(); + + /** + * Returns the serial number of primary device. Used by HTTP parameter + * {@code target_terminal_sn}. + * + * <p>This is a non-standard params required by some carriers. + */ + @NonNull + public abstract String targetTerminalSerialNumber(); + + /** + * Returns the model of primary device. Used by HTTP parameter + * {@code target_terminal_model}. + * + * <p>This is a non-standard params required by some carriers. + */ + @NonNull + public abstract String targetTerminalModel(); + + /** + * Returns the unique identifier of the old device eSIM, like the IMEI associated with the + * eSIM. Used by HTTP parameter {@code old_terminal_id}. + */ + @NonNull + public abstract String oldTerminalId(); + + /** + * Returns the ICCID of old device eSIM. Used by HTTP parameter {@code old_terminal_iccid}. + */ + @NonNull + public abstract String oldTerminalIccid(); + + /** + * Returns the identifier of the specific plan offered by an MNO. Used by HTTP parameter + * {@code plan_id}. + */ + @NonNull + public abstract String planId(); + + /** + * Returns the notification token used to register for entitlement configuration request + * from network. Used by HTTP parameter {@code notif_token}. + */ + @NonNull + public abstract String notificationToken(); + + /** + * Returns the action associated with the notification token. Used by HTTP parameter + * {@code notif_action}. + */ + @NotificationAction + public abstract int notificationAction(); + + /** Returns a new {@link Builder} object. */ + @NonNull + public static Builder builder() { + return new AutoValue_ManageSubscriptionOperation_ManageSubscriptionRequest.Builder() + .setAppId(Ts43Constants.APP_UNKNOWN) + .setOperationType(EsimOdsaOperation.OPERATION_TYPE_NOT_SET) + .setCompanionTerminalId("") + .setCompanionTerminalVendor("") + .setCompanionTerminalModel("") + .setCompanionTerminalSoftwareVersion("") + .setCompanionTerminalFriendlyName("") + .setCompanionTerminalService(EsimOdsaOperation.COMPANION_SERVICE_UNKNOWN) + .setCompanionTerminalIccid("") + .setCompanionTerminalEid("") + .setTerminalIccid("") + .setTerminalEid("") + .setTargetTerminalId("") + .setTargetTerminalIds(ImmutableList.of()) + .setTargetTerminalIccid("") + .setTargetTerminalEid("") + .setTargetTerminalSerialNumber("") + .setTargetTerminalModel("") + .setOldTerminalId("") + .setOldTerminalIccid("") + .setPlanId("") + .setNotificationToken("") + .setNotificationAction(Ts43Constants.NOTIFICATION_ACTION_ENABLE_FCM); + } + + /** Builder */ + @AutoValue.Builder + public abstract static class Builder { + /** + * Sets the application id. + * + * @param appId The application id. Can only be + * {@link Ts43Constants#APP_ODSA_COMPANION}, + * {@link Ts43Constants#APP_ODSA_PRIMARY}, or {@link + * Ts43Constants#APP_ODSA_SERVER_INITIATED_REQUESTS}. + * @return The builder. + */ + @NonNull + public abstract Builder setAppId(@NonNull @AppId String appId); + + /** + * Sets the detailed type of the eSIM ODSA operation. Used by HTTP parameter + * {@code operation_type} if set. + * + * @param operationType The detailed type of the eSIM ODSA operation. + * @return The builder. + */ + @NonNull + public abstract Builder setOperationType(@OdsaOperationType int operationType); + + /** + * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter + * {@code companion_terminal_id} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalId The unique identifier of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalId(String companionTerminalId); + + /** + * Sets the OEM of the companion device. Used by HTTP parameter + * {@code companion_terminal_vendor} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalVendor The OEM of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalVendor( + @NonNull String companionTerminalVendor); + + /** + * Sets the model of the companion device. Used by HTTP parameter + * {@code companion_terminal_model} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalModel The model of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalModel( + @NonNull String companionTerminalModel); + + /** + * Sets the software version of the companion device. Used by HTTP parameter + * {@code companion_terminal_sw_version} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalSoftwareVersion The software version of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalSoftwareVersion( + @NonNull String companionTerminalSoftwareVersion); + + /** + * Sets the user-friendly version of the companion device. Used by HTTP parameter + * {@code companion_terminal_friendly_name} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalFriendlyName The user-friendly version of the companion + * device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalFriendlyName( + @NonNull String companionTerminalFriendlyName); + + /** + * Sets the service type of the companion device, e.g. if the MSISDN is same as the + * primary device. Used by HTTP parameter {@code companion_terminal_service} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalService The service type of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalService( + @NonNull @CompanionService String companionTerminalService); + + /** + * Sets the ICCID of the companion device. Used by HTTP parameter + * {@code companion_terminal_iccid} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalIccid The ICCID of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalIccid( + @NonNull String companionTerminalIccid); + + /** + * Sets the eUICC identifier (EID) of the companion device. Used by HTTP parameter + * {@code companion_terminal_eid} if set. + * + * <p>Used by companion device ODSA operation. + * + * @param companionTerminalEid The eUICC identifier (EID) of the companion device. + * @return The builder. + */ + @NonNull + public abstract Builder setCompanionTerminalEid(@NonNull String companionTerminalEid); + + /** + * Sets the ICCID of the primary device eSIM in case of primary SIM not present. Used by + * HTTP parameter {@code terminal_eid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param terminalIccid The ICCID of the primary device eSIM in case of primary SIM not + * present. + * @return The builder. + */ + @NonNull + public abstract Builder setTerminalIccid(@NonNull String terminalIccid); + + /** + * Sets the eUICC identifier (EID) of the primary device eSIM in case of primary SIM not + * present. Used by HTTP parameter {@code terminal_eid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param terminalEid The eUICC identifier (EID) of the primary device eSIM in case of + * primary SIM not present. + * @return The builder. + */ + @NonNull + public abstract Builder setTerminalEid(@NonNull String terminalEid); + + /** + * Sets the unique identifiers of the primary device eSIM if more than one, like the + * IMEIs on dual-SIM devices. Used by HTTP parameter {@code target_terminal_imeis} + * if set. + * + * <p>This is a non-standard params required by some carriers. + * + * @param targetTerminalIds The unique identifiers of the primary device eSIM if more + * than one. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalIds( + @NonNull ImmutableList<String> targetTerminalIds); + + /** + * Sets the unique identifier of the primary device eSIM in case of multiple SIM, like + * the IMEI associated with the eSIM. Used by HTTP parameter {@code target_terminal_id} + * if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalId The unique identifier of the primary device eSIM in case of + * multiple SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalId(@NonNull String targetTerminalId); + + /** + * Sets the ICCID primary device eSIM in case of multiple SIM. Used by HTTP parameter + * {@code target_terminal_iccid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalIccid The ICCID primary device eSIM in case of multiple SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalIccid(@NonNull String targetTerminalIccid); + + /** + * Sets the eUICC identifier (EID) of the primary device eSIM in case of multiple SIM. + * Used by HTTP parameter {@code target_terminal_eid} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalEid The eUICC identifier (EID) of the primary device eSIM in + * case of multiple SIM. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalEid(@NonNull String targetTerminalEid); + + /** + * Sets the serial number of primary device. Used by HTTP parameter + * {@code target_terminal_sn} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalSerialNumber The serial number of primary device. This is a + * non-standard params required by some carriers. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalSerialNumber( + @NonNull String targetTerminalSerialNumber); + + /** + * Sets the model of primary device. Used by HTTP parameter + * {@code target_terminal_model} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param targetTerminalModel The model of primary device. This is a non-standard params + * required by some carriers. + * @return The builder. + */ + @NonNull + public abstract Builder setTargetTerminalModel(@NonNull String targetTerminalModel); + + /** + * Sets the unique identifier of the old device eSIM, like the IMEI associated with the + * eSIM.Used by HTTP parameter {@code old_terminal_id} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param oldTerminalId The unique identifier of the old device eSIM. + * @return The builder. + */ + @NonNull + public abstract Builder setOldTerminalId(@NonNull String oldTerminalId); + + /** + * Sets the ICCID old device eSIM. Used by HTTP parameter {@code old_terminal_iccid} + * if set. + * + * <p>Used by primary device ODSA operation. + * + * @param oldTerminalIccid The ICCID old device eSIM. + * @return The builder. + */ + @NonNull + public abstract Builder setOldTerminalIccid(@NonNull String oldTerminalIccid); + + /** + * Sets the identifier of the specific plan offered by an MNO. Used by HTTP parameter + * {@code plan_id} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param planId The identifier of the specific plan offered by an MNO. + * @return The builder. + */ + @NonNull + public abstract Builder setPlanId(@NonNull String planId); + + /** + * Sets the notification token used to register for entitlement configuration request + * from network. Used by HTTP parameter {@code notif_token} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param notificationToken The notification token used to register for entitlement + * configuration request from network. + * @return The builder. + */ + @NonNull + public abstract Builder setNotificationToken(@NonNull String notificationToken); + + /** + * Sets the action associated with the notification token. Used by HTTP parameter + * {@code notif_action} if set. + * + * <p>Used by primary device ODSA operation. + * + * @param notificationAction The action associated with the notification token. + * @return The builder. + */ + @NonNull + public abstract Builder setNotificationAction( + @NotificationAction int notificationAction); + + /** Returns the {@link ManageSubscriptionRequest} object. */ + @NonNull + public abstract ManageSubscriptionRequest build(); + } + } + + /** + * Manage subscription response described in GSMA Service Entitlement Configuration section + * 6.5.3 table 37. + */ + @AutoValue + public abstract static class ManageSubscriptionResponse extends OdsaResponse { + /** Subscription result unknown. */ + public static final int SUBSCRIPTION_RESULT_UNKNOWN = -1; + + /** + * Indicates that end-user must go through the subscription web view procedure, using + * information included below. + */ + public static final int SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET = 1; + + /** + * Indicates that a eSIM profile must be downloaded by the device, with further information + * included in response. + */ + public static final int SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE = 2; + + /** + * Indicates that subscription flow has ended and the end-user has already downloaded the + * eSIM profile so there is no need to perform any other action. + */ + public static final int SUBSCRIPTION_RESULT_DONE = 3; + + /** + * Indicates that an eSIM profile is not ready to be downloaded when a user requests to + * transfer subscription or to add the new subscription through native UX on the eSIM + * device. + */ + public static final int SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD = 4; + + /** + * Indicates that subscription flow has ended without completing the ODSA procedure. An eSIM + * profile is not available. + */ + public static final int SUBSCRIPTION_RESULT_DISMISS = 5; + + /** + * Indicates that the profile in use needs to be deleted to complete the subscription + * transfer. + */ + public static final int SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE = 6; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SUBSCRIPTION_RESULT_UNKNOWN, + SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET, + SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE, + SUBSCRIPTION_RESULT_DONE, + SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD, + SUBSCRIPTION_RESULT_DISMISS, + SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE + }) + public @interface SubscriptionResult { + } + + /** The subscription result. */ + @SubscriptionResult + public abstract int subscriptionResult(); + + /** + * URL refers to web views responsible for a certain action on the eSIM device subscription. + * The + * Service Provider can provide different URL based on the operation_type input parameter + * ({@link EsimOdsaOperation#OPERATION_TYPE_SUBSCRIBE}, {@link + * EsimOdsaOperation#OPERATION_TYPE_UNSUBSCRIBE}, {@link + * EsimOdsaOperation#OPERATION_TYPE_CHANGE_SUBSCRIPTION}). + * + * <p>{@code null} if {@link #subscriptionResult()} is not {@link + * #SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET}. + */ + @Nullable + public abstract URL subscriptionServiceUrl(); + + /** + * User data sent to the Service Provider when requesting the + * {@link #subscriptionServiceUrl()} + * web view. It should contain user-specific attributes to improve user experience. + * + * <p>{@code null} if {@link #subscriptionResult()} is not {@link + * #SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET}. + */ + @Nullable + public abstract String subscriptionServiceUserData(); + + /** + * Specifies content and HTTP method to use when reaching out to the web server specified by + * {@link #subscriptionServiceUrl()}. + */ + @ContentType + public abstract int subscriptionServiceContentsType(); + + /** + * Specifies how and where to download the eSIM profile associated with the companion or + * primary device. + * + * <p>{@code null} if {@link #subscriptionResult()} is not {@link + * #SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE}. + */ + @Nullable + public abstract DownloadInfo downloadInfo(); + + /** Returns the builder. */ + @NonNull + public static Builder builder() { + return new AutoValue_ManageSubscriptionOperation_ManageSubscriptionResponse.Builder() + .setSubscriptionResult(SUBSCRIPTION_RESULT_UNKNOWN) + .setSubscriptionServiceContentsType(HttpConstants.UNKNOWN); + } + + /** Builder */ + @AutoValue.Builder + public abstract static class Builder extends OdsaResponse.Builder { + /** + * Set subscription result. + * + * @param subscriptionResult The subscription result. + * @return The builder. + */ + @NonNull + public abstract Builder setSubscriptionResult( + @NonNull @SubscriptionResult int subscriptionResult); + + /** + * Set the URL refers to web views responsible for a certain action on the eSIM device + * subscription. + * + * @param url URL refers to web views responsible for a certain action on the eSIM + * device subscription. The Service Provider can provide different URL based + * on the operation_type input parameter ( + * {@link EsimOdsaOperation#OPERATION_TYPE_SUBSCRIBE}, {@link + * EsimOdsaOperation#OPERATION_TYPE_UNSUBSCRIBE}, {@link + * EsimOdsaOperation#OPERATION_TYPE_CHANGE_SUBSCRIPTION}). + * @return The builder. + */ + @NonNull + public abstract Builder setSubscriptionServiceUrl(@NonNull URL url); + + /** + * Set user data sent to the Service Provider. + * + * @param userData User data sent to the Service Provider when requesting the {@link + * #subscriptionServiceUrl()} web view. It should contain user-specific + * attributes to improve user experience. + * @return The builder. + */ + @NonNull + public abstract Builder setSubscriptionServiceUserData(@NonNull String userData); + + /** + * Set the content type. + * + * @param contentType Specifies content and HTTP method to use when reaching out to the + * web server specified by {@link #subscriptionServiceUrl()}. + * @return The builder. + */ + @NonNull + public abstract Builder setSubscriptionServiceContentsType( + @ContentType int contentType); + + /** + * Set download information of eSIM profile associated with the companion or primary + * device. + * + * @param downloadInfo Specifies how and where to download the eSIM profile associated + * with the companion or primary device. + * @return The builder. + */ + @NonNull + public abstract Builder setDownloadInfo(@NonNull DownloadInfo downloadInfo); + + /** Returns the {@link ManageSubscriptionResponse} object. */ + @NonNull + public abstract ManageSubscriptionResponse build(); + } + } + + private ManageSubscriptionOperation() { + } +} diff --git a/java/com/android/libraries/entitlement/odsa/OdsaResponse.java b/java/com/android/libraries/entitlement/odsa/OdsaResponse.java new file mode 100644 index 0000000..f81a64b --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/OdsaResponse.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.libraries.entitlement.EsimOdsaOperation.OdsaOperationResult; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; + +import java.net.URL; + +/** ODSA general response described in GSMA Service Entitlement Configuration section 6.5.1. */ +public abstract class OdsaResponse { + /** Operation result. */ + @OdsaOperationResult + public abstract int operationResult(); + + /** + * The provided URL shall present a web view to user on the reason(s) why the authentication + * failed. + */ + @Nullable + public abstract URL generalErrorUrl(); + + /** + * User data sent to the Service Provider when requesting the {@link #generalErrorUrl()} web + * view. It should contain user-specific attributes to improve user experience. + */ + @Nullable + public abstract String generalErrorUserData(); + + /** Builder */ + public abstract static class Builder { + /** + * Set the operation result. + * + * @param operationResult The operation result. + * @return The builder. + */ + @NonNull + @CanIgnoreReturnValue + public abstract Builder setOperationResult(@OdsaOperationResult int operationResult); + + /** + * Set the URL to the web view to user on the reason(s) why the authentication failed. + * + * @param url The provided URL shall present a web view to user on the reason(s) why the + * authentication failed. + * @return The builder. + */ + @NonNull + @CanIgnoreReturnValue + public abstract Builder setGeneralErrorUrl(@NonNull URL url); + + /** + * Set the user data of {@link #generalErrorUrl()}. + * + * @param userData User data sent to the Service Provider when requesting the {@link + * #generalErrorUrl()} web view. It should contain user-specific attributes + * to improve user + * experience. + * @return The builder. + */ + @NonNull + @CanIgnoreReturnValue + public abstract Builder setGeneralErrorUserData(@NonNull String userData); + } +} diff --git a/java/com/android/libraries/entitlement/odsa/PlanOffer.java b/java/com/android/libraries/entitlement/odsa/PlanOffer.java new file mode 100644 index 0000000..123e5ec --- /dev/null +++ b/java/com/android/libraries/entitlement/odsa/PlanOffer.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.odsa; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +/** Mobile plan described in GSMA Service Entitlement Configuration section 6.5.6 table 43. */ +@AutoValue +public abstract class PlanOffer { + /** ID for the plan offered by the MNO. */ + @NonNull + public abstract String planId(); + + /** + * Name of the plan offered by the MNO. It is considered as an optional parameter due to it is + * not required in any request, but it is recommended to make easier the plan identification. + */ + @Nullable + public abstract String planName(); + + /** + * Description of the plan offered by the MNO. It is considered as an optional parameter due to + * it is not required in any request, but it is recommended to make easier the plan + * identification. + */ + @Nullable + public abstract String planDescription(); + + /** Returns the builder of {@link PlanOffer}. */ + public static Builder builder() { + return new AutoValue_PlanOffer.Builder(); + } + + /** Builder of PlanOffer */ + @AutoValue.Builder + public abstract static class Builder { + /** Sets ID for the plan offered by the MNO. */ + @NonNull + public abstract Builder setPlanId(@NonNull String planId); + + /** + * Sets name of the plan offered by the MNO. It is considered as an optional parameter due + * to it is not required in any request, but it is recommended to make easier the plan + * identification. + */ + @NonNull + public abstract Builder setPlanName(@NonNull String planName); + + /** + * Sets description of the plan offered by the MNO. It is considered as an optional + * parameter due to it is not required in any request, but it is recommended to make easier + * the plan identification. + */ + @NonNull + public abstract Builder setPlanDescription(@NonNull String planDescription); + + /** Build the {@link PlanOffer} object. */ + @NonNull + public abstract PlanOffer build(); + } +} diff --git a/java/com/android/libraries/entitlement/utils/DebugUtils.java b/java/com/android/libraries/entitlement/utils/DebugUtils.java index 062c9b4..d89c572 100644 --- a/java/com/android/libraries/entitlement/utils/DebugUtils.java +++ b/java/com/android/libraries/entitlement/utils/DebugUtils.java @@ -18,14 +18,19 @@ package com.android.libraries.entitlement.utils; import android.os.Build; import android.os.SystemProperties; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; + /** Provides API for debugging and not allow to debug on user build. */ public final class DebugUtils { private static final String TAG = "ServiceEntitlement"; private static final String PROP_PII_LOGGABLE = "dbg.se.pii_loggable"; private static final String BUILD_TYPE_USER = "user"; + private static final String PROP_FAKE_EAP_AKA_RESPONSE = + "persist.entitlement.fake_eap_aka_response"; private DebugUtils() {} @@ -36,6 +41,25 @@ public final class DebugUtils { } } + /** + * Get the bypass EAP-AKA response. This is only available on debug builds and can be set by + * running the following commands, where {@code response} should be the expected response from + * an EAP-AKA request: + * adb root + * adb shell setprop persist.entitlement.fake_eap_aka_response response + * + * @return The bypass EAP-AKA response, or an empty string if it is either not set or the device + * is not on a debug build. + */ + @NonNull + public static String getBypassEapAkaResponse() { + String bypassResponse = SystemProperties.get(PROP_FAKE_EAP_AKA_RESPONSE); + if (TextUtils.isEmpty(bypassResponse) || !isDebugBuild()) { + return ""; + } + return bypassResponse; + } + private static boolean isDebugBuild() { return !BUILD_TYPE_USER.equals(Build.TYPE); } diff --git a/java/com/android/libraries/entitlement/utils/HttpConstants.java b/java/com/android/libraries/entitlement/utils/HttpConstants.java new file mode 100644 index 0000000..1cd194b --- /dev/null +++ b/java/com/android/libraries/entitlement/utils/HttpConstants.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.utils; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Http constants using for entitlement flow of TS.43. */ +public final class HttpConstants { + /** HTTP content is unknown. */ + public static final int UNKNOWN = -1; + + /** HTTP content is JSON. */ + public static final int JSON = 0; + + /** HTTP content is XML. */ + public static final int XML = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({UNKNOWN, JSON, XML}) + public @interface ContentType {} + + private HttpConstants() {} +}
\ No newline at end of file diff --git a/java/com/android/libraries/entitlement/utils/Ts43Constants.java b/java/com/android/libraries/entitlement/utils/Ts43Constants.java new file mode 100644 index 0000000..ea7bca7 --- /dev/null +++ b/java/com/android/libraries/entitlement/utils/Ts43Constants.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.utils; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.StringDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Defines the constants used for TS43 operations. */ +public final class Ts43Constants { + /** App ID unknown. For initialization only. */ + public static final String APP_UNKNOWN = ""; + + /** App ID for Voice-Over-LTE entitlement. */ + public static final String APP_VOLTE = "ap2003"; + + /** App ID for Voice-Over-WiFi entitlement. */ + public static final String APP_VOWIFI = "ap2004"; + + /** App ID for SMS-Over-IP entitlement. */ + public static final String APP_SMSOIP = "ap2005"; + + /** App ID for on device service activation (ODSA) for companion device. */ + public static final String APP_ODSA_COMPANION = "ap2006"; + + /** App ID for on device service activation (ODSA) for primary device. */ + public static final String APP_ODSA_PRIMARY = "ap2009"; + + /** App ID for data plan information entitlement. */ + public static final String APP_DATA_PLAN_BOOST = "ap2010"; + + /** App ID for server initiated requests, entitlement and activation. */ + public static final String APP_ODSA_SERVER_INITIATED_REQUESTS = "ap2011"; + + /** App ID for direct carrier billing. */ + public static final String APP_DIRECT_CARRIER_BILLING = "ap2012"; + + /** App ID for private user identity. */ + public static final String APP_PRIVATE_USER_IDENTITY = "ap2013"; + + /** App ID for phone number information. */ + public static final String APP_PHONE_NUMBER_INFORMATION = "ap2014"; + + /** App ID for satellite entitlement. */ + public static final String APP_SATELLITE_ENTITLEMENT = "ap2016"; + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + APP_UNKNOWN, + APP_VOLTE, + APP_VOWIFI, + APP_SMSOIP, + APP_ODSA_COMPANION, + APP_ODSA_PRIMARY, + APP_DATA_PLAN_BOOST, + APP_ODSA_SERVER_INITIATED_REQUESTS, + APP_DIRECT_CARRIER_BILLING, + APP_PRIVATE_USER_IDENTITY, + APP_PHONE_NUMBER_INFORMATION, + APP_SATELLITE_ENTITLEMENT + }) + public @interface AppId { + } + + /** + * Check if the application id is valid. + * + * @param appId The application id. + * @return {@code true} if valid, otherwise {@code false}. + */ + public static boolean isValidAppId(@NonNull @AppId String appId) { + switch (appId) { + case APP_VOLTE: + case APP_VOWIFI: + case APP_SMSOIP: + case APP_ODSA_COMPANION: + case APP_ODSA_PRIMARY: + case APP_DATA_PLAN_BOOST: + case APP_ODSA_SERVER_INITIATED_REQUESTS: + case APP_DIRECT_CARRIER_BILLING: + case APP_PRIVATE_USER_IDENTITY: + case APP_PHONE_NUMBER_INFORMATION: + case APP_SATELLITE_ENTITLEMENT: + return true; + default: // fall through + } + return false; + } + + /** + * Action to disable notification token. + */ + public static final int NOTIFICATION_ACTION_DISABLE = 0; + + /** + * Action to enable GCM notification token. + */ + public static final int NOTIFICATION_ACTION_ENABLE_GCM = 1; + + /** + * Action to enable FCM notification token. + */ + public static final int NOTIFICATION_ACTION_ENABLE_FCM = 2; + + /** + * Action to enable WNS push notification token. + */ + public static final int NOTIFICATION_ACTION_ENABLE_WNS = 3; + + /** + * Action to enable APNS notification token. + */ + public static final int NOTIFICATION_ACTION_ENABLE_APNS = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NOTIFICATION_ACTION_DISABLE, + NOTIFICATION_ACTION_ENABLE_GCM, + NOTIFICATION_ACTION_ENABLE_FCM, + NOTIFICATION_ACTION_ENABLE_WNS, + NOTIFICATION_ACTION_ENABLE_APNS, + }) + public @interface NotificationAction {} + + /** + * Check if the notification action is valid. + * + * @param notificationAction The notification action. + * @return {@code true} if valid, otherwise {@code false}. + */ + public static boolean isValidNotificationAction(@NotificationAction int notificationAction) { + switch (notificationAction) { + case NOTIFICATION_ACTION_DISABLE: + case NOTIFICATION_ACTION_ENABLE_GCM: + case NOTIFICATION_ACTION_ENABLE_FCM: + case NOTIFICATION_ACTION_ENABLE_WNS: + case NOTIFICATION_ACTION_ENABLE_APNS: + return true; + default: // fall through + } + return false; + } + + /** Default entitlement version. */ + public static final String DEFAULT_ENTITLEMENT_VERSION = "2.0"; + + private Ts43Constants() { + } +}
\ No newline at end of file diff --git a/java/com/android/libraries/entitlement/utils/Ts43XmlDoc.java b/java/com/android/libraries/entitlement/utils/Ts43XmlDoc.java new file mode 100644 index 0000000..ec6910c --- /dev/null +++ b/java/com/android/libraries/entitlement/utils/Ts43XmlDoc.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement.utils; + +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.common.collect.ImmutableList; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** Wraps the TS.43 XML raw string and parses it into nodes. */ +public final class Ts43XmlDoc { + private static final String TAG = "Ts43XmlDoc"; + private static final String NODE_CHARACTERISTIC = "characteristic"; + private static final String NODE_PARM = "parm"; + private static final String PARM_NAME = "name"; + private static final String PARM_VALUE = "value"; + + /** Type names of characteristics. */ + public static final class CharacteristicType { + private CharacteristicType() { + } + + public static final String APPLICATION = "APPLICATION"; + public static final String PRIMARY_CONFIGURATION = "PrimaryConfiguration"; + public static final String COMPANION_CONFIGURATIONS = "CompanionConfigurations"; + public static final String COMPANION_CONFIGURATION = "CompanionConfiguration"; + public static final String ENTERPRISE_CONFIGURATION = "EnterpriseConfiguration"; + public static final String USER = "USER"; + public static final String TOKEN = "TOKEN"; + public static final String DOWNLOAD_INFO = "DownloadInfo"; + } + + /** Names of parameters. */ + public static final class Parm { + private Parm() { + } + + public static final String TOKEN = "token"; + public static final String APP_ID = "AppID"; + public static final String VERSION = "version"; + public static final String VALIDITY = "validity"; + public static final String OPERATION_RESULT = "OperationResult"; + public static final String GENERAL_ERROR_URL = "GeneralErrorURL"; + public static final String GENERAL_ERROR_USER_DATA = "GeneralErrorUserData"; + public static final String PRIMARY_APP_ELIGIBILITY = "PrimaryAppEligibility"; + public static final String COMPANION_APP_ELIGIBILITY = "CompanionAppEligibility"; + public static final String ENTERPRISE_APP_ELIGIBILITY = "EnterpriseAppEligibility"; + public static final String NOT_ENABLED_URL = "NotEnabledURL"; + public static final String NOT_ENABLED_USER_DATA = "NotEnabledUserData"; + public static final String NOT_ENABLED_CONTENTS_TYPE = "NotEnabledContentsType"; + public static final String COMPANION_DEVICE_SERVICES = "CompanionDeviceServices"; + public static final String TEMPORARY_TOKEN = "TemporaryToken"; + public static final String TEMPORARY_TOKEN_EXPIRY = "TemporaryTokenExpiry"; + public static final String MSISDN = "msisdn"; + public static final String ICCID = "ICCID"; + public static final String SERVICE_STATUS = "ServiceStatus"; + public static final String POLLING_INTERVAL = "PollingInterval"; + public static final String SUBSCRIPTION_RESULT = "SubscriptionResult"; + public static final String SUBSCRIPTION_SERVICE_URL = "SubscriptionServiceURL"; + public static final String SUBSCRIPTION_SERVICE_USER_DATA = "SubscriptionServiceUserData"; + public static final String SUBSCRIPTION_SERVICE_CONTENTS_TYPE = + "SubscriptionServiceContentsType"; + public static final String PROFILE_ACTIVATION_CODE = "ProfileActivationCode"; + public static final String PROFILE_ICCID = "ProfileIccid"; + public static final String PROFILE_SMDP_ADDRESS = "ProfileSmdpAddress"; + public static final String OPERATION_TARGETS = "OperationTargets"; + } + + /** Parameter values of XML response content. */ + public static final class ParmValues { + private ParmValues() { + } + + public static final String OPERATION_RESULT_SUCCESS = "1"; + public static final String OPERATION_RESULT_ERROR_GENERAL = "100"; + public static final String OPERATION_RESULT_ERROR_INVALID_OPERATION = "101"; + public static final String OPERATION_RESULT_ERROR_INVALID_PARAMETER = "102"; + public static final String OPERATION_RESULT_WARNING_NOT_SUPPORTED_OPERATION = "103"; + public static final String PRIMARY_APP_ELIGIBILITY_ENABLED = "1"; + public static final String SERVICE_STATUS_ACTIVATED = "1"; + public static final String SERVICE_STATUS_ACTIVATING = "2"; + public static final String SERVICE_STATUS_DEACTIVATED = "3"; + public static final String SERVICE_STATUS_DEACTIVATED_NO_REUSE = "4"; + public static final String SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET = "1"; + public static final String SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE = "2"; + public static final String SUBSCRIPTION_RESULT_DONE = "3"; + public static final String SUBSCRIPTION_RESULT_DELAYED_DOWNLOAD = "4"; + public static final String SUBSCRIPTION_RESULT_DISMISS = "5"; + public static final String SUBSCRIPTION_RESULT_DELETE_PROFILE_IN_USE = "6"; + public static final String CONTENTS_TYPE_XML = "xml"; + public static final String CONTENTS_TYPE_JSON = "json"; + public static final String DISABLED = "0"; + public static final String ENABLED = "1"; + public static final String INCOMPATIBLE = "2"; + } + + /** + * Maps characteristics to a map of parameters. Key is the characteristic type. Value is + * parameter + * name and value. Example: {"APPLICATION" -> {"AppId" -> "ap2009", "OperationResult" -> "1"}, + * "APPLICATION|PrimaryConfiguration" -> {"ICCID" -> "123", "ServiceStatus" -> "2", + * "PollingInterval" -> "1"} } + */ + private final Map<String, Map<String, String>> mCharacteristicsMap = new ArrayMap<>(); + + public Ts43XmlDoc(String responseBody) { + parseXmlResponse(responseBody); + } + + /** Returns {@code true} if a node structure exists for a given characteristicTypes. */ + public boolean contains(ImmutableList<String> characteristicTypes) { + return mCharacteristicsMap.containsKey(TextUtils.join("|", characteristicTypes)); + } + + /** + * Returns param value for given characteristicType and parameterName, or {@code null} if not + * found. + */ + @Nullable + public String get(ImmutableList<String> characteristicTypes, String parameterName) { + Map<String, String> parmMap = mCharacteristicsMap.get( + TextUtils.join("|", characteristicTypes)); + return parmMap == null ? null : parmMap.get(parameterName.toLowerCase(Locale.ROOT)); + } + + /** + * Parses the response body as per format defined in TS.43 New Characteristics for XML-Based + * Document. + */ + private void parseXmlResponse(String responseBody) { + if (responseBody == null) { + return; + } + // Workaround: some server doesn't escape "&" in XML response and that will cause XML parser + // failure later. + // This is a quick impl of escaping w/o introducing a ton of new dependencies. + responseBody = responseBody.replace("&", "&").replace("&amp;", "&"); + try { + InputSource inputSource = new InputSource(new StringReader(responseBody)); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = builderFactory.newDocumentBuilder(); + Document doc = docBuilder.parse(inputSource); + doc.getDocumentElement().normalize(); + NodeList nodeList = doc.getDocumentElement().getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + parseNode(new ArrayList<>(), Objects.requireNonNull(nodeList.item(i))); + } + } catch (ParserConfigurationException | IOException | SAXException e) { + // Nodes that failed to parse won't be stored in nodesMap + Log.w(TAG, "e=" + e); + } + } + + @SuppressWarnings("AndroidJdkLibsChecker") // java.util.Map#getOrDefault + /* Parses characteristics and parm values into characteristicsMap. */ + private void parseNode(List<String> characteristics, Node node) { + String nodeName = node.getNodeName(); + NamedNodeMap attributes = node.getAttributes(); + if (attributes == null) { + return; + } + if (nodeName.equals(NODE_CHARACTERISTIC)) { + Node typeNode = attributes.getNamedItem("type"); + if (typeNode == null) { + return; + } + characteristics.add(Objects.requireNonNull(typeNode.getNodeValue())); + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + parseNode(characteristics, Objects.requireNonNull(children.item(i))); + } + characteristics.remove(characteristics.size() - 1); + } else if (nodeName.equals(NODE_PARM)) { + Node parmNameNode = attributes.getNamedItem(PARM_NAME); + Node parmValueNode = attributes.getNamedItem(PARM_VALUE); + if (parmNameNode == null || parmValueNode == null) { + return; + } + String characteristicKey = TextUtils.join("|", characteristics); + Map<String, String> parmMap = + mCharacteristicsMap.getOrDefault(characteristicKey, new ArrayMap<>()); + parmMap.put( + Objects.requireNonNull(parmNameNode.getNodeValue().toLowerCase(Locale.ROOT)), + Objects.requireNonNull(parmValueNode.getNodeValue())); + mCharacteristicsMap.put(characteristicKey, parmMap); + } + } + + @NonNull + @Override + public String toString() { + return "Ts43XmlDoc: " + mCharacteristicsMap; + } +} diff --git a/tests/Android.bp b/tests/Android.bp index 4bafe3a..914b3ab 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -48,7 +48,7 @@ android_test { "service-entitlement-testing-utils", "testables", "testng", - "truth-prebuilt", + "truth", ], certificate: "platform", test_suites: ["device-tests"], diff --git a/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java b/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java index 7e2b0f1..25812a9 100644 --- a/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java +++ b/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java @@ -29,6 +29,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; import com.android.libraries.entitlement.eapaka.EapAkaApi; +import com.android.libraries.entitlement.http.HttpResponse; import com.google.common.collect.ImmutableList; @@ -46,6 +47,9 @@ public class ServiceEntitlementTest { private static final String QUERY_APP_VOWIFI_RESULT = "QUERY_APP_VOWIFI_RESULT"; private static final String QUERY_APP_ODSA_COMPANION_RESULT = "QUERY_APP_ODSA_COMPANION_RESULT"; private static final String QUERY_APP_ODSA_PRIMARY_RESULT = "QUERY_APP_ODSA_PRIMARY_RESULT"; + private static final String QUERY_OIDC_RESULT = "QUERY_OIDC_RESULT"; + private static final String QUERY_ENTITLEMENT_STATUS_FROM_OIDC = + "QUERY_ENTITLEMENT_STATUS_FROM_OIDC"; private static final String TEST_URL = "https://test.url"; private static final String IMSI = "234107813240779"; @@ -54,6 +58,7 @@ public class ServiceEntitlementTest { @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Mock EapAkaApi mMockEapAkaApi; + @Mock HttpResponse mMockHttpResponse; @Mock private TelephonyManager mMockTelephonyManager; @Mock private TelephonyManager mMockTelephonyManagerForSubId; @@ -66,6 +71,11 @@ public class ServiceEntitlementTest { mCarrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); mServiceEntitlement = new ServiceEntitlement(mCarrierConfig, mMockEapAkaApi); mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mMockTelephonyManager); + when(mMockTelephonyManager.createForSubscriptionId(SUB_ID)) + .thenReturn(mMockTelephonyManagerForSubId); + when(mMockTelephonyManagerForSubId.getSubscriberId()).thenReturn(IMSI); + when(mMockTelephonyManagerForSubId.getSimOperator()).thenReturn(MCCMNC); } @Test @@ -73,20 +83,16 @@ public class ServiceEntitlementTest { CarrierConfig config = CarrierConfig.builder().build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); ServiceEntitlement serviceEntitlement = new ServiceEntitlement(mContext, config, SUB_ID); - when(mContext.getSystemService(TelephonyManager.class)) - .thenReturn(mMockTelephonyManager); - when(mMockTelephonyManager.createForSubscriptionId(SUB_ID)) - .thenReturn(mMockTelephonyManagerForSubId); - when(mMockTelephonyManagerForSubId.getSubscriberId()).thenReturn(IMSI); - when(mMockTelephonyManagerForSubId.getSimOperator()).thenReturn(MCCMNC); - ServiceEntitlementException exception = expectThrows( - ServiceEntitlementException.class, - () -> serviceEntitlement.queryEntitlementStatus( - ImmutableList.of(ServiceEntitlement.APP_VOWIFI), request)); + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + serviceEntitlement.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), request)); - assertThat(exception.getErrorCode()).isEqualTo( - ServiceEntitlementException.ERROR_SERVER_NOT_CONNECTABLE); + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_SERVER_NOT_CONNECTABLE); assertThat(exception.getMessage()).isEqualTo("Configure connection failed!"); assertThat(exception.getHttpStatus()).isEqualTo(0); assertThat(exception.getRetryAfter()).isEmpty(); @@ -96,11 +102,16 @@ public class ServiceEntitlementTest { public void queryEntitlementStatus_appVolte_returnResult() throws Exception { ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); when(mMockEapAkaApi.queryEntitlementStatus( - ImmutableList.of(ServiceEntitlement.APP_VOLTE), mCarrierConfig, request)) - .thenReturn(QUERY_APP_VOLTE_RESULT); + ImmutableList.of(ServiceEntitlement.APP_VOLTE), mCarrierConfig, request)) + .thenAnswer( + invocation -> { + when(mMockHttpResponse.body()).thenReturn(QUERY_APP_VOLTE_RESULT); + return mMockHttpResponse; + }); assertThat( - mServiceEntitlement.queryEntitlementStatus(ServiceEntitlement.APP_VOLTE, request)) + mServiceEntitlement.queryEntitlementStatus( + ServiceEntitlement.APP_VOLTE, request)) .isEqualTo(QUERY_APP_VOLTE_RESULT); } @@ -108,13 +119,16 @@ public class ServiceEntitlementTest { public void queryEntitlementStatus_appVowifi_returnResult() throws Exception { ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); when(mMockEapAkaApi.queryEntitlementStatus( - ImmutableList.of(ServiceEntitlement.APP_VOWIFI), mCarrierConfig, request)) - .thenReturn(QUERY_APP_VOWIFI_RESULT); + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), mCarrierConfig, request)) + .thenAnswer( + invocation -> { + when(mMockHttpResponse.body()).thenReturn(QUERY_APP_VOWIFI_RESULT); + return mMockHttpResponse; + }); assertThat( - mServiceEntitlement.queryEntitlementStatus( - ImmutableList.of(ServiceEntitlement.APP_VOWIFI), - request)) + mServiceEntitlement.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), request)) .isEqualTo(QUERY_APP_VOWIFI_RESULT); } @@ -123,12 +137,20 @@ public class ServiceEntitlementTest { ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); EsimOdsaOperation odsaOperation = EsimOdsaOperation.builder().build(); when(mMockEapAkaApi.performEsimOdsaOperation( - ServiceEntitlement.APP_ODSA_COMPANION, mCarrierConfig, request, odsaOperation)) - .thenReturn(QUERY_APP_ODSA_COMPANION_RESULT); + ServiceEntitlement.APP_ODSA_COMPANION, + mCarrierConfig, + request, + odsaOperation)) + .thenAnswer( + invocation -> { + when(mMockHttpResponse.body()) + .thenReturn(QUERY_APP_ODSA_COMPANION_RESULT); + return mMockHttpResponse; + }); assertThat( - mServiceEntitlement.performEsimOdsa( - ServiceEntitlement.APP_ODSA_COMPANION, request, odsaOperation)) + mServiceEntitlement.performEsimOdsa( + ServiceEntitlement.APP_ODSA_COMPANION, request, odsaOperation)) .isEqualTo(QUERY_APP_ODSA_COMPANION_RESULT); } @@ -137,12 +159,50 @@ public class ServiceEntitlementTest { ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); EsimOdsaOperation odsaOperation = EsimOdsaOperation.builder().build(); when(mMockEapAkaApi.performEsimOdsaOperation( - ServiceEntitlement.APP_ODSA_PRIMARY, mCarrierConfig, request, odsaOperation)) - .thenReturn(QUERY_APP_ODSA_PRIMARY_RESULT); + ServiceEntitlement.APP_ODSA_PRIMARY, + mCarrierConfig, + request, + odsaOperation)) + .thenAnswer( + invocation -> { + when(mMockHttpResponse.body()) + .thenReturn(QUERY_APP_ODSA_PRIMARY_RESULT); + return mMockHttpResponse; + }); assertThat( - mServiceEntitlement.performEsimOdsa( - ServiceEntitlement.APP_ODSA_PRIMARY, request, odsaOperation)) + mServiceEntitlement.performEsimOdsa( + ServiceEntitlement.APP_ODSA_PRIMARY, request, odsaOperation)) .isEqualTo(QUERY_APP_ODSA_PRIMARY_RESULT); } + + @Test + public void acquireOidcAuthenticationEndpoint_returnResult() throws Exception { + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + when(mMockEapAkaApi.acquireOidcAuthenticationEndpoint( + ServiceEntitlement.APP_ODSA_COMPANION, mCarrierConfig, request)) + .thenReturn(QUERY_OIDC_RESULT); + + assertThat( + mServiceEntitlement.acquireOidcAuthenticationEndpoint( + ServiceEntitlement.APP_ODSA_COMPANION, request)) + .isEqualTo(QUERY_OIDC_RESULT); + } + + @Test + public void queryEntitlementStatusFromOidc_returnResult() throws Exception { + when(mMockEapAkaApi.queryEntitlementStatusFromOidc( + ServiceEntitlement.APP_ODSA_PRIMARY, mCarrierConfig, null)) + .thenAnswer( + invocation -> { + when(mMockHttpResponse.body()) + .thenReturn(QUERY_ENTITLEMENT_STATUS_FROM_OIDC); + return mMockHttpResponse; + }); + + assertThat( + mServiceEntitlement.queryEntitlementStatusFromOidc( + ServiceEntitlement.APP_ODSA_PRIMARY)) + .isEqualTo(QUERY_ENTITLEMENT_STATUS_FROM_OIDC); + } } diff --git a/tests/src/com/android/libraries/entitlement/Ts43AuthenticationTest.java b/tests/src/com/android/libraries/entitlement/Ts43AuthenticationTest.java new file mode 100644 index 0000000..f30e171 --- /dev/null +++ b/tests/src/com/android/libraries/entitlement/Ts43AuthenticationTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; + +import android.content.Context; +import android.telephony.TelephonyManager; +import android.testing.AndroidTestingRunner; + +import com.android.libraries.entitlement.Ts43Authentication.Ts43AuthToken; +import com.android.libraries.entitlement.eapaka.EapAkaApi; +import com.android.libraries.entitlement.http.HttpResponse; +import com.android.libraries.entitlement.utils.Ts43Constants; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.net.URL; + +@RunWith(AndroidTestingRunner.class) +public class Ts43AuthenticationTest { + private static final String TEST_URL = "https://test.url"; + private static final String ENTITLEMENT_VERSION = "9.0"; + private static final String APP_NAME = "com.fake.app"; + private static final String APP_VERSION = "1.0"; + private static final String TOKEN = "ASH127AHHA88SF"; + private static final long VALIDITY = 86400; + private static final String IMEI = "861536030196001"; + private static final ImmutableList<String> COOKIES = + ImmutableList.of("key1=value1", "key2=value2"); + + private static final String HTTP_RESPONSE_WITH_TOKEN = + "<?xml version=\"1.0\"?>" + + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\" + " + VALIDITY + "\"/>" + + " </characteristic>" + + " <characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"" + TOKEN + "\"/>" + + " <parm name=\"validity\" value=\"" + VALIDITY + "\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + private static final String HTTP_RESPONSE_WITHOUT_TOKEN = + "<?xml version=\"1.0\"?>" + + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\" + " + VALIDITY + "\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + + private static final String HTTP_RESPONSE_WITHOUT_VALIDITY = + "<?xml version=\"1.0\"?>" + + "<wap-provisioningdoc version=\"1.1\">" + + " <characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\" + " + VALIDITY + "\"/>" + + " </characteristic>" + + " <characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"" + TOKEN + "\"/>" + + " </characteristic>" + + "</wap-provisioningdoc>"; + + private Ts43Authentication mTs43Authentication; + + @Mock + private EapAkaApi mMockEapAkaApi; + + @Mock + private HttpResponse mMockHttpResponse; + + @Mock + private Context mContext; + + @Mock + private TelephonyManager mTelephonyManager; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlement serviceEntitlement = new ServiceEntitlement(carrierConfig, + mMockEapAkaApi); + mTs43Authentication = new Ts43Authentication(mContext, new URL(TEST_URL), + ENTITLEMENT_VERSION); + + Field field = Ts43Authentication.class.getDeclaredField("mServiceEntitlement"); + field.setAccessible(true); + field.set(mTs43Authentication, serviceEntitlement); + + doReturn(2).when(mTelephonyManager).getActiveModemCount(); + doReturn(IMEI).when(mTelephonyManager).getImei(0); + doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt()); + doReturn(Context.TELEPHONY_SERVICE).when(mContext) + .getSystemServiceName(TelephonyManager.class); + doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE); + doReturn(mMockHttpResponse).when(mMockEapAkaApi) + .queryEntitlementStatus(any(), any(), any()); + doReturn(COOKIES).when(mMockHttpResponse).cookies(); + } + + @Test + public void testGetAuthToken_receivedValidToken() throws Exception { + doReturn(HTTP_RESPONSE_WITH_TOKEN).when(mMockHttpResponse).body(); + Ts43AuthToken mToken = mTs43Authentication.getAuthToken( + 0, Ts43Constants.APP_ODSA_PRIMARY, APP_NAME, APP_VERSION); + assertThat(mToken.token()).isEqualTo(TOKEN); + assertThat(mToken.validity()).isEqualTo(VALIDITY); + } + + @Test + public void testGetAuthToken_invalidParams_throwException() { + assertThrows(NullPointerException.class, () -> new Ts43Authentication( + null, new URL(TEST_URL), ENTITLEMENT_VERSION)); + + assertThrows(NullPointerException.class, () -> new Ts43Authentication( + mContext, null, ENTITLEMENT_VERSION)); + } + + @Test + public void testGetAuthToken_invalidAppId_throwException() { + assertThrows(NullPointerException.class, () -> mTs43Authentication.getAuthToken( + 0, null, APP_NAME, APP_VERSION)); + assertThrows(IllegalArgumentException.class, () -> mTs43Authentication.getAuthToken( + 0, "invalid_app_id", APP_NAME, APP_VERSION)); + } + + @Test + public void testGetAuthToken_invalidSlotIndex_throwException() { + assertThrows(IllegalArgumentException.class, () -> mTs43Authentication.getAuthToken( + 5, Ts43Constants.APP_ODSA_PRIMARY, APP_NAME, APP_VERSION)); + } + + @Test + public void testGetAuthToken_tokenNotAvailable_throwException() { + doReturn(HTTP_RESPONSE_WITHOUT_TOKEN).when(mMockHttpResponse).body(); + + try { + mTs43Authentication.getAuthToken( + 0, Ts43Constants.APP_ODSA_PRIMARY, APP_NAME, APP_VERSION); + fail("Expected to get exception."); + } catch (ServiceEntitlementException e) { + assertThat(e.getErrorCode()).isEqualTo( + ServiceEntitlementException.ERROR_TOKEN_NOT_AVAILABLE); + } + } + + @Test + public void testGetAuthToken_validityNotAvailable() throws Exception { + doReturn(HTTP_RESPONSE_WITHOUT_VALIDITY).when(mMockHttpResponse).body(); + Ts43AuthToken mToken = mTs43Authentication.getAuthToken( + 0, Ts43Constants.APP_ODSA_PRIMARY, APP_NAME, APP_VERSION); + assertThat(mToken.token()).isEqualTo(TOKEN); + assertThat(mToken.validity()).isEqualTo(Ts43AuthToken.VALIDITY_NOT_AVAILABLE); + } + + @Test + public void testGetAuthToken_httpResponseError() throws Exception { + doThrow(new ServiceEntitlementException( + ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS, 1234, "http error")) + .when(mMockEapAkaApi).queryEntitlementStatus(any(), any(), any()); + try { + mTs43Authentication.getAuthToken( + 0, Ts43Constants.APP_ODSA_PRIMARY, APP_NAME, APP_VERSION); + fail("Expected to get exception."); + } catch (ServiceEntitlementException e) { + assertThat(e.getErrorCode()).isEqualTo( + ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS); + assertThat(e.getHttpStatus()).isEqualTo(1234); + } + } +} diff --git a/tests/src/com/android/libraries/entitlement/Ts43OperationTest.java b/tests/src/com/android/libraries/entitlement/Ts43OperationTest.java new file mode 100644 index 0000000..97e2193 --- /dev/null +++ b/tests/src/com/android/libraries/entitlement/Ts43OperationTest.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2023 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.libraries.entitlement; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; + +import android.content.Context; +import android.telephony.TelephonyManager; +import android.testing.AndroidTestingRunner; + +import com.android.libraries.entitlement.eapaka.EapAkaApi; +import com.android.libraries.entitlement.http.HttpResponse; +import com.android.libraries.entitlement.odsa.AcquireConfigurationOperation.AcquireConfigurationRequest; +import com.android.libraries.entitlement.odsa.AcquireConfigurationOperation.AcquireConfigurationResponse; +import com.android.libraries.entitlement.odsa.AcquireTemporaryTokenOperation.AcquireTemporaryTokenRequest; +import com.android.libraries.entitlement.odsa.AcquireTemporaryTokenOperation.AcquireTemporaryTokenResponse; +import com.android.libraries.entitlement.odsa.CheckEligibilityOperation; +import com.android.libraries.entitlement.odsa.CheckEligibilityOperation.CheckEligibilityRequest; +import com.android.libraries.entitlement.odsa.CheckEligibilityOperation.CheckEligibilityResponse; +import com.android.libraries.entitlement.odsa.GetPhoneNumberOperation.GetPhoneNumberRequest; +import com.android.libraries.entitlement.odsa.GetPhoneNumberOperation.GetPhoneNumberResponse; +import com.android.libraries.entitlement.odsa.ManageServiceOperation.ManageServiceRequest; +import com.android.libraries.entitlement.odsa.ManageServiceOperation.ManageServiceResponse; +import com.android.libraries.entitlement.odsa.ManageSubscriptionOperation.ManageSubscriptionRequest; +import com.android.libraries.entitlement.odsa.ManageSubscriptionOperation.ManageSubscriptionResponse; +import com.android.libraries.entitlement.utils.Ts43Constants; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.net.URL; + +@RunWith(AndroidTestingRunner.class) +public class Ts43OperationTest { + private static final String TEST_URL = "https://test.url"; + private static final String ENTITLEMENT_VERSION = "9.0"; + private static final String TOKEN = "ASH127AHHA88SF"; + private static final String NEW_TOKEN = "ES7WLERXJH"; + private static final String SUBSCRIPTION_SERVICE_URL = "http://www.MNO.org/CDSubs"; + private static final String SUBSCRIPTION_SERVICE_USER_DATA = "imsi=XX"; + private static final String IMEI = "861536030196001"; + private static final String TERMINAL_ID = "861536030196005"; + private static final String COMPANION_TERMINAL_ID = "98112687006099944"; + private static final String COMPANION_TERMINAL_EID = "JHSDHljhsdfy763hh"; + private static final String ICCID = "123456789"; + private static final String PROFILE_SMDP_ADDRESS = "SMDP+ ADDR"; + + private static final String TEMPORARY_TOKEN = "A8daAd8ads7fau34789947kjhsfad;kjfh"; + + private static final String TEMPORARY_TOKEN_EXPIRY = "2019-01-29T13:15:31Z"; + + private static final String NOT_ENABLED_URL = "http://www.MNO.org/AppNotAllowed"; + + private static final String NOT_ENABLED_USER_DATA = "msisdn=XX"; + + private static final String MSISDN = "+16502530000"; + + private static final String MANAGE_SUBSCRIPTION_RESPONSE_CONTINUE_TO_WEBSHEET = + "<?xml version=\"1.0\"?>" + + "<wap-provisioningdoc version=\"1.1\">" + + "<characteristic type=\"VERS\">" + + " <parm name=\"version\" value=\"1\"/>" + + " <parm name=\"validity\" value=\"172800\"/>" + + "</characteristic>" + + "<characteristic type=\"TOKEN\">" + + " <parm name=\"token\" value=\"" + NEW_TOKEN + "\"/>" + + "</characteristic>" + + "<characteristic type=\"APPLICATION\">" + + " <parm name=\"AppID\" value=\"ap2006\"/>" + + " <parm name=\"SubscriptionServiceURL\"" + + " value=\"" + SUBSCRIPTION_SERVICE_URL + "\"/>" + + " <parm name=\"SubscriptionServiceUserData\"" + + " value=\"" + SUBSCRIPTION_SERVICE_USER_DATA + "\"/>" + + " <parm name=\"SubscriptionResult\" value=\"1\"/>" + + " <parm name=\"OperationResult\" value=\"1\"/>" + + "</characteristic>" + + "</wap-provisioningdoc>"; + + private static final String MANAGE_SUBSCRIPTION_RESPONSE_DOWNLOAD_PROFILE = + "<?xml version=\"1.0\"?>\n" + + "<wap-provisioningdoc version=\"1.1\">\n" + + " <characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"172800\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"ASH127AHHA88SF\"/>\n" + + " </characteristic>\n" + + " <characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2006\"/>\n" + + " <characteristic type=\"DownloadInfo\">\n" + + " <parm name=\"ProfileIccid\" value=\"" + ICCID + "\"/>\n" + + " <parm name=\"ProfileSmdpAddress\" value=\"" + + PROFILE_SMDP_ADDRESS + "\"/>\n" + + " </characteristic>\n" + + " <parm name=\"SubscriptionResult\" value=\"2\"/>\n" + + " <parm name=\"OperationResult\" value=\"1\"/>\n" + + " </characteristic>\n" + + "</wap-provisioningdoc>"; + + private static final String ACQUIRE_TEMPORARY_TOKEN_RESPONSE = + "<?xml version=\"1.0\"?>\n" + + "<wap-provisioningdoc version=\"1.1\">\n" + + "<characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"172800\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"ASH127AHHA88SF\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2009\"/>\n" + + " <parm name=\"TemporaryToken\" value=\"" + TEMPORARY_TOKEN + "\"/>\n" + + " <parm name=\"TemporaryTokenExpiry\" " + + " value=\"" + TEMPORARY_TOKEN_EXPIRY + "\"/>\n" + + " <parm name=\"OperationTargets\"\n" + + " value=\"ManageSubscription,AcquireConfiguration\"/>\n" + + " <parm name=\"OperationResult\" value=\"1\"/>\n" + + "</characteristic>\n" + + "</wap-provisioningdoc>"; + + private static final String ACQUIRE_CONFIGURATION_RESPONSE = + "<?xml version=\"1.0\"?>\n" + + "<wap-provisioningdoc version=\"1.1\">\n" + + "<characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"172800\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"ASH127AHHA88SF\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2006\"/>\n" + + " <characteristic type=\"PrimaryConfiguration\">\n" + + " <parm name=\"ICCID\" value=\"" + ICCID + "\"/>\n" + + " <characteristic type=\"DownloadInfo\">\n" + + " <parm name=\"ProfileIccid\" value=\"" + ICCID + "\"/>\n" + + " <parm name=\"ProfileSmdpAddress\" value=\"" + + PROFILE_SMDP_ADDRESS + "\"/>\n" + + " </characteristic>\n" + + " <parm name=\"ServiceStatus\" value=\"1\"/>\n" + + " </characteristic>\n" + + " <parm name=\"OperationResult\" value=\"1\"/>\n" + + "</characteristic>\n" + + "</wap-provisioningdoc>\n"; + + private static final String CHECK_ELIGIBILITY_RESPONSE = + "<?xml version=\"1.0\"?>\n" + + "<wap-provisioningdoc version=\"1.1\">\n" + + "<characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"172800\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"ASH127AHHA88SF\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2006\"/>\n" + + " <parm name=\"CompanionAppEligibility\" value=\"1\"/>\n" + + " <parm name=\"CompanionDeviceServices\" value=\"SharedNumber\"/>\n" + + " <parm name=\"NotEnabledURL\" value=\"" + NOT_ENABLED_URL + "\"/>\n" + + " <parm name=\"NotEnabledUserData\" value=\"" + NOT_ENABLED_USER_DATA + + "\"/>\n" + + " <parm name=\"OperationResult\" value=\"1\"/>\n" + + "</characteristic>\n" + + "</wap-provisioningdoc>"; + + public String MANAGE_SERVICE_RESPONSE = + "<?xml version=\"1.0\"?>\n" + + "<wap-provisioningdoc version=\"1.1\">\n" + + "<characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"172800\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"ASH127AHHA88SF\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2006\"/>\n" + + " <parm name=\"ServiceStatus\" value=\"3\"/>\n" + + " <parm name=\"OperationResult\" value=\"1\"/>\n" + + "</characteristic>\n" + + "</wap-provisioningdoc>"; + + public String GET_PHONE_NUMBER_RESPONSE = + "<?xml version=\"1.0\"?>\n" + + "<wap-provisioningdoc version=\"1.1\">\n" + + "<characteristic type=\"VERS\">\n" + + " <parm name=\"version\" value=\"1\"/>\n" + + " <parm name=\"validity\" value=\"172800\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"TOKEN\">\n" + + " <parm name=\"token\" value=\"ASH127AHHA88SF\"/>\n" + + "</characteristic>\n" + + "<characteristic type=\"APPLICATION\">\n" + + " <parm name=\"AppID\" value=\"ap2014\"/>\n" + + " <parm name=\"OperationResult\" value=\"1\"/>\n" + + " <parm name=\"MSISDN\" value=\"" + MSISDN + "\"/>\n" + + "</characteristic>\n" + + "</wap-provisioningdoc>"; + + @Mock + private EapAkaApi mMockEapAkaApi; + + @Mock + private HttpResponse mMockHttpResponse; + + @Mock + private Context mContext; + + @Mock + private TelephonyManager mTelephonyManager; + + private Ts43Operation mTs43Operation; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlement serviceEntitlement = + new ServiceEntitlement(carrierConfig, mMockEapAkaApi); + doReturn(mMockHttpResponse).when(mMockEapAkaApi) + .performEsimOdsaOperation(any(), any(), any(), any()); + + doReturn(2).when(mTelephonyManager).getActiveModemCount(); + doReturn(IMEI).when(mTelephonyManager).getImei(0); + doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt()); + doReturn(Context.TELEPHONY_SERVICE).when(mContext) + .getSystemServiceName(TelephonyManager.class); + doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE); + + mTs43Operation = new Ts43Operation(mContext, 0, new URL(TEST_URL), + ENTITLEMENT_VERSION, TOKEN, Ts43Operation.TOKEN_TYPE_NORMAL); + + Field field = Ts43Operation.class.getDeclaredField("mServiceEntitlement"); + field.setAccessible(true); + field.set(mTs43Operation, serviceEntitlement); + } + + @Test + public void testManageSubscription_continueToWebsheet() throws Exception { + doReturn(MANAGE_SUBSCRIPTION_RESPONSE_CONTINUE_TO_WEBSHEET).when(mMockHttpResponse).body(); + + ManageSubscriptionRequest request = ManageSubscriptionRequest.builder() + .setAppId(Ts43Constants.APP_ODSA_PRIMARY) + .setOperationType(EsimOdsaOperation.OPERATION_TYPE_SUBSCRIBE) + .setCompanionTerminalId(COMPANION_TERMINAL_ID) + .setCompanionTerminalEid(COMPANION_TERMINAL_EID) + .build(); + + ManageSubscriptionResponse response = mTs43Operation.manageSubscription(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.subscriptionResult()).isEqualTo( + ManageSubscriptionResponse.SUBSCRIPTION_RESULT_CONTINUE_TO_WEBSHEET); + assertThat(response.subscriptionServiceUrl()).isEqualTo(new URL(SUBSCRIPTION_SERVICE_URL)); + assertThat(response.subscriptionServiceUserData()) + .isEqualTo(SUBSCRIPTION_SERVICE_USER_DATA); + } + + @Test + public void testManageSubscription_downloadProfile() throws Exception { + doReturn(MANAGE_SUBSCRIPTION_RESPONSE_DOWNLOAD_PROFILE).when(mMockHttpResponse).body(); + + ManageSubscriptionRequest request = ManageSubscriptionRequest.builder() + .setAppId(Ts43Constants.APP_ODSA_PRIMARY) + .setOperationType(EsimOdsaOperation.OPERATION_TYPE_SUBSCRIBE) + .setCompanionTerminalId(COMPANION_TERMINAL_ID) + .setCompanionTerminalEid(COMPANION_TERMINAL_EID) + .build(); + + ManageSubscriptionResponse response = mTs43Operation.manageSubscription(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.subscriptionResult()).isEqualTo( + ManageSubscriptionResponse.SUBSCRIPTION_RESULT_DOWNLOAD_PROFILE); + assertThat(response.downloadInfo().profileIccid()).isEqualTo(ICCID); + assertThat(response.downloadInfo().profileSmdpAddresses()) + .isEqualTo(ImmutableList.of(PROFILE_SMDP_ADDRESS)); + } + + @Test + public void testAcquireTemporaryToken() throws Exception { + doReturn(ACQUIRE_TEMPORARY_TOKEN_RESPONSE).when(mMockHttpResponse).body(); + + AcquireTemporaryTokenRequest request = AcquireTemporaryTokenRequest.builder() + .setAppId(Ts43Constants.APP_ODSA_PRIMARY) + .setOperationTargets(ImmutableList.of( + EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION, + EsimOdsaOperation.OPERATION_ACQUIRE_CONFIGURATION)) + .build(); + AcquireTemporaryTokenResponse response = mTs43Operation.acquireTemporaryToken(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.temporaryToken()).isEqualTo(TEMPORARY_TOKEN); + assertThat(response.temporaryTokenExpiry().toString()).isEqualTo(TEMPORARY_TOKEN_EXPIRY); + assertThat(response.operationTargets()).isEqualTo(ImmutableList.of( + EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION, + EsimOdsaOperation.OPERATION_ACQUIRE_CONFIGURATION)); + } + + @Test + public void testAcquireConfiguration() throws Exception { + doReturn(ACQUIRE_CONFIGURATION_RESPONSE).when(mMockHttpResponse).body(); + AcquireConfigurationRequest request = AcquireConfigurationRequest.builder() + .setAppId(Ts43Constants.APP_ODSA_PRIMARY) + .build(); + + AcquireConfigurationResponse response = mTs43Operation.acquireConfiguration(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.configurations()).hasSize(1); + AcquireConfigurationResponse.Configuration config = response.configurations().get(0); + assertThat(config.iccid()).isEqualTo(ICCID); + assertThat(config.downloadInfo().profileIccid()).isEqualTo(ICCID); + assertThat(config.downloadInfo().profileSmdpAddresses()).isEqualTo( + ImmutableList.of(PROFILE_SMDP_ADDRESS)); + assertThat(config.serviceStatus()).isEqualTo(EsimOdsaOperation.SERVICE_STATUS_ACTIVATED); + } + + @Test + public void testCheckEligibility() throws Exception { + doReturn(CHECK_ELIGIBILITY_RESPONSE).when(mMockHttpResponse).body(); + CheckEligibilityRequest request = CheckEligibilityRequest.builder() + .setAppId(Ts43Constants.APP_ODSA_PRIMARY) + .build(); + + CheckEligibilityResponse response = mTs43Operation.checkEligibility(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.appEligibility()).isEqualTo( + CheckEligibilityOperation.ELIGIBILITY_RESULT_ENABLED); + assertThat(response.companionDeviceServices()).containsExactly( + EsimOdsaOperation.COMPANION_SERVICE_SHARED_NUMBER); + assertThat(response.notEnabledUrl()).isEqualTo(new URL(NOT_ENABLED_URL)); + assertThat(response.notEnabledUserData()).isEqualTo(NOT_ENABLED_USER_DATA); + } + + @Test + public void testManageService() throws Exception { + doReturn(MANAGE_SERVICE_RESPONSE).when(mMockHttpResponse).body(); + ManageServiceRequest request = ManageServiceRequest.builder() + .setAppId(Ts43Constants.APP_ODSA_PRIMARY) + .build(); + + ManageServiceResponse response = mTs43Operation.manageService(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.serviceStatus()).isEqualTo( + EsimOdsaOperation.SERVICE_STATUS_DEACTIVATED); + } + + @Test + public void testGetPhoneNumber() throws Exception { + doReturn(GET_PHONE_NUMBER_RESPONSE).when(mMockHttpResponse).body(); + + GetPhoneNumberRequest request = GetPhoneNumberRequest.builder() + .setTerminalId(TERMINAL_ID) + .build(); + + GetPhoneNumberResponse response = mTs43Operation.getPhoneNumber(request); + assertThat(response.operationResult()).isEqualTo( + EsimOdsaOperation.OPERATION_RESULT_SUCCESS); + assertThat(response.msisdn()).isEqualTo(MSISDN); + } +} diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java index 8e331f3..8a4a185 100644 --- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java +++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java @@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -32,6 +33,8 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.expectThrows; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.net.Network; import android.telephony.TelephonyManager; @@ -45,6 +48,7 @@ import com.android.libraries.entitlement.ServiceEntitlementException; import com.android.libraries.entitlement.ServiceEntitlementRequest; import com.android.libraries.entitlement.http.HttpClient; import com.android.libraries.entitlement.http.HttpConstants.ContentType; +import com.android.libraries.entitlement.http.HttpConstants.RequestMethod; import com.android.libraries.entitlement.http.HttpRequest; import com.android.libraries.entitlement.http.HttpResponse; @@ -67,8 +71,11 @@ public class EapAkaApiTest { private static final String TEST_URL = "https://test.url/test-path"; private static final String EAP_AKA_CHALLENGE = "{\"eap-relay-packet\":\"" + EAP_AKA_CHALLENGE_REQUEST + "\"}"; + private static final String INVALID_EAP_AKA_CHALLENGE = + "{\"invalid-eap-relay-packet\":\"" + EAP_AKA_CHALLENGE_REQUEST + "\"}"; // com.google.common.net.HttpHeaders.COOKIE private static final String HTTP_HEADER_COOKIE = "Cookie"; + private static final String HTTP_HEADER_LOCATION = "Location"; private static final String COOKIE_VALUE = "COOKIE=abcdefg"; private static final String COOKIE_VALUE_1 = "COOKIE=hijklmn"; private static final String RESPONSE_XML = @@ -96,9 +103,21 @@ public class EapAkaApiTest { private static final String ACCEPT_CONTENT_TYPE_JSON_AND_XML = "application/vnd.gsma.eap-relay.v1.0+json, text/vnd.wap.connectivity-xml"; private static final String BYPASS_EAP_AKA_RESPONSE = "abc"; + private static final String VENDOR = "VEND"; + private static final String MODEL = "MODEL"; + private static final String SW_VERSION = "SW_VERSION"; + private static final String LONG_VENDOR = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + private static final String LONG_MODEL = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + private static final String LONG_SW_VERSION = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + private static final String LONG_VENDOR_TRIMMED = "aaaa"; + private static final String LONG_MODEL_TRIMMED = "aaaaaaaaaa"; + private static final String LONG_SW_VERSION_TRIMMED = "aaaaaaaaaaaaaaaaaaaa"; + private static final String APP_VERSION = "APP_VERSION"; @Rule public final MockitoRule rule = MockitoJUnit.rule(); + @Mock private PackageManager mMockPackageManager; + @Mock private PackageInfo mMockPackageInfo; @Mock private HttpClient mMockHttpClient; @Mock private Network mMockNetwork; @Mock private TelephonyManager mMockTelephonyManager; @@ -110,23 +129,28 @@ public class EapAkaApiTest { private EapAkaApi mEapAkaApiBypassAuthentication; @Before - public void setUp() { + public void setUp() throws Exception { mContext = spy(ApplicationProvider.getApplicationContext()); - mEapAkaApi = new EapAkaApi(mContext, SUB_ID, mMockHttpClient, ""); - mEapAkaApiBypassAuthentication = - new EapAkaApi(mContext, SUB_ID, mMockHttpClient, BYPASS_EAP_AKA_RESPONSE); - when(mContext.getSystemService(TelephonyManager.class)) - .thenReturn(mMockTelephonyManager); + when(mContext.getPackageManager()).thenReturn(mMockPackageManager); + mMockPackageInfo.versionName = APP_VERSION; + when(mMockPackageManager.getPackageInfo(anyString(), anyInt())) + .thenReturn(mMockPackageInfo); + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mMockTelephonyManager); when(mMockTelephonyManager.createForSubscriptionId(SUB_ID)) .thenReturn(mMockTelephonyManagerForSubId); when(mMockTelephonyManagerForSubId.getSubscriberId()).thenReturn(IMSI); when(mMockTelephonyManagerForSubId.getSimOperator()).thenReturn(MCCMNC); + mEapAkaApi = new EapAkaApi(mContext, SUB_ID, mMockHttpClient, ""); + mEapAkaApiBypassAuthentication = + new EapAkaApi(mContext, SUB_ID, mMockHttpClient, BYPASS_EAP_AKA_RESPONSE); } @Test public void queryEntitlementStatus_hasAuthenticationToken() throws Exception { HttpResponse httpResponse = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); when(mMockHttpClient.request(any())).thenReturn(httpResponse); CarrierConfig carrierConfig = @@ -134,62 +158,331 @@ public class EapAkaApiTest { ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().setAuthenticationToken(TOKEN).build(); - String response = + HttpResponse response = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(response).isEqualTo(httpResponse); + verify(mMockHttpClient).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getValue().timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getValue().network()).isEqualTo(mMockNetwork); + assertThat(mHttpRequestCaptor.getValue().requestMethod()).isEqualTo(RequestMethod.GET); + } + + @Test + public void queryEntitlementStatus_hasAuthenticationToken_useHttpPost() throws Exception { + HttpResponse httpResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())).thenReturn(httpResponse); + CarrierConfig carrierConfig = + CarrierConfig.builder() + .setServerUrl(TEST_URL) + .setNetwork(mMockNetwork) + .setUseHttpPost(true) + .build(); + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder().setAuthenticationToken(TOKEN).build(); + + HttpResponse response = mEapAkaApi.queryEntitlementStatus( ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); - assertThat(response).isEqualTo(RESPONSE_XML); + assertThat(response).isEqualTo(httpResponse); verify(mMockHttpClient).request(mHttpRequestCaptor.capture()); assertThat(mHttpRequestCaptor.getValue().timeoutInSec()) .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); assertThat(mHttpRequestCaptor.getValue().network()).isEqualTo(mMockNetwork); + assertThat(mHttpRequestCaptor.getValue().requestMethod()).isEqualTo(RequestMethod.POST); } @Test public void queryEntitlementStatus_noAuthenticationToken() throws Exception { when(mMockTelephonyManagerForSubId.getIccAuthentication( - TelephonyManager.APPTYPE_USIM, - TelephonyManager.AUTHTYPE_EAP_AKA, - EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); HttpResponse eapChallengeResponse = - HttpResponse - .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) - .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)).build(); + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); HttpResponse xmlResponse = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); when(mMockHttpClient.request(any())) - .thenReturn(eapChallengeResponse).thenReturn(xmlResponse); + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); - String respopnse = + HttpResponse response = mEapAkaApi.queryEntitlementStatus( ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); - assertThat(respopnse).isEqualTo(RESPONSE_XML); + assertThat(response).isEqualTo(xmlResponse); + verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.GET); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestMethod()) + .isEqualTo(RequestMethod.POST); // Verify that the 2nd request has cookies set by the 1st response + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsAtLeast( + HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_useHttpPost() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = + CarrierConfig.builder().setServerUrl(TEST_URL).setUseHttpPost(true).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + HttpResponse response = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(response).isEqualTo(xmlResponse); verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.POST); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestMethod()) + .isEqualTo(RequestMethod.POST); + // Verify that the 2nd request has cookies set by the 1st response + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsAtLeast( + HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_invalidChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(INVALID_EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getMessage()) + .isEqualTo("Failed to parse EAP-AKA challenge: " + INVALID_EAP_AKA_CHALLENGE); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_secondChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + HttpResponse response = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(response).isEqualTo(xmlResponse); + // Verify that the subsequent requests have cookies set by the 1st response + verify(mMockHttpClient, times(3)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsAtLeast( + HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(2).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(2).network()).isNull(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_thirdChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + HttpResponse response = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(response).isEqualTo(xmlResponse); + // Verify that the subsequent requests have cookies set by the 1st response + verify(mMockHttpClient, times(4)).request(mHttpRequestCaptor.capture()); assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) - .containsAtLeast(HTTP_HEADER_COOKIE, COOKIE_VALUE, - HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + .containsAtLeast( + HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(2).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(2).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(3).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(3).network()).isNull(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_fourthChallenge_throwException() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_EAP_AKA_FAILURE); + assertThat(exception.getMessage()).isEqualTo("Unable to EAP-AKA authenticate"); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); } @Test public void queryEntitlementStatus_hasAuthenticationToken_multipleAppIds() throws Exception { HttpResponse response = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); when(mMockHttpClient.request(any())).thenReturn(response); - ImmutableList<String> appIds = ImmutableList.of(ServiceEntitlement.APP_VOWIFI, - ServiceEntitlement.APP_VOLTE); + ImmutableList<String> appIds = + ImmutableList.of(ServiceEntitlement.APP_VOWIFI, ServiceEntitlement.APP_VOLTE); CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).setTimeoutInSec(70).build(); ServiceEntitlementRequest request = @@ -210,19 +503,20 @@ public class EapAkaApiTest { HttpResponse eapChallengeResponse = HttpResponse.builder().setContentType(ContentType.JSON).build(); when(mMockHttpClient.request(any())).thenReturn(eapChallengeResponse); - CarrierConfig carrierConfig = - CarrierConfig.builder().setServerUrl(TEST_URL).build(); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); - ServiceEntitlementException exception = expectThrows( - ServiceEntitlementException.class, - () -> mEapAkaApi.queryEntitlementStatus( - ImmutableList.of(ServiceEntitlement.APP_VOWIFI), - carrierConfig, - request)); + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); - assertThat(exception.getErrorCode()).isEqualTo( - ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); assertThat(exception.getMessage()).isEqualTo("Failed to parse json object"); assertThat(exception.getCause()).isInstanceOf(JSONException.class); assertThat(exception.getHttpStatus()).isEqualTo(0); @@ -233,17 +527,21 @@ public class EapAkaApiTest { public void queryEntitlementStatus_noAuthenticationToken_handleEapAkaSyncFailure() throws Exception { when(mMockTelephonyManagerForSubId.getIccAuthentication( - TelephonyManager.APPTYPE_USIM, - TelephonyManager.AUTHTYPE_EAP_AKA, - EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); HttpResponse eapChallengeResponse = - HttpResponse - .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) - .setCookies(ImmutableList.of(COOKIE_VALUE)).build(); + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); HttpResponse xmlResponse = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); when(mMockHttpClient.request(any())) .thenReturn(eapChallengeResponse) @@ -252,11 +550,11 @@ public class EapAkaApiTest { CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); - String response = + HttpResponse response = mEapAkaApi.queryEntitlementStatus( ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); - assertThat(response).isEqualTo(RESPONSE_XML); + assertThat(response).isEqualTo(xmlResponse); // Verify that the 2nd/3rd request has cookie set by the 1st/2nd response verify(mMockHttpClient, times(3)).request(mHttpRequestCaptor.capture()); assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) @@ -266,30 +564,123 @@ public class EapAkaApiTest { } @Test + public void queryEntitlementStatus_noAuthenticationToken_eapAkaSyncFailure_invalidChallenge() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + HttpResponse invalidEapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(INVALID_EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(invalidEapChallengeResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getMessage()) + .isEqualTo("Failed to parse EAP-AKA challenge: " + INVALID_EAP_AKA_CHALLENGE); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_fourthEapAkaSyncFailure() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE); + assertThat(exception.getMessage()) + .isEqualTo("Unable to recover from EAP-AKA synchroinization failure"); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test public void queryEntitlementStatus_hasNoAuthenticationToken_bypassAuthentication() throws Exception { HttpResponse eapChallengeResponse = - HttpResponse - .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) - .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)).build(); + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); HttpResponse xmlResponse = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); when(mMockHttpClient.request(any())) - .thenReturn(eapChallengeResponse).thenReturn(xmlResponse); + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); - String respopnse = + HttpResponse response = mEapAkaApiBypassAuthentication.queryEntitlementStatus( ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); - assertThat(respopnse).isEqualTo(RESPONSE_XML); + assertThat(response).isEqualTo(xmlResponse); // Verify that the 2nd request has cookies set by the 1st response verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) - .containsAtLeast(HTTP_HEADER_COOKIE, COOKIE_VALUE, - HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + .containsAtLeast( + HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); @@ -314,8 +705,7 @@ public class EapAkaApiTest { when(mMockHttpClient.request(any())).thenReturn(response); CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = - ServiceEntitlementRequest - .builder() + ServiceEntitlementRequest.builder() .setAuthenticationToken(TOKEN) .setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_XML) .build(); @@ -335,10 +725,7 @@ public class EapAkaApiTest { when(mMockHttpClient.request(any())).thenReturn(response); CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = - ServiceEntitlementRequest - .builder() - .setAuthenticationToken(TOKEN) - .build(); + ServiceEntitlementRequest.builder().setAuthenticationToken(TOKEN).build(); mEapAkaApi.queryEntitlementStatus( ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); @@ -349,53 +736,392 @@ public class EapAkaApiTest { } @Test + public void queryEntitlementStatus_terminalVendorModelSWVersionTrimmed() throws Exception { + CarrierConfig carrierConfig = + CarrierConfig.builder() + .setServerUrl(TEST_URL) + .setClientTs43(CarrierConfig.CLIENT_TS_43_IMS_ENTITLEMENT) + .build(); + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder() + .setAuthenticationToken(TOKEN) + .setTerminalVendor(LONG_VENDOR) + .setTerminalModel(LONG_MODEL) + .setTerminalSoftwareVersion(LONG_SW_VERSION) + .build(); + + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + verify(mMockHttpClient).request(mHttpRequestCaptor.capture()); + String urlParams = + String.format( + "terminal_vendor=%s&terminal_model=%s&terminal_sw_version=%s", + LONG_VENDOR_TRIMMED, LONG_MODEL_TRIMMED, LONG_SW_VERSION_TRIMMED); + assertThat(mHttpRequestCaptor.getValue().url()).contains(urlParams); + } + + @Test + public void queryEntitlementStatus_userAgentSet() throws Exception { + CarrierConfig carrierConfig = + CarrierConfig.builder() + .setServerUrl(TEST_URL) + .setClientTs43(CarrierConfig.CLIENT_TS_43_IMS_ENTITLEMENT) + .build(); + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder() + .setAuthenticationToken(TOKEN) + .setTerminalVendor(VENDOR) + .setTerminalModel(MODEL) + .setTerminalSoftwareVersion(SW_VERSION) + .build(); + + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + verify(mMockHttpClient).request(mHttpRequestCaptor.capture()); + String userAgent = + String.format( + "PRD-TS43 term-%s/%s %s/%s OS-Android/%s", + VENDOR, MODEL, carrierConfig.clientTs43(), APP_VERSION, SW_VERSION); + assertThat( + mHttpRequestCaptor + .getValue() + .requestProperties() + .get(HttpHeaders.USER_AGENT) + .get(0)) + .isEqualTo(userAgent); + } + + @Test + public void queryEntitlementStatus_userAgentSet_duringEapAka() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = + CarrierConfig.builder() + .setServerUrl(TEST_URL) + .setClientTs43(CarrierConfig.CLIENT_TS_43_IMS_ENTITLEMENT) + .build(); + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder() + .setTerminalVendor(VENDOR) + .setTerminalModel(MODEL) + .setTerminalSoftwareVersion(SW_VERSION) + .build(); + + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + String userAgent = + String.format( + "PRD-TS43 term-%s/%s %s/%s OS-Android/%s", + VENDOR, MODEL, carrierConfig.clientTs43(), APP_VERSION, SW_VERSION); + assertThat( + mHttpRequestCaptor + .getAllValues() + .get(0) + .requestProperties() + .get(HttpHeaders.USER_AGENT) + .get(0)) + .isEqualTo(userAgent); + assertThat( + mHttpRequestCaptor + .getAllValues() + .get(1) + .requestProperties() + .get(HttpHeaders.USER_AGENT) + .get(0)) + .isEqualTo(userAgent); + } + + @Test + public void queryEntitlementStatus_userAgentTrimmed() throws Exception { + CarrierConfig carrierConfig = + CarrierConfig.builder() + .setServerUrl(TEST_URL) + .setClientTs43(CarrierConfig.CLIENT_TS_43_IMS_ENTITLEMENT) + .build(); + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder() + .setAuthenticationToken(TOKEN) + .setTerminalVendor(LONG_VENDOR) + .setTerminalModel(LONG_MODEL) + .setTerminalSoftwareVersion(LONG_SW_VERSION) + .build(); + + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + verify(mMockHttpClient).request(mHttpRequestCaptor.capture()); + String userAgent = + String.format( + "PRD-TS43 term-%s/%s %s/%s OS-Android/%s", + LONG_VENDOR_TRIMMED, + LONG_MODEL_TRIMMED, + carrierConfig.clientTs43(), + APP_VERSION, + LONG_SW_VERSION_TRIMMED); + assertThat( + mHttpRequestCaptor + .getValue() + .requestProperties() + .get(HttpHeaders.USER_AGENT) + .get(0)) + .isEqualTo(userAgent); + } + + @Test public void performEsimOdsaOperation_noAuthenticationToken_returnsResult() throws Exception { when(mMockTelephonyManagerForSubId.getIccAuthentication( - TelephonyManager.APPTYPE_USIM, - TelephonyManager.AUTHTYPE_EAP_AKA, - EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); HttpResponse eapChallengeResponse = - HttpResponse - .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) - .setCookies(ImmutableList.of(COOKIE_VALUE)).build(); + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); HttpResponse xmlResponse = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); when(mMockHttpClient.request(any())) - .thenReturn(eapChallengeResponse).thenReturn(xmlResponse); + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); EsimOdsaOperation operation = EsimOdsaOperation.builder().build(); - String response = - mEapAkaApi.performEsimOdsaOperation(ServiceEntitlement.APP_ODSA_COMPANION, - carrierConfig, request, operation); + HttpResponse response = + mEapAkaApi.performEsimOdsaOperation( + ServiceEntitlement.APP_ODSA_COMPANION, carrierConfig, request, operation); + + assertThat(response).isEqualTo(xmlResponse); + verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.GET); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestMethod()) + .isEqualTo(RequestMethod.POST); + } - assertThat(response).isEqualTo(RESPONSE_XML); - verify(mMockHttpClient, times(2)).request(any()); + @Test + public void performEsimOdsaOperation_noAuthenticationToken_useHttpPost_returnsResult() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = + CarrierConfig.builder().setServerUrl(TEST_URL).setUseHttpPost(true).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + EsimOdsaOperation operation = EsimOdsaOperation.builder().build(); + + HttpResponse response = + mEapAkaApi.performEsimOdsaOperation( + ServiceEntitlement.APP_ODSA_COMPANION, carrierConfig, request, operation); + + assertThat(response).isEqualTo(xmlResponse); + verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.POST); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestMethod()) + .isEqualTo(RequestMethod.POST); } @Test public void performEsimOdsaOperation_manageSubscription_returnsResult() throws Exception { - HttpResponse httpResponse = - HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) .build(); - when(mMockHttpClient.request(any())).thenReturn(httpResponse); + when(mMockHttpClient.request(any())).thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = + ServiceEntitlementRequest.builder().setAuthenticationToken(TOKEN).build(); + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION) + .setOperationType(EsimOdsaOperation.OPERATION_TYPE_SUBSCRIBE) + .build(); + + HttpResponse response = + mEapAkaApi.performEsimOdsaOperation( + ServiceEntitlement.APP_ODSA_COMPANION, carrierConfig, request, operation); + + assertThat(response).isEqualTo(xmlResponse); + verify(mMockHttpClient, times(1)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.GET); + } + + @Test + public void performEsimOdsaOperation_manageSubscription_useHttpPost_returnsResult() + throws Exception { + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())).thenReturn(xmlResponse); CarrierConfig carrierConfig = - CarrierConfig.builder().setServerUrl(TEST_URL).build(); + CarrierConfig.builder().setServerUrl(TEST_URL).setUseHttpPost(true).build(); ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().setAuthenticationToken(TOKEN).build(); - EsimOdsaOperation operation = EsimOdsaOperation.builder() - .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION) - .setOperationType(EsimOdsaOperation.OPERATION_TYPE_SUBSCRIBE) - .build(); + EsimOdsaOperation operation = + EsimOdsaOperation.builder() + .setOperation(EsimOdsaOperation.OPERATION_MANAGE_SUBSCRIPTION) + .setOperationType(EsimOdsaOperation.OPERATION_TYPE_SUBSCRIBE) + .build(); + + HttpResponse response = + mEapAkaApi.performEsimOdsaOperation( + ServiceEntitlement.APP_ODSA_COMPANION, carrierConfig, request, operation); + + assertThat(response).isEqualTo(xmlResponse); + verify(mMockHttpClient, times(1)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.POST); + } + + @Test + public void performEsimOdsaOperation_noAuthenticationToken_invalidChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(INVALID_EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + EsimOdsaOperation operation = EsimOdsaOperation.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.performEsimOdsaOperation( + ServiceEntitlement.APP_ODSA_COMPANION, + carrierConfig, + request, + operation)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getMessage()) + .isEqualTo("Failed to parse EAP-AKA challenge: " + INVALID_EAP_AKA_CHALLENGE); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } - String response = - mEapAkaApi.performEsimOdsaOperation(ServiceEntitlement.APP_ODSA_COMPANION, - carrierConfig, request, operation); + @Test + public void acquireOidcAuthenticationEndpoint() throws Exception { + HttpResponse response = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setLocation(HTTP_HEADER_LOCATION) + .build(); + when(mMockHttpClient.request(any())).thenReturn(response); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + String endpoint = + mEapAkaApi.acquireOidcAuthenticationEndpoint( + ServiceEntitlement.APP_ODSA_COMPANION, carrierConfig, request); + + assertThat(endpoint).isEqualTo(HTTP_HEADER_LOCATION); + verify(mMockHttpClient, times(1)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.GET); + } + + @Test + public void acquireOidcAuthenticationEndpoint_useHttpPost() throws Exception { + HttpResponse response = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setLocation(HTTP_HEADER_LOCATION) + .build(); + when(mMockHttpClient.request(any())).thenReturn(response); + CarrierConfig carrierConfig = + CarrierConfig.builder().setServerUrl(TEST_URL).setUseHttpPost(true).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + String endpoint = + mEapAkaApi.acquireOidcAuthenticationEndpoint( + ServiceEntitlement.APP_ODSA_COMPANION, carrierConfig, request); + + assertThat(endpoint).isEqualTo(HTTP_HEADER_LOCATION); + verify(mMockHttpClient, times(1)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(0).requestMethod()) + .isEqualTo(RequestMethod.POST); + } + + @Test + public void queryEntitlementStatusFromOidc() throws Exception { + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())).thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + HttpResponse response = + mEapAkaApi.queryEntitlementStatusFromOidc(TEST_URL, carrierConfig, request); - assertThat(response).isEqualTo(RESPONSE_XML); + assertThat(response).isEqualTo(xmlResponse); verify(mMockHttpClient, times(1)).request(any()); } } |