Snap for 10453563 from 1a392f75763d5679f669b26d97269f275491e79e to mainline-adservices-release

Change-Id: Ie7d93493642e83a60219a0ea5886250cd1cc0287
diff --git a/Android.bp b/Android.bp
index b989425..dbedea3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -96,6 +96,9 @@
         "java/com/android/libraries/entitlement/ServiceEntitlementException.java",
         "java/com/android/libraries/entitlement/ServiceEntitlementRequest.java",
     ],
+    static_libs: [
+        "guava",
+    ],
     apex_available: [
         "//apex_available:platform",
         "com.android.wifi",
diff --git a/OWNERS b/OWNERS
index a167e8d..34b7bde 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,3 @@
 mewan@google.com
-samalin@google.com
-danielwbhuang@google.com
+kiwonp@google.com
+akaustubh@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ec047b7..2f98cca 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -3,10 +3,5 @@
     {
       "name": "service-entitlement-tests"
     }
-  ],
-  "postsubmit": [
-    {
-      "name": "service-entitlement-tests"
-    }
   ]
 }
diff --git a/java/com/android/libraries/entitlement/EsimOdsaOperation.java b/java/com/android/libraries/entitlement/EsimOdsaOperation.java
index 20f4fa3..9a3eae6 100644
--- a/java/com/android/libraries/entitlement/EsimOdsaOperation.java
+++ b/java/com/android/libraries/entitlement/EsimOdsaOperation.java
@@ -17,6 +17,7 @@
 package com.android.libraries.entitlement;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 
 /**
  * HTTP request parameters specific to on device service actiavation (ODSA). See GSMA spec TS.43
@@ -40,6 +41,10 @@
      * OSDA operation: AcquireConfiguration.
      */
     public static final String OPERATION_ACQUIRE_CONFIGURATION = "AcquireConfiguration";
+    /**
+     * OSDA operation: AcquireTemporaryToken.
+     */
+    public static final String OPERATION_ACQUIRE_TEMPORARY_TOKEN = "AcquireTemporaryToken";
 
     /**
      * Indicates that operation_type is not set.
@@ -97,6 +102,12 @@
     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".
+     */
+    public abstract ImmutableList<String> operationTargets();
+
+    /**
      * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter
      * "companion_terminal_id".
      */
@@ -170,6 +181,18 @@
      */
     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".
+     */
+    public abstract String oldTerminalId();
+
+    /**
+     * Returns the ICCID of old device eSIM. Used by HTTP parameter "old_terminal_iccid".
+     */
+    public abstract String oldTerminalIccid();
+
     /**
      * Returns a new {@link Builder} object.
      */
@@ -177,6 +200,7 @@
         return new AutoValue_EsimOdsaOperation.Builder()
                 .setOperation("")
                 .setOperationType(OPERATION_TYPE_NOT_SET)
+                .setOperationTargets(ImmutableList.of())
                 .setCompanionTerminalId("")
                 .setCompanionTerminalVendor("")
                 .setCompanionTerminalModel("")
@@ -189,7 +213,9 @@
                 .setTerminalEid("")
                 .setTargetTerminalId("")
                 .setTargetTerminalIccid("")
-                .setTargetTerminalEid("");
+                .setTargetTerminalEid("")
+                .setOldTerminalId("")
+                .setOldTerminalIccid("");
     }
 
     /**
@@ -230,6 +256,12 @@
         public abstract Builder setOperationType(int value);
 
         /**
+         * Sets the operation targets to be used with temporary token from AcquireTemporaryToken
+         * operation. Used by HTTP parameter "operation_targets" if set.
+         */
+        public abstract Builder setOperationTargets(ImmutableList<String> value);
+
+        /**
          * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter
          * "companion_terminal_id" if set.
          *
@@ -336,6 +368,21 @@
          */
         public abstract Builder setTargetTerminalEid(String value);
 
+        /**
+         * 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.
+         *
+         * <p>Used by primary device ODSA operation.
+         */
+        public abstract Builder setOldTerminalId(String value);
+
+        /**
+         * Sets the ICCID old device eSIM. Used by HTTP parameter "old_terminal_iccid" if set.
+         *
+         * <p>Used by primary device ODSA operation.
+         */
+        public abstract Builder setOldTerminalIccid(String value);
+
         public abstract EsimOdsaOperation build();
     }
 }
