aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp3
-rw-r--r--OWNERS4
-rwxr-xr-xcoverage.sh92
-rw-r--r--java/com/android/libraries/entitlement/EsimOdsaOperation.java49
-rw-r--r--java/com/android/libraries/entitlement/ServiceEntitlement.java78
-rw-r--r--java/com/android/libraries/entitlement/ServiceEntitlementRequest.java30
-rw-r--r--java/com/android/libraries/entitlement/eapaka/EapAkaApi.java75
-rw-r--r--java/com/android/libraries/entitlement/http/HttpClient.java35
-rw-r--r--java/com/android/libraries/entitlement/http/HttpResponse.java7
-rw-r--r--tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java74
-rw-r--r--tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java55
-rw-r--r--tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java2
-rw-r--r--tests/src/com/android/libraries/entitlement/http/HttpClientTest.java91
-rw-r--r--tests/src/com/android/libraries/entitlement/utils/BytesConverterTest.java41
-rw-r--r--tests/utils/com/android/libraries/entitlement/testing/FakeURLStreamHandler.java22
15 files changed, 628 insertions, 30 deletions
diff --git a/Android.bp b/Android.bp
index b45b167..e339fd9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -84,4 +84,7 @@ java_library {
"java/com/android/libraries/entitlement/ServiceEntitlementException.java",
"java/com/android/libraries/entitlement/ServiceEntitlementRequest.java",
],
+ static_libs: [
+ "guava",
+ ],
}
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/coverage.sh b/coverage.sh
new file mode 100755
index 0000000..7cd2bf2
--- /dev/null
+++ b/coverage.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+##### App specific parameters #####
+
+PACKAGE_NAME='com.android.libraries.entitlement'
+MODULE_NAME='service-entitlement'
+MODULE_PATH='frameworks/libs/service_entitlement'
+
+TEST_PACKAGE='com.android.libraries.entitlement.tests'
+TEST_MODULE_NAME='service-entitlement-tests'
+TEST_MODULE_PATH='frameworks/libs/service_entitlement/tests'
+TEST_MODULE_INSTALL_PATH="testcases/$TEST_MODULE_NAME/arm64/$TEST_MODULE_NAME.apk"
+TEST_RUNNER="$TEST_PACKAGE/androidx.test.runner.AndroidJUnitRunner"
+
+##### End app specific parameters #####
+
+if [[ $# != 0 && ! ($# == 1 && ($1 == "html" || $1 == "xml" || $1 == "csv")) ]]; then
+ echo "$0: usage: coverage.sh [REPORT_TYPE]"
+ echo "REPORT_TYPE [html | xml | csv] : the type of the report (default is html)"
+ exit 1
+fi
+
+REPORT_TYPE=${1:-html}
+
+if [ -z $ANDROID_BUILD_TOP ]; then
+ echo "You need to source and lunch before you can use this script"
+ exit 1
+fi
+
+REPORTER_JAR="$ANDROID_BUILD_TOP/out/soong/host/linux-x86/framework/jacoco-cli.jar"
+
+OUTPUT_DIR="$ANDROID_BUILD_TOP/out/coverage/$MODULE_NAME"
+
+echo "Running tests and generating coverage report"
+echo "Output dir: $OUTPUT_DIR"
+echo "Report type: $REPORT_TYPE"
+
+# location on the device to store coverage results, need to be accessible by the app
+REMOTE_COVERAGE_OUTPUT_FILE="/data/user/0/$TEST_PACKAGE/files/coverage.ec"
+
+COVERAGE_OUTPUT_FILE="$ANDROID_BUILD_TOP/out/$PACKAGE_NAME.ec"
+OUT_COMMON="$ANDROID_BUILD_TOP/out/target/common"
+COVERAGE_CLASS_FILE="$OUT/obj/JAVA_LIBRARIES/${MODULE_NAME}_intermediates/javalib.jar"
+
+source $ANDROID_BUILD_TOP/build/envsetup.sh
+
+set -e # fail early
+
+echo ""
+echo "BUILDING PACKAGE $PACKAGE_NAME"
+echo "============================================"
+(cd "$ANDROID_BUILD_TOP/$MODULE_PATH" && EMMA_INSTRUMENT=true EMMA_INSTRUMENT_STATIC=true mma -j32)
+echo "============================================"
+
+echo ""
+echo "BUILDING TEST PACKAGE $TEST_MODULE_NAME"
+echo "============================================"
+(cd "$ANDROID_BUILD_TOP/$TEST_MODULE_PATH" && EMMA_INSTRUMENT=true EMMA_INSTRUMENT_STATIC=true mma -j32)
+echo "============================================"
+
+#set -x # print commands
+
+adb root
+adb wait-for-device
+
+adb shell rm -f "$REMOTE_COVERAGE_OUTPUT_FILE"
+
+adb install -r -g "$OUT/$TEST_MODULE_INSTALL_PATH"
+
+echo ""
+echo "RUNNING TESTS $TEST_RUNNER"
+echo "============================================"
+adb shell am instrument -e coverage true -w $TEST_RUNNER
+echo "============================================"
+
+mkdir -p "$OUTPUT_DIR"
+
+adb pull "$REMOTE_COVERAGE_OUTPUT_FILE" "$COVERAGE_OUTPUT_FILE"
+
+java -jar "$REPORTER_JAR" \
+ report "$COVERAGE_OUTPUT_FILE" \
+ --$REPORT_TYPE "$OUTPUT_DIR" \
+ --classfiles "$COVERAGE_CLASS_FILE" \
+ --sourcefiles "$ANDROID_BUILD_TOP/$MODULE_PATH/java"
+
+#set +x
+
+# Echo the file as URI to quickly open the result using ctrl-click in terminal
+if [[ REPORT_TYPE == html ]] ; then
+ echo "COVERAGE RESULTS IN:"
+ echo "file://$OUTPUT_DIR/index.html"
+fi
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 @@ public abstract class EsimOdsaOperation {
* 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 class EsimOdsaOperation {
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 class EsimOdsaOperation {
*/
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 @@ public abstract class EsimOdsaOperation {
return new AutoValue_EsimOdsaOperation.Builder()
.setOperation("")
.setOperationType(OPERATION_TYPE_NOT_SET)
+ .setOperationTargets(ImmutableList.of())
.setCompanionTerminalId("")
.setCompanionTerminalVendor("")
.setCompanionTerminalModel("")
@@ -189,7 +213,9 @@ public abstract class EsimOdsaOperation {
.setTerminalEid("")
.setTargetTerminalId("")
.setTargetTerminalIccid("")
- .setTargetTerminalEid("");
+ .setTargetTerminalEid("")
+ .setOldTerminalId("")
+ .setOldTerminalIccid("");
}
/**
@@ -230,6 +256,12 @@ public abstract class EsimOdsaOperation {
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 class EsimOdsaOperation {
*/
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..e050cd3 100644
--- a/java/com/android/libraries/entitlement/ServiceEntitlement.java
+++ b/java/com/android/libraries/entitlement/ServiceEntitlement.java
@@ -25,6 +25,8 @@ import com.android.libraries.entitlement.eapaka.EapAkaApi;
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 @@ public class ServiceEntitlement {
* App ID for on device service activation (OSDA) for primary device.
*/
public static final String APP_ODSA_PRIMARY = "ap2009";
+ /**
+ * App ID for premium network slice entitlement
+ */
+ public static final String APP_PREMIUM_NETWORK_SLICE = "ap2012";
private final CarrierConfig carrierConfig;
private final EapAkaApi eapAkaApi;
@@ -67,8 +73,63 @@ public class ServiceEntitlement {
* 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 @@ public class ServiceEntitlement {
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/ServiceEntitlementRequest.java b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java
index c7b0ad3..59e70ef 100644
--- a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java
+++ b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java
@@ -62,6 +62,11 @@ public abstract class ServiceEntitlementRequest {
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,12 @@ public abstract class ServiceEntitlementRequest {
public abstract String acceptContentType();
/**
+ * Returns the network identifier for premium network. Used for premium network slice
+ * entitlement.
+ */
+ public abstract String networkIdentifier();
+
+ /**
* Returns a new {@link Builder} object.
*/
public static Builder builder() {
@@ -125,6 +136,7 @@ public abstract class ServiceEntitlementRequest {
.setConfigurationVersion(DEFAULT_CONFIGURATION_VERSION)
.setEntitlementVersion(DEFAULT_ENTITLEMENT_VERSION)
.setAuthenticationToken("")
+ .setTemporaryToken("")
.setTerminalId("")
.setTerminalVendor(Build.MANUFACTURER)
.setTerminalModel(Build.MODEL)
@@ -133,7 +145,8 @@ public abstract class ServiceEntitlementRequest {
.setAppVersion("")
.setNotificationToken("")
.setNotificationAction(NOTICATION_ACTION_ENABLE_FCM)
- .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML);
+ .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML)
+ .setNetworkIdentifier("");
}
/**
@@ -167,6 +180,13 @@ public abstract class ServiceEntitlementRequest {
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 +263,14 @@ 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.
+ *
+ * <p>Optional.
+ */
+ public abstract Builder setNetworkIdentifier(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..2847c16 100644
--- a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java
+++ b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java
@@ -43,6 +43,8 @@ import com.google.common.net.HttpHeaders;
import org.json.JSONException;
import org.json.JSONObject;
+import java.util.List;
+
public class EapAkaApi {
private static final String TAG = "ServiceEntitlement";
@@ -58,6 +60,7 @@ public class EapAkaApi {
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 +68,7 @@ public class EapAkaApi {
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 +86,37 @@ public class EapAkaApi {
private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid";
private static final String TARGET_TERMINAL_EID = "target_terminal_eid";
+ 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";
+
// 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 final Context mContext;
private final int mSimSubscriptionId;
private final HttpClient mHttpClient;
-
- public EapAkaApi(Context context, int simSubscriptionId) {
- this(context, simSubscriptionId, new HttpClient());
+ private final String mBypassEapAkaResponse;
+
+ 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;
}
/**
@@ -167,6 +186,13 @@ public class EapAkaApi {
throw new ServiceEntitlementException(
ERROR_MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException);
}
+ if (!mBypassEapAkaResponse.isEmpty()) {
+ return challengeResponse(
+ mBypassEapAkaResponse,
+ carrierConfig,
+ response.cookies(),
+ contentType);
+ }
EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge);
EapAkaResponse eapAkaResponse =
EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge);
@@ -241,7 +267,8 @@ public class EapAkaApi {
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(
@@ -268,17 +295,20 @@ public class EapAkaApi {
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 +328,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());
for (String appId : appIds) {
urlBuilder.appendQueryParameter(APP, appId);
@@ -320,6 +351,10 @@ public class EapAkaApi {
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 +380,10 @@ public class EapAkaApi {
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)
@@ -385,4 +424,18 @@ public class EapAkaApi {
}
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.io.OutputStream;
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 @@ public class HttpClient {
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 @@ public class HttpClient {
}
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 @@ public abstract class HttpResponse {
.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/ServiceEntitlementTest.java b/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java
index e54d87f..7e2b0f1 100644
--- a/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java
+++ b/tests/src/com/android/libraries/entitlement/ServiceEntitlementTest.java
@@ -18,8 +18,14 @@ package com.android.libraries.entitlement;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+import android.content.Context;
+import android.telephony.TelephonyManager;
+
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
import com.android.libraries.entitlement.eapaka.EapAkaApi;
@@ -38,13 +44,20 @@ import org.mockito.junit.MockitoRule;
public class ServiceEntitlementTest {
private static final String QUERY_APP_VOLTE_RESULT = "QUERY_APP_VOLTE_RESULT";
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 TEST_URL = "https://test.url";
- @Rule
- public final MockitoRule rule = MockitoJUnit.rule();
- @Mock
- EapAkaApi mMockEapAkaApi;
+ private static final String IMSI = "234107813240779";
+ private static final String MCCMNC = "23410";
+ private static final int SUB_ID = 1;
+
+ @Rule public final MockitoRule rule = MockitoJUnit.rule();
+ @Mock EapAkaApi mMockEapAkaApi;
+ @Mock private TelephonyManager mMockTelephonyManager;
+ @Mock private TelephonyManager mMockTelephonyManagerForSubId;
+ private Context mContext;
private ServiceEntitlement mServiceEntitlement;
private CarrierConfig mCarrierConfig;
@@ -52,6 +65,31 @@ public class ServiceEntitlementTest {
public void setUp() {
mCarrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build();
mServiceEntitlement = new ServiceEntitlement(mCarrierConfig, mMockEapAkaApi);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ }
+
+ @Test
+ public void queryEntitlementStatus_noServerAddress_throwException() throws Exception {
+ 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));
+
+ 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();
}
@Test
@@ -79,4 +117,32 @@ public class ServiceEntitlementTest {
request))
.isEqualTo(QUERY_APP_VOWIFI_RESULT);
}
+
+ @Test
+ public void performEsimOdsa_appOdsaCompanion_returnResult() throws Exception {
+ 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);
+
+ assertThat(
+ mServiceEntitlement.performEsimOdsa(
+ ServiceEntitlement.APP_ODSA_COMPANION, request, odsaOperation))
+ .isEqualTo(QUERY_APP_ODSA_COMPANION_RESULT);
+ }
+
+ @Test
+ public void performEsimOdsa_appOdsaPrimary_returnResult() throws Exception {
+ 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);
+
+ assertThat(
+ mServiceEntitlement.performEsimOdsa(
+ ServiceEntitlement.APP_ODSA_PRIMARY, request, odsaOperation))
+ .isEqualTo(QUERY_APP_ODSA_PRIMARY_RESULT);
+ }
}
diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java
index f655258..8e331f3 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.android.libraries.entitlement.eapaka.EapAkaResponseTest.EAP_AK
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;
@@ -94,6 +95,7 @@ public class EapAkaApiTest {
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 +107,14 @@ public class EapAkaApiTest {
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))
@@ -220,6 +225,8 @@ public class EapAkaApiTest {
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);
+ assertThat(exception.getRetryAfter()).isEmpty();
}
@Test
@@ -259,11 +266,53 @@ public class EapAkaApiTest {
}
@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();
when(mMockHttpClient.request(any())).thenReturn(response);
- CarrierConfig carrierConfig = CarrierConfig.builder().build();
+ CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build();
ServiceEntitlementRequest request =
ServiceEntitlementRequest
.builder()
@@ -284,7 +333,7 @@ public class EapAkaApiTest {
throws Exception {
HttpResponse response = HttpResponse.builder().setBody(RESPONSE_XML).build();
when(mMockHttpClient.request(any())).thenReturn(response);
- CarrierConfig carrierConfig = CarrierConfig.builder().build();
+ CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build();
ServiceEntitlementRequest request =
ServiceEntitlementRequest
.builder()
diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java
index 219737d..e6d1c0c 100644
--- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java
+++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaSecurityContextTest.java
@@ -104,6 +104,8 @@ public class EapAkaSecurityContextTest {
.isEqualTo(ServiceEntitlementException.ERROR_ICC_AUTHENTICATION_NOT_AVAILABLE);
assertThat(exception.getMessage())
.isEqualTo("Invalid SIM EAP-AKA authentication response!");
+ assertThat(exception.getHttpStatus()).isEqualTo(0);
+ assertThat(exception.getRetryAfter()).isEmpty();
}
@Test
diff --git a/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java b/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java
index 5838bb7..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 org.junit.runner.RunWith;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.util.List;
import java.util.Map;
@RunWith(AndroidJUnit4.class)
@@ -70,7 +71,7 @@ public class HttpClientTest {
// Reset sFakeURLStreamHandler
sFakeURLStreamHandler.stubResponse(ImmutableMap.of());
- mHttpClient = new HttpClient();
+ mHttpClient = new HttpClient(true);
}
@Test
@@ -192,4 +193,92 @@ public class HttpClientTest {
FakeHttpsURLConnection connection = sFakeURLStreamHandler.getConnections().get(0);
assertThat(connection.getBytesWrittenToOutputStream()).isEqualTo(postData.getBytes(UTF_8));
}
+
+ @Test
+ public void request_getResponseCodeFailed_expectThrowsException() {
+ HttpRequest request =
+ HttpRequest.builder()
+ .setUrl(TEST_URL)
+ .setRequestMethod(RequestMethod.GET)
+ .build();
+ FakeResponse responseContent =
+ FakeResponse.builder()
+ .setResponseBody(TEST_RESPONSE_BODY.getBytes(UTF_8))
+ .setContentType(CONTENT_TYPE_STRING_JSON)
+ .setHasException(true)
+ .build();
+ Map<String, FakeResponse> response = ImmutableMap.of(TEST_URL, responseContent);
+ sFakeURLStreamHandler.stubResponse(response);
+
+ ServiceEntitlementException exception = expectThrows(
+ ServiceEntitlementException.class, () -> mHttpClient.request(request));
+
+ assertThat(exception.getErrorCode()).isEqualTo(
+ ServiceEntitlementException.ERROR_HTTP_STATUS_NOT_SUCCESS);
+ assertThat(exception.getMessage()).isEqualTo("Read response code failed!");
+ assertThat(exception.getHttpStatus()).isEqualTo(0);
+ assertThat(exception.getRetryAfter()).isEmpty();
+ }
+
+ @Test
+ public void request_getResponseBodyFailed_expectThrowsException() {
+ HttpRequest request =
+ HttpRequest.builder()
+ .setUrl(TEST_URL)
+ .setRequestMethod(RequestMethod.GET)
+ .build();
+ FakeResponse responseContent =
+ FakeResponse.builder()
+ .setResponseCode(HttpURLConnection.HTTP_OK)
+ .setContentType(CONTENT_TYPE_STRING_JSON)
+ .setHasException(true)
+ .build();
+ Map<String, FakeResponse> response = ImmutableMap.of(TEST_URL, responseContent);
+ sFakeURLStreamHandler.stubResponse(response);
+
+ ServiceEntitlementException exception = expectThrows(
+ ServiceEntitlementException.class, () -> mHttpClient.request(request));
+
+ assertThat(exception.getErrorCode()).isEqualTo(
+ ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE);
+ assertThat(exception.getMessage()).isEqualTo("Read response body/message failed!");
+ 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();
+ }
}
diff --git a/tests/src/com/android/libraries/entitlement/utils/BytesConverterTest.java b/tests/src/com/android/libraries/entitlement/utils/BytesConverterTest.java
new file mode 100644
index 0000000..41c8445
--- /dev/null
+++ b/tests/src/com/android/libraries/entitlement/utils/BytesConverterTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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 static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BytesConverterTest {
+ @Test
+ public void convertBytesToHexString_nullBytes_returnsNull() {
+ assertThat(BytesConverter.convertBytesToHexString(null)).isNull();
+ }
+
+ @Test
+ public void convertBytesToHexString_integerBytes_returnsHexString() {
+ byte[] integerBytes = BytesConverter.convertIntegerTo4Bytes(123);
+
+ String hexString = BytesConverter.convertBytesToHexString(integerBytes);
+
+ assertThat(hexString).isEqualTo("0000007B");
+ }
+}
diff --git a/tests/utils/com/android/libraries/entitlement/testing/FakeURLStreamHandler.java b/tests/utils/com/android/libraries/entitlement/testing/FakeURLStreamHandler.java
index a5bdb4c..9f1d233 100644
--- a/tests/utils/com/android/libraries/entitlement/testing/FakeURLStreamHandler.java
+++ b/tests/utils/com/android/libraries/entitlement/testing/FakeURLStreamHandler.java
@@ -16,6 +16,8 @@
package com.android.libraries.entitlement.testing;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -90,6 +92,9 @@ public class FakeURLStreamHandler extends URLStreamHandler implements URLStreamH
@Override
public InputStream getInputStream() throws IOException {
+ if (mResponse.hasException() && mResponse.responseBody().length == 0) {
+ throw new IOException("stub exception");
+ }
return new ByteArrayInputStream(mResponse.responseBody());
}
@@ -98,12 +103,20 @@ public class FakeURLStreamHandler extends URLStreamHandler implements URLStreamH
return mOutputStream;
}
+ @Override
+ public InputStream getErrorStream() {
+ return new ByteArrayInputStream("stub error".getBytes(UTF_8));
+ }
+
public byte[] getBytesWrittenToOutputStream() {
return mOutputStream.toByteArray();
}
@Override
- public int getResponseCode() {
+ public int getResponseCode() throws IOException {
+ if (mResponse.hasException() && mResponse.responseCode() == 0) {
+ throw new IOException("stub exception");
+ }
return mResponse.responseCode();
}
@@ -170,13 +183,16 @@ public class FakeURLStreamHandler extends URLStreamHandler implements URLStreamH
public abstract String retryAfter();
+ abstract boolean hasException();
+
public static Builder builder() {
return new AutoValue_FakeURLStreamHandler_FakeResponse.Builder()
.setResponseBody(new byte[]{})
.setContentType("")
.setResponseCode(0)
.setResponseLocation("")
- .setRetryAfter("");
+ .setRetryAfter("")
+ .setHasException(false);
}
@AutoValue.Builder
@@ -191,6 +207,8 @@ public class FakeURLStreamHandler extends URLStreamHandler implements URLStreamH
public abstract Builder setRetryAfter(String retryAfter);
+ public abstract Builder setHasException(boolean hasException);
+
public abstract FakeResponse build();
}
}