blob: 233adf1c37e550fe25302f3baa0559d6025141b6 [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 static android.provider.Telephony.CarrierId;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
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.metrics.TelephonyMetrics;
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;
/**
* 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_";
// cached matching rules based mccmnc to speed up resolution
private List<CarrierMatchingRule> mCarrierMatchingRulesOnMccMnc = new ArrayList<>();
// cached carrier Id
private int mCarrierId = TelephonyManager.UNKNOWN_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) {
if (CONTENT_URL_PREFER_APN.equals(uri.getLastPathSegment())) {
logd("onChange URI: " + uri);
sendEmptyMessage(PREFER_APN_UPDATE_EVENT);
} else if (CarrierId.All.CONTENT_URI.equals(uri)) {
logd("onChange URI: " + uri);
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(
CarrierId.All.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(TelephonyManager.UNKNOWN_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:
final IccRecords newIccRecords = mPhone.getIccRecords();
if (mIccRecords != newIccRecords) {
if (mIccRecords != null) {
logd("Removing stale icc objects.");
mIccRecords.unregisterForSpnUpdate(this);
mIccRecords.unregisterForRecordsLoaded(this);
mIccRecords = null;
}
if (newIccRecords != null) {
logd("new Icc object");
newIccRecords.registerForSpnUpdate(this, SPN_OVERRIDE_EVENT, null);
newIccRecords.registerForRecordsLoaded(this, SIM_LOAD_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(
CarrierId.All.CONTENT_URI,
/* projection */ null,
/* selection */ CarrierId.All.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) {
mCarrierIdLocalLog.log("[updateCarrierIdAndName] cid:" + mCarrierId + " name:"
+ mCarrierName);
final Intent intent = new Intent(TelephonyManager
.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, mCarrierId);
intent.putExtra(TelephonyManager.EXTRA_CARRIER_NAME, mCarrierName);
intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId());
mContext.sendBroadcast(intent);
// update current subscriptions
ContentValues cv = new ContentValues();
cv.put(CarrierId.CARRIER_ID, mCarrierId);
cv.put(CarrierId.CARRIER_NAME, mCarrierName);
mContext.getContentResolver().update(
Uri.withAppendedPath(CarrierId.CONTENT_URI,
Integer.toString(mPhone.getSubId())), cv, null, null);
}
}
private CarrierMatchingRule makeCarrierMatchingRule(Cursor cursor) {
return new CarrierMatchingRule(
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.MCCMNC)),
cursor.getString(cursor.getColumnIndexOrThrow(
CarrierId.All.IMSI_PREFIX_XPATTERN)),
cursor.getString(cursor.getColumnIndexOrThrow(
CarrierId.All.ICCID_PREFIX)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.GID1)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.GID2)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.PLMN)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.SPN)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.APN)),
cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_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 << 7;
private static final int SCORE_IMSI_PREFIX = 1 << 6;
private static final int SCORE_ICCID_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 mIccidPrefix;
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 iccidPrefix,
String gid1, String gid2, String plmn, String spn, String apn, int cid,
String name) {
mMccMnc = mccmnc;
mImsiPrefixPattern = imsiPrefixPattern;
mIccidPrefix = iccidPrefix;
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 match with highest score 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 (mIccidPrefix != null) {
if (!iccidPrefixMatch(subscriptionRule.mIccidPrefix, mIccidPrefix)) {
mScore = SCORE_INVALID;
return;
}
mScore += SCORE_ICCID_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;
}
private boolean iccidPrefixMatch(String iccid, String prefix) {
if (iccid == null || prefix == null) {
return false;
}
return iccid.startsWith(prefix);
}
public String toString() {
return "[CarrierMatchingRule] -"
+ " mccmnc: " + mMccMnc
+ " gid1: " + mGid1
+ " gid2: " + mGid2
+ " plmn: " + mPlmn
+ " imsi_prefix: " + mImsiPrefixPattern
+ " iccid_prefix" + mIccidPrefix
+ " 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 iccid = mPhone.getIccSerialNumber();
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]"
+ " mnnmnc:" + mccmnc
+ " gid1: " + gid1
+ " gid2: " + gid2
+ " imsi: " + Rlog.pii(LOG_TAG, imsi)
+ " iccid: " + Rlog.pii(LOG_TAG, iccid)
+ " plmn: " + plmn
+ " spn: " + spn
+ " apn: " + apn);
}
CarrierMatchingRule subscriptionRule = new CarrierMatchingRule(
mccmnc, imsi, iccid, gid1, gid2, plmn, spn, apn,
TelephonyManager.UNKNOWN_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: " + TelephonyManager.UNKNOWN_CARRIER_ID
+ " name: " + null);
updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null);
} else {
logd("[matchCarrier] cid: " + maxRule.mCid + " name: " + maxRule.mName);
updateCarrierIdAndName(maxRule.mCid, maxRule.mName);
}
/*
* Write Carrier Identification Matching event, logging with the
* carrierId, mccmnc, gid1 and carrier list version to differentiate below cases of metrics:
* 1) unknown mccmnc - the Carrier Id provider contains no rule that matches the
* read mccmnc.
* 2) the Carrier Id provider contains some rule(s) that match the read mccmnc,
* but the read gid1 is not matched within the highest-scored rule.
* 3) successfully found a matched carrier id in the provider.
* 4) use carrier list version to compare the unknown carrier ratio between each version.
*/
String unknownGid1ToLog = ((maxScore & CarrierMatchingRule.SCORE_GID1) == 0
&& !TextUtils.isEmpty(subscriptionRule.mGid1)) ? subscriptionRule.mGid1 : null;
String unknownMccmncToLog = ((maxScore == CarrierMatchingRule.SCORE_INVALID
|| (maxScore & CarrierMatchingRule.SCORE_GID1) == 0)
&& !TextUtils.isEmpty(subscriptionRule.mMccMnc)) ? subscriptionRule.mMccMnc : null;
TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId,
unknownMccmncToLog, unknownGid1ToLog);
}
public int getCarrierListVersion() {
final Cursor cursor = mContext.getContentResolver().query(
Uri.withAppendedPath(CarrierId.All.CONTENT_URI,
"get_version"), null, null, null);
cursor.moveToFirst();
return cursor.getInt(0);
}
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("version: " + getCarrierListVersion());
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();
}
}