/* * 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 com.google.auto.value.AutoValue; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; 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.Objects; import java.util.Optional; 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 - 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; static ErrorPolicy.Builder builder() { return new AutoValue_ErrorPolicyManager_ErrorPolicy.Builder() .setInfiniteRetriesWithLastRetryTime(false); } @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; /** Private IKEv2 notify message types, as defined in TS 124 502 (section 9.2.4.1) */ private static final int IKE_PROTOCOL_ERROR_CONGESTION = 15500; private final String LOG_TAG; private static final Map mInstances = new ConcurrentHashMap<>(); private final Context mContext; private final 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 final Map> mCarrierConfigPolicies = new HashMap<>(); // String APN as key to identify the ErrorInfo associated with that APN private final Map mLastErrorForApn = new ConcurrentHashMap<>(); // Records the most recently reported IwlanError (including NO_ERROR), and the corresponding // APN. private ApnWithIwlanError mMostRecentError; // List of current Unthrottling events registered with IwlanEventListener private Set mUnthrottlingEvents; private final ErrorStats mErrorStats = new ErrorStats(); private HandlerThread mHandlerThread; @VisibleForTesting Handler mHandler; private int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID; private String mCarrierConfigErrorPolicyString; @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)); } @VisibleForTesting public static void resetAllInstances() { mInstances.clear(); } /** * Release or reset the instance. */ 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; mMostRecentError = new ApnWithIwlanError(apn, iwlanError); 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); ErrorPolicy policy = findErrorPolicy(apn, iwlanError); ErrorInfo errorInfo = new ErrorInfo(iwlanError, policy); if (mLastErrorForApn.containsKey(apn)) { ErrorInfo prevErrorInfo = mLastErrorForApn.get(apn); IwlanError prevIwlanError = prevErrorInfo.getError(); // If prev and current error are both IKE_PROTOCOL_EXCEPTION, keep the retry index // TODO: b/292312000 - Workaround for RetryIndex lost if (iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION && prevIwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) { errorInfo.setCurrentRetryIndex(prevErrorInfo.getCurrentRetryIndex()); } } 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 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); 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(); return getDataFailCause(error); } private int getDataFailCause(IwlanError error) { int ret = DataFailCause.ERROR_UNSPECIFIED; if (error.getErrorType() == IwlanError.NO_ERROR) { ret = DataFailCause.NONE; } else if (error.getErrorType() == IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED) { ret = DataFailCause.IWLAN_DNS_RESOLUTION_NAME_FAILURE; } else if (error.getErrorType() == IwlanError.EPDG_ADDRESS_ONLY_IPV4_ALLOWED) { ret = DataFailCause.ONLY_IPV4_ALLOWED; } else if (error.getErrorType() == IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED) { ret = DataFailCause.ONLY_IPV6_ALLOWED; } else if (error.getErrorType() == IwlanError.IKE_INTERNAL_IO_EXCEPTION) { ret = DataFailCause.IWLAN_IKEV2_MSG_TIMEOUT; } else if (error.getErrorType() == IwlanError.SIM_NOT_READY_EXCEPTION) { ret = DataFailCause.SIM_CARD_CHANGED; } else if (error.getErrorType() == IwlanError.IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED) { ret = DataFailCause.IWLAN_IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED; } else if (error.getErrorType() == IwlanError.TUNNEL_NOT_FOUND) { ret = DataFailCause.IWLAN_TUNNEL_NOT_FOUND; } else if (error.getErrorType() == IwlanError.IKE_INIT_TIMEOUT) { ret = DataFailCause.IWLAN_IKE_INIT_TIMEOUT; } else if (error.getErrorType() == IwlanError.IKE_MOBILITY_TIMEOUT) { ret = DataFailCause.IWLAN_IKE_MOBILITY_TIMEOUT; } else if (error.getErrorType() == IwlanError.IKE_DPD_TIMEOUT) { ret = DataFailCause.IWLAN_IKE_DPD_TIMEOUT; } else if (error.getErrorType() == IwlanError.TUNNEL_TRANSFORM_FAILED) { ret = DataFailCause.IWLAN_TUNNEL_TRANSFORM_FAILED; } else if (error.getErrorType() == IwlanError.IKE_NETWORK_LOST_EXCEPTION) { ret = DataFailCause.IWLAN_IKE_NETWORK_LOST_EXCEPTION; } else if (error.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) { Exception exception = error.getException(); if (exception instanceof IkeProtocolException) { int protocolErrorType = ((IkeProtocolException) exception).getErrorType(); switch (protocolErrorType) { case IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED: ret = DataFailCause.IWLAN_IKEV2_AUTH_FAILURE; break; case IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE: ret = DataFailCause.IWLAN_EPDG_INTERNAL_ADDRESS_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; case IKE_PROTOCOL_ERROR_CONGESTION: ret = DataFailCause.IWLAN_CONGESTION; break; default: ret = DataFailCause.IWLAN_IKE_PRIVATE_PROTOCOL_ERROR; break; } } } return ret; } public synchronized int getMostRecentDataFailCause() { if (mMostRecentError != null) { return getDataFailCause(mMostRecentError.mIwlanError); } return DataFailCause.NONE; } /** * 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 index of the FQDN to use for ePDG server selection, based on how many FQDNs are * available, the position of the RetryArray index, and configuration of 'NumAttemptsPerFqdn'. * * @param numFqdns number of FQDNs discovered during ePDG server selection. * @return int index of the FQDN to use for ePDG server selection. -1 (invalid) if RetryArray or * 'NumAttemptsPerFqdn' is not specified in the ErrorPolicy. */ public synchronized int getCurrentFqdnIndex(int numFqdns) { String apn = mMostRecentError.mApn; if (!mLastErrorForApn.containsKey(apn)) { return -1; } ErrorInfo errorInfo = mLastErrorForApn.get(apn); return errorInfo.getCurrentFqdnIndex(numFqdns); } /** * 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); } /** * Returns whether framework should retry tunnel setup with initial PDN bringup request when * handover request fails. * * @param apn apn name * @return boolean result of whether framework should retry tunnel setup with initial PDN * bringup request when handover request fails */ public synchronized boolean shouldRetryWithInitialAttach(String apn) { ErrorInfo errorInfo = mLastErrorForApn.get(apn); return errorInfo != null && errorInfo.shouldRetryWithInitialAttach(); } 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(PrintWriter pw) { 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); } mCarrierConfigErrorPolicyString = 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; } @VisibleForTesting void initHandler() { mHandler = new EpmHandler(getLooper()); } @VisibleForTesting Looper getLooper() { mHandlerThread = new HandlerThread("ErrorPolicyManagerThread"); mHandlerThread.start(); return 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(); } @VisibleForTesting 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; if ((errorType = getErrorPolicyErrorType(errorTypeStr)) == UNKNOWN_ERROR_TYPE) { throw new IllegalArgumentException("Unknown error type in the parsing"); } List retryArray = parseRetryArray((JSONArray) errorTypeObject.get("RetryArray")); ErrorPolicy.Builder errorPolicyBuilder = builder() .setErrorType(errorType) .setErrorDetails(parseErrorDetails(errorType, errorDetailArray)) .setRetryArray(retryArray) .setUnthrottlingEvents( parseUnthrottlingEvents( (JSONArray) errorTypeObject.get("UnthrottlingEvents"))); if (!retryArray.isEmpty() && retryArray.get(retryArray.size() - 1) == -1L) { errorPolicyBuilder.setInfiniteRetriesWithLastRetryTime(true); } if (errorTypeObject.has("NumAttemptsPerFqdn")) { errorPolicyBuilder.setNumAttemptsPerFqdn( errorTypeObject.getInt("NumAttemptsPerFqdn")); } if (errorTypeObject.has("HandoverAttemptCount")) { if (errorType != IKE_PROTOCOL_ERROR_TYPE) { throw new IllegalArgumentException( "Handover attempt count should not be applied when errorType is not" + " explicitly defined as IKE_PROTOCOL_ERROR_TYPE"); } errorPolicyBuilder.setHandoverAttemptCount( errorTypeObject.getInt("HandoverAttemptCount")); } ErrorPolicy errorPolicy = errorPolicyBuilder.build(); 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.unthrottlingEvents()); } } for (Map.Entry> entry : mDefaultPolicies.entrySet()) { List errorPolicies = entry.getValue(); for (ErrorPolicy errorPolicy : errorPolicies) { events.addAll(errorPolicy.unthrottlingEvents()); } } 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 = IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId); if (carrierConfigErrorPolicy == null) { Log.e(LOG_TAG, "ErrorPolicy from Carrier Config is NULL"); mCarrierConfigPolicies.clear(); mCarrierConfigErrorPolicyString = null; return; } try { Map> errorPolicies = readErrorPolicies(new JSONArray(carrierConfigErrorPolicy)); if (errorPolicies.size() > 0) { mCarrierConfigErrorPolicyString = 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); mCarrierConfigPolicies.clear(); mCarrierConfigErrorPolicyString = null; e.printStackTrace(); } } private void updateUnthrottlingEvents() { Set registerEvents, unregisterEvents; unregisterEvents = mUnthrottlingEvents; registerEvents = getAllUnthrottlingEvents(); mUnthrottlingEvents = getAllUnthrottlingEvents(); if (unregisterEvents != null) { registerEvents.removeAll(unregisterEvents); unregisterEvents.removeAll(mUnthrottlingEvents); } 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; } @AutoValue abstract static class ErrorPolicy { private static final String LOG_TAG = ErrorPolicyManager.class.getSimpleName(); abstract @ErrorPolicyErrorType int errorType(); abstract List errorDetails(); abstract List retryArray(); abstract Boolean infiniteRetriesWithLastRetryTime(); abstract List unthrottlingEvents(); abstract Optional numAttemptsPerFqdn(); abstract Optional handoverAttemptCount(); @AutoValue.Builder abstract static class Builder { abstract Builder setErrorType(int errorType); abstract Builder setErrorDetails(List errorDetails); abstract Builder setRetryArray(List retryArray); abstract Builder setInfiniteRetriesWithLastRetryTime( Boolean infiniteRetriesWithLastRetryTime); abstract Builder setUnthrottlingEvents(List unthrottlingEvents); abstract Builder setNumAttemptsPerFqdn(Integer numAttemptsPerFqdn); abstract Builder setHandoverAttemptCount(Integer handoverAttemptCount); abstract ErrorPolicy build(); } long getRetryTime(int index) { long retryTime = -1; if (retryArray().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 (infiniteRetriesWithLastRetryTime()) { index = Math.min(index, retryArray().size() - 2); } if (index >= 0 && index < retryArray().size()) { retryTime = retryArray().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; } int getCurrentFqdnIndex(int retryIndex, int numFqdns) { int result = -1; if (numAttemptsPerFqdn().isEmpty() || retryArray().size() <= 0) { return result; } // Cycles between 0 and (numFqdns - 1), based on the current attempt count and size of // mRetryArray. return (retryIndex + 1) / numAttemptsPerFqdn().get() % numFqdns; } @ErrorPolicyErrorType int getErrorType() { return errorType(); } int getHandoverAttemptCount() { return handoverAttemptCount().orElse(Integer.MAX_VALUE); } synchronized boolean canUnthrottle(int event) { return unthrottlingEvents().contains(event); } boolean match(IwlanError iwlanError) { // Generic by default to match to generic policy. String iwlanErrorDetail; if (errorType() == FALLBACK_ERROR_TYPE) { return true; } else if (errorType() == IKE_PROTOCOL_ERROR_TYPE && iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) { IkeProtocolException exception = (IkeProtocolException) iwlanError.getException(); iwlanErrorDetail = String.valueOf(exception.getErrorType()); } else if (errorType() == GENERIC_ERROR_TYPE) { iwlanErrorDetail = getGenericErrorDetailString(iwlanError); if (iwlanErrorDetail.equals("UNKNOWN")) { return false; } } else { return false; } boolean ret = false; for (String errorDetail : errorDetails()) { if (errorType() == 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: " + errorType()); Log.d(LOG_TAG, "ErrorDetail: " + Arrays.toString(errorDetails().toArray())); Log.d(LOG_TAG, "RetryArray: " + Arrays.toString(retryArray().toArray())); Log.d( LOG_TAG, "InfiniteRetriesWithLastRetryTime: " + infiniteRetriesWithLastRetryTime()); Log.d( LOG_TAG, "UnthrottlingEvents: " + Arrays.toString(unthrottlingEvents().toArray())); Log.d(LOG_TAG, "NumAttemptsPerFqdn: " + numAttemptsPerFqdn()); Log.d(LOG_TAG, "handoverAttemptCount: " + handoverAttemptCount()); } boolean isFallback() { return (errorType() == FALLBACK_ERROR_TYPE) || (errorDetails().size() == 1 && errorDetails().get(0).equals("*")); } 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; case IwlanError.IKE_NETWORK_LOST_EXCEPTION: ret = "IKE_NETWORK_LOST_EXCEPTION"; break; case IwlanError.EPDG_ADDRESS_ONLY_IPV4_ALLOWED: ret = "EPDG_ADDRESS_ONLY_IPV4_ALLOWED"; break; case IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED: ret = "EPDG_ADDRESS_ONLY_IPV6_ALLOWED"; break; // TODO: Add TIMEOUT_EXCEPTION processing case IwlanError.IKE_INIT_TIMEOUT: ret = "IKE_INIT_TIMEOUT"; break; case IwlanError.IKE_MOBILITY_TIMEOUT: ret = "IKE_MOBILITY_TIMEOUT"; break; case IwlanError.IKE_DPD_TIMEOUT: ret = "IKE_DPD_TIMEOUT"; break; } return ret; } } class ErrorInfo { IwlanError mError; ErrorPolicy mErrorPolicy; // For the lifetime of the ErrorInfo object, this is a monotonically incremented value that // can go beyond the size of mErrorPolicy's mRetryArray. int mCurrentRetryIndex; long mLastErrorTime; boolean mIsBackOffTimeValid; long mBackOffTime; ErrorInfo(IwlanError error, ErrorPolicy errorPolicy) { mError = error; mErrorPolicy = errorPolicy; mCurrentRetryIndex = -1; mLastErrorTime = IwlanHelper.elapsedRealtime(); } ErrorInfo(IwlanError error, ErrorPolicy errorPolicy, long backOffTime) { mError = error; mErrorPolicy = errorPolicy; mCurrentRetryIndex = -1; mIsBackOffTimeValid = true; mBackOffTime = backOffTime; mLastErrorTime = IwlanHelper.elapsedRealtime(); } int getCurrentRetryIndex() { return mCurrentRetryIndex; } void setCurrentRetryIndex(int currentRetryIndex) { mCurrentRetryIndex = currentRetryIndex; } /** * 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 = IwlanHelper.elapsedRealtime(); 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 = IwlanHelper.elapsedRealtime(); time = Math.max(0, time - (currentTime - mLastErrorTime)); Log.d( LOG_TAG, "Current RetryArray index: " + mCurrentRetryIndex + " and time: " + time); return time; } int getCurrentFqdnIndex(int numFqdns) { ErrorPolicy errorPolicy = getErrorPolicy(); return errorPolicy.getCurrentFqdnIndex(mCurrentRetryIndex, numFqdns); } boolean isBackOffTimeValid() { return mIsBackOffTimeValid; } void setBackOffTime(long backOffTime) { mBackOffTime = backOffTime; mLastErrorTime = IwlanHelper.elapsedRealtime(); } 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 = IwlanHelper.elapsedRealtime(); long timeDifference = currentTime - mLastErrorTime; if (timeDifference < retryTime) { ret = false; } return ret; } boolean shouldRetryWithInitialAttach() { // UE should only uses initial attach to reset network failure, not for UE internal or // DNS errors. When the number of handover failures due to network issues exceeds the // configured threshold, UE should request network with initial attach instead of // handover request. return mErrorPolicy.getErrorType() == IKE_PROTOCOL_ERROR_TYPE && mCurrentRetryIndex + 1 >= mErrorPolicy.getHandoverAttemptCount(); } ErrorPolicy getErrorPolicy() { return mErrorPolicy; } IwlanError getError() { return mError; } } static class ApnWithIwlanError { @NonNull final String mApn; @NonNull final IwlanError mIwlanError; ApnWithIwlanError(@NonNull String apn, @NonNull IwlanError iwlanError) { mApn = apn; mIwlanError = iwlanError; } } private boolean isValidCarrierConfigChangedEvent(int currentCarrierId) { String errorPolicyConfig = IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId); return (currentCarrierId != carrierId) || (mCarrierConfigErrorPolicyString == null) || (errorPolicyConfig != null && !Objects.equals(mCarrierConfigErrorPolicyString, errorPolicyConfig)); } 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 static class ErrorStats { @VisibleForTesting Map> mStats = new HashMap<>(); private Date mStartTime; private int mStatCount; private static final int APN_COUNT_MAX = 10; private static 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: ").append(mStartTime); sb.append("\nErrorStats"); for (Map.Entry> entry : mStats.entrySet()) { sb.append("\n\tApn: ").append(entry.getKey()); for (Map.Entry errorEntry : entry.getValue().entrySet()) { sb.append("\n\t ") .append(errorEntry.getKey()) .append(" : ") .append(errorEntry.getValue()); } } return sb.toString(); } } }