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