/* * 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 * *

This broadcast is not effective on user build. * *

Example: To override the current country * adb root * adb shell am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE * --es country us * *

To remove the override * adb root * adb shell am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE * --ez reset true */ 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 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 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 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 cellInfoList = (List) 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 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; } }