diff --git a/java/com/android/libraries/entitlement/ServiceEntitlement.java b/java/com/android/libraries/entitlement/ServiceEntitlement.java
index d723e4c..c0d6d55 100644
--- a/java/com/android/libraries/entitlement/ServiceEntitlement.java
+++ b/java/com/android/libraries/entitlement/ServiceEntitlement.java
@@ -25,6 +25,8 @@
 
 import com.google.common.collect.ImmutableList;
 
+import java.util.List;
+
 /**
  * Implemnets protocol for carrier service entitlement configuration query and operation, based on
  * GSMA TS.43 spec.
@@ -50,6 +52,10 @@
      * App ID for on device service activation (OSDA) 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";
 
     private final CarrierConfig carrierConfig;
     private final EapAkaApi eapAkaApi;
@@ -67,8 +73,63 @@
      *                          for how to get the subscroption ID.
      */
     public ServiceEntitlement(Context context, CarrierConfig carrierConfig, int simSubscriptionId) {
+        this(
+                context,
+                carrierConfig,
+                simSubscriptionId,
+                /* saveHttpHistory= */ false,
+                /* bypassEapAkaResponse= */ "");
+    }
+
+    /**
+     * 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
+     *     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.
+     * @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.
+     */
+    public ServiceEntitlement(
+            Context context,
+            CarrierConfig carrierConfig,
+            int simSubscriptionId,
+            boolean saveHttpHistory) {
+        this(
+                context,
+                carrierConfig,
+                simSubscriptionId,
+                saveHttpHistory,
+                /* bypassEapAkaResponse= */ "");
+    }
+
+    /**
+     * 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
+     *     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.
+     * @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.
+     *     The client will accept any challenge from the server and return this string as a
+     *     response. Must not be {@code null}. Intended for testing.
+     */
+    public ServiceEntitlement(
+            Context context,
+            CarrierConfig carrierConfig,
+            int simSubscriptionId,
+            boolean saveHttpHistory,
+            String bypassEapAkaResponse) {
         this.carrierConfig = carrierConfig;
-        this.eapAkaApi = new EapAkaApi(context, simSubscriptionId);
+        this.eapAkaApi =
+                new EapAkaApi(context, simSubscriptionId, saveHttpHistory, bypassEapAkaResponse);
     }
 
     @VisibleForTesting
@@ -151,4 +212,19 @@
             throws ServiceEntitlementException {
         return eapAkaApi.performEsimOdsaOperation(appId, carrierConfig, request, operation);
     }
+
+    /**
+     * Retrieves the history of past HTTP request and responses if {@code saveHttpHistory} was set
+     * in constructor.
+     */
+    public List<String> getHistory() {
+        return eapAkaApi.getHistory();
+    }
+
+    /**
+     * Clears the history of past HTTP request and responses.
+     */
+    public void clearHistory() {
+        eapAkaApi.clearHistory();
+    }
 }
diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementException.java b/java/com/android/libraries/entitlement/ServiceEntitlementException.java
index 45b1b9b..b1cb50f 100644
--- a/java/com/android/libraries/entitlement/ServiceEntitlementException.java
+++ b/java/com/android/libraries/entitlement/ServiceEntitlementException.java
@@ -43,6 +43,11 @@
      * 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 = 21;
 
     // HTTP related failures
     /**
diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java
index c7b0ad3..e0ecbf7 100644
--- a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java
+++ b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java
@@ -62,6 +62,11 @@
     public abstract String authenticationToken();
 
     /**
+     * Returns the temporary token. Used by HTTP parameter "temporary_token".
+     */
+    public abstract String temporaryToken();
+
+    /**
      * Returns the unique identifier of the device like IMEI. Used by HTTP parameter "terminal_id".
      */
     public abstract String terminalId();
