1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony;
18 
19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.SharedPreferences;
29 import android.os.AsyncResult;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.os.UserHandle;
34 import android.sysprop.TelephonyProperties;
35 import android.telephony.CellInfo;
36 import android.telephony.ServiceState;
37 import android.telephony.SubscriptionManager;
38 import android.telephony.TelephonyManager;
39 import android.text.TextUtils;
40 import android.util.LocalLog;
41 
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.internal.telephony.MccTable.MccMnc;
44 import com.android.internal.telephony.flags.FeatureFlags;
45 import com.android.internal.telephony.util.TelephonyUtils;
46 import com.android.internal.util.IndentingPrintWriter;
47 import com.android.telephony.Rlog;
48 
49 import java.io.FileDescriptor;
50 import java.io.PrintWriter;
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Objects;
56 
57 /**
58  * The locale tracker keeps tracking the current locale of the phone.
59  */
60 public class LocaleTracker extends Handler {
61     private static final boolean DBG = true;
62 
63     /** Event for getting cell info from the modem */
64     private static final int EVENT_REQUEST_CELL_INFO = 1;
65 
66     /** Event for service state changed */
67     private static final int EVENT_SERVICE_STATE_CHANGED = 2;
68 
69     /** Event for sim state changed */
70     private static final int EVENT_SIM_STATE_CHANGED = 3;
71 
72     /** Event for incoming unsolicited cell info */
73     private static final int EVENT_UNSOL_CELL_INFO = 4;
74 
75     /** Event for incoming cell info */
76     private static final int EVENT_RESPONSE_CELL_INFO = 5;
77 
78     /** Event to fire if the operator from ServiceState is considered truly lost */
79     private static final int EVENT_OPERATOR_LOST = 6;
80 
81     /** Event to override the current locale */
82     private static final int EVENT_OVERRIDE_LOCALE = 7;
83 
84     /**
85      * The broadcast intent action to override the current country for testing purposes
86      *
87      * <p> This broadcast is not effective on user build.
88      *
89      * <p>Example: To override the current country <code>
90      * adb root
91      * adb shell am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE
92      * --es country us </code>
93      *
94      * <p> To remove the override <code>
95      * adb root
96      * adb shell am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE
97      * --ez reset true</code>
98      */
99     private static final String ACTION_COUNTRY_OVERRIDE =
100             "com.android.internal.telephony.action.COUNTRY_OVERRIDE";
101 
102     /** The extra for country override */
103     private static final String EXTRA_COUNTRY = "country";
104 
105     /** The extra for country override reset */
106     private static final String EXTRA_RESET = "reset";
107 
108     // Todo: Read this from Settings.
109     /** The minimum delay to get cell info from the modem */
110     private static final long CELL_INFO_MIN_DELAY_MS = 2 * SECOND_IN_MILLIS;
111 
112     // Todo: Read this from Settings.
113     /** The maximum delay to get cell info from the modem */
114     private static final long CELL_INFO_MAX_DELAY_MS = 10 * MINUTE_IN_MILLIS;
115 
116     // Todo: Read this from Settings.
117     /** The delay for periodically getting cell info from the modem */
118     private static final long CELL_INFO_PERIODIC_POLLING_DELAY_MS = 10 * MINUTE_IN_MILLIS;
119 
120     /**
121      * The delay after the last time the device camped on a cell before declaring that the
122      * ServiceState's MCC information can no longer be used (and thus kicking in the CellInfo
123      * based tracking.
124      */
125     private static final long SERVICE_OPERATOR_LOST_DELAY_MS = 10 * MINUTE_IN_MILLIS;
126 
127     /** The maximum fail count to prevent delay time overflow */
128     private static final int MAX_FAIL_COUNT = 30;
129 
130     /** The last known country iso */
131     private static final String LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY =
132             "last_known_country_iso";
133 
134     private String mTag;
135 
136     private final Phone mPhone;
137 
138     private final NitzStateMachine mNitzStateMachine;
139 
140     /** SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX */
141     private int mSimState;
142 
143     /** Current serving PLMN's MCC/MNC */
144     @Nullable
145     private String mOperatorNumeric;
146 
147     /** Current cell tower information */
148     @Nullable
149     private List<CellInfo> mCellInfoList;
150 
151     /** Count of invalid cell info we've got so far. Will reset once we get a successful one */
152     private int mFailCellInfoCount;
153 
154     /** The ISO-3166 two-letter code of device's current country */
155     @Nullable
156     private String mCurrentCountryIso;
157 
158     @NonNull private final FeatureFlags mFeatureFlags;
159 
160     /** The country override for testing purposes */
161     @Nullable
162     private String mCountryOverride;
163 
164     /** Current service state. Must be one of ServiceState.STATE_XXX. */
165     private int mLastServiceState = ServiceState.STATE_POWER_OFF;
166 
167     private boolean mIsTracking = false;
168 
169     private final LocalLog mLocalLog = new LocalLog(32, false /* useLocalTimestamps */);
170 
171     /** Broadcast receiver to get SIM card state changed event */
172     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
173         @Override
174         public void onReceive(Context context, Intent intent) {
175             if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(intent.getAction())) {
176                 int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, 0);
177                 if (phoneId == mPhone.getPhoneId()) {
178                     obtainMessage(EVENT_SIM_STATE_CHANGED,
179                             intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE,
180                                     TelephonyManager.SIM_STATE_UNKNOWN), 0).sendToTarget();
181                 }
182             } else if (ACTION_COUNTRY_OVERRIDE.equals(intent.getAction())) {
183                 // note: need to set ServiceStateTracker#PROP_FORCE_ROAMING to force roaming.
184                 String countryOverride = intent.getStringExtra(EXTRA_COUNTRY);
185                 boolean reset = intent.getBooleanExtra(EXTRA_RESET, false);
186                 if (reset) countryOverride = null;
187                 log("Received country override: " + countryOverride);
188                 // countryOverride null to reset the override.
189                 obtainMessage(EVENT_OVERRIDE_LOCALE, countryOverride).sendToTarget();
190             }
191         }
192     };
193 
194     /**
195      * Message handler
196      *
197      * @param msg The message
198      */
199     @Override
handleMessage(Message msg)200     public void handleMessage(Message msg) {
201         switch (msg.what) {
202             case EVENT_REQUEST_CELL_INFO:
203                 mPhone.requestCellInfoUpdate(null, obtainMessage(EVENT_RESPONSE_CELL_INFO));
204                 break;
205 
206             case EVENT_UNSOL_CELL_INFO:
207                 processCellInfo((AsyncResult) msg.obj);
208                 // If the unsol happened to be useful, use it; otherwise, pretend it didn't happen.
209                 if (mCellInfoList != null && mCellInfoList.size() > 0) requestNextCellInfo(true);
210                 break;
211 
212             case EVENT_RESPONSE_CELL_INFO:
213                 processCellInfo((AsyncResult) msg.obj);
214                 // If the cellInfo was non-empty then it's business as usual. Either way, this
215                 // cell info was requested by us, so it's our trigger to schedule another one.
216                 requestNextCellInfo(mCellInfoList != null && mCellInfoList.size() > 0);
217                 break;
218 
219             case EVENT_SERVICE_STATE_CHANGED:
220                 AsyncResult ar = (AsyncResult) msg.obj;
221                 onServiceStateChanged((ServiceState) ar.result);
222                 break;
223 
224             case EVENT_SIM_STATE_CHANGED:
225                 onSimCardStateChanged(msg.arg1);
226                 break;
227 
228             case EVENT_OPERATOR_LOST:
229                 updateOperatorNumericImmediate("");
230                 updateTrackingStatus();
231                 break;
232 
233             case EVENT_OVERRIDE_LOCALE:
234                 mCountryOverride = (String) msg.obj;
235                 updateLocale();
236                 break;
237 
238             default:
239                 throw new IllegalStateException("Unexpected message arrives. msg = " + msg.what);
240         }
241     }
242 
243     /**
244      * Constructor
245      *
246      * @param phone The phone object
247      * @param nitzStateMachine NITZ state machine
248      * @param looper The looper message handler
249      */
LocaleTracker(Phone phone, NitzStateMachine nitzStateMachine, Looper looper, FeatureFlags featureFlags)250     public LocaleTracker(Phone phone, NitzStateMachine nitzStateMachine, Looper looper,
251             FeatureFlags featureFlags)  {
252         super(looper);
253         mPhone = phone;
254         mNitzStateMachine = nitzStateMachine;
255         mSimState = TelephonyManager.SIM_STATE_UNKNOWN;
256         mTag = LocaleTracker.class.getSimpleName() + "-" + mPhone.getPhoneId();
257         mFeatureFlags = featureFlags;
258 
259         final IntentFilter filter = new IntentFilter();
260         filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
261         if (TelephonyUtils.IS_DEBUGGABLE) {
262             filter.addAction(ACTION_COUNTRY_OVERRIDE);
263         }
264         mPhone.getContext().registerReceiver(mBroadcastReceiver, filter);
265 
266         mPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
267         mPhone.registerForCellInfo(this, EVENT_UNSOL_CELL_INFO, null);
268     }
269 
270     /**
271      * Get the device's current country.
272      *
273      * @return The device's current country. Empty string if the information is not available.
274      */
275     @NonNull
getCurrentCountry()276     public String getCurrentCountry() {
277         return (mCurrentCountryIso != null) ? mCurrentCountryIso : "";
278     }
279 
280     /**
281      * Get the MCC from cell tower information.
282      *
283      * @return MCC in string format. Null if the information is not available.
284      */
285     @Nullable
getMccFromCellInfo()286     private String getMccFromCellInfo() {
287         String selectedMcc = null;
288         if (mCellInfoList != null) {
289             Map<String, Integer> mccMap = new HashMap<>();
290             int maxCount = 0;
291             for (CellInfo cellInfo : mCellInfoList) {
292                 String mcc = cellInfo.getCellIdentity().getMccString();
293                 if (mcc != null) {
294                     int count = 1;
295                     if (mccMap.containsKey(mcc)) {
296                         count = mccMap.get(mcc) + 1;
297                     }
298                     mccMap.put(mcc, count);
299                     // This is unlikely, but if MCC from cell info looks different, we choose the
300                     // MCC that occurs most.
301                     if (count > maxCount) {
302                         maxCount = count;
303                         selectedMcc = mcc;
304                     }
305                 }
306             }
307         }
308         return selectedMcc;
309     }
310 
311     /**
312      * Get the most frequent MCC + MNC combination with the specified MCC using cell tower
313      * information. If no one combination is more frequent than any other an arbitrary MCC + MNC is
314      * returned with the matching MCC. The MNC value returned can be null if it is not provided by
315      * the cell tower information.
316      *
317      * @param mccToMatch the MCC to match
318      * @return a matching {@link MccMnc}. Null if the information is not available.
319      */
320     @Nullable
getMccMncFromCellInfo(@onNull String mccToMatch)321     private MccMnc getMccMncFromCellInfo(@NonNull String mccToMatch) {
322         MccMnc selectedMccMnc = null;
323         if (mCellInfoList != null) {
324             Map<MccMnc, Integer> mccMncMap = new HashMap<>();
325             int maxCount = 0;
326             for (CellInfo cellInfo : mCellInfoList) {
327                 String mcc = cellInfo.getCellIdentity().getMccString();
328                 if (Objects.equals(mcc, mccToMatch)) {
329                     String mnc = cellInfo.getCellIdentity().getMncString();
330                     MccMnc mccMnc = new MccMnc(mcc, mnc);
331                     int count = 1;
332                     if (mccMncMap.containsKey(mccMnc)) {
333                         count = mccMncMap.get(mccMnc) + 1;
334                     }
335                     mccMncMap.put(mccMnc, count);
336                     // We keep track of the MCC+MNC combination that occurs most frequently, if
337                     // there is one. A null MNC is treated like any other distinct MCC+MNC
338                     // combination.
339                     if (count > maxCount) {
340                         maxCount = count;
341                         selectedMccMnc = mccMnc;
342                     }
343                 }
344             }
345         }
346         return selectedMccMnc;
347     }
348 
349     /**
350      * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get
351      * cell info from the network. Other SIM states like NOT_READY might be just a transitioning
352      * state.
353      *
354      * @param state SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX.
355      */
onSimCardStateChanged(int state)356     private void onSimCardStateChanged(int state) {
357         mSimState = state;
358         updateLocale();
359         updateTrackingStatus();
360     }
361 
362     /**
363      * Called when service state changed.
364      *
365      * @param serviceState Service state
366      */
onServiceStateChanged(ServiceState serviceState)367     private void onServiceStateChanged(ServiceState serviceState) {
368         mLastServiceState = serviceState.getState();
369         updateLocale();
370         updateTrackingStatus();
371     }
372 
373     /**
374      * Update MCC/MNC from network service state.
375      *
376      * @param operatorNumeric MCC/MNC of the operator
377      */
updateOperatorNumeric(String operatorNumeric)378     public void updateOperatorNumeric(String operatorNumeric) {
379         if (TextUtils.isEmpty(operatorNumeric)) {
380             if (!hasMessages(EVENT_OPERATOR_LOST)) {
381                 sendMessageDelayed(obtainMessage(EVENT_OPERATOR_LOST),
382                         SERVICE_OPERATOR_LOST_DELAY_MS);
383             }
384         } else {
385             removeMessages(EVENT_OPERATOR_LOST);
386             updateOperatorNumericImmediate(operatorNumeric);
387         }
388     }
389 
updateOperatorNumericImmediate(String operatorNumeric)390     private void updateOperatorNumericImmediate(String operatorNumeric) {
391         // Check if the operator numeric changes.
392         if (!operatorNumeric.equals(mOperatorNumeric)) {
393             String msg = "Operator numeric changes to \"" + operatorNumeric + "\"";
394             if (DBG) log(msg);
395             mLocalLog.log(msg);
396             mOperatorNumeric = operatorNumeric;
397             updateLocale();
398         }
399     }
400 
processCellInfo(AsyncResult ar)401     private void processCellInfo(AsyncResult ar) {
402         if (ar == null || ar.exception != null) {
403             mCellInfoList = null;
404             return;
405         }
406         List<CellInfo> cellInfoList = (List<CellInfo>) ar.result;
407         String msg = "processCellInfo: cell info=" + cellInfoList;
408         if (DBG) log(msg);
409         mCellInfoList = cellInfoList;
410         updateLocale();
411     }
412 
requestNextCellInfo(boolean succeeded)413     private void requestNextCellInfo(boolean succeeded) {
414         if (!mIsTracking) return;
415 
416         removeMessages(EVENT_REQUEST_CELL_INFO);
417         if (succeeded) {
418             resetCellInfoRetry();
419             // Now we need to get the cell info from the modem periodically
420             // even if we already got the cell info because the user can move.
421             removeMessages(EVENT_UNSOL_CELL_INFO);
422             removeMessages(EVENT_RESPONSE_CELL_INFO);
423             sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO),
424                     CELL_INFO_PERIODIC_POLLING_DELAY_MS);
425         } else {
426             // If we can't get a valid cell info. Try it again later.
427             long delay = getCellInfoDelayTime(++mFailCellInfoCount);
428             if (DBG) log("Can't get cell info. Try again in " + delay / 1000 + " secs.");
429             sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO), delay);
430         }
431     }
432 
433     /**
434      * Get the delay time to get cell info from modem. The delay time grows exponentially to prevent
435      * battery draining.
436      *
437      * @param failCount Count of invalid cell info we've got so far.
438      * @return The delay time for next get cell info
439      */
440     @VisibleForTesting
getCellInfoDelayTime(int failCount)441     public static long getCellInfoDelayTime(int failCount) {
442         // Exponentially grow the delay time. Note we limit the fail count to MAX_FAIL_COUNT to
443         // prevent overflow in Math.pow().
444         long delay = CELL_INFO_MIN_DELAY_MS
445                 * (long) Math.pow(2, Math.min(failCount, MAX_FAIL_COUNT) - 1);
446         return Math.min(Math.max(delay, CELL_INFO_MIN_DELAY_MS), CELL_INFO_MAX_DELAY_MS);
447     }
448 
449     /**
450      * Stop retrying getting cell info from the modem. It cancels any scheduled cell info retrieving
451      * request.
452      */
resetCellInfoRetry()453     private void resetCellInfoRetry() {
454         mFailCellInfoCount = 0;
455         removeMessages(EVENT_REQUEST_CELL_INFO);
456     }
457 
updateTrackingStatus()458     private void updateTrackingStatus() {
459         boolean shouldTrackLocale =
460                 (mSimState == TelephonyManager.SIM_STATE_ABSENT
461                         || TextUtils.isEmpty(mOperatorNumeric))
462                 && (mLastServiceState == ServiceState.STATE_OUT_OF_SERVICE
463                         || mLastServiceState == ServiceState.STATE_EMERGENCY_ONLY);
464         if (shouldTrackLocale) {
465             startTracking();
466         } else {
467             stopTracking();
468         }
469     }
470 
stopTracking()471     private void stopTracking() {
472         if (!mIsTracking) return;
473         mIsTracking = false;
474         String msg = "Stopping LocaleTracker";
475         if (DBG) log(msg);
476         mLocalLog.log(msg);
477         mCellInfoList = null;
478         resetCellInfoRetry();
479     }
480 
startTracking()481     private void startTracking() {
482         if (mIsTracking) return;
483         String msg = "Starting LocaleTracker";
484         mLocalLog.log(msg);
485         if (DBG) log(msg);
486         mIsTracking = true;
487         sendMessage(obtainMessage(EVENT_REQUEST_CELL_INFO));
488     }
489 
490     /**
491      * Update the device's current locale
492      */
updateLocale()493     private synchronized void updateLocale() {
494         // If MCC is available from network service state, use it first.
495         String countryIso = "";
496         String countryIsoDebugInfo = "empty as default";
497 
498         if (!TextUtils.isEmpty(mOperatorNumeric)) {
499             MccMnc mccMnc = MccMnc.fromOperatorNumeric(mOperatorNumeric);
500             if (mccMnc != null) {
501                 countryIso = MccTable.geoCountryCodeForMccMnc(mccMnc);
502                 countryIsoDebugInfo = "OperatorNumeric(" + mOperatorNumeric
503                         + "): MccTable.geoCountryCodeForMccMnc(\"" + mccMnc + "\")";
504             } else {
505                 loge("updateLocale: Can't get country from operator numeric. mOperatorNumeric = "
506                         + mOperatorNumeric);
507             }
508         }
509 
510         // If for any reason we can't get country from operator numeric, try to get it from cell
511         // info.
512         if (TextUtils.isEmpty(countryIso)) {
513             // Find the most prevalent MCC from surrounding cell towers.
514             String mcc = getMccFromCellInfo();
515             if (mcc != null) {
516                 countryIso = MccTable.countryCodeForMcc(mcc);
517                 countryIsoDebugInfo = "CellInfo: MccTable.countryCodeForMcc(\"" + mcc + "\")";
518 
519                 // Some MCC+MNC combinations are known to be used in countries other than those
520                 // that the MCC alone would suggest. Do a second pass of nearby cells that match
521                 // the most frequently observed MCC to see if this could be one of those cases.
522                 MccMnc mccMnc = getMccMncFromCellInfo(mcc);
523                 if (mccMnc != null) {
524                     countryIso = MccTable.geoCountryCodeForMccMnc(mccMnc);
525                     countryIsoDebugInfo =
526                             "CellInfo: MccTable.geoCountryCodeForMccMnc(" + mccMnc + ")";
527                 }
528             }
529         }
530 
531         if (mCountryOverride != null) {
532             countryIso = mCountryOverride;
533             countryIsoDebugInfo = "mCountryOverride = \"" + mCountryOverride + "\"";
534         }
535 
536         if (!mPhone.isRadioOn()) {
537             countryIso = "";
538             countryIsoDebugInfo = "radio off";
539 
540             // clear cell infos, we don't know where the next network to camp on.
541             mCellInfoList = null;
542         }
543 
544         log("updateLocale: countryIso = " + countryIso
545                 + ", countryIsoDebugInfo = " + countryIsoDebugInfo);
546         if (!Objects.equals(countryIso, mCurrentCountryIso)) {
547             String msg = "updateLocale: Change the current country to \"" + countryIso + "\""
548                     + ", countryIsoDebugInfo = " + countryIsoDebugInfo
549                     + ", mCellInfoList = " + mCellInfoList;
550             log(msg);
551             mLocalLog.log(msg);
552             mCurrentCountryIso = countryIso;
553 
554             // Update the last known country ISO
555             if (!TextUtils.isEmpty(mCurrentCountryIso)) {
556                 updateLastKnownCountryIso(mCurrentCountryIso);
557             }
558 
559             int phoneId = mPhone.getPhoneId();
560             if (SubscriptionManager.isValidPhoneId(phoneId)) {
561                 List<String> newProp = new ArrayList<>(
562                         TelephonyProperties.operator_iso_country());
563                 while (newProp.size() <= phoneId) newProp.add(null);
564                 newProp.set(phoneId, mCurrentCountryIso);
565                 TelephonyProperties.operator_iso_country(newProp);
566             }
567 
568             if (mFeatureFlags.oemEnabledSatelliteFlag()) {
569                 TelephonyCountryDetector.getInstance(mPhone.getContext())
570                         .onNetworkCountryCodeChanged(mPhone, countryIso);
571             }
572             Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
573             intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, countryIso);
574             intent.putExtra(TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
575                     getLastKnownCountryIso());
576             SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
577             mPhone.getContext().sendBroadcastAsUser(intent, UserHandle.ALL);
578         }
579 
580         // Pass the geographical country information to the telephony time zone detection code.
581 
582         String timeZoneCountryIso = countryIso;
583         String timeZoneCountryIsoDebugInfo = countryIsoDebugInfo;
584         boolean isTestMcc = false;
585         if (!TextUtils.isEmpty(mOperatorNumeric)) {
586             // For a test cell (MCC 001), the NitzStateMachine requires handleCountryDetected("") in
587             // order to pass compliance tests. http://b/142840879
588             if (mOperatorNumeric.startsWith("001")) {
589                 isTestMcc = true;
590                 timeZoneCountryIso = "";
591                 timeZoneCountryIsoDebugInfo = "Test cell: " + mOperatorNumeric;
592             }
593         }
594         log("updateLocale: timeZoneCountryIso = " + timeZoneCountryIso
595                 + ", timeZoneCountryIsoDebugInfo = " + timeZoneCountryIsoDebugInfo);
596 
597         if (TextUtils.isEmpty(timeZoneCountryIso) && !isTestMcc) {
598             mNitzStateMachine.handleCountryUnavailable();
599         } else {
600             mNitzStateMachine.handleCountryDetected(timeZoneCountryIso);
601         }
602     }
603 
604     /** Exposed for testing purposes */
isTracking()605     public boolean isTracking() {
606         return mIsTracking;
607     }
608 
updateLastKnownCountryIso(String countryIso)609     private void updateLastKnownCountryIso(String countryIso) {
610         if (!TextUtils.isEmpty(countryIso)) {
611             final SharedPreferences prefs = mPhone.getContext().getSharedPreferences(
612                     LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, Context.MODE_PRIVATE);
613             final SharedPreferences.Editor editor = prefs.edit();
614             editor.putString(LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, countryIso);
615             editor.commit();
616             log("update country iso in sharedPrefs " + countryIso);
617         }
618     }
619 
620     /**
621      *  Return the last known country ISO before device is not camping on a network
622      *  (e.g. Airplane Mode)
623      *
624      *  @return The device's last known country ISO.
625      */
626     @NonNull
getLastKnownCountryIso()627     public String getLastKnownCountryIso() {
628         final SharedPreferences prefs = mPhone.getContext().getSharedPreferences(
629                 LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, Context.MODE_PRIVATE);
630         return prefs.getString(LAST_KNOWN_COUNTRY_ISO_SHARED_PREFS_KEY, "");
631     }
632 
log(String msg)633     private void log(String msg) {
634         Rlog.d(mTag, msg);
635     }
636 
loge(String msg)637     private void loge(String msg) {
638         Rlog.e(mTag, msg);
639     }
640 
641     /**
642      * Print the DeviceStateMonitor into the given stream.
643      *
644      * @param fd The raw file descriptor that the dump is being sent to.
645      * @param pw A PrintWriter to which the dump is to be set.
646      * @param args Additional arguments to the dump request.
647      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)648     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
649         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
650         pw.println("LocaleTracker-" + mPhone.getPhoneId() + ":");
651         ipw.increaseIndent();
652         ipw.println("mIsTracking = " + mIsTracking);
653         ipw.println("mOperatorNumeric = " + mOperatorNumeric);
654         ipw.println("mSimState = " + mSimState);
655         ipw.println("mCellInfoList = " + mCellInfoList);
656         ipw.println("mCurrentCountryIso = " + mCurrentCountryIso);
657         ipw.println("mFailCellInfoCount = " + mFailCellInfoCount);
658         ipw.println("Local logs:");
659         ipw.increaseIndent();
660         mLocalLog.dump(fd, ipw, args);
661         ipw.decreaseIndent();
662         ipw.decreaseIndent();
663         ipw.flush();
664     }
665 
666     /**
667      *  This getter should only be used for testing purposes in classes that wish to spoof the
668      *  country ISO. An example of how this can be done is in ServiceStateTracker#InSameCountry
669      * @return spoofed country iso.
670      */
671     @VisibleForTesting
getCountryOverride()672     public String getCountryOverride() {
673         return mCountryOverride;
674     }
675 }
676