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