@@ -118,6 +123,11 @@
     public abstract String acceptContentType();
 
     /**
+     * Returns the boost type for premium network. Used for premium network slice entitlement.
+     */
+    public abstract String boostType();
+
+    /**
      * Returns a new {@link Builder} object.
      */
     public static Builder builder() {
@@ -125,6 +135,7 @@
                 .setConfigurationVersion(DEFAULT_CONFIGURATION_VERSION)
                 .setEntitlementVersion(DEFAULT_ENTITLEMENT_VERSION)
                 .setAuthenticationToken("")
+                .setTemporaryToken("")
                 .setTerminalId("")
                 .setTerminalVendor(Build.MANUFACTURER)
                 .setTerminalModel(Build.MODEL)
@@ -133,7 +144,8 @@
                 .setAppVersion("")
                 .setNotificationToken("")
                 .setNotificationAction(NOTICATION_ACTION_ENABLE_FCM)
-                .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML);
+                .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML)
+                .setBoostType("");
     }
 
     /**
@@ -167,6 +179,13 @@
         public abstract Builder setAuthenticationToken(String value);
 
         /**
+         * Sets the temporary token. Used by HTTP parameter "temporary_token".
+         *
+         * <p>Optional.
+         */
+        public abstract Builder setTemporaryToken(String value);
+
+        /**
          * Sets the unique identifier of the device like IMEI. Used by HTTP parameter
          * "terminal_id".
          *
@@ -243,6 +262,14 @@
          */
         public abstract Builder setAcceptContentType(String contentType);
 
+        /**
+         * 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 setBoostType(String value);
+
         public abstract ServiceEntitlementRequest build();
     }
 }
diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java
index 4482bf7..be41ca7 100644
--- a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java
+++ b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java
@@ -16,6 +16,7 @@
 
 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_MALFORMED_HTTP_RESPONSE;
 
@@ -33,6 +34,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;
@@ -43,6 +45,8 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.List;
+
 public class EapAkaApi {
     private static final String TAG = "ServiceEntitlement";
 
@@ -58,6 +62,7 @@
     private static final String EAP_ID = "EAP_ID";
     private static final String IMSI = "IMSI";
     private static final String TOKEN = "token";
+    private static final String TEMPORARY_TOKEN = "temporary_token";
     private static final String NOTIF_ACTION = "notif_action";
     private static final String NOTIF_TOKEN = "notif_token";
     private static final String APP_VERSION = "app_version";
@@ -65,6 +70,7 @@
 
     private static final String OPERATION = "operation";
     private static final String OPERATION_TYPE = "operation_type";
+    private static final String OPERATION_TARGETS = "operation_targets";
     private static final String COMPANION_TERMINAL_ID = "companion_terminal_id";
     private static final String COMPANION_TERMINAL_VENDOR = "companion_terminal_vendor";
     private static final String COMPANION_TERMINAL_MODEL = "companion_terminal_model";
@@ -82,22 +88,38 @@
     private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid";
     private static final String TARGET_TERMINAL_EID = "target_terminal_eid";
 
-    // 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;
+    private static final String OLD_TERMINAL_ID = "old_terminal_id";
+    private static final String OLD_TERMINAL_ICCID = "old_terminal_iccid";
+
+    private static final String BOOST_TYPE = "boost_type";
+
+    // 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;
 
     private final Context mContext;
     private final int mSimSubscriptionId;
     private final HttpClient mHttpClient;
+    private final String mBypassEapAkaResponse;
 
-    public EapAkaApi(Context context, int simSubscriptionId) {
-        this(context, simSubscriptionId, new HttpClient());
+    public EapAkaApi(
+            Context context,
+            int simSubscriptionId,
+            boolean saveHistory,
+            String bypassEapAkaResponse) {
+        this(context, simSubscriptionId, new HttpClient(saveHistory), bypassEapAkaResponse);
     }
 
     @VisibleForTesting
-    EapAkaApi(Context context, int simSubscriptionId, HttpClient httpClient) {
+    EapAkaApi(
+            Context context,
+            int simSubscriptionId,
+            HttpClient httpClient,
+            String bypassEapAkaResponse) {
         this.mContext = context;
         this.mSimSubscriptionId = simSubscriptionId;
         this.mHttpClient = httpClient;
+        this.mBypassEapAkaResponse = bypassEapAkaResponse;
     }
 
     /**
@@ -126,10 +148,17 @@
                         urlBuilder.toString(),
                         carrierConfig,
                         ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
+            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,
+                    eapAkaChallenge,
+                    challengeResponse.cookies(),
+                    MAX_EAP_AKA_ATTEMPTS,
                     request.acceptContentType())
                     .body();
         }
@@ -139,55 +168,79 @@
      * 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
      */
     private HttpResponse respondToEapAkaChallenge(
             CarrierConfig carrierConfig,
-            HttpResponse response,
-            int followSyncFailureCount,
+            String eapAkaChallenge,
+            ImmutableList<String> cookies,
+            int remainingAttempts,
             String contentType)
             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, cookies, contentType);
         }
