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.TelephonyManager;
40 import android.text.TextUtils;
41 import android.util.LocalLog;
42 
43 import com.android.internal.util.CollectionUtils;
44 import com.android.internal.util.IndentingPrintWriter;
45 
46 import java.io.FileDescriptor;
47 import java.io.PrintWriter;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 
53 /**
54  * The locale tracker keeps tracking the current locale of the phone.
55  */
56 public class LocaleTracker extends Handler {
57     private static final boolean DBG = true;
58     private static final String TAG = LocaleTracker.class.getSimpleName();
59 
60     /** Event for getting cell info from the modem */
61     private static final int EVENT_GET_CELL_INFO                = 1;
62 
63     /** Event for operator numeric update */
64     private static final int EVENT_UPDATE_OPERATOR_NUMERIC      = 2;
65 
66     /** Event for service state changed */
67     private static final int EVENT_SERVICE_STATE_CHANGED        = 3;
68 
69     // Todo: Read this from Settings.
70     /** The minimum delay to get cell info from the modem */
71     private static final long CELL_INFO_MIN_DELAY_MS = 2 * SECOND_IN_MILLIS;
72 
73     // Todo: Read this from Settings.
74     /** The maximum delay to get cell info from the modem */
75     private static final long CELL_INFO_MAX_DELAY_MS = 10 * MINUTE_IN_MILLIS;
76 
77     // Todo: Read this from Settings.
78     /** The delay for periodically getting cell info from the modem */
79     private static final long CELL_INFO_PERIODIC_POLLING_DELAY_MS = 10 * MINUTE_IN_MILLIS;
80 
81     private final Phone mPhone;
82 
83     /** SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX */
84     private int mSimState;
85 
86     /** Current serving PLMN's MCC/MNC */
87     @Nullable
88     private String mOperatorNumeric;
89 
90     /** Current cell tower information */
91     @Nullable
92     private List<CellInfo> mCellInfo;
93 
94     /** Count of invalid cell info we've got so far. Will reset once we get a successful one */
95     private int mFailCellInfoCount;
96 
97     /** The ISO-3166 code of device's current country */
98     @Nullable
99     private String mCurrentCountryIso;
100 
101     /** Current service state. Must be one of ServiceState.STATE_XXX. */
102     private int mLastServiceState = -1;
103 
104     private final LocalLog mLocalLog = new LocalLog(50);
105 
106     /** Broadcast receiver to get SIM card state changed event */
107     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
108         @Override
109         public void onReceive(Context context, Intent intent) {
110             if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(intent.getAction())) {
111                 int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, 0);
112                 if (phoneId == mPhone.getPhoneId()) {
113                     onSimCardStateChanged(intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE,
114                             TelephonyManager.SIM_STATE_UNKNOWN));
115                 }
116             }
117         }
118     };
119 
120     /**
121      * Message handler
122      *
123      * @param msg The message
124      */
125     @Override
handleMessage(Message msg)126     public void handleMessage(Message msg) {
127         switch (msg.what) {
128             case EVENT_GET_CELL_INFO:
129                 synchronized (this) {
130                     getCellInfo();
131                     updateLocale();
132                 }
133                 break;
134             case EVENT_UPDATE_OPERATOR_NUMERIC:
135                 updateOperatorNumericSync((String) msg.obj);
136                 break;
137             case EVENT_SERVICE_STATE_CHANGED:
138                 AsyncResult ar = (AsyncResult) msg.obj;
139                 onServiceStateChanged((ServiceState) ar.result);
140                 break;
141             default:
142                 throw new IllegalStateException("Unexpected message arrives. msg = " + msg.what);
143         }
144     }
145 
146     /**
147      * Constructor
148      *
149      * @param phone The phone object
150      * @param looper The looper message handler
151      */
LocaleTracker(Phone phone, Looper looper)152     public LocaleTracker(Phone phone, Looper looper)  {
153         super(looper);
154         mPhone = phone;
155         mSimState = TelephonyManager.SIM_STATE_UNKNOWN;
156 
157         final IntentFilter filter = new IntentFilter();
158         filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
159         mPhone.getContext().registerReceiver(mBroadcastReceiver, filter);
160 
161         mPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
162     }
163 
164     /**
165      * Get the device's current country.
166      *
167      * @return The device's current country. Empty string if the information is not available.
168      */
169     @NonNull
getCurrentCountry()170     public synchronized String getCurrentCountry() {
171         return (mCurrentCountryIso != null) ? mCurrentCountryIso : "";
172     }
173 
174     /**
175      * Get the MCC from cell tower information.
176      *
177      * @return MCC in string format. Null if the information is not available.
178      */
179     @Nullable
getMccFromCellInfo()180     private String getMccFromCellInfo() {
181         String selectedMcc = null;
182         if (mCellInfo != null) {
183             Map<String, Integer> countryCodeMap = new HashMap<>();
184             int maxCount = 0;
185             for (CellInfo cellInfo : mCellInfo) {
186                 String mcc = null;
187                 if (cellInfo instanceof CellInfoGsm) {
188                     mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString();
189                 } else if (cellInfo instanceof CellInfoLte) {
190                     mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString();
191                 } else if (cellInfo instanceof CellInfoWcdma) {
192                     mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString();
193                 }
194                 if (mcc != null) {
195                     int count = 1;
196                     if (countryCodeMap.containsKey(mcc)) {
197                         count = countryCodeMap.get(mcc) + 1;
198                     }
199                     countryCodeMap.put(mcc, count);
200                     // This is unlikely, but if MCC from cell info looks different, we choose the
201                     // MCC that occurs most.
202                     if (count > maxCount) {
203                         maxCount = count;
204                         selectedMcc = mcc;
205                     }
206                 }
207             }
208         }
209         return selectedMcc;
210     }
211 
212     /**
213      * Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get
214      * cell info from the network. Other SIM states like NOT_READY might be just a transitioning
215      * state.
216      *
217      * @param state SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX.
218      */
onSimCardStateChanged(int state)219     private synchronized void onSimCardStateChanged(int state) {
220         if (mSimState != state && state == TelephonyManager.SIM_STATE_ABSENT) {
221             if (DBG) log("Sim absent. Get latest cell info from the modem.");
222             getCellInfo();
223             updateLocale();
224         }
225         mSimState = state;
226     }
227 
228     /**
229      * Called when service state changed.
230      *
231      * @param serviceState Service state
232      */
onServiceStateChanged(ServiceState serviceState)233     private void onServiceStateChanged(ServiceState serviceState) {
234         int state = serviceState.getState();
235         if (state != mLastServiceState) {
236             if (state != ServiceState.STATE_POWER_OFF && TextUtils.isEmpty(mOperatorNumeric)) {
237                 // When the device is out of airplane mode or powered on, and network's MCC/MNC is
238                 // not available, we get cell info from the modem.
239                 String msg = "Service state " + ServiceState.rilServiceStateToString(state)
240                         + ". Get cell info now.";
241                 if (DBG) log(msg);
242                 mLocalLog.log(msg);
243                 getCellInfo();
244             } else if (state == ServiceState.STATE_POWER_OFF) {
245                 // Clear the cell info when the device is in airplane mode.
246                 if (mCellInfo != null) mCellInfo.clear();
247                 stopCellInfoRetry();
248             }
249             updateLocale();
250             mLastServiceState = state;
251         }
252     }
253 
254     /**
255      * Update MCC/MNC from network service state synchronously. Note if this is called from phone
256      * process's main thread and if the update operation requires getting cell info from the modem,
257      * the cached cell info will be used to determine the locale. If the cached cell info is not
258      * acceptable, use {@link #updateOperatorNumericAsync(String)} instead.
259      *
260      * @param operatorNumeric MCC/MNC of the operator
261      */
updateOperatorNumericSync(String operatorNumeric)262     public synchronized void updateOperatorNumericSync(String operatorNumeric) {
263         // Check if the operator numeric changes.
264         if (DBG) log("updateOperatorNumericSync. mcc/mnc=" + operatorNumeric);
265         if (!Objects.equals(mOperatorNumeric, operatorNumeric)) {
266             String msg = "Operator numeric changes to " + operatorNumeric;
267             if (DBG) log(msg);
268             mLocalLog.log(msg);
269             mOperatorNumeric = operatorNumeric;
270 
271             // If the operator numeric becomes unavailable, we need to get the latest cell info so
272             // that we can get MCC from it.
273             if (TextUtils.isEmpty(mOperatorNumeric)) {
274                 if (DBG) {
275                     log("Operator numeric unavailable. Get latest cell info from the modem.");
276                 }
277                 getCellInfo();
278             } else {
279                 // If operator numeric is available, that means we camp on network. So we should
280                 // clear the cell info and stop cell info retry.
281                 if (mCellInfo != null) mCellInfo.clear();
282                 stopCellInfoRetry();
283             }
284             updateLocale();
285         }
286     }
287 
288     /**
289      * Update MCC/MNC from network service state asynchronously. The update operation will run
290      * in locale tracker's handler's thread, which can get cell info synchronously from service
291      * state tracker. Note that the country code will not be available immediately after calling
292      * this method.
293      *
294      * @param operatorNumeric MCC/MNC of the operator
295      */
updateOperatorNumericAsync(String operatorNumeric)296     public void updateOperatorNumericAsync(String operatorNumeric) {
297         if (DBG) log("updateOperatorNumericAsync. mcc/mnc=" + operatorNumeric);
298         sendMessage(obtainMessage(EVENT_UPDATE_OPERATOR_NUMERIC, operatorNumeric));
299     }
300 
301     /**
302      * Get the delay time to get cell info from modem. The delay time grows exponentially to prevent
303      * battery draining.
304      *
305      * @param failCount Count of invalid cell info we've got so far.
306      * @return The delay time for next get cell info
307      */
getCellInfoDelayTime(int failCount)308     private long getCellInfoDelayTime(int failCount) {
309         // Exponentially grow the delay time
310         long delay = CELL_INFO_MIN_DELAY_MS * (long) Math.pow(2, failCount - 1);
311         if (delay < CELL_INFO_MIN_DELAY_MS) {
312             delay = CELL_INFO_MIN_DELAY_MS;
313         } else if (delay > CELL_INFO_MAX_DELAY_MS) {
314             delay = CELL_INFO_MAX_DELAY_MS;
315         }
316         return delay;
317     }
318 
319     /**
320      * Stop retrying getting cell info from the modem. It cancels any scheduled cell info retrieving
321      * request.
322      */
stopCellInfoRetry()323     private void stopCellInfoRetry() {
324         mFailCellInfoCount = 0;
325         removeMessages(EVENT_GET_CELL_INFO);
326     }
327 
328     /**
329      * Get cell info from the modem.
330      */
getCellInfo()331     private void getCellInfo() {
332         String msg;
333         if (!mPhone.getServiceStateTracker().getDesiredPowerState()) {
334             msg = "Radio is off. Stopped cell info retry. Cleared the previous cached cell info.";
335             if (mCellInfo != null) mCellInfo.clear();
336             if (DBG) log(msg);
337             mLocalLog.log(msg);
338             stopCellInfoRetry();
339             return;
340         }
341 
342         // Get all cell info. Passing null to use default worksource, which indicates the original
343         // request is from telephony internally.
344         mCellInfo = mPhone.getAllCellInfo(null);
345         msg = "getCellInfo: cell info=" + mCellInfo;
346         if (DBG) log(msg);
347         mLocalLog.log(msg);
348         if (CollectionUtils.isEmpty(mCellInfo)) {
349             // If we can't get a valid cell info. Try it again later.
350             long delay = getCellInfoDelayTime(++mFailCellInfoCount);
351             if (DBG) log("Can't get cell info. Try again in " + delay / 1000 + " secs.");
352             removeMessages(EVENT_GET_CELL_INFO);
353             sendMessageDelayed(obtainMessage(EVENT_GET_CELL_INFO), delay);
354         } else {
355             // We successfully got cell info from the modem. We should stop cell info retry.
356             stopCellInfoRetry();
357 
358             // Now we need to get the cell info from the modem periodically even if we already got
359             // the cell info because the user can move.
360             sendMessageDelayed(obtainMessage(EVENT_GET_CELL_INFO),
361                     CELL_INFO_PERIODIC_POLLING_DELAY_MS);
362         }
363     }
364 
365     /**
366      * Update the device's current locale
367      */
updateLocale()368     private void updateLocale() {
369         // If MCC is available from network service state, use it first.
370         String mcc = null;
371         String countryIso = "";
372         if (!TextUtils.isEmpty(mOperatorNumeric)) {
373             try {
374                 mcc = mOperatorNumeric.substring(0, 3);
375                 countryIso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
376             } catch (StringIndexOutOfBoundsException | NumberFormatException ex) {
377                 loge("updateLocale: Can't get country from operator numeric. mcc = "
378                         + mcc + ". ex=" + ex);
379             }
380         }
381 
382         // If for any reason we can't get country from operator numeric, try to get it from cell
383         // info.
384         if (TextUtils.isEmpty(countryIso)) {
385             mcc = getMccFromCellInfo();
386             if (!TextUtils.isEmpty(mcc)) {
387                 try {
388                     countryIso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
389                 } catch (NumberFormatException ex) {
390                     loge("updateLocale: Can't get country from cell info. mcc = "
391                             + mcc + ". ex=" + ex);
392                 }
393             }
394         }
395 
396         String msg = "updateLocale: mcc = " + mcc + ", country = " + countryIso;
397         log(msg);
398         mLocalLog.log(msg);
399         if (!Objects.equals(countryIso, mCurrentCountryIso)) {
400             msg = "updateLocale: Change the current country to " + countryIso;
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) mPhone.getContext().getSystemService(Context.WIFI_SERVICE))
412                     .setCountryCode(countryIso);
413         }
414     }
415 
log(String msg)416     private void log(String msg) {
417         Rlog.d(TAG, msg);
418     }
419 
loge(String msg)420     private void loge(String msg) {
421         Rlog.e(TAG, msg);
422     }
423 
424     /**
425      * Print the DeviceStateMonitor into the given stream.
426      *
427      * @param fd The raw file descriptor that the dump is being sent to.
428      * @param pw A PrintWriter to which the dump is to be set.
429      * @param args Additional arguments to the dump request.
430      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)431     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
432         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
433         pw.println("LocaleTracker:");
434         ipw.increaseIndent();
435         ipw.println("mOperatorNumeric = " + mOperatorNumeric);
436         ipw.println("mSimState = " + mSimState);
437         ipw.println("mCellInfo = " + mCellInfo);
438         ipw.println("mCurrentCountryIso = " + mCurrentCountryIso);
439         ipw.println("mFailCellInfoCount = " + mFailCellInfoCount);
440         ipw.println("Local logs:");
441         ipw.increaseIndent();
442         mLocalLog.dump(fd, ipw, args);
443         ipw.decreaseIndent();
444         ipw.decreaseIndent();
445         ipw.flush();
446     }
447 }
448