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