+
         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(
-                            eapAkaResponse.response(),
-                            carrierConfig,
-                            response.cookies(),
-                            contentType);
+        // This could be a successful authentication, another challenge, or synchronization failure.
+        if (eapAkaResponse.response() != null) {
+            HttpResponse response =
+                    challengeResponse(
+                            eapAkaResponse.response(), carrierConfig, cookies, contentType);
+            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,
+                        contentType);
+            } 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(),
+                            cookies,
                             ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
-            if (followSyncFailureCount > 0) {
+            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,
+                        contentType);
             } else {
                 throw new ServiceEntitlementException(
                         ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE,
@@ -241,7 +294,8 @@
         appendParametersForServiceEntitlementRequest(urlBuilder, ImmutableList.of(appId), request);
         appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation);
 
-        if (!TextUtils.isEmpty(request.authenticationToken())) {
+        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(
@@ -254,10 +308,17 @@
                             urlBuilder.toString(),
                             carrierConfig,
                             ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
+            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,
+                    eapAkaChallenge,
+                    challengeResponse.cookies(),
+                    MAX_EAP_AKA_ATTEMPTS,
                     request.acceptContentType())
                     .body();
         }
@@ -268,17 +329,20 @@
             ServiceEntitlementRequest request) {
         TelephonyManager telephonyManager = mContext.getSystemService(
                 TelephonyManager.class).createForSubscriptionId(mSimSubscriptionId);
-        if (TextUtils.isEmpty(request.authenticationToken())) {
+        if (!TextUtils.isEmpty(request.authenticationToken())) {
+            // IMSI and token required for fast AuthN.
+            urlBuilder
+                    .appendQueryParameter(IMSI, telephonyManager.getSubscriberId())
+                    .appendQueryParameter(TOKEN, request.authenticationToken());
+        } else if (!TextUtils.isEmpty(request.temporaryToken())) {
+            // temporary_token required for fast AuthN.
+            urlBuilder.appendQueryParameter(TEMPORARY_TOKEN, request.temporaryToken());
+        } else {
             // EAP_ID required for initial AuthN
             urlBuilder.appendQueryParameter(
                     EAP_ID,
                     getImsiEap(telephonyManager.getSimOperator(),
                             telephonyManager.getSubscriberId()));
-        } else {
-            // IMSI and token required for fast AuthN.
-            urlBuilder
-                    .appendQueryParameter(IMSI, telephonyManager.getSubscriberId())
-                    .appendQueryParameter(TOKEN, request.authenticationToken());
         }
 
         if (!TextUtils.isEmpty(request.notificationToken())) {
@@ -298,6 +362,7 @@
         // Optional query parameters, append them if not empty
         appendOptionalQueryParameter(urlBuilder, APP_VERSION, request.appVersion());
         appendOptionalQueryParameter(urlBuilder, APP_NAME, request.appName());
+        appendOptionalQueryParameter(urlBuilder, BOOST_TYPE, request.boostType());
 
         for (String appId : appIds) {
             urlBuilder.appendQueryParameter(APP, appId);
@@ -320,6 +385,10 @@
             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,
@@ -345,6 +414,10 @@
                 odsaOperation.targetTerminalIccid());
         appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_EID,
                 odsaOperation.targetTerminalEid());
+        appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ICCID,
+                odsaOperation.oldTerminalIccid());
+        appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ID,
+                odsaOperation.oldTerminalId());
     }
 
     private HttpResponse httpGet(String url, CarrierConfig carrierConfig, String contentType)
@@ -366,6 +439,30 @@
         }
     }
 
