/*
* Copyright 2020 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.google.android.iwlan;
import android.content.Context;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.telephony.DataFailCause;
import android.telephony.TelephonyManager;
import android.telephony.data.DataService;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class ErrorPolicyManager {
/**
* This type is not to be used in config. This is only used internally to catch errors in
* parsing the error type.
*/
private static final int UNKNOWN_ERROR_TYPE = -1;
/**
* This value represents that the error tye is to be used as a fallback to represent all the
* errors.
*/
private static final int FALLBACK_ERROR_TYPE = 1;
/**
* This value represents rest of the errors that are not defined above. ErrorDetails should
* mention the specific error. If it doesn't not - the policy will be used as a fallback global
* policy. Currently Supported ErrorDetails "IO_EXCEPTION" "TIMEOUT_EXCEPTION"
* "SERVER_SELECTION_FAILED" "TUNNEL_TRANSFORM_FAILED"
*/
private static final int GENERIC_ERROR_TYPE = 2;
/**
* This value represents IKE Protocol Error/Notify Error.
*
* @see RFC 4306,Internet Key
* Exchange (IKEv2) Protocol for global errors and carrier specific requirements for
* other carrier specific error codes. ErrorDetails defined for this type is always in
* numeric form representing the error codes. Examples: "24", "9000-9050"
*/
private static final int IKE_PROTOCOL_ERROR_TYPE = 3;
@IntDef({UNKNOWN_ERROR_TYPE, FALLBACK_ERROR_TYPE, GENERIC_ERROR_TYPE, IKE_PROTOCOL_ERROR_TYPE})
@interface ErrorPolicyErrorType {};
private static final String[] GENERIC_ERROR_DETAIL_STRINGS = {
"*",
"IO_EXCEPTION",
"TIMEOUT_EXCEPTION",
"SERVER_SELECTION_FAILED",
"TUNNEL_TRANSFORM_FAILED"
};
/** Private IKEv2 notify message types. As defined in TS 124 302 (section 8.1.2.2) */
private static final int IKE_PROTOCOL_ERROR_PDN_CONNECTION_REJECTION = 8192;
private static final int IKE_PROTOCOL_ERROR_MAX_CONNECTION_REACHED = 8193;
private static final int IKE_PROTOCOL_ERROR_SEMANTIC_ERROR_IN_THE_TFT_OPERATION = 8241;
private static final int IKE_PROTOCOL_ERROR_SYNTACTICAL_ERROR_IN_THE_TFT_OPERATION = 8242;
private static final int IKE_PROTOCOL_ERROR_SEMANTIC_ERRORS_IN_PACKET_FILTERS = 8244;
private static final int IKE_PROTOCOL_ERROR_SYNTACTICAL_ERRORS_IN_PACKET_FILTERS = 8245;
private static final int IKE_PROTOCOL_ERROR_NON_3GPP_ACCESS_TO_EPC_NOT_ALLOWED = 9000;
private static final int IKE_PROTOCOL_ERROR_USER_UNKNOWN = 9001;
private static final int IKE_PROTOCOL_ERROR_NO_APN_SUBSCRIPTION = 9002;
private static final int IKE_PROTOCOL_ERROR_AUTHORIZATION_REJECTED = 9003;
private static final int IKE_PROTOCOL_ERROR_ILLEGAL_ME = 9006;
private static final int IKE_PROTOCOL_ERROR_NETWORK_FAILURE = 10500;
private static final int IKE_PROTOCOL_ERROR_RAT_TYPE_NOT_ALLOWED = 11001;
private static final int IKE_PROTOCOL_ERROR_IMEI_NOT_ACCEPTED = 11005;
private static final int IKE_PROTOCOL_ERROR_PLMN_NOT_ALLOWED = 11011;
private static final int IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED = 11055;
@IntDef({
IKE_PROTOCOL_ERROR_PDN_CONNECTION_REJECTION,
IKE_PROTOCOL_ERROR_MAX_CONNECTION_REACHED,
IKE_PROTOCOL_ERROR_SEMANTIC_ERROR_IN_THE_TFT_OPERATION,
IKE_PROTOCOL_ERROR_SYNTACTICAL_ERROR_IN_THE_TFT_OPERATION,
IKE_PROTOCOL_ERROR_SEMANTIC_ERRORS_IN_PACKET_FILTERS,
IKE_PROTOCOL_ERROR_SYNTACTICAL_ERRORS_IN_PACKET_FILTERS,
IKE_PROTOCOL_ERROR_NON_3GPP_ACCESS_TO_EPC_NOT_ALLOWED,
IKE_PROTOCOL_ERROR_USER_UNKNOWN,
IKE_PROTOCOL_ERROR_NO_APN_SUBSCRIPTION,
IKE_PROTOCOL_ERROR_AUTHORIZATION_REJECTED,
IKE_PROTOCOL_ERROR_ILLEGAL_ME,
IKE_PROTOCOL_ERROR_NETWORK_FAILURE,
IKE_PROTOCOL_ERROR_RAT_TYPE_NOT_ALLOWED,
IKE_PROTOCOL_ERROR_IMEI_NOT_ACCEPTED,
IKE_PROTOCOL_ERROR_PLMN_NOT_ALLOWED,
IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED
})
@interface IkeProtocolErrorType {};
private final String LOG_TAG;
private static Map mInstances = new ConcurrentHashMap<>();
private Context mContext;
private int mSlotId;
// Policies read from defaultiwlanerrorconfig.json
// String APN as key to identify the ErrorPolicies associated with it.
private final Map> mDefaultPolicies = new HashMap<>();
// Policies read from CarrierConfig
// String APN as key to identify the ErrorPolicies associated with it.
private Map> mCarrierConfigPolicies = new HashMap<>();
// String APN as key to identify the ErrorInfo associated with that APN
private Map mLastErrorForApn = new HashMap<>();
// List of current Unthrottling events registered with IwlanEventListener
private Set mUnthrottlingEvents;
private ErrorStats mErrorStats = new ErrorStats();
private HandlerThread mHandlerThread;
@VisibleForTesting Handler mHandler;
private int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
private String carrierConfigErrorPolicyString;
@VisibleForTesting
static final String KEY_ERROR_POLICY_CONFIG_STRING = "iwlan.key_error_policy_config_string";
/**
* Returns ErrorPolicyManager instance for the subId
*
* @param context
* @param slotId
*/
public static ErrorPolicyManager getInstance(@NonNull Context context, int slotId) {
return mInstances.computeIfAbsent(slotId, k -> new ErrorPolicyManager(context, slotId));
}
/**
* Release or reset the instance.
*
* @param context
* @param slotId
*/
public void releaseInstance() {
Log.d(LOG_TAG, "Release Instance with slotId: " + mSlotId);
IwlanEventListener.getInstance(mContext, mSlotId).removeEventListener(mHandler);
mHandlerThread.quit();
mInstances.remove(mSlotId);
}
/**
* Updates the last error details and returns the retry time. Return value is -1, which should
* be ignored, when the error is IwlanError.NO_ERROR.
*
* @param apn apn name for which the error happened
* @param iwlanError Error
* @return retry time. 0 = immediate retry, -1 = fail and n = retry after n seconds
*/
public synchronized long reportIwlanError(String apn, IwlanError iwlanError) {
// Fail by default
long retryTime = -1;
if (iwlanError.getErrorType() == IwlanError.NO_ERROR) {
Log.d(LOG_TAG, "reportIwlanError: NO_ERROR");
mLastErrorForApn.remove(apn);
return retryTime;
}
mErrorStats.update(apn, iwlanError);
// remove the entry with the same error if it has back off time
if (mLastErrorForApn.containsKey(apn)
&& mLastErrorForApn.get(apn).getError().equals(iwlanError)
&& mLastErrorForApn.get(apn).isBackOffTimeValid()) {
mLastErrorForApn.remove(apn);
}
if (!mLastErrorForApn.containsKey(apn)
|| !mLastErrorForApn.get(apn).getError().equals(iwlanError)) {
Log.d(LOG_TAG, "Doesn't match to the previous error" + iwlanError.toString());
ErrorPolicy policy = findErrorPolicy(apn, iwlanError);
ErrorInfo errorInfo = new ErrorInfo(iwlanError, policy);
mLastErrorForApn.put(apn, errorInfo);
}
retryTime = mLastErrorForApn.get(apn).updateCurrentRetryTime();
return retryTime;
}
/**
* Updates the last error details with back off time. Return value is -1, which should be
* ignored, when the error is IwlanError.NO_ERROR.
*
* @param apn apn name for which the error happened
* @param iwlanError Error
* @param long backOffTime in seconds
* @return retry time which is the backoff time. -1 if it is NO_ERROR
*/
public synchronized long reportIwlanError(String apn, IwlanError iwlanError, long backOffTime) {
// Fail by default
long retryTime = -1;
if (iwlanError.getErrorType() == IwlanError.NO_ERROR) {
Log.d(LOG_TAG, "reportIwlanError: NO_ERROR");
mLastErrorForApn.remove(apn);
return retryTime;
}
mErrorStats.update(apn, iwlanError);
// remove the entry with the same error if it doesn't have back off time.
if (mLastErrorForApn.containsKey(apn)
&& mLastErrorForApn.get(apn).getError().equals(iwlanError)
&& !mLastErrorForApn.get(apn).isBackOffTimeValid()) {
mLastErrorForApn.remove(apn);
}
retryTime = backOffTime;
if (!mLastErrorForApn.containsKey(apn)
|| !mLastErrorForApn.get(apn).getError().equals(iwlanError)) {
Log.d(LOG_TAG, "Doesn't match to the previous error" + iwlanError.toString());
ErrorPolicy policy = findErrorPolicy(apn, iwlanError);
ErrorInfo errorInfo = new ErrorInfo(iwlanError, policy, backOffTime);
mLastErrorForApn.put(apn, errorInfo);
} else {
ErrorInfo info = mLastErrorForApn.get(apn);
info.setBackOffTime(backOffTime);
}
return retryTime;
}
/**
* Checks whether we can bring up Epdg Tunnel - Based on lastErrorForApn
*
* @param apn apn for which tunnel bring up needs to be checked
* @return true if tunnel can be brought up, false otherwise
*/
public synchronized boolean canBringUpTunnel(String apn) {
boolean ret = true;
if (mLastErrorForApn.containsKey(apn)) {
ret = mLastErrorForApn.get(apn).canBringUpTunnel();
}
Log.d(LOG_TAG, "canBringUpTunnel: " + ret);
return ret;
}
// TODO: Modify framework/base/Android.bp to get access to Annotation.java to use
// @DataFailureCause
// annotation as return type here. (after moving to aosp?)
/**
* Returns the DataFailCause based on the lastErrorForApn
*
* @param apn apn name for which DataFailCause is needed
* @return DataFailCause corresponding to the error for the apn
*/
public synchronized int getDataFailCause(String apn) {
if (!mLastErrorForApn.containsKey(apn)) {
return DataFailCause.NONE;
}
IwlanError error = mLastErrorForApn.get(apn).getError();
int ret = DataFailCause.ERROR_UNSPECIFIED;
if (error.getErrorType() == IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED) {
ret = DataFailCause.IWLAN_DNS_RESOLUTION_NAME_FAILURE;
} else if (error.getErrorType() == IwlanError.IKE_INTERNAL_IO_EXCEPTION) {
ret = DataFailCause.IWLAN_IKEV2_MSG_TIMEOUT;
} else if (error.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) {
Exception exception = error.getException();
if (exception != null && exception instanceof IkeProtocolException) {
int protocolErrorType = ((IkeProtocolException) exception).getErrorType();
switch (protocolErrorType) {
case IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED:
ret = DataFailCause.IWLAN_IKEV2_AUTH_FAILURE;
break;
case IKE_PROTOCOL_ERROR_PDN_CONNECTION_REJECTION:
ret = DataFailCause.IWLAN_PDN_CONNECTION_REJECTION;
break;
case IKE_PROTOCOL_ERROR_MAX_CONNECTION_REACHED:
ret = DataFailCause.IWLAN_MAX_CONNECTION_REACHED;
break;
case IKE_PROTOCOL_ERROR_SEMANTIC_ERROR_IN_THE_TFT_OPERATION:
ret = DataFailCause.IWLAN_SEMANTIC_ERROR_IN_THE_TFT_OPERATION;
break;
case IKE_PROTOCOL_ERROR_SYNTACTICAL_ERROR_IN_THE_TFT_OPERATION:
ret = DataFailCause.IWLAN_SYNTACTICAL_ERROR_IN_THE_TFT_OPERATION;
break;
case IKE_PROTOCOL_ERROR_SEMANTIC_ERRORS_IN_PACKET_FILTERS:
ret = DataFailCause.IWLAN_SEMANTIC_ERRORS_IN_PACKET_FILTERS;
break;
case IKE_PROTOCOL_ERROR_SYNTACTICAL_ERRORS_IN_PACKET_FILTERS:
ret = DataFailCause.IWLAN_SYNTACTICAL_ERRORS_IN_PACKET_FILTERS;
break;
case IKE_PROTOCOL_ERROR_NON_3GPP_ACCESS_TO_EPC_NOT_ALLOWED:
ret = DataFailCause.IWLAN_NON_3GPP_ACCESS_TO_EPC_NOT_ALLOWED;
break;
case IKE_PROTOCOL_ERROR_USER_UNKNOWN:
ret = DataFailCause.IWLAN_USER_UNKNOWN;
break;
case IKE_PROTOCOL_ERROR_NO_APN_SUBSCRIPTION:
ret = DataFailCause.IWLAN_NO_APN_SUBSCRIPTION;
break;
case IKE_PROTOCOL_ERROR_AUTHORIZATION_REJECTED:
ret = DataFailCause.IWLAN_AUTHORIZATION_REJECTED;
break;
case IKE_PROTOCOL_ERROR_ILLEGAL_ME:
ret = DataFailCause.IWLAN_ILLEGAL_ME;
break;
case IKE_PROTOCOL_ERROR_NETWORK_FAILURE:
ret = DataFailCause.IWLAN_NETWORK_FAILURE;
break;
case IKE_PROTOCOL_ERROR_RAT_TYPE_NOT_ALLOWED:
ret = DataFailCause.IWLAN_RAT_TYPE_NOT_ALLOWED;
break;
case IKE_PROTOCOL_ERROR_IMEI_NOT_ACCEPTED:
ret = DataFailCause.IWLAN_IMEI_NOT_ACCEPTED;
break;
case IKE_PROTOCOL_ERROR_PLMN_NOT_ALLOWED:
ret = DataFailCause.IWLAN_PLMN_NOT_ALLOWED;
break;
case IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED:
ret = DataFailCause.IWLAN_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED;
break;
default:
ret = DataFailCause.IWLAN_NETWORK_FAILURE;
break;
}
}
}
return ret;
}
/**
* Returns the current retryTime based on the lastErrorForApn
*
* @param apn apn name for which curren retry time is needed
* @return long current retry time in milliseconds
*/
public synchronized long getCurrentRetryTimeMs(String apn) {
if (!mLastErrorForApn.containsKey(apn)) {
return -1;
}
return mLastErrorForApn.get(apn).getCurrentRetryTime();
}
/**
* Returns the last error for that apn
*
* @param apn apn name
* @return IwlanError or null if there is no error
*/
public synchronized IwlanError getLastError(String apn) {
if (mLastErrorForApn.containsKey(apn)) {
return mLastErrorForApn.get(apn).getError();
}
return new IwlanError(IwlanError.NO_ERROR);
}
public void logErrorPolicies() {
Log.d(LOG_TAG, "mCarrierConfigPolicies:");
for (Map.Entry> entry : mCarrierConfigPolicies.entrySet()) {
Log.d(LOG_TAG, "Apn: " + entry.getKey());
for (ErrorPolicy policy : entry.getValue()) {
policy.log();
}
}
Log.d(LOG_TAG, "mDefaultPolicies:");
for (Map.Entry> entry : mDefaultPolicies.entrySet()) {
Log.d(LOG_TAG, "Apn: " + entry.getKey());
for (ErrorPolicy policy : entry.getValue()) {
policy.log();
}
}
}
public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("---- ErrorPolicyManager ----");
for (Map.Entry entry : mLastErrorForApn.entrySet()) {
pw.print("APN: " + entry.getKey() + " IwlanError: " + entry.getValue().getError());
pw.println(" currentRetryTime: " + entry.getValue().getCurrentRetryTime());
}
pw.println(mErrorStats);
pw.println("----------------------------");
}
private ErrorPolicyManager(Context context, int slotId) {
mContext = context;
mSlotId = slotId;
LOG_TAG = ErrorPolicyManager.class.getSimpleName() + "[" + slotId + "]";
initHandler();
// read from default error policy config file
try {
mDefaultPolicies.putAll(readErrorPolicies(new JSONArray(getDefaultJSONConfig())));
} catch (IOException | JSONException | IllegalArgumentException e) {
throw new AssertionError(e);
}
carrierConfigErrorPolicyString = null;
readFromCarrierConfig(IwlanHelper.getCarrierId(mContext, mSlotId));
updateUnthrottlingEvents();
}
private ErrorPolicy findErrorPolicy(String apn, IwlanError iwlanError) {
ErrorPolicy policy = null;
if (mCarrierConfigPolicies.containsKey(apn)) {
policy = getPreferredErrorPolicy(mCarrierConfigPolicies.get(apn), iwlanError);
}
if (policy == null && mCarrierConfigPolicies.containsKey("*")) {
policy = getPreferredErrorPolicy(mCarrierConfigPolicies.get("*"), iwlanError);
}
if (policy == null && mDefaultPolicies.containsKey(apn)) {
policy = getPreferredErrorPolicy(mDefaultPolicies.get(apn), iwlanError);
}
if (policy == null && mDefaultPolicies.containsKey("*")) {
policy = getPreferredErrorPolicy(mDefaultPolicies.get("*"), iwlanError);
} else if (policy == null) {
// there should at least be one default policy defined in Default config
// that will apply to all errors.
logErrorPolicies();
throw new AssertionError("no Default policy defined in the config");
}
return policy;
}
private ErrorPolicy getPreferredErrorPolicy(
List errorPolicies, IwlanError iwlanError) {
ErrorPolicy selectedPolicy = null;
for (ErrorPolicy policy : errorPolicies) {
if (policy.match(iwlanError)) {
if (!policy.isFallback()) {
selectedPolicy = policy;
break;
}
if (selectedPolicy == null || policy.getErrorType() != GENERIC_ERROR_TYPE) {
selectedPolicy = policy;
}
}
}
return selectedPolicy;
}
private void initHandler() {
mHandlerThread = new HandlerThread("ErrorPolicyManagerThread");
mHandlerThread.start();
mHandler = new EpmHandler(mHandlerThread.getLooper());
}
private String getDefaultJSONConfig() throws IOException {
String str = "";
StringBuilder stringBuilder = new StringBuilder();
InputStream is = mContext.getAssets().open("defaultiwlanerrorconfig.json");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
while ((str = reader.readLine()) != null && str.length() > 0) {
// ignore the lines starting with '#' as they are intended to be
// comments
if (str.charAt(0) == '#') {
continue;
}
stringBuilder.append(str).append("\n");
}
is.close();
return stringBuilder.toString();
}
private Map> readErrorPolicies(JSONArray apnArray)
throws JSONException, IllegalArgumentException {
Map> errorPolicies = new HashMap<>();
for (int i = 0; i < apnArray.length(); i++) {
JSONObject apnDetails = apnArray.getJSONObject(i);
String apnName = ((String) apnDetails.get("ApnName")).trim();
JSONArray errorTypeArray = (JSONArray) apnDetails.get("ErrorTypes");
for (int j = 0; j < errorTypeArray.length(); j++) {
JSONObject errorTypeObject = errorTypeArray.getJSONObject(j);
String errorTypeStr = ((String) errorTypeObject.get("ErrorType")).trim();
JSONArray errorDetailArray = (JSONArray) errorTypeObject.get("ErrorDetails");
int errorType = UNKNOWN_ERROR_TYPE;
if ((errorType = getErrorPolicyErrorType(errorTypeStr)) == UNKNOWN_ERROR_TYPE) {
throw new IllegalArgumentException("Unknown error type in the parsing");
}
ErrorPolicy errorPolicy =
new ErrorPolicy(
errorType,
parseErrorDetails(errorType, errorDetailArray),
parseRetryArray((JSONArray) errorTypeObject.get("RetryArray")),
parseUnthrottlingEvents(
(JSONArray) errorTypeObject.get("UnthrottlingEvents")));
errorPolicies.putIfAbsent(apnName, new ArrayList());
errorPolicies.get(apnName).add(errorPolicy);
}
}
return errorPolicies;
}
private List parseRetryArray(JSONArray retryArray)
throws JSONException, IllegalArgumentException {
List ret = new ArrayList<>();
for (int i = 0; i < retryArray.length(); i++) {
String retryTime = retryArray.getString(i).trim();
// catch misplaced -1 retry times in the array.
// 1. if it is not placed at the last position in the array
// 2. if it is placed in the first position (catches the case where it is
// the only element.
if (retryTime.equals("-1") && (i != retryArray.length() - 1 || i == 0)) {
throw new IllegalArgumentException("Misplaced -1 in retry array");
}
if (TextUtils.isDigitsOnly(retryTime) || retryTime.equals("-1")) {
ret.add(Integer.parseInt(retryTime));
} else if (retryTime.contains("+r")) {
// randomized retry time
String[] times = retryTime.split("\\+r");
if (times.length == 2
&& TextUtils.isDigitsOnly(times[0])
&& TextUtils.isDigitsOnly(times[1])) {
ret.add(
Integer.parseInt(times[0])
+ (int) (Math.random() * Long.parseLong(times[1])));
} else {
throw new IllegalArgumentException(
"Randomized Retry time is not in acceptable format");
}
} else {
throw new IllegalArgumentException("Retry time is not in acceptable format");
}
}
return ret;
}
private List parseUnthrottlingEvents(JSONArray unthrottlingEvents)
throws JSONException, IllegalArgumentException {
List ret = new ArrayList<>();
for (int i = 0; i < unthrottlingEvents.length(); i++) {
int event =
IwlanEventListener.getUnthrottlingEvent(unthrottlingEvents.getString(i).trim());
if (event == IwlanEventListener.UNKNOWN_EVENT) {
throw new IllegalArgumentException(
"Unexpected unthrottlingEvent " + unthrottlingEvents.getString(i));
}
ret.add(event);
}
return ret;
}
private List parseErrorDetails(int errorType, JSONArray errorDetailArray)
throws JSONException, IllegalArgumentException {
List ret = new ArrayList<>();
boolean isValidErrorDetail = true;
for (int i = 0; i < errorDetailArray.length(); i++) {
String errorDetail = errorDetailArray.getString(i).trim();
switch (errorType) {
case IKE_PROTOCOL_ERROR_TYPE:
isValidErrorDetail = verifyIkeProtocolErrorDetail(errorDetail);
break;
case GENERIC_ERROR_TYPE:
isValidErrorDetail = verifyGenericErrorDetail(errorDetail);
break;
}
if (!isValidErrorDetail) {
throw new IllegalArgumentException(
"Invalid ErrorDetail: " + errorDetail + " for ErrorType: " + errorType);
}
ret.add(errorDetail);
}
return ret;
}
/** Allowed formats are: number(Integer), range(Integers separated by -) and "*" */
private boolean verifyIkeProtocolErrorDetail(String errorDetailStr) {
boolean ret = true;
if (errorDetailStr.contains("-")) {
// verify range format
String rangeNumbers[] = errorDetailStr.split("-");
if (rangeNumbers.length == 2) {
for (String range : rangeNumbers) {
if (!TextUtils.isDigitsOnly(range)) {
ret = false;
}
}
} else {
ret = false;
}
} else if (!errorDetailStr.equals("*") && !TextUtils.isDigitsOnly(errorDetailStr)) {
ret = false;
}
return ret;
}
/**
* Allowed strings are: "IO_EXCEPTION", "TIMEOUT_EXCEPTION", "SERVER_SELECTION_FAILED",
* "TUNNEL_TRANSFORM_FAILED" and "*"
*/
private boolean verifyGenericErrorDetail(String errorDetailStr) {
boolean ret = false;
for (String str : GENERIC_ERROR_DETAIL_STRINGS) {
if (errorDetailStr.equals(str)) {
ret = true;
break;
}
}
return ret;
}
private @ErrorPolicyErrorType int getErrorPolicyErrorType(String errorType) {
int ret = UNKNOWN_ERROR_TYPE;
switch (errorType) {
case "IKE_PROTOCOL_ERROR_TYPE":
ret = IKE_PROTOCOL_ERROR_TYPE;
break;
case "GENERIC_ERROR_TYPE":
ret = GENERIC_ERROR_TYPE;
break;
case "*":
ret = FALLBACK_ERROR_TYPE;
break;
}
return ret;
}
private synchronized Set getAllUnthrottlingEvents() {
Set events = new HashSet<>();
for (Map.Entry> entry : mCarrierConfigPolicies.entrySet()) {
List errorPolicies = entry.getValue();
for (ErrorPolicy errorPolicy : errorPolicies) {
events.addAll(errorPolicy.mUnthrottlingEvents);
}
}
for (Map.Entry> entry : mDefaultPolicies.entrySet()) {
List errorPolicies = entry.getValue();
for (ErrorPolicy errorPolicy : errorPolicies) {
events.addAll(errorPolicy.mUnthrottlingEvents);
}
}
events.add(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT);
return events;
}
/**
* This method is called once on initialization of this class And is also called from handler on
* CARRIER_CONFIG_CHANGED event. There is no race condition between both as we register for the
* events after the calling this method.
*/
private synchronized void readFromCarrierConfig(int currentCarrierId) {
String carrierConfigErrorPolicy =
(String) IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
if (carrierConfigErrorPolicy == null) {
Log.e(LOG_TAG, "ErrorPolicy from Carrier Config is NULL");
return;
}
try {
Map> errorPolicies =
readErrorPolicies(new JSONArray(carrierConfigErrorPolicy));
if (errorPolicies.size() > 0) {
carrierConfigErrorPolicyString = carrierConfigErrorPolicy;
carrierId = currentCarrierId;
mCarrierConfigPolicies.clear();
mCarrierConfigPolicies.putAll(errorPolicies);
}
} catch (JSONException | IllegalArgumentException e) {
Log.e(
LOG_TAG,
"Unable to parse the ErrorPolicy from CarrierConfig\n"
+ carrierConfigErrorPolicy);
if (mCarrierConfigPolicies != null) {
mCarrierConfigPolicies.clear();
}
carrierConfigErrorPolicyString = null;
e.printStackTrace();
}
}
private void updateUnthrottlingEvents() {
Set registerEvents, unregisterEvents;
unregisterEvents = mUnthrottlingEvents;
registerEvents = getAllUnthrottlingEvents();
mUnthrottlingEvents = getAllUnthrottlingEvents();
if (registerEvents != null && unregisterEvents != null) {
registerEvents.removeAll(unregisterEvents);
unregisterEvents.removeAll(mUnthrottlingEvents);
}
if (registerEvents != null) {
IwlanEventListener.getInstance(mContext, mSlotId)
.addEventListener(new ArrayList(registerEvents), mHandler);
}
if (unregisterEvents != null) {
IwlanEventListener.getInstance(mContext, mSlotId)
.removeEventListener(new ArrayList(unregisterEvents), mHandler);
}
Log.d(
LOG_TAG,
"UnthrottlingEvents: "
+ (mUnthrottlingEvents != null
? Arrays.toString(mUnthrottlingEvents.toArray())
: "null"));
}
private synchronized void unthrottleLastErrorOnEvent(int event) {
Log.d(LOG_TAG, "unthrottleLastErrorOnEvent: " + event);
if (event == IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT) {
mLastErrorForApn.clear();
return;
}
String apn;
for (Map.Entry entry : mLastErrorForApn.entrySet()) {
ErrorPolicy errorPolicy = entry.getValue().getErrorPolicy();
if (errorPolicy.canUnthrottle(event)) {
apn = entry.getKey();
mLastErrorForApn.remove(apn);
DataService.DataServiceProvider provider =
IwlanDataService.getDataServiceProvider(mSlotId);
if (provider != null) {
provider.notifyApnUnthrottled(apn);
}
Log.d(LOG_TAG, "unthrottled error for: " + apn);
}
}
}
@VisibleForTesting
ErrorStats getErrorStats() {
return mErrorStats;
}
class ErrorPolicy {
@ErrorPolicyErrorType int mErrorType;
List mErrorDetails;
List mRetryArray;
List mUnthrottlingEvents;
ErrorPolicy(
@ErrorPolicyErrorType int errorType,
List errorDetails,
List retryArray,
List unthrottlingEvents) {
mErrorType = errorType;
mErrorDetails = errorDetails;
mRetryArray = retryArray;
mUnthrottlingEvents = unthrottlingEvents;
}
long getRetryTime(int index) {
long retryTime = -1;
if (mRetryArray.size() > 0) {
// If the index is greater than or equal to the last element's index
// and if the last item in the retryArray is "-1" use the retryTime
// of the element before the last element to repeat the element.
if (index >= mRetryArray.size() - 1
&& mRetryArray.get(mRetryArray.size() - 1) == -1L) {
index = mRetryArray.size() - 2;
}
if (index >= 0 && index < mRetryArray.size()) {
retryTime = mRetryArray.get(index);
}
}
// retryTime -1 represents indefinite failure. In that case
// return time that represents 1 day to not retry for that day.
if (retryTime == -1L) {
retryTime = TimeUnit.DAYS.toSeconds(1);
}
return retryTime;
}
@ErrorPolicyErrorType
int getErrorType() {
return mErrorType;
}
synchronized boolean canUnthrottle(int event) {
return mUnthrottlingEvents.contains(event);
}
boolean match(IwlanError iwlanError) {
// Generic by default to match to generic policy.
String iwlanErrorDetail = "*";
if (mErrorType == FALLBACK_ERROR_TYPE) {
return true;
} else if (mErrorType == IKE_PROTOCOL_ERROR_TYPE
&& iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) {
IkeProtocolException exception = (IkeProtocolException) iwlanError.getException();
iwlanErrorDetail = String.valueOf(exception.getErrorType());
} else if (mErrorType == GENERIC_ERROR_TYPE) {
iwlanErrorDetail = getGenericErrorDetailString(iwlanError);
if (iwlanErrorDetail.equals("UNKNOWN")) {
return false;
}
} else {
return false;
}
boolean ret = false;
for (String errorDetail : mErrorDetails) {
if (mErrorType == IKE_PROTOCOL_ERROR_TYPE
&& iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION
&& errorDetail.contains("-")) {
// error detail is stored in range format.
// ErrorPolicyManager#verifyIkeProtocolErrorDetail will make sure that
// this is stored correctly in "min-max" format.
String range[] = errorDetail.split("-");
int min = Integer.parseInt(range[0]);
int max = Integer.parseInt(range[1]);
int error = Integer.parseInt(iwlanErrorDetail);
if (error >= min && error <= max) {
ret = true;
break;
}
} else if (errorDetail.equals(iwlanErrorDetail) || errorDetail.equals("*")) {
ret = true;
break;
}
}
return ret;
}
void log() {
Log.d(LOG_TAG, "ErrorType: " + mErrorType);
Log.d(LOG_TAG, "ErrorDetail: " + Arrays.toString(mErrorDetails.toArray()));
Log.d(LOG_TAG, "RetryArray: " + Arrays.toString(mRetryArray.toArray()));
Log.d(LOG_TAG, "unthrottlingEvents: " + Arrays.toString(mUnthrottlingEvents.toArray()));
}
boolean isFallback() {
if ((mErrorType == FALLBACK_ERROR_TYPE)
|| (mErrorDetails.size() == 1 && mErrorDetails.get(0).equals("*"))) {
return true;
}
return false;
}
String getGenericErrorDetailString(IwlanError iwlanError) {
String ret = "UNKNOWN";
switch (iwlanError.getErrorType()) {
case IwlanError.IKE_INTERNAL_IO_EXCEPTION:
ret = "IO_EXCEPTION";
break;
case IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED:
ret = "SERVER_SELECTION_FAILED";
break;
case IwlanError.TUNNEL_TRANSFORM_FAILED:
ret = "TUNNEL_TRANSFORM_FAILED";
break;
// TODO: Add TIMEOUT_EXCEPTION processing
}
return ret;
}
}
class ErrorInfo {
IwlanError mError;
ErrorPolicy mErrorPolicy;
int mCurrentRetryIndex;
long mLastErrorTime;
boolean mIsBackOffTimeValid = false;
long mBackOffTime;
ErrorInfo(IwlanError error, ErrorPolicy errorPolicy) {
mError = error;
mErrorPolicy = errorPolicy;
mCurrentRetryIndex = -1;
mLastErrorTime = new Date().getTime();
}
ErrorInfo(IwlanError error, ErrorPolicy errorPolicy, long backOffTime) {
mError = error;
mErrorPolicy = errorPolicy;
mCurrentRetryIndex = -1;
mIsBackOffTimeValid = true;
mBackOffTime = backOffTime;
mLastErrorTime = new Date().getTime();
}
/**
* Updates the current retry index and returns the retry time at new index position and also
* updates mLastErrorTime to current time. returns -1 if the index is out of bounds
*/
long updateCurrentRetryTime() {
if (mErrorPolicy == null) {
return -1;
}
long time = mErrorPolicy.getRetryTime(++mCurrentRetryIndex);
mLastErrorTime = new Date().getTime();
Log.d(LOG_TAG, "Current RetryArray index: " + mCurrentRetryIndex + " time: " + time);
return time;
}
/**
* Return the current retry time without changing the index. returns -1 if the index is out
* of bounds.
*/
long getCurrentRetryTime() {
long time = -1;
if (mIsBackOffTimeValid) {
time = TimeUnit.SECONDS.toMillis(mBackOffTime);
} else if (mErrorPolicy == null) {
return time;
} else {
time = TimeUnit.SECONDS.toMillis(mErrorPolicy.getRetryTime(mCurrentRetryIndex));
}
long currentTime = new Date().getTime();
time = Math.max(0, time - (currentTime - mLastErrorTime));
Log.d(
LOG_TAG,
"Current RetryArray index: " + mCurrentRetryIndex + " and time: " + time);
return time;
}
boolean isBackOffTimeValid() {
return mIsBackOffTimeValid;
}
void setBackOffTime(long backOffTime) {
mBackOffTime = backOffTime;
mLastErrorTime = new Date().getTime();
}
boolean canBringUpTunnel() {
long retryTime;
boolean ret = true;
if (mIsBackOffTimeValid) {
retryTime = TimeUnit.SECONDS.toMillis(mBackOffTime);
} else if (mErrorPolicy == null) {
return ret;
} else {
retryTime =
TimeUnit.SECONDS.toMillis(mErrorPolicy.getRetryTime(mCurrentRetryIndex));
}
long currentTime = new Date().getTime();
long timeDifference = currentTime - mLastErrorTime;
if (timeDifference < retryTime) {
ret = false;
}
return ret;
}
ErrorPolicy getErrorPolicy() {
return mErrorPolicy;
}
IwlanError getError() {
return mError;
}
}
private boolean isValidCarrierConfigChangedEvent(int currentCarrierId) {
String errorPolicyConfig =
(String) IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
boolean isValidEvent =
(currentCarrierId != carrierId)
|| (carrierConfigErrorPolicyString == null)
|| (errorPolicyConfig != null
&& !carrierConfigErrorPolicyString.equals(errorPolicyConfig));
return isValidEvent;
}
private final class EpmHandler extends Handler {
private final String TAG = EpmHandler.class.getSimpleName();
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "msg.what = " + msg.what);
switch (msg.what) {
case IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT:
Log.d(TAG, "On CARRIER_CONFIG_CHANGED_EVENT");
int currentCarrierId = IwlanHelper.getCarrierId(mContext, mSlotId);
if (isValidCarrierConfigChangedEvent(currentCarrierId)) {
Log.d(TAG, "Unthrottle last error and read from carrier config");
unthrottleLastErrorOnEvent(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT);
readFromCarrierConfig(currentCarrierId);
updateUnthrottlingEvents();
}
break;
case IwlanEventListener.APM_ENABLE_EVENT:
case IwlanEventListener.APM_DISABLE_EVENT:
case IwlanEventListener.WIFI_DISABLE_EVENT:
case IwlanEventListener.WIFI_CALLING_DISABLE_EVENT:
unthrottleLastErrorOnEvent(msg.what);
break;
default:
Log.d(TAG, "Unknown message received!");
break;
}
}
EpmHandler(Looper looper) {
super(looper);
}
}
@VisibleForTesting
class ErrorStats {
@VisibleForTesting Map> mStats = new HashMap<>();
private Date mStartTime;
private int mStatCount = 0;
private final int APN_COUNT_MAX = 10;
private final int ERROR_COUNT_MAX = 1000;
ErrorStats() {
mStartTime = Calendar.getInstance().getTime();
mStatCount = 0;
}
void update(String apn, IwlanError error) {
if (mStats.size() >= APN_COUNT_MAX || mStatCount >= ERROR_COUNT_MAX) {
reset();
}
if (!mStats.containsKey(apn)) {
mStats.put(apn, new HashMap());
}
Map errorMap = mStats.get(apn);
String errorString = error.toString();
if (!errorMap.containsKey(errorString)) {
errorMap.put(errorString, 0L);
}
long count = errorMap.get(errorString);
errorMap.put(errorString, ++count);
mStats.put(apn, errorMap);
mStatCount++;
}
void reset() {
mStartTime = Calendar.getInstance().getTime();
mStats = new HashMap>();
mStatCount = 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("mStartTime: " + mStartTime);
sb.append("\nErrorStats");
for (Map.Entry> entry : mStats.entrySet()) {
sb.append("\n\tApn: " + entry.getKey());
for (Map.Entry errorEntry : entry.getValue().entrySet()) {
sb.append("\n\t " + errorEntry.getKey() + " : " + errorEntry.getValue());
}
}
return sb.toString();
}
}
}