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.net.wifi.WifiManager;
29 import android.os.AsyncResult;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.telephony.CellInfo;
34 import android.telephony.CellInfoGsm;
35 import android.telephony.CellInfoLte;
36 import android.telephony.CellInfoWcdma;
37 import android.telephony.Rlog;
38 import android.telephony.ServiceState;
39 import android.telephony.SubscriptionManager;
40 import android.telephony.TelephonyManager;
41 import android.text.TextUtils;
42 import android.util.LocalLog;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.util.IndentingPrintWriter;
46 
47 import java.io.FileDescriptor;
48 import java.io.PrintWriter;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 
54 /**
55  * The locale tracker keeps tracking the current locale of the phone.
56  */
57 public class LocaleTracker extends Handler {
58     private static final boolean DBG = true;
59     private static final String TAG = LocaleTracker.class.getSimpleName();
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     // Todo: Read this from Settings.
77     /** The minimum delay to get cell info from the modem */
78     private static final long CELL_INFO_MIN_DELAY_MS = 2 * SECOND_IN_MILLIS;
79 
80     // Todo: Read this from Settings.
81     /** The maximum delay to get cell info from the modem */
82     private static final long CELL_INFO_MAX_DELAY_MS = 10 * MINUTE_IN_MILLIS;
83 
84     // Todo: Read this from Settings.
85     /** The delay for periodically getting cell info from the modem */
86     private static final long CELL_INFO_PERIODIC_POLLING_DELAY_MS = 10 * MINUTE_IN_MILLIS;
87 
88     /** The maximum fail count to prevent delay time overflow */
89     private static final int MAX_FAIL_COUNT = 30;
90 
91     private final Phone mPhone;
92 
93     private final NitzStateMachine mNitzStateMachine;
94 
95     /** SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX */
96     private int mSimState;
97 
98     /** Current serving PLMN's MCC/MNC */
99     @Nullable
100     private String mOperatorNumeric;
101 
102     /** Current cell tower information */
103     @Nullable
104     private List<CellInfo> mCellInfoList;
105 
106     /** Count of invalid cell info we've got so far. Will reset once we get a successful one */
107     private int mFailCellInfoCount;
108 
109     /** The ISO-3166 code of device's current country */
110     @Nullable
111     private String mCurrentCountryIso;
112 
113     /** Current service state. Must be one of ServiceState.STATE_XXX. */
114     private int mLastServiceState = ServiceState.STATE_POWER_OFF;
115 
116     private boolean mIsTracking = false;
117 
118     private final LocalLog mLocalLog = new LocalLog(50);
119 
120     /** Broadcast receiver to get SIM card state changed event */
121     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
122         @Override
123         public void onReceive(Context context, Intent intent) {
124             if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(intent.getAction())) {
125                 int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, 0);
126                 if (phoneId == mPhone.getPhoneId()) {
127                     obtainMessage(EVENT_SIM_STATE_CHANGED,
128                             intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE,
129                                     TelephonyManager.SIM_STATE_UNKNOWN), 0).sendToTarget();
130                 }
131             }
132         }
133     };
134 
135     /**
136      * Message handler
137      *
138      * @param msg The message
139      */
140     @Override
handleMessage(Message msg)141     public void handleMessage(Message msg) {
142         switch (msg.what) {
143             case EVENT_REQUEST_CELL_INFO:
144                 mPhone.requestCellInfoUpdate(null, obtainMessage(EVENT_RESPONSE_CELL_INFO));
145                 break;
146 
147             case EVENT_UNSOL_CELL_INFO:
148                 processCellInfo((AsyncResult) msg.obj);
149                 // If the unsol happened to be useful, use it; otherwise, pretend it didn't happen.
150                 if (mCellInfoList != null && mCellInfoList.size() > 0) requestNextCellInfo(true);
151                 break;
152 
153             case EVENT_RESPONSE_CELL_INFO:
154                 processCellInfo((AsyncResult) msg.obj);
155                 // If the cellInfo was non-empty then it's business as usual. Either way, this
156                 // cell info was requested by us, so it's our trigger to schedule another one.
157                 requestNextCellInfo(mCellInfoList != null && mCellInfoList.size() > 0);
158                 break;
159 
160             case EVENT_SERVICE_STATE_CHANGED:
161                 AsyncResult ar = (AsyncResult) msg.obj;
162                 onServiceStateChanged((ServiceState) ar.result);
163                 break;
164 
165             case EVENT_SIM_STATE_CHANGED:
166                 onSimCardStateChanged(msg.arg1);
167                 break;
168 
169             default:
170                 throw new IllegalStateException("Unexpected message arrives. msg = " + msg.what);
171         }
172     }
173 
174     /**
175      * Constructor
176      *
177      * @param phone The phone object
178      * @param nitzStateMachine NITZ state machine
179      * @param looper The looper message handler
180      */
LocaleTracker(Phone phone, NitzStateMachine nitzStateMachine, Looper looper)181     public LocaleTracker(Phone phone, NitzStateMachine nitzStateMachine, Looper looper)  {
182         super(looper);
183         mPhone = phone;
184         mNitzStateMachine = nitzStateMachine;
185         mSimState = TelephonyManager.SIM_STATE_UNKNOWN;
186 
187         final IntentFilter filter = new IntentFilter();
188         filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
189         mPhone.getContext().registerReceiver(mBroadcastReceiver, filter);
190 
191         mPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
192         mPhone.registerForCellInfo(this, EVENT_UNSOL_CELL_INFO, null);
193     }
194 
195     /**
196      * Get the device's current country.
197      *
198      * @return The device's current country. Empty string if the information is not available.
199      */
200     @NonNull
getCurrentCountry()201     public String getCurrentCountry() {
202         return (mCurrentCountryIso != null) ? mCurrentCountryIso : "";
203     }
204 
205     /**
206      * Get the MCC from cell tower information.
207      *
208      * @return MCC in string format. Null if the information is not available.
209      */
210     @Nullable
getMccFromCellInfo()211     private String getMccFromCellInfo() {
212         String selectedMcc = null;
213         if (mCellInfoList != null) {
214             Map<String, Integer> countryCodeMap = new HashMap<>();
215             int maxCount = 0;
216             for (CellInfo cellInfo : mCellInfoList) {
217                 String mcc = null;
218                 if (cellInfo instanceof CellInfoGsm) {
219                     mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString();
220                 } else if (cellInfo instanceof CellInfoLte) {
221                     mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString();
222                 } else if (cellInfo instanceof CellInfoWcdma) {
223                     mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString();
224                 }
225                 if (mcc != null) {
226                     int count = 1;
227                     if (countryCodeMap.containsKey(mcc)) {
228                         count = countryCodeMap.get(mcc) + 1;
229                     }
230                     countryCodeMap.put(mcc, count);
231                     // This is unlikely, but if MCC from cell info looks different, we choose the
232                     // MCC that occurs most.
233                     if (count > maxCount) {
234                         maxCount = count;
235                         selectedMcc = mcc;
236                     }
237                 }
238             }
239         }
240         return selectedMcc;
241     }
242 
243     /**
244      * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get
245      * cell info from the network. Other SIM states like NOT_READY might be just a transitioning
246      * state.
247      *
248      * @param state SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX.
249      */
onSimCardStateChanged(int state)250     private synchronized void onSimCardStateChanged(int state) {
251         mSimState = state;
252         updateLocale();
253         updateTrackingStatus();
254     }
255 
256     /**
257      * Called when service state changed.
258      *
259      * @param serviceState Service state
260      */
onServiceStateChanged(ServiceState serviceState)261     private void onServiceStateChanged(ServiceState serviceState) {
262         mLastServiceState = serviceState.getState();
263         updateLocale();
264         updateTrackingStatus();
265     }
266 
267     /**
268      * Update MCC/MNC from network service state.
269      *
270      * @param operatorNumeric MCC/MNC of the operator
271      */
updateOperatorNumeric(String operatorNumeric)272     public void updateOperatorNumeric(String operatorNumeric) {
273         // Check if the operator numeric changes.
274         if (!Objects.equals(mOperatorNumeric, operatorNumeric)) {
275             String msg = "Operator numeric changes to \"" + operatorNumeric + "\"";
276             if (DBG) log(msg);
277             mLocalLog.log(msg);
278             mOperatorNumeric = operatorNumeric;
279             updateLocale();
280         }
281     }
282 
processCellInfo(AsyncResult ar)283     private void processCellInfo(AsyncResult ar) {
284         if (ar == null || ar.exception != null) {
285             mCellInfoList = null;
286             return;
287         }
288         List<CellInfo> cellInfoList = (List<CellInfo>) ar.result;
289         String msg = "processCellInfo: cell info=" + cellInfoList;
290         if (DBG) log(msg);
291         mCellInfoList = cellInfoList;
292         updateLocale();
293     }
294 
requestNextCellInfo(boolean succeeded)295     private void requestNextCellInfo(boolean succeeded) {
296         if (!mIsTracking) return;
297 
298         removeMessages(EVENT_REQUEST_CELL_INFO);
299         if (succeeded) {
300             resetCellInfoRetry();
301             // Now we need to get the cell info from the modem periodically
302             // even if we already got the cell info because the user can move.
303             removeMessages(EVENT_UNSOL_CELL_INFO);
304             removeMessages(EVENT_RESPONSE_CELL_INFO);
305             sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO),
306                     CELL_INFO_PERIODIC_POLLING_DELAY_MS);
307         } else {
308             // If we can't get a valid cell info. Try it again later.
309             long delay = getCellInfoDelayTime(++mFailCellInfoCount);
310             if (DBG) log("Can't get cell info. Try again in " + delay / 1000 + " secs.");
311             sendMessageDelayed(obtainMessage(EVENT_REQUEST_CELL_INFO), delay);
312         }
313     }
314 
315     /**
316      * Get the delay time to get cell info from modem. The delay time grows exponentially to prevent
317      * battery draining.
318      *
319      * @param failCount Count of invalid cell info we've got so far.
320      * @return The delay time for next get cell info
321      */
322     @VisibleForTesting
getCellInfoDelayTime(int failCount)323     public static long getCellInfoDelayTime(int failCount) {
324         // Exponentially grow the delay time. Note we limit the fail count to MAX_FAIL_COUNT to
325         // prevent overflow in Math.pow().
326         long delay = CELL_INFO_MIN_DELAY_MS
327                 * (long) Math.pow(2, Math.min(failCount, MAX_FAIL_COUNT) - 1);
328         return Math.min(Math.max(delay, CELL_INFO_MIN_DELAY_MS), CELL_INFO_MAX_DELAY_MS);
329     }
330 
331     /**
332      * Stop retrying getting cell info from the modem. It cancels any scheduled cell info retrieving
333      * request.
334      */
resetCellInfoRetry()335     private void resetCellInfoRetry() {
336         mFailCellInfoCount = 0;
337         removeMessages(EVENT_REQUEST_CELL_INFO);
338     }
339 
updateTrackingStatus()340     private void updateTrackingStatus() {
341         boolean shouldTrackLocale =
342                 (mSimState == TelephonyManager.SIM_STATE_ABSENT
343                         || TextUtils.isEmpty(mOperatorNumeric))
344                 && (mLastServiceState == ServiceState.STATE_OUT_OF_SERVICE
345                         || mLastServiceState == ServiceState.STATE_EMERGENCY_ONLY);
346         if (shouldTrackLocale) {
347             startTracking();
348         } else {
349             stopTracking();
350         }
351     }
352 
stopTracking()353     private void stopTracking() {
354         if (!mIsTracking) return;
355         mIsTracking = false;
356         String msg = "Stopping LocaleTracker";
357         if (DBG) log(msg);
358         mLocalLog.log(msg);
359         mCellInfoList = null;
360         resetCellInfoRetry();
361     }
362 
startTracking()363     private void startTracking() {
364         if (mIsTracking) return;
365         String msg = "Starting LocaleTracker";
366         mLocalLog.log(msg);
367         if (DBG) log(msg);
368         mIsTracking = true;
369         sendMessage(obtainMessage(EVENT_REQUEST_CELL_INFO));
370     }
371 
372     /**
373      * Update the device's current locale
374      */
updateLocale()375     private synchronized void updateLocale() {
376         // If MCC is available from network service state, use it first.
377         String mcc = null;
378         String countryIso = "";
379         if (!TextUtils.isEmpty(mOperatorNumeric)) {
380             try {
381                 mcc = mOperatorNumeric.substring(0, 3);
382                 countryIso = MccTable.countryCodeForMcc(mcc);
383             } catch (StringIndexOutOfBoundsException ex) {
384                 loge("updateLocale: Can't get country from operator numeric. mcc = "
385                         + mcc + ". ex=" + ex);
386             }
387         }
388 
389         // If for any reason we can't get country from operator numeric, try to get it from cell
390         // info.
391         if (TextUtils.isEmpty(countryIso)) {
392             mcc = getMccFromCellInfo();
393             countryIso = MccTable.countryCodeForMcc(mcc);
394         }
395 
396         log("updateLocale: mcc = " + mcc + ", country = " + countryIso);
397         boolean countryChanged = false;
398         if (!Objects.equals(countryIso, mCurrentCountryIso)) {
399             String msg = "updateLocale: Change the current country to \"" + countryIso
400                     + "\", mcc = " + mcc + ", mCellInfoList = " + mCellInfoList;
401             log(msg);
402             mLocalLog.log(msg);
403             mCurrentCountryIso = countryIso;
404 
405             TelephonyManager.setTelephonyProperty(mPhone.getPhoneId(),
406                     TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, mCurrentCountryIso);
407 
408             // Set the country code for wifi. This sets allowed wifi channels based on the
409             // country of the carrier we see. If we can't see any, reset to 0 so we don't
410             // broadcast on forbidden channels.
411             WifiManager wifiManager = (WifiManager) mPhone.getContext()
412                     .getSystemService(Context.WIFI_SERVICE);
413             if (wifiManager != null) {
414                 wifiManager.setCountryCode(countryIso);
415             } else {
416                 msg = "Wifi manager is not available.";
417                 log(msg);
418                 mLocalLog.log(msg);
419             }
420 
421 
422             Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
423             intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, countryIso);
424             SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());
425             mPhone.getContext().sendBroadcast(intent);
426 
427             countryChanged = true;
428         }
429 
430         if (TextUtils.isEmpty(countryIso)) {
431             mNitzStateMachine.handleNetworkCountryCodeUnavailable();
432         } else {
433             mNitzStateMachine.handleNetworkCountryCodeSet(countryChanged);
434         }
435     }
436 
437     /** Exposed for testing purposes */
isTracking()438     public boolean isTracking() {
439         return mIsTracking;
440     }
441 
log(String msg)442     private void log(String msg) {
443         Rlog.d(TAG, msg);
444     }
445 
loge(String msg)446     private void loge(String msg) {
447         Rlog.e(TAG, msg);
448     }
449 
450     /**
451      * Print the DeviceStateMonitor into the given stream.
452      *
453      * @param fd The raw file descriptor that the dump is being sent to.
454      * @param pw A PrintWriter to which the dump is to be set.
455      * @param args Additional arguments to the dump request.
456      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)457     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
458         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
459         pw.println("LocaleTracker:");
460         ipw.increaseIndent();
461         ipw.println("mIsTracking = " + mIsTracking);
462         ipw.println("mOperatorNumeric = " + mOperatorNumeric);
463         ipw.println("mSimState = " + mSimState);
464         ipw.println("mCellInfoList = " + mCellInfoList);
465         ipw.println("mCurrentCountryIso = " + mCurrentCountryIso);
466         ipw.println("mFailCellInfoCount = " + mFailCellInfoCount);
467         ipw.println("Local logs:");
468         ipw.increaseIndent();
469         mLocalLog.dump(fd, ipw, args);
470         ipw.decreaseIndent();
471         ipw.decreaseIndent();
472         ipw.flush();
473     }
474 }
475