+    @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) {
+            // TODO: possibly support parsing eap-relay-packet in XML format
+            return null;
+        } else {
+            throw new ServiceEntitlementException(
+                    ERROR_MALFORMED_HTTP_RESPONSE, "Unknown HTTP content type");
+        }
+        return eapAkaChallenge;
+    }
+
     /**
      * Returns the IMSI EAP value. The resulting realm part of the Root NAI in 3GPP TS 23.003 clause
      * 19.3.2 will be in the form:
@@ -385,4 +482,18 @@
         }
         return "0" + imsi + "@nai.epc.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org";
     }
+
+    /**
+     * Retrieves the history of past HTTP request and responses.
+     */
+    public List<String> getHistory() {
+        return mHttpClient.getHistory();
+    }
+
+    /**
+     * 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 9ccb5ee..f2b394d 100644
--- a/java/com/android/libraries/entitlement/http/HttpClient.java
+++ b/java/com/android/libraries/entitlement/http/HttpClient.java
@@ -47,6 +47,7 @@
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLConnection;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -55,9 +56,19 @@
     private static final String TAG = "ServiceEntitlement";
 
     private HttpURLConnection mConnection;
+    private boolean mSaveHistory;
+    private ArrayList<String> mHistory;
+
+    public HttpClient(boolean saveHistory) {
+        mSaveHistory = saveHistory;
+        mHistory = new ArrayList<>();
+    }
 
     @WorkerThread
     public HttpResponse request(HttpRequest request) throws ServiceEntitlementException {
+        if (mSaveHistory) {
+            mHistory.add(request.toString());
+        }
         logPii("HttpClient.request url: " + request.url());
         createConnection(request);
         logPii("HttpClient.request headers (partial): " + mConnection.getRequestProperties());
@@ -73,18 +84,38 @@
             }
             mConnection.connect(); // This is to trigger SocketTimeoutException early
             HttpResponse response = getHttpResponse(mConnection);
-            Log.d(TAG, "HttpClient.response : " + response);
+            Log.d(TAG, "HttpClient.response : " + response.toShortDebugString());
+            if (mSaveHistory) {
+                mHistory.add(response.toString());
+            }
             return response;
         } catch (IOException ioe) {
             throw new ServiceEntitlementException(
                     ERROR_HTTP_STATUS_NOT_SUCCESS,
-                    StreamUtils.inputStreamToStringSafe(mConnection.getErrorStream()),
+                    "Connection error stream: "
+                            + StreamUtils.inputStreamToStringSafe(mConnection.getErrorStream())
+                            + " IOException: "
+                            + ioe.toString(),
                     ioe);
         } finally {
             closeConnection();
         }
     }
 
+    /**
+     * Retrieves the history of past HTTP request and responses.
+     */
+    public List<String> getHistory() {
+        return mHistory;
+    }
+
+    /**
+     * Clears the history of past HTTP request and responses.
+     */
+    public void clearHistory() {
+        mHistory.clear();
+    }
+
     private void createConnection(HttpRequest request) throws ServiceEntitlementException {
         try {
             URL url = new URL(request.url());
diff --git a/java/com/android/libraries/entitlement/http/HttpResponse.java b/java/com/android/libraries/entitlement/http/HttpResponse.java
index f495578..142639e 100644
--- a/java/com/android/libraries/entitlement/http/HttpResponse.java
+++ b/java/com/android/libraries/entitlement/http/HttpResponse.java
@@ -74,8 +74,11 @@
                 .setCookies(ImmutableList.of());
     }
 
-    @Override
-    public final String toString() {
+    /**
+     * Returns a short string representation for debugging purposes. Doesn't include the cookie or
+     * full body to prevent leaking sensitive data.
+     */
+    public String toShortDebugString() {
         return new StringBuilder("HttpResponse{")
                 .append("contentType=")
                 .append(contentType())
diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java
index b837695..aff8f4b 100644
--- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java
+++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java
@@ -24,6 +24,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.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -66,6 +67,8 @@
     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 COOKIE_VALUE = "COOKIE=abcdefg";
@@ -94,6 +97,7 @@
     private static final int SUB_ID = 1;
     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";
 
     @Rule public final MockitoRule rule = MockitoJUnit.rule();
 
@@ -105,11 +109,14 @@
 
     private Context mContext;
     private EapAkaApi mEapAkaApi;
+    private EapAkaApi mEapAkaApiBypassAuthentication;
 
     @Before
     public void setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext());
-        mEapAkaApi = new EapAkaApi(mContext, SUB_ID, mMockHttpClient);
+        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(mMockTelephonyManager.createForSubscriptionId(SUB_ID))
@@ -164,8 +171,8 @@
                         ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request);
 
         assertThat(respopnse).isEqualTo(RESPONSE_XML);
-        // Verify that the 2nd request has cookies set by the 1st response
         verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture());
