blob: e8be159ba7162141707c04a02acbf924990737c1 [file] [log] [blame]
/*
* Copyright 2017 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 android.content.Context;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.provider.Telephony;
import android.telephony.Rlog;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
import com.android.internal.telephony.uicc.IccRecords;
import com.android.internal.telephony.uicc.UiccController;
import com.android.internal.util.IndentingPrintWriter;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static android.provider.Telephony.CarrierIdentification;
/**
* CarrierIdentifier identifies the subscription carrier and returns a canonical carrier Id
* and a user friendly carrier name. CarrierIdentifier reads subscription info and check against
* all carrier matching rules stored in CarrierIdProvider. It is msim aware, each phone has a
* dedicated CarrierIdentifier.
*/
public class CarrierIdentifier extends Handler {
private static final String LOG_TAG = CarrierIdentifier.class.getSimpleName();
private static final boolean DBG = true;
private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
// events to trigger carrier identification
private static final int SIM_LOAD_EVENT = 1;
private static final int SIM_ABSENT_EVENT = 2;
private static final int SPN_OVERRIDE_EVENT = 3;
private static final int ICC_CHANGED_EVENT = 4;
private static final int PREFER_APN_UPDATE_EVENT = 5;
private static final int CARRIER_ID_DB_UPDATE_EVENT = 6;
private static final Uri CONTENT_URL_PREFER_APN = Uri.withAppendedPath(
Telephony.Carriers.CONTENT_URI, "preferapn");
private static final String OPERATOR_BRAND_OVERRIDE_PREFIX = "operator_branding_";
private static final int INVALID_CARRIER_ID = -1;
// cached matching rules based mccmnc to speed up resolution
private List<CarrierMatchingRule> mCarrierMatchingRulesOnMccMnc = new ArrayList<>();
// cached carrier Id
private int mCarrierId = INVALID_CARRIER_ID;
// cached carrier name
private String mCarrierName;
// cached preferapn name
private String mPreferApn;
// cached service provider name. telephonyManager API returns empty string as default value.
// some carriers need to target devices with Empty SPN. In that case, carrier matching rule
// should specify "" spn explicitly.
private String mSpn = "";
private Context mContext;
private Phone mPhone;
private IccRecords mIccRecords;
private final LocalLog mCarrierIdLocalLog = new LocalLog(20);
private final TelephonyManager mTelephonyMgr;
private final SubscriptionsChangedListener mOnSubscriptionsChangedListener =
new SubscriptionsChangedListener();
private final SharedPreferenceChangedListener mSharedPrefListener =
new SharedPreferenceChangedListener();
private final ContentObserver mContentObserver = new ContentObserver(this) {
@Override
public void onChange(boolean selfChange, Uri uri) {
logd("onChange URI: " + uri);
if (CONTENT_URL_PREFER_APN.equals(uri.getLastPathSegment())) {
sendEmptyMessage(PREFER_APN_UPDATE_EVENT);
} else {
sendEmptyMessage(CARRIER_ID_DB_UPDATE_EVENT);
}
}
};
private class SubscriptionsChangedListener
extends SubscriptionManager.OnSubscriptionsChangedListener {
final AtomicInteger mPreviousSubId =
new AtomicInteger(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
/**
* Callback invoked when there is any change to any SubscriptionInfo. Typically
* this method would invoke {@link SubscriptionManager#getActiveSubscriptionInfoList}
*/
@Override
public void onSubscriptionsChanged() {
int subId = mPhone.getSubId();
if (mPreviousSubId.getAndSet(subId) != subId) {
if (DBG) {
logd("SubscriptionListener.onSubscriptionInfoChanged subId: "
+ mPreviousSubId);
}
if (SubscriptionManager.isValidSubscriptionId(subId)) {
sendEmptyMessage(SIM_LOAD_EVENT);
} else {
sendEmptyMessage(SIM_ABSENT_EVENT);
}
}
}
}
private class SharedPreferenceChangedListener implements
SharedPreferences.OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
if (TextUtils.equals(key, OPERATOR_BRAND_OVERRIDE_PREFIX
+ mPhone.getIccSerialNumber())) {
// SPN override from carrier privileged apps
logd("[onSharedPreferenceChanged]: " + key);
sendEmptyMessage(SPN_OVERRIDE_EVENT);
}
}
}
public CarrierIdentifier(Phone phone) {
logd("Creating CarrierIdentifier[" + phone.getPhoneId() + "]");
mContext = phone.getContext();
mPhone = phone;
mTelephonyMgr = TelephonyManager.from(mContext);
// register events
mContext.getContentResolver().registerContentObserver(CONTENT_URL_PREFER_APN, false,
mContentObserver);
mContext.getContentResolver().registerContentObserver(
Telephony.CarrierIdentification.CONTENT_URI, false, mContentObserver);
SubscriptionManager.from(mContext).addOnSubscriptionsChangedListener(
mOnSubscriptionsChangedListener);
PreferenceManager.getDefaultSharedPreferences(mContext)
.registerOnSharedPreferenceChangeListener(mSharedPrefListener);
UiccController.getInstance().registerForIccChanged(this, ICC_CHANGED_EVENT, null);
}
/**
* Entry point for the carrier identification.
*
* 1. SIM_LOAD_EVENT
* This indicates that all SIM records has been loaded and its first entry point for the
* carrier identification. Note, there are other attributes could be changed on the fly
* like APN and SPN. We cached all carrier matching rules based on MCCMNC to speed
* up carrier resolution on following trigger events.
*
* 2. PREFER_APN_UPDATE_EVENT
* This indicates prefer apn has been changed. It could be triggered when user modified
* APN settings or when default data connection first establishes on the current carrier.
* We follow up on this by querying prefer apn sqlite and re-issue carrier identification
* with the updated prefer apn name.
*
* 3. SPN_OVERRIDE_EVENT
* This indicates that SPN value as been changed. It could be triggered from EF_SPN
* record loading, carrier config override
* {@link android.telephony.CarrierConfigManager#KEY_CARRIER_NAME_STRING}
* or carrier app override {@link TelephonyManager#setOperatorBrandOverride(String)}.
* we follow up this by checking the cached mSPN against the latest value and issue
* carrier identification only if spn changes.
*
* 4. CARRIER_ID_DB_UPDATE_EVENT
* This indicates that carrierIdentification database which stores all matching rules
* has been updated. It could be triggered from OTA or assets update.
*/
@Override
public void handleMessage(Message msg) {
if (VDBG) logd("handleMessage: " + msg.what);
switch (msg.what) {
case SIM_LOAD_EVENT:
case CARRIER_ID_DB_UPDATE_EVENT:
mSpn = mTelephonyMgr.getSimOperatorNameForPhone(mPhone.getPhoneId());
mPreferApn = getPreferApn();
loadCarrierMatchingRulesOnMccMnc();
break;
case SIM_ABSENT_EVENT:
mCarrierMatchingRulesOnMccMnc.clear();
mSpn = null;
mPreferApn = null;
updateCarrierIdAndName(INVALID_CARRIER_ID, null);
break;
case PREFER_APN_UPDATE_EVENT:
String preferApn = getPreferApn();
if (!equals(mPreferApn, preferApn, true)) {
logd("[updatePreferApn] from:" + mPreferApn + " to:" + preferApn);
mPreferApn = preferApn;
matchCarrier();
}
break;
case SPN_OVERRIDE_EVENT:
String spn = mTelephonyMgr.getSimOperatorNameForPhone(mPhone.getPhoneId());
if (!equals(mSpn, spn, true)) {
logd("[updateSpn] from:" + mSpn + " to:" + spn);
mSpn = spn;
matchCarrier();
}
break;
case ICC_CHANGED_EVENT:
IccRecords newIccRecords = UiccController.getInstance().getIccRecords(
mPhone.getPhoneId(), UiccController.APP_FAM_3GPP);
if (mIccRecords != newIccRecords) {
if (mIccRecords != null) {
logd("Removing stale icc objects.");
mIccRecords.unregisterForSpnUpdate(this);
mIccRecords = null;
}
if (newIccRecords != null) {
logd("new Icc object");
newIccRecords.registerForSpnUpdate(this, SPN_OVERRIDE_EVENT, null);
mIccRecords = newIccRecords;
}
}
break;
default:
loge("invalid msg: " + msg.what);
break;
}
}
private void loadCarrierMatchingRulesOnMccMnc() {
try {
String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
Cursor cursor = mContext.getContentResolver().query(CarrierIdentification.CONTENT_URI,
/* projection */ null,
/* selection */ CarrierIdentification.MCCMNC + "=?",
/* selectionArgs */ new String[]{mccmnc}, null);
try {
if (cursor != null) {
if (VDBG) {
logd("[loadCarrierMatchingRules]- " + cursor.getCount()
+ " Records(s) in DB" + " mccmnc: " + mccmnc);
}
mCarrierMatchingRulesOnMccMnc.clear();
while (cursor.moveToNext()) {
mCarrierMatchingRulesOnMccMnc.add(makeCarrierMatchingRule(cursor));
}
matchCarrier();
}
} finally {
if (cursor != null) {
cursor.close();
}
}
} catch (Exception ex) {
loge("[loadCarrierMatchingRules]- ex: " + ex);
}
}
private String getPreferApn() {
Cursor cursor = mContext.getContentResolver().query(
Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "preferapn/subId/"
+ mPhone.getSubId()), /* projection */ new String[]{Telephony.Carriers.APN},
/* selection */ null, /* selectionArgs */ null, /* sortOrder */ null);
try {
if (cursor != null) {
if (VDBG) {
logd("[getPreferApn]- " + cursor.getCount() + " Records(s) in DB");
}
while (cursor.moveToNext()) {
String apn = cursor.getString(cursor.getColumnIndexOrThrow(
Telephony.Carriers.APN));
logd("[getPreferApn]- " + apn);
return apn;
}
}
} catch (Exception ex) {
loge("[getPreferApn]- exception: " + ex);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
private void updateCarrierIdAndName(int cid, String name) {
boolean update = false;
if (!equals(name, mCarrierName, true)) {
logd("[updateCarrierName] from:" + mCarrierName + " to:" + name);
mCarrierName = name;
update = true;
}
if (cid != mCarrierId) {
logd("[updateCarrierId] from:" + mCarrierId + " to:" + cid);
mCarrierId = cid;
update = true;
}
if (update) {
// TODO new public intent CARRIER_ID_CHANGED
mCarrierIdLocalLog.log("[updateCarrierIdAndName] cid:" + mCarrierId + " name:"
+ mCarrierName);
}
}
private CarrierMatchingRule makeCarrierMatchingRule(Cursor cursor) {
return new CarrierMatchingRule(
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.MCCMNC)),
cursor.getString(cursor.getColumnIndexOrThrow(
CarrierIdentification.IMSI_PREFIX_XPATTERN)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.GID1)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.GID2)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.PLMN)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.SPN)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.APN)),
cursor.getInt(cursor.getColumnIndexOrThrow(CarrierIdentification.CID)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.NAME)));
}
/**
* carrier matching attributes with corresponding cid
*/
private static class CarrierMatchingRule {
/**
* These scores provide the hierarchical relationship between the attributes, intended to
* resolve conflicts in a deterministic way. The scores are constructed such that a match
* from a higher tier will beat any subsequent match which does not match at that tier,
* so MCCMNC beats everything else. This avoids problems when two (or more) carriers rule
* matches as the score helps to find the best match uniquely. e.g.,
* rule 1 {mccmnc, imsi} rule 2 {mccmnc, imsi, gid1} and rule 3 {mccmnc, imsi, gid2} all
* matches with subscription data. rule 2 wins with the highest matching score.
*/
private static final int SCORE_MCCMNC = 1 << 6;
private static final int SCORE_IMSI_PREFIX = 1 << 5;
private static final int SCORE_GID1 = 1 << 4;
private static final int SCORE_GID2 = 1 << 3;
private static final int SCORE_PLMN = 1 << 2;
private static final int SCORE_SPN = 1 << 1;
private static final int SCORE_APN = 1 << 0;
private static final int SCORE_INVALID = -1;
// carrier matching attributes
private String mMccMnc;
private String mImsiPrefixPattern;
private String mGid1;
private String mGid2;
private String mPlmn;
private String mSpn;
private String mApn;
// user-facing carrier name
private String mName;
// unique carrier id
private int mCid;
private int mScore = 0;
CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String gid1, String gid2,
String plmn, String spn, String apn, int cid, String name) {
mMccMnc = mccmnc;
mImsiPrefixPattern = imsiPrefixPattern;
mGid1 = gid1;
mGid2 = gid2;
mPlmn = plmn;
mSpn = spn;
mApn = apn;
mCid = cid;
mName = name;
}
// Calculate matching score. Values which aren't set in the rule are considered "wild".
// All values in the rule must match in order for the subscription to be considered part of
// the carrier. otherwise, a invalid score -1 will be assigned. A match from a higher tier
// will beat any subsequent match which does not match at that tier. When there are multiple
// matches at the same tier, the longest, best match will be used.
public void match(CarrierMatchingRule subscriptionRule) {
mScore = 0;
if (mMccMnc != null) {
if (!CarrierIdentifier.equals(subscriptionRule.mMccMnc, mMccMnc, false)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_MCCMNC;
}
if (mImsiPrefixPattern != null) {
if (!imsiPrefixMatch(subscriptionRule.mImsiPrefixPattern, mImsiPrefixPattern)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_IMSI_PREFIX;
}
if (mGid1 != null) {
// full string match. carrier matching should cover the corner case that gid1
// with garbage tail due to SIM manufacture issues.
if (!CarrierIdentifier.equals(subscriptionRule.mGid1, mGid1, true)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_GID1;
}
if (mGid2 != null) {
// full string match. carrier matching should cover the corner case that gid2
// with garbage tail due to SIM manufacture issues.
if (!CarrierIdentifier.equals(subscriptionRule.mGid2, mGid2, true)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_GID2;
}
if (mPlmn != null) {
if (!CarrierIdentifier.equals(subscriptionRule.mPlmn, mPlmn, true)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_PLMN;
}
if (mSpn != null) {
if (!CarrierIdentifier.equals(subscriptionRule.mSpn, mSpn, true)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_SPN;
}
if (mApn != null) {
if (!CarrierIdentifier.equals(subscriptionRule.mApn, mApn, true)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_APN;
}
}
private boolean imsiPrefixMatch(String imsi, String prefixXPattern) {
if (TextUtils.isEmpty(prefixXPattern)) return true;
if (TextUtils.isEmpty(imsi)) return false;
if (imsi.length() < prefixXPattern.length()) {
return false;
}
for (int i = 0; i < prefixXPattern.length(); i++) {
if ((prefixXPattern.charAt(i) != 'x') && (prefixXPattern.charAt(i) != 'X')
&& (prefixXPattern.charAt(i) != imsi.charAt(i))) {
return false;
}
}
return true;
}
public String toString() {
return "[CarrierMatchingRule] -"
+ " mccmnc: " + mMccMnc
+ " gid1: " + mGid1
+ " gid2: " + mGid2
+ " plmn: " + mPlmn
+ " imsi_prefix: " + mImsiPrefixPattern
+ " spn: " + mSpn
+ " apn: " + mApn
+ " name: " + mName
+ " cid: " + mCid
+ " score: " + mScore;
}
}
/**
* find the best matching carrier from candidates with matched MCCMNC and notify
* all interested parties on carrier id change.
*/
private void matchCarrier() {
if (!SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
logd("[matchCarrier]" + "skip before sim records loaded");
return;
}
final String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
final String gid1 = mPhone.getGroupIdLevel1();
final String gid2 = mPhone.getGroupIdLevel2();
final String imsi = mPhone.getSubscriberId();
final String plmn = mPhone.getPlmn();
final String spn = mSpn;
final String apn = mPreferApn;
if (VDBG) {
logd("[matchCarrier]"
+ " gid1: " + gid1
+ " gid2: " + gid2
+ " imsi: " + Rlog.pii(LOG_TAG, imsi)
+ " plmn: " + plmn
+ " spn: " + spn
+ " apn: " + apn);
}
CarrierMatchingRule subscriptionRule = new CarrierMatchingRule(
mccmnc, imsi, gid1, gid2, plmn, spn, apn, INVALID_CARRIER_ID, null);
int maxScore = CarrierMatchingRule.SCORE_INVALID;
CarrierMatchingRule maxRule = null;
for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) {
rule.match(subscriptionRule);
if (rule.mScore > maxScore) {
maxScore = rule.mScore;
maxRule = rule;
}
}
if (maxScore == CarrierMatchingRule.SCORE_INVALID) {
logd("[matchCarrier - no match] cid: " + INVALID_CARRIER_ID + " name: " + null);
updateCarrierIdAndName(INVALID_CARRIER_ID, null);
} else {
logd("[matchCarrier] cid: " + maxRule.mCid + " name: " + maxRule.mName);
updateCarrierIdAndName(maxRule.mCid, maxRule.mName);
}
}
public int getCarrierId() {
return mCarrierId;
}
public String getCarrierName() {
return mCarrierName;
}
private static boolean equals(String a, String b, boolean ignoreCase) {
if (a == null && b == null) return true;
if (a != null && b != null) {
return (ignoreCase) ? a.equalsIgnoreCase(b) : a.equals(b);
}
return false;
}
private static void logd(String str) {
Rlog.d(LOG_TAG, str);
}
private static void loge(String str) {
Rlog.e(LOG_TAG, str);
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
ipw.println("mCarrierIdLocalLogs:");
ipw.increaseIndent();
mCarrierIdLocalLog.dump(fd, pw, args);
ipw.decreaseIndent();
ipw.println("mCarrierId: " + mCarrierId);
ipw.println("mCarrierName: " + mCarrierName);
ipw.println("mCarrierMatchingRules on mccmnc: "
+ mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId()));
ipw.increaseIndent();
for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) {
ipw.println(rule.toString());
}
ipw.decreaseIndent();
ipw.println("mSpn: " + mSpn);
ipw.println("mPreferApn: " + mPreferApn);
ipw.flush();
}
}