/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.telephony;

import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.sysprop.TelephonyProperties;
import android.telephony.CellInfo;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.LocalLog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.MccTable.MccMnc;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.telephony.Rlog;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * The locale tracker keeps tracking the current locale of the phone.
 */
public class LocaleTracker extends Handler {
    private static final boolean DBG = true;

    /** Event for getting cell info from the modem */
    private static final int EVENT_REQUEST_CELL_INFO = 1;

    /** Event for service state changed */
    private static final int EVENT_SERVICE_STATE_CHANGED = 2;

    /** Event for sim state changed */
    private static final int EVENT_SIM_STATE_CHANGED = 3;

    /** Event for incoming unsolicited cell info */
    private static final int EVENT_UNSOL_CELL_INFO = 4;

    /** Event for incoming cell info */
    private static final int EVENT_RESPONSE_CELL_INFO = 5;

    /** Event to fire if the operator from ServiceState is considered truly lost */
    private static final int EVENT_OPERATOR_LOST = 6;

    /** Event to override the current locale */
    private static final int EVENT_OVERRIDE_LOCALE = 7;

    /**
     * The broadcast intent action to override the current country for testing purposes
     *
     * <p> This broadcast is not effective on user build.
     *
     * <p>Example: To override the current country <code>
     * adb root
     * adb shell am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE
     * --es country us </code>
     *
     * <p> To remove the override <code>
     * adb root
     * adb shell am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE
     * --ez reset true</code>
     */
    private static final String ACTION_COUNTRY_OVERRIDE =
            "com.android.internal.telephony.action.COUNTRY_OVERRIDE";

    /** The extra for country override */
    private static final String EXTRA_COUNTRY = "country";

    /** The extra for country override reset */
    private static final String EXTRA_RESET = "reset";

    // Todo: Read this from Settings.
    /** The minimum delay to get cell info from the modem */
    private static final long CELL_INFO_MIN_DELAY_MS = 2 * SECOND_IN_MILLIS;

    // Todo: Read this from Settings.
    /** The maximum delay to get cell info from the modem */
    private static final long CELL_INFO_MAX_DELAY_MS = 10 * MINUTE_IN_MILLIS;

    // Todo: Read this from Settings.
    /** The delay for periodically getting cell info from the modem */
    private static final long CELL_INFO_PERIODIC_POLLING_DELAY_MS = 10 * MINUTE_IN_MILLIS;

    /**
     * The delay after the last time the device camped on a cell before declaring that the
     * ServiceState's MCC information can no longer be used (and thus kicking in the CellInfo
     * based tracking.
     */
    private static final long SERVICE_OPERATOR_LOST_DELAY_MS = 10 * MINUTE_IN_MILLIS;

    /** The maximum fail count to prevent delay time overflow */
    private static final int MAX_FAIL_COUNT = 30;

    /** The last known country iso */
    private static final String LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY =
            "last_known_country_iso";

    private String mTag;

    private final Phone mPhone;

    private final NitzStateMachine mNitzStateMachine;

    /** SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX */
    private int mSimState;

    /** Current serving PLMN's MCC/MNC */
    @Nullable
    private String mOperatorNumeric;

    /** Current cell tower information */
    @Nullable
    private List<CellInfo> mCellInfoList;

    /** Count of invalid cell info we've got so far. Will reset once we get a successful one */
    private int mFailCellInfoCount;

    /** The ISO-3166 two-letter code of device's current country */
    @Nullable
    private String mCurrentCountryIso;

    @NonNull private final FeatureFlags mFeatureFlags;

    /** The country override for testing purposes */
    @Nullable
    private String mCountryOverride;

    /** Current service state. Must be one of ServiceState.STATE_XXX. */
    private int mLastServiceState = ServiceState.STATE_POWER_OFF;

    private boolean mIsTracking = false;

    private final LocalLog mLocalLog = new LocalLog(32, false /* useLocalTimestamps */);