+        // 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);
@@ -178,6 +185,180 @@
     }
 
     @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();
+
+        String respopnse =
+                mEapAkaApi.queryEntitlementStatus(
+                        ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request);
+
+        assertThat(respopnse).isEqualTo(RESPONSE_XML);
+        // 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();
+
+        String respopnse =
+                mEapAkaApi.queryEntitlementStatus(
+                        ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request);
+
+        assertThat(respopnse).isEqualTo(RESPONSE_XML);
+        // 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);
+        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)
@@ -261,6 +442,131 @@
     }
 
     @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 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();
+
+        String respopnse =
+                mEapAkaApiBypassAuthentication.queryEntitlementStatus(
+                        ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request);
+
+        assertThat(respopnse).isEqualTo(RESPONSE_XML);
+        // 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);
+        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();
+        verify(mMockTelephonyManagerForSubId, times(0))
+                .getIccAuthentication(anyInt(), anyInt(), any());
+        assertThat(
+                        mHttpRequestCaptor
+                                .getAllValues()
+                                .get(1)
+                                .postData()
+                                .get(EapAkaApi.EAP_CHALLENGE_RESPONSE))
+                .isEqualTo(BYPASS_EAP_AKA_RESPONSE);
+    }
+
+    @Test
     public void queryEntitlementStatus_acceptContentTypeSpecified_verfityAcceptContentType()
             throws Exception {
         HttpResponse response = HttpResponse.builder().setBody(RESPONSE_XML).build();
@@ -351,4 +657,48 @@
         assertThat(response).isEqualTo(RESPONSE_XML);
         verify(mMockHttpClient, times(1)).request(any());
     }
+
+    @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();
+    }
 }
diff --git a/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java b/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java
index 505e8b5..9f05828 100644
--- a/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java
+++ b/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java
@@ -46,6 +46,7 @@
 
 import java.net.HttpURLConnection;
 import java.net.URL;
+import java.util.List;
 import java.util.Map;
 
 @RunWith(AndroidJUnit4.class)
@@ -70,7 +71,7 @@
         // Reset sFakeURLStreamHandler
         sFakeURLStreamHandler.stubResponse(ImmutableMap.of());
 
-        mHttpClient = new HttpClient();
+        mHttpClient = new HttpClient(true);
     }
 
     @Test
@@ -244,4 +245,40 @@
         assertThat(exception.getHttpStatus()).isEqualTo(0);
         assertThat(exception.getRetryAfter()).isEmpty();
     }
+
+    @Test
+    public void history() throws Exception {
+        FakeResponse responseContent =
+                FakeResponse.builder()
+                        .setResponseCode(HttpURLConnection.HTTP_OK)
+                        .setResponseLocation(null)
+                        .setResponseBody(TEST_RESPONSE_BODY.getBytes(UTF_8))
+                        .setContentType(CONTENT_TYPE_STRING_JSON)
+                        .build();
+        Map<String, FakeResponse> response = ImmutableMap.of(TEST_URL, responseContent);
+        sFakeURLStreamHandler.stubResponse(response);
+        HttpRequest request =
+                HttpRequest.builder()
+                        .setUrl(TEST_URL)
+                        .setRequestMethod(RequestMethod.GET)
+                        .setTimeoutInSec(70)
+                        .build();
+
+        HttpResponse httpResponse0 = mHttpClient.request(request);
+        HttpResponse httpResponse1 = mHttpClient.request(request);
+        List<String> history = mHttpClient.getHistory();
+
+        assertThat(history)
+                .containsExactly(
+                        request.toString(),
+                        httpResponse0.toString(),
+                        request.toString(),
+                        httpResponse1.toString())
+                .inOrder();
+
+        mHttpClient.clearHistory();
+        history = mHttpClient.getHistory();
+
+        assertThat(history).isEmpty();
+    }
 }