/* * 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.support.annotation.Nullable; 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 static final int IWLAN_NO_ERROR_RETRY_TIME = -1; private static final ErrorPolicy FALLBACK_ERROR_POLICY = builder() .setErrorType(FALLBACK_ERROR_TYPE) .setRetryArray(List.of(5, -1)) .setErrorDetails(List.of("*")) .setUnthrottlingEvents(List.of()) .build(); 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 {@link ApnRetryActionStore} associated with that APN */ private final Map mRetryActionStoreByApn = 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 mMostRecentError = new ApnWithIwlanError(apn, iwlanError); if (iwlanError.getErrorType() == IwlanError.NO_ERROR) { Log.d(LOG_TAG, "reportIwlanError: NO_ERROR"); mRetryActionStoreByApn.remove(apn); return IWLAN_NO_ERROR_RETRY_TIME; } mErrorStats.update(apn, iwlanError); PolicyDerivedRetryAction newRetryAction = mRetryActionStoreByApn .computeIfAbsent(apn, ApnRetryActionStore::new) .generateRetryAction(iwlanError); Log.d( LOG_TAG, "Current RetryAction index: " + newRetryAction.currentRetryIndex() + " and time: " + newRetryAction.totalRetryTimeMs()); return newRetryAction.totalRetryTimeMs() / 1000; } /** * Updates the last error details with backoff time. * * @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 {@link IwlanError#NO_ERROR} */ public synchronized long reportIwlanError(String apn, IwlanError iwlanError, long backoffTime) { // Fail by default if (iwlanError.getErrorType() == IwlanError.NO_ERROR) { Log.d(LOG_TAG, "reportIwlanError: NO_ERROR"); mRetryActionStoreByApn.remove(apn); return IWLAN_NO_ERROR_RETRY_TIME; } mErrorStats.update(apn, iwlanError); IkeBackoffNotifyRetryAction newRetryAction = mRetryActionStoreByApn .computeIfAbsent(apn, ApnRetryActionStore::new) .generateRetryAction(iwlanError, backoffTime); Log.d(LOG_TAG, "Current configured backoff time: " + newRetryAction.backoffTime()); return newRetryAction.backoffTime(); } /** * 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) { RetryAction lastRetryAction = getLastRetryAction(apn); boolean canBringUp = lastRetryAction == null || getRemainingRetryTimeMs(lastRetryAction) <= 0; Log.d(LOG_TAG, "canBringUpTunnel: " + canBringUp); return canBringUp; } // 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) { RetryAction lastRetryAction = getLastRetryAction(apn); return lastRetryAction == null ? DataFailCause.NONE : getDataFailCause(lastRetryAction.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 getRemainingRetryTimeMs(String apn) { RetryAction lastRetryAction = getLastRetryAction(apn); return lastRetryAction == null ? -1 : getRemainingRetryTimeMs(lastRetryAction); } /** * Get the remaining time in millis should be waited before retry, based on the current time and * the RetryAction. */ private static long getRemainingRetryTimeMs(RetryAction retryAction) { long totalRetryTimeMs = retryAction.totalRetryTimeMs(); long errorTime = retryAction.lastErrorTime(); long currentTime = IwlanHelper.elapsedRealtime(); return Math.max(0, totalRetryTimeMs - (currentTime - errorTime)); } /** * Gets the last error count of the APN * * @param apn the APN * @return the error count for the last error cause of the APN, 0 if no error or unthrottled */ public synchronized int getLastErrorCountOfSameCause(String apn) { RetryAction retryAction = getLastRetryAction(apn); return retryAction != null ? retryAction.errorCountOfSameCause() : 0; } /** * 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'. * This method assumes backoff time is not configured. * * @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; RetryAction lastRetryAction = getLastRetryAction(apn); return lastRetryAction == null ? -1 : lastRetryAction.getCurrentFqdnIndex(numFqdns); } @Nullable private synchronized RetryAction getLastRetryAction(String apn) { ApnRetryActionStore retryActionStore = mRetryActionStoreByApn.get(apn); return retryActionStore == null ? null : retryActionStore.getLastRetryAction(); } /** * 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) { RetryAction lastRetryAction = getLastRetryAction(apn); return lastRetryAction == null ? new IwlanError(IwlanError.NO_ERROR) : lastRetryAction.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) { RetryAction retryAction = getLastRetryAction(apn); return retryAction != null && retryAction.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 ----"); mRetryActionStoreByApn.forEach( (apn, retryActionStore) -> { pw.println("APN: " + apn); pw.println("Last RetryAction: " + retryActionStore.getLastRetryAction()); retryActionStore.mLastRetryActionByCause.forEach( (cause, retryAction) -> { pw.println(cause); pw.println(retryAction); }); }); 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); } if (policy == null) { // there should at least be one default policy defined in Default config // that will apply to all errors. // should not reach here in any situation, default config should be configured in // defaultiwlanerrorconfig.json. here is just for prevent runtime exception logErrorPolicies(); Log.e(LOG_TAG, "No matched error policy"); policy = FALLBACK_ERROR_POLICY; } 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); // Pass the other events to RetryActionStore to check if can unthrottle mRetryActionStoreByApn.forEach( (apn, retryActionStore) -> retryActionStore.handleUnthrottlingEvent(event)); // Carrier Config Changed should clear all RetryActionStore if (event == IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT) { mRetryActionStoreByApn.clear(); } } @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; } } /** * A data class to store the error cause and the applied error policy. This class is responsible * to calculate the retry time base on the error policy / config. */ interface RetryAction { IwlanError error(); ErrorPolicy errorPolicy(); long lastErrorTime(); /** The total time should be waited between lastErrorTime and next retry. */ long totalRetryTimeMs(); /** The number of same cause error observed since last success / unthrottle event. */ int errorCountOfSameCause(); boolean shouldRetryWithInitialAttach(); int getCurrentFqdnIndex(int numFqdns); } /** RetryAction with retry time defined by retry index and error policy */ @AutoValue abstract static class PolicyDerivedRetryAction implements RetryAction { abstract int currentRetryIndex(); @Override public long totalRetryTimeMs() { return TimeUnit.SECONDS.toMillis(errorPolicy().getRetryTime(currentRetryIndex())); } @Override public int getCurrentFqdnIndex(int numFqdns) { ErrorPolicy errorPolicy = errorPolicy(); return errorPolicy.getCurrentFqdnIndex(currentRetryIndex(), numFqdns); } @Override public 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. ErrorPolicy errorPolicy = errorPolicy(); return errorPolicy.getErrorType() == IKE_PROTOCOL_ERROR_TYPE && currentRetryIndex() + 1 >= errorPolicy.getHandoverAttemptCount(); } /** Create a new PolicyDerivedRetryAction */ static PolicyDerivedRetryAction create( IwlanError error, ErrorPolicy errorPolicy, int errorCountOfSameCause, int currentRetryIndex) { return new AutoValue_ErrorPolicyManager_PolicyDerivedRetryAction( error, errorPolicy, IwlanHelper.elapsedRealtime(), errorCountOfSameCause, currentRetryIndex); } } /** RetryAction with retry time defined by backoff time in tunnel config */ @AutoValue abstract static class IkeBackoffNotifyRetryAction implements RetryAction { abstract long backoffTime(); @Override public long totalRetryTimeMs() { return TimeUnit.SECONDS.toMillis(backoffTime()); } @Override public int getCurrentFqdnIndex(int numFqdns) { // Not applicable for backoff time configured case, therefore returning 0 here return 0; } @Override public boolean shouldRetryWithInitialAttach() { // TODO(b/308745683): Initial attach condition is undefined for backoff config case ErrorPolicy errorPolicy = errorPolicy(); return errorPolicy.getErrorType() == IKE_PROTOCOL_ERROR_TYPE && errorPolicy.getHandoverAttemptCount() == 0; } static IkeBackoffNotifyRetryAction create( IwlanError error, ErrorPolicy errorPolicy, int errorCountOfSameCause, long backoffTime) { return new AutoValue_ErrorPolicyManager_IkeBackoffNotifyRetryAction( error, errorPolicy, IwlanHelper.elapsedRealtime(), errorCountOfSameCause, backoffTime); } } interface ErrorCause { @IwlanError.IwlanErrorType int iwlanErrorType(); static ErrorCause fromIwlanError(IwlanError iwlanError) { if (iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) { return new AutoValue_ErrorPolicyManager_IkeProtocolErrorCause( /* ikeProtocolErrorType= */ ((IkeProtocolException) iwlanError.getException()) .getErrorType()); } return new AutoValue_ErrorPolicyManager_NonIkeProtocolErrorCause( /* iwlanErrorType= */ iwlanError.getErrorType()); } } @AutoValue abstract static class NonIkeProtocolErrorCause implements ErrorCause {} /** * An IkeProtocolErrorCause will carry the ike protocol error type, so that different protocol * error will be treated as different error cause */ @AutoValue abstract static class IkeProtocolErrorCause implements ErrorCause { @Override @IwlanError.IwlanErrorType public int iwlanErrorType() { return IwlanError.IKE_PROTOCOL_EXCEPTION; } // @IkeProtocolException.ErrorType is hidden API abstract int ikeProtocolErrorType(); } /** * This class manage and store the RetryAction of the APN, and responsible to create RetryAction * when IwlanError received. */ class ApnRetryActionStore { final String mApn; final ConcurrentHashMap mLastRetryActionByCause; @Nullable RetryAction mLastRetryAction; ApnRetryActionStore(String apn) { mApn = apn; mLastRetryActionByCause = new ConcurrentHashMap<>(); } /** * Determines whether the new {@link RetryAction} should accumulate the retry index from * {@code prevRetryAction}. * * @param prevRetryAction the previous RetryAction (can be null). * @param newIwlanError the new IwlanError. * @return true if {@code prevRetryAction} is an instance of {@link * PolicyDerivedRetryAction} and is the same {@link ErrorCause} as {@code * newIwlanError}, false otherwise. */ private boolean shouldAccumulateRetryIndex( @Nullable RetryAction prevRetryAction, IwlanError newIwlanError) { if (!(prevRetryAction instanceof PolicyDerivedRetryAction)) { return false; } boolean isSameIwlanError = prevRetryAction.error().equals(newIwlanError); // If prev and current error are both IKE_PROTOCOL_EXCEPTION, keep the retry index // TODO: b/292312000 - Workaround for RetryIndex lost boolean areBothIkeProtocolException = (newIwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION && prevRetryAction.error().getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION); boolean shouldAccumulateRetryIndex = isSameIwlanError || areBothIkeProtocolException; if (!shouldAccumulateRetryIndex) { Log.d(LOG_TAG, "Doesn't match to the previous error" + newIwlanError); } return shouldAccumulateRetryIndex; } private PolicyDerivedRetryAction generateRetryAction(IwlanError iwlanError) { ErrorCause errorCause = ErrorCause.fromIwlanError(iwlanError); @Nullable RetryAction prevRetryAction = mLastRetryActionByCause.get(errorCause); int newErrorCount = prevRetryAction != null ? prevRetryAction.errorCountOfSameCause() + 1 : 1; boolean shouldAccumulateRetryIndex = shouldAccumulateRetryIndex(prevRetryAction, iwlanError); int newRetryIndex = shouldAccumulateRetryIndex ? ((PolicyDerivedRetryAction) prevRetryAction).currentRetryIndex() + 1 : 0; ErrorPolicy policy = findErrorPolicy(mApn, iwlanError); PolicyDerivedRetryAction newRetryAction = PolicyDerivedRetryAction.create( iwlanError, policy, newErrorCount, newRetryIndex); mLastRetryActionByCause.put(errorCause, newRetryAction); mLastRetryAction = newRetryAction; return newRetryAction; } private IkeBackoffNotifyRetryAction generateRetryAction( IwlanError iwlanError, long backoffTime) { ErrorCause errorCause = ErrorCause.fromIwlanError(iwlanError); @Nullable RetryAction prevRetryAction = mLastRetryActionByCause.get(errorCause); int newErrorCount = prevRetryAction != null ? prevRetryAction.errorCountOfSameCause() + 1 : 1; ErrorPolicy policy = findErrorPolicy(mApn, iwlanError); // For configured back off time case, simply create new RetryAction, nothing need to // keep IkeBackoffNotifyRetryAction newRetryAction = IkeBackoffNotifyRetryAction.create( iwlanError, policy, newErrorCount, backoffTime); mLastRetryActionByCause.put(errorCause, newRetryAction); mLastRetryAction = newRetryAction; return newRetryAction; } /** * Set {@code lastRetryAction} to null if {@code lastRetryAction} can be unthrottled by the * event. Clear those reserved retry index and the {@link RetryAction} if any {@link * RetryAction} in {@code mLastRetryActionByCause} can be unthrottled by the event. * * @param event the handling event */ private void handleUnthrottlingEvent(int event) { if (event == IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT) { mLastRetryActionByCause.clear(); } else { // Check all stored RetryAction, remove from the store if it can be unthrottle. // By removing it, the retry index (for PolicyDerived) will reset as 0 mLastRetryActionByCause .entrySet() .removeIf(it -> it.getValue().errorPolicy().canUnthrottle(event)); } DataService.DataServiceProvider provider = IwlanDataService.getDataServiceProvider(mSlotId); boolean isCarrierConfigChanged = event == IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT; boolean isLastRetryActionCanUnthrottle = mLastRetryAction != null && mLastRetryAction.errorPolicy().canUnthrottle(event); if (isCarrierConfigChanged || isLastRetryActionCanUnthrottle) { mLastRetryAction = null; if (provider == null) { Log.w(LOG_TAG, "DataServiceProvider not found for slot: " + mSlotId); } else { provider.notifyApnUnthrottled(mApn); Log.d(LOG_TAG, "unthrottled error for: " + mApn); } } } @Nullable private RetryAction getLastRetryAction() { return mLastRetryAction; } } 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: case IwlanEventListener.WIFI_AP_CHANGED_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(); } } }