    /** Broadcast receiver to get SIM card state changed event */
    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(intent.getAction())) {
                int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, 0);
                if (phoneId == mPhone.getPhoneId()) {
                    obtainMessage(EVENT_SIM_STATE_CHANGED,
                            intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE,
                                    TelephonyManager.SIM_STATE_UNKNOWN), 0).sendToTarget();
                }
            } else if (ACTION_COUNTRY_OVERRIDE.equals(intent.getAction())) {
                // note: need to set ServiceStateTracker#PROP_FORCE_ROAMING to force roaming.
                String countryOverride = intent.getStringExtra(EXTRA_COUNTRY);
                boolean reset = intent.getBooleanExtra(EXTRA_RESET, false);
                if (reset) countryOverride = null;
                log("Received country override: " + countryOverride);
                // countryOverride null to reset the override.
                obtainMessage(EVENT_OVERRIDE_LOCALE, countryOverride).sendToTarget();
            }
        }
    };

    /**
     * Message handler
     *
     * @param msg The message
     */
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case EVENT_REQUEST_CELL_INFO:
                mPhone.requestCellInfoUpdate(null, obtainMessage(EVENT_RESPONSE_CELL_INFO));
                break;

            case EVENT_UNSOL_CELL_INFO:
                processCellInfo((AsyncResult) msg.obj);
                // If the unsol happened to be useful, use it; otherwise, pretend it didn't happen.
                if (mCellInfoList != null && mCellInfoList.size() > 0) requestNextCellInfo(true);
                break;

            case EVENT_RESPONSE_CELL_INFO:
                processCellInfo((AsyncResult) msg.obj);
                // If the cellInfo was non-empty then it's business as usual. Either way, this
                // cell info was requested by us, so it's our trigger to schedule another one.
                requestNextCellInfo(mCellInfoList != null && mCellInfoList.size() > 0);
                break;

            case EVENT_SERVICE_STATE_CHANGED:
                AsyncResult ar = (AsyncResult) msg.obj;
                onServiceStateChanged((ServiceState) ar.result);
                break;

            case EVENT_SIM_STATE_CHANGED:
                onSimCardStateChanged(msg.arg1);
                break;

            case EVENT_OPERATOR_LOST:
                updateOperatorNumericImmediate("");
                updateTrackingStatus();
                break;

            case EVENT_OVERRIDE_LOCALE:
                mCountryOverride = (String) msg.obj;
                updateLocale();
                break;

            default:
                throw new IllegalStateException("Unexpected message arrives. msg = " + msg.what);
        }
    }

    /**
     * Constructor
     *
     * @param phone The phone object
     * @param nitzStateMachine NITZ state machine
     * @param looper The looper message handler
     */
    public LocaleTracker(Phone phone, NitzStateMachine nitzStateMachine, Looper looper,
            FeatureFlags featureFlags)  {
        super(looper);
        mPhone = phone;
        mNitzStateMachine = nitzStateMachine;
        mSimState = TelephonyManager.SIM_STATE_UNKNOWN;
        mTag = LocaleTracker.class.getSimpleName() + "-" + mPhone.getPhoneId();
        mFeatureFlags = featureFlags;

        final IntentFilter filter = new IntentFilter();
        filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
        if (TelephonyUtils.IS_DEBUGGABLE) {
            filter.addAction(ACTION_COUNTRY_OVERRIDE);
        }
        mPhone.getContext().registerReceiver(mBroadcastReceiver, filter);

        mPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
        mPhone.registerForCellInfo(this, EVENT_UNSOL_CELL_INFO, null);
    }

    /**
     * Get the device's current country.
     *
     * @return The device's current country. Empty string if the information is not available.
     */
    @NonNull
    public String getCurrentCountry() {
        return (mCurrentCountryIso != null) ? mCurrentCountryIso : "";
    }

    /**
     * Get the MCC from cell tower information.
     *
     * @return MCC in string format. Null if the information is not available.
     */
    @Nullable
    private String getMccFromCellInfo() {
        String selectedMcc = null;
        if (mCellInfoList != null) {
            Map<String, Integer> mccMap = new HashMap<>();
            int maxCount = 0;
            for (CellInfo cellInfo : mCellInfoList) {
                String mcc = cellInfo.getCellIdentity().getMccString();
                if (mcc != null) {
                    int count = 1;
                    if (mccMap.containsKey(mcc)) {
                        count = mccMap.get(mcc) + 1;
                    }
                    mccMap.put(mcc, count);
                    // This is unlikely, but if MCC from cell info looks different, we choose the
                    // MCC that occurs most.
                    if (count > maxCount) {
                        maxCount = count;
                        selectedMcc = mcc;
                    }
                }
            }
        }
        return selectedMcc;
    }

    /**
     * Get the most frequent MCC + MNC combination with the specified MCC using cell tower
     * information. If no one combination is more frequent than any other an arbitrary MCC + MNC is
     * returned with the matching MCC. The MNC value returned can be null if it is not provided by
     * the cell tower information.
     *
     * @param mccToMatch the MCC to match
     * @return a matching {@link MccMnc}. Null if the information is not available.
     */
    @Nullable
    private MccMnc getMccMncFromCellInfo(@NonNull String mccToMatch) {
        MccMnc selectedMccMnc = null;
        if (mCellInfoList != null) {
            Map<MccMnc, Integer> mccMncMap = new HashMap<>();
            int maxCount = 0;
            for (CellInfo cellInfo : mCellInfoList) {
                String mcc = cellInfo.getCellIdentity().getMccString();
                if (Objects.equals(mcc, mccToMatch)) {
                    String mnc = cellInfo.getCellIdentity().getMncString();
                    MccMnc mccMnc = new MccMnc(mcc, mnc);
                    int count = 1;
                    if (mccMncMap.containsKey(mccMnc)) {
                        count = mccMncMap.get(mccMnc) + 1;
                    }
                    mccMncMap.put(mccMnc, count);
                    // We keep track of the MCC+MNC combination that occurs most frequently, if
                    // there is one. A null MNC is treated like any other distinct MCC+MNC
                    // combination.
                    if (count > maxCount) {
                        maxCount = count;
                        selectedMccMnc = mccMnc;
                    }
                }
            }
        }
        return selectedMccMnc;
    }

    /**
     * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get
     * cell info from the network. Other SIM states like NOT_READY might be just a transitioning
     * state.
     *
     * @param state SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX.
     */
    private void onSimCardStateChanged(int state) {
        mSimState = state;
        updateLocale();
        updateTrackingStatus();
    }

    /**
     * Called when service state changed.
     *
     * @param serviceState Service state
     */
    private void onServiceStateChanged(ServiceState serviceState) {
        mLastServiceState = serviceState.getState();
        updateLocale();
        updateTrackingStatus();
    }

    /**
     * Update MCC/MNC from network service state.
     *
     * @param operatorNumeric MCC/MNC of the operator
     */
    public void updateOperatorNumeric(String operatorNumeric) {
        if (TextUtils.isEmpty(operatorNumeric)) {
            if (!hasMessages(EVENT_OPERATOR_LOST)) {
                sendMessageDelayed(obtainMessage(EVENT_OPERATOR_LOST),
                        SERVICE_OPERATOR_LOST_DELAY_MS);
            }
        } else {
            removeMessages(EVENT_OPERATOR_LOST);
            updateOperatorNumericImmediate(operatorNumeric);
        }
    }

    private void updateOperatorNumericImmediate(String operatorNumeric) {
        // Check if the operator numeric changes.
        if (!operatorNumeric.equals(mOperatorNumeric)) {
            String msg = "Operator numeric changes to \"" + operatorNumeric + "\"";
            if (DBG) log(msg);
            mLocalLog.log(msg);
            mOperatorNumeric = operatorNumeric;
            updateLocale();
        }
    }

    private void processCellInfo(AsyncResult ar) {
        if (ar == null || ar.exception != null) {
            mCellInfoList = null;
            return;
        }
        List<CellInfo> cellInfoList = (List<CellInfo>) ar.result;
        String msg = "processCellInfo: cell info=" + cellInfoList;
        if (DBG) log(msg);
        mCellInfoList = cellInfoList;
        updateLocale();
    }

    private void requestNextCellInfo(boolean succeeded) {
        if (!mIsTracking) return;

        removeMessages(EVENT_REQUEST_CELL_INFO);
        if (succeeded) {
            resetCellInfoRetry();
            // Now we need to get the cell info from the modem periodically
            // even if we already got the cell info because the user can move.
            removeMessages(EVENT_UNSOL_CELL_INFO);
            removeMessages(EVENT_RESPONSE_CELL_INFO);
            sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO),
                    CELL_INFO_PERIODIC_POLLING_DELAY_MS);
        } else {
            // If we can't get a valid cell info. Try it again later.
            long delay = getCellInfoDelayTime(++mFailCellInfoCount);
            if (DBG) log("Can't get cell info. Try again in " + delay / 1000 + " secs.");
            sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO), delay);
        }
    }

    /**
     * Get the delay time to get cell info from modem. The delay time grows exponentially to prevent
     * battery draining.
     *
     * @param failCount Count of invalid cell info we've got so far.
     * @return The delay time for next get cell info
     */
    @VisibleForTesting
    public static long getCellInfoDelayTime(int failCount) {
        // Exponentially grow the delay time. Note we limit the fail count to MAX_FAIL_COUNT to
        // prevent overflow in Math.pow().
        long delay = CELL_INFO_MIN_DELAY_MS
                * (long) Math.pow(2, Math.min(failCount, MAX_FAIL_COUNT) - 1);
        return Math.min(Math.max(delay, CELL_INFO_MIN_DELAY_MS), CELL_INFO_MAX_DELAY_MS);
    }

    /**
     * Stop retrying getting cell info from the modem. It cancels any scheduled cell info retrieving
     * request.
     */
    private void resetCellInfoRetry() {
        mFailCellInfoCount = 0;
        removeMessages(EVENT_REQUEST_CELL_INFO);
    }

    private void updateTrackingStatus() {
        boolean shouldTrackLocale =
                (mSimState == TelephonyManager.SIM_STATE_ABSENT
                        || TextUtils.isEmpty(mOperatorNumeric))
                && (mLastServiceState == ServiceState.STATE_OUT_OF_SERVICE
                        || mLastServiceState == ServiceState.STATE_EMERGENCY_ONLY);
        if (shouldTrackLocale) {
            startTracking();
        } else {
            stopTracking();
        }
    }

    private void stopTracking() {
        if (!mIsTracking) return;
        mIsTracking = false;
        String msg = "Stopping LocaleTracker";
        if (DBG) log(msg);
        mLocalLog.log(msg);
        mCellInfoList = null;
        resetCellInfoRetry();
    }

    private void startTracking() {
        if (mIsTracking) return;
        String msg = "Starting LocaleTracker";
        mLocalLog.log(msg);
        if (DBG) log(msg);
        mIsTracking = true;
        sendMessage(obtainMessage(EVENT_REQUEST_CELL_INFO));
    }

    /**
     * Update the device's current locale
     */
    private synchronized void updateLocale() {
        // If MCC is available from network service state, use it first.
        String countryIso = "";
        String countryIsoDebugInfo = "empty as default";

        if (!TextUtils.isEmpty(mOperatorNumeric)) {
            MccMnc mccMnc = MccMnc.fromOperatorNumeric(mOperatorNumeric);
            if (mccMnc != null) {
                countryIso = MccTable.geoCountryCodeForMccMnc(mccMnc);
                countryIsoDebugInfo = "OperatorNumeric(" + mOperatorNumeric
                        + "): MccTable.geoCountryCodeForMccMnc(\"" + mccMnc + "\")";
            } else {
                loge("updateLocale: Can't get country from operator numeric. mOperatorNumeric = "
                        + mOperatorNumeric);
            }
        }

        // If for any reason we can't get country from operator numeric, try to get it from cell
        // info.
        if (TextUtils.isEmpty(countryIso)) {
            // Find the most prevalent MCC from surrounding cell towers.
            String mcc = getMccFromCellInfo();
            if (mcc != null) {
                countryIso = MccTable.countryCodeForMcc(mcc);
                countryIsoDebugInfo = "CellInfo: MccTable.countryCodeForMcc(\"" + mcc + "\")";

                // Some MCC+MNC combinations are known to be used in countries other than those
                // that the MCC alone would suggest. Do a second pass of nearby cells that match
                // the most frequently observed MCC to see if this could be one of those cases.
                MccMnc mccMnc = getMccMncFromCellInfo(mcc);
                if (mccMnc != null) {
                    countryIso = MccTable.geoCountryCodeForMccMnc(mccMnc);
                    countryIsoDebugInfo =
                            "CellInfo: MccTable.geoCountryCodeForMccMnc(" + mccMnc + ")";
                }
            }
        }

        if (mCountryOverride != null) {
            countryIso = mCountryOverride;
            countryIsoDebugInfo = "mCountryOverride = \"" + mCountryOverride + "\"";
        }

        if (!mPhone.isRadioOn()) {
            countryIso = "";
            countryIsoDebugInfo = "radio off";

            // clear cell infos, we don't know where the next network to camp on.
            mCellInfoList = null;
        }

        log("updateLocale: countryIso = " + countryIso
                + ", countryIsoDebugInfo = " + countryIsoDebugInfo);
        if (!Objects.equals(countryIso, mCurrentCountryIso)) {
            String msg = "updateLocale: Change the current country to \"" + countryIso + "\""
                    + ", countryIsoDebugInfo = " + countryIsoDebugInfo
                    + ", mCellInfoList = " + mCellInfoList;
            log(msg);
            mLocalLog.log(msg);
            mCurrentCountryIso = countryIso;

            // Update the last known country ISO
            if (!TextUtils.isEmpty(mCurrentCountryIso)) {
                updateLastKnownCountryIso(mCurrentCountryIso);
            }

            int phoneId = mPhone.getPhoneId();
            if (SubscriptionManager.isValidPhoneId(phoneId)) {
                List<String> newProp = new ArrayList<>(
                        TelephonyProperties.operator_iso_country());
                while (newProp.size() <= phoneId) newProp.add(null);
                newProp.set(phoneId, mCurrentCountryIso);
                TelephonyProperties.operator_iso_country(newProp);
            }

            if (mFeatureFlags.oemEnabledSatelliteFlag()) {
                TelephonyCountryDetector.getInstance(mPhone.getContext())
                        .onNetworkCountryCodeChanged(mPhone, countryIso);
            }
            Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
            intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, countryIso);
            intent.putExtra(TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
                    getLastKnownCountryIso());
            SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
            mPhone.getContext().sendBroadcastAsUser(intent, UserHandle.ALL);
        }

        // Pass the geographical country information to the telephony time zone detection code.

        String timeZoneCountryIso = countryIso;
        String timeZoneCountryIsoDebugInfo = countryIsoDebugInfo;
        boolean isTestMcc = false;
        if (!TextUtils.isEmpty(mOperatorNumeric)) {
            // For a test cell (MCC 001), the NitzStateMachine requires handleCountryDetected("") in
            // order to pass compliance tests. http://b/142840879
            if (mOperatorNumeric.startsWith("001")) {
                isTestMcc = true;
                timeZoneCountryIso = "";
                timeZoneCountryIsoDebugInfo = "Test cell: " + mOperatorNumeric;
            }
        }
        log("updateLocale: timeZoneCountryIso = " + timeZoneCountryIso
                + ", timeZoneCountryIsoDebugInfo = " + timeZoneCountryIsoDebugInfo);

        if (TextUtils.isEmpty(timeZoneCountryIso) && !isTestMcc) {
            mNitzStateMachine.handleCountryUnavailable();
        } else {
            mNitzStateMachine.handleCountryDetected(timeZoneCountryIso);
        }
    }

    /** Exposed for testing purposes */
    public boolean isTracking() {
        return mIsTracking;
    }

    private void updateLastKnownCountryIso(String countryIso) {
        if (!TextUtils.isEmpty(countryIso)) {
            final SharedPreferences prefs = mPhone.getContext().getSharedPreferences(
                    LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, Context.MODE_PRIVATE);
            final SharedPreferences.Editor editor = prefs.edit();
            editor.putString(LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, countryIso);
            editor.commit();
            log("update country iso in sharedPrefs " + countryIso);
        }
    }

    /**
     *  Return the last known country ISO before device is not camping on a network
     *  (e.g. Airplane Mode)
     *
     *  @return The device's last known country ISO.
     */
    @NonNull
    public String getLastKnownCountryIso() {
        final SharedPreferences prefs = mPhone.getContext().getSharedPreferences(
                LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, Context.MODE_PRIVATE);
        return prefs.getString(LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, "");
    }

    private void log(String msg) {
        Rlog.d(mTag, msg);
    }

    private void loge(String msg) {
        Rlog.e(mTag, msg);
    }

    /**
     * Print the DeviceStateMonitor into the given stream.
     *
     * @param fd The raw file descriptor that the dump is being sent to.
     * @param pw A PrintWriter to which the dump is to be set.
     * @param args Additional arguments to the dump request.
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
        pw.println("LocaleTracker-" + mPhone.getPhoneId() + ":");
        ipw.increaseIndent();
        ipw.println("mIsTracking = " + mIsTracking);
        ipw.println("mOperatorNumeric = " + mOperatorNumeric);
        ipw.println("mSimState = " + mSimState);
        ipw.println("mCellInfoList = " + mCellInfoList);
        ipw.println("mCurrentCountryIso = " + mCurrentCountryIso);
        ipw.println("mFailCellInfoCount = " + mFailCellInfoCount);
        ipw.println("Local logs:");
        ipw.increaseIndent();
        mLocalLog.dump(fd, ipw, args);
        ipw.decreaseIndent();
        ipw.decreaseIndent();
        ipw.flush();
    }

    /**
     *  This getter should only be used for testing purposes in classes that wish to spoof the
     *  country ISO. An example of how this can be done is in ServiceStateTracker#InSameCountry
     * @return spoofed country iso.
     */
    @VisibleForTesting
    public String getCountryOverride() {
        return mCountryOverride;
    }
}
