blob: 996e288dc499539f3bd9eb85196e5110dd424097 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.telephony;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiManager;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.telephony.CellInfo;
import android.telephony.CellInfoGsm;
import android.telephony.CellInfoLte;
import android.telephony.CellInfoWcdma;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.LocalLog;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.IndentingPrintWriter;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* The locale tracker keeps tracking the current locale of the phone.
*/
public class LocaleTracker extends Handler {
private static final boolean DBG = true;
private static final String TAG = LocaleTracker.class.getSimpleName();
/** Event for getting cell info from the modem */
private static final int EVENT_GET_CELL_INFO = 1;
/** Event for operator numeric update */
private static final int EVENT_UPDATE_OPERATOR_NUMERIC = 2;
/** Event for service state changed */
private static final int EVENT_SERVICE_STATE_CHANGED = 3;
// Todo: Read this from Settings.
/** The minimum delay to get cell info from the modem */
private static final long CELL_INFO_MIN_DELAY_MS = 2 * SECOND_IN_MILLIS;
// Todo: Read this from Settings.
/** The maximum delay to get cell info from the modem */
private static final long CELL_INFO_MAX_DELAY_MS = 10 * MINUTE_IN_MILLIS;
// Todo: Read this from Settings.
/** The delay for periodically getting cell info from the modem */
private static final long CELL_INFO_PERIODIC_POLLING_DELAY_MS = 10 * MINUTE_IN_MILLIS;
private final Phone mPhone;
/** SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX */
private int mSimState;
/** Current serving PLMN's MCC/MNC */
@Nullable
private String mOperatorNumeric;
/** Current cell tower information */
@Nullable
private List<CellInfo> mCellInfo;
/** Count of invalid cell info we've got so far. Will reset once we get a successful one */
private int mFailCellInfoCount;
/** The ISO-3166 code of device's current country */
@Nullable
private String mCurrentCountryIso;
/** Current service state. Must be one of ServiceState.STATE_XXX. */
private int mLastServiceState = -1;
private final LocalLog mLocalLog = new LocalLog(50);
/** Broadcast receiver to get SIM card state changed event */
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(intent.getAction())) {
int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, 0);
if (phoneId == mPhone.getPhoneId()) {
onSimCardStateChanged(intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE,
TelephonyManager.SIM_STATE_UNKNOWN));
}
}
}
};
/**
* Message handler
*
* @param msg The message
*/
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_GET_CELL_INFO:
synchronized (this) {
getCellInfo();
updateLocale();
}
break;
case EVENT_UPDATE_OPERATOR_NUMERIC:
updateOperatorNumericSync((String) msg.obj);
break;
case EVENT_SERVICE_STATE_CHANGED:
AsyncResult ar = (AsyncResult) msg.obj;
onServiceStateChanged((ServiceState) ar.result);
break;
default:
throw new IllegalStateException("Unexpected message arrives. msg = " + msg.what);
}
}
/**
* Constructor
*
* @param phone The phone object
* @param looper The looper message handler
*/
public LocaleTracker(Phone phone, Looper looper) {
super(looper);
mPhone = phone;
mSimState = TelephonyManager.SIM_STATE_UNKNOWN;
final IntentFilter filter = new IntentFilter();
filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
mPhone.getContext().registerReceiver(mBroadcastReceiver, filter);
mPhone.registerForServiceStateChanged(this, EVENT_SERVICE_STATE_CHANGED, null);
}
/**
* Get the device's current country.
*
* @return The device's current country. Empty string if the information is not available.
*/
@NonNull
public synchronized String getCurrentCountry() {
return (mCurrentCountryIso != null) ? mCurrentCountryIso : "";
}
/**
* Get the MCC from cell tower information.
*
* @return MCC in string format. Null if the information is not available.
*/
@Nullable
private String getMccFromCellInfo() {
String selectedMcc = null;
if (mCellInfo != null) {
Map<String, Integer> countryCodeMap = new HashMap<>();
int maxCount = 0;
for (CellInfo cellInfo : mCellInfo) {
String mcc = null;
if (cellInfo instanceof CellInfoGsm) {
mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMccString();
} else if (cellInfo instanceof CellInfoLte) {
mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString();
} else if (cellInfo instanceof CellInfoWcdma) {
mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMccString();
}
if (mcc != null) {
int count = 1;
if (countryCodeMap.containsKey(mcc)) {
count = countryCodeMap.get(mcc) + 1;
}
countryCodeMap.put(mcc, count);
// This is unlikely, but if MCC from cell info looks different, we choose the
// MCC that occurs most.
if (count > maxCount) {
maxCount = count;
selectedMcc = mcc;
}
}
}
}
return selectedMcc;
}
/**
* Called when SIM card state changed. Only when we absolutely know the SIM is absent, we get
* cell info from the network. Other SIM states like NOT_READY might be just a transitioning
* state.
*
* @param state SIM card state. Must be one of TelephonyManager.SIM_STATE_XXX.
*/
private synchronized void onSimCardStateChanged(int state) {
if (mSimState != state && state == TelephonyManager.SIM_STATE_ABSENT) {
if (DBG) log("Sim absent. Get latest cell info from the modem.");
getCellInfo();
updateLocale();
}
mSimState = state;
}
/**
* Called when service state changed.
*
* @param serviceState Service state
*/
private void onServiceStateChanged(ServiceState serviceState) {
int state = serviceState.getState();
if (state != mLastServiceState) {
if (state != ServiceState.STATE_POWER_OFF && TextUtils.isEmpty(mOperatorNumeric)) {
// When the device is out of airplane mode or powered on, and network's MCC/MNC is
// not available, we get cell info from the modem.
String msg = "Service state " + ServiceState.rilServiceStateToString(state)
+ ". Get cell info now.";
if (DBG) log(msg);
mLocalLog.log(msg);
getCellInfo();
} else if (state == ServiceState.STATE_POWER_OFF) {
// Clear the cell info when the device is in airplane mode.
if (mCellInfo != null) mCellInfo.clear();
stopCellInfoRetry();
}
updateLocale();
mLastServiceState = state;
}
}
/**
* Update MCC/MNC from network service state synchronously. Note if this is called from phone
* process's main thread and if the update operation requires getting cell info from the modem,
* the cached cell info will be used to determine the locale. If the cached cell info is not
* acceptable, use {@link #updateOperatorNumericAsync(String)} instead.
*
* @param operatorNumeric MCC/MNC of the operator
*/
public synchronized void updateOperatorNumericSync(String operatorNumeric) {
// Check if the operator numeric changes.
if (DBG) log("updateOperatorNumericSync. mcc/mnc=" + operatorNumeric);
if (!Objects.equals(mOperatorNumeric, operatorNumeric)) {
String msg = "Operator numeric changes to " + operatorNumeric;
if (DBG) log(msg);
mLocalLog.log(msg);
mOperatorNumeric = operatorNumeric;
// If the operator numeric becomes unavailable, we need to get the latest cell info so
// that we can get MCC from it.
if (TextUtils.isEmpty(mOperatorNumeric)) {
if (DBG) {
log("Operator numeric unavailable. Get latest cell info from the modem.");
}
getCellInfo();
} else {
// If operator numeric is available, that means we camp on network. So we should
// clear the cell info and stop cell info retry.
if (mCellInfo != null) mCellInfo.clear();
stopCellInfoRetry();
}
updateLocale();
}
}
/**
* Update MCC/MNC from network service state asynchronously. The update operation will run
* in locale tracker's handler's thread, which can get cell info synchronously from service
* state tracker. Note that the country code will not be available immediately after calling
* this method.
*
* @param operatorNumeric MCC/MNC of the operator
*/
public void updateOperatorNumericAsync(String operatorNumeric) {
if (DBG) log("updateOperatorNumericAsync. mcc/mnc=" + operatorNumeric);
sendMessage(obtainMessage(EVENT_UPDATE_OPERATOR_NUMERIC, operatorNumeric));
}
/**
* Get the delay time to get cell info from modem. The delay time grows exponentially to prevent
* battery draining.
*
* @param failCount Count of invalid cell info we've got so far.
* @return The delay time for next get cell info
*/
private long getCellInfoDelayTime(int failCount) {
// Exponentially grow the delay time
long delay = CELL_INFO_MIN_DELAY_MS * (long) Math.pow(2, failCount - 1);
if (delay < CELL_INFO_MIN_DELAY_MS) {
delay = CELL_INFO_MIN_DELAY_MS;
} else if (delay > CELL_INFO_MAX_DELAY_MS) {
delay = CELL_INFO_MAX_DELAY_MS;
}
return delay;
}
/**
* Stop retrying getting cell info from the modem. It cancels any scheduled cell info retrieving
* request.
*/
private void stopCellInfoRetry() {
mFailCellInfoCount = 0;
removeMessages(EVENT_GET_CELL_INFO);
}
/**
* Get cell info from the modem.
*/
private void getCellInfo() {
String msg;
if (!mPhone.getServiceStateTracker().getDesiredPowerState()) {
msg = "Radio is off. Stopped cell info retry. Cleared the previous cached cell info.";
if (mCellInfo != null) mCellInfo.clear();
if (DBG) log(msg);
mLocalLog.log(msg);
stopCellInfoRetry();
return;
}
// Get all cell info. Passing null to use default worksource, which indicates the original
// request is from telephony internally.
mCellInfo = mPhone.getAllCellInfo(null);
msg = "getCellInfo: cell info=" + mCellInfo;
if (DBG) log(msg);
mLocalLog.log(msg);
if (CollectionUtils.isEmpty(mCellInfo)) {
// If we can't get a valid cell info. Try it again later.
long delay = getCellInfoDelayTime(++mFailCellInfoCount);
if (DBG) log("Can't get cell info. Try again in " + delay / 1000 + " secs.");
removeMessages(EVENT_GET_CELL_INFO);
sendMessageDelayed(obtainMessage(EVENT_GET_CELL_INFO), delay);
} else {
// We successfully got cell info from the modem. We should stop cell info retry.
stopCellInfoRetry();
// Now we need to get the cell info from the modem periodically even if we already got
// the cell info because the user can move.
sendMessageDelayed(obtainMessage(EVENT_GET_CELL_INFO),
CELL_INFO_PERIODIC_POLLING_DELAY_MS);
}
}
/**
* Update the device's current locale
*/
private void updateLocale() {
// If MCC is available from network service state, use it first.
String mcc = null;
String countryIso = "";
if (!TextUtils.isEmpty(mOperatorNumeric)) {
try {
mcc = mOperatorNumeric.substring(0, 3);
countryIso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
} catch (StringIndexOutOfBoundsException | NumberFormatException ex) {
loge("updateLocale: Can't get country from operator numeric. mcc = "
+ mcc + ". ex=" + ex);
}
}
// If for any reason we can't get country from operator numeric, try to get it from cell
// info.
if (TextUtils.isEmpty(countryIso)) {
mcc = getMccFromCellInfo();
if (!TextUtils.isEmpty(mcc)) {
try {
countryIso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
} catch (NumberFormatException ex) {
loge("updateLocale: Can't get country from cell info. mcc = "
+ mcc + ". ex=" + ex);
}
}
}
String msg = "updateLocale: mcc = " + mcc + ", country = " + countryIso;
log(msg);
mLocalLog.log(msg);
if (!Objects.equals(countryIso, mCurrentCountryIso)) {
msg = "updateLocale: Change the current country to " + countryIso;
log(msg);
mLocalLog.log(msg);
mCurrentCountryIso = countryIso;
TelephonyManager.setTelephonyProperty(mPhone.getPhoneId(),
TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, mCurrentCountryIso);
// Set the country code for wifi. This sets allowed wifi channels based on the
// country of the carrier we see. If we can't see any, reset to 0 so we don't
// broadcast on forbidden channels.
((WifiManager) mPhone.getContext().getSystemService(Context.WIFI_SERVICE))
.setCountryCode(countryIso);
}
}
private void log(String msg) {
Rlog.d(TAG, msg);
}
private void loge(String msg) {
Rlog.e(TAG, msg);
}
/**
* Print the DeviceStateMonitor into the given stream.
*
* @param fd The raw file descriptor that the dump is being sent to.
* @param pw A PrintWriter to which the dump is to be set.
* @param args Additional arguments to the dump request.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
pw.println("LocaleTracker:");
ipw.increaseIndent();
ipw.println("mOperatorNumeric = " + mOperatorNumeric);
ipw.println("mSimState = " + mSimState);
ipw.println("mCellInfo = " + mCellInfo);
ipw.println("mCurrentCountryIso = " + mCurrentCountryIso);
ipw.println("mFailCellInfoCount = " + mFailCellInfoCount);
ipw.println("Local logs:");
ipw.increaseIndent();
mLocalLog.dump(fd, ipw, args);
ipw.decreaseIndent();
ipw.decreaseIndent();
ipw.flush();
}
}