/* * 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.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.AsyncResult; import android.os.Handler; import android.os.Message; import android.provider.Telephony; import android.service.carrier.CarrierIdentifier; import android.telephony.CarrierConfigManager; import android.telephony.PhoneStateListener; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.metrics.CarrierIdMatchStats; import com.android.internal.telephony.metrics.TelephonyMetrics; import com.android.internal.telephony.subscription.SubscriptionManagerService; import com.android.internal.telephony.uicc.IccRecords; import com.android.internal.telephony.uicc.UiccController; import com.android.internal.telephony.util.TelephonyUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.telephony.Rlog; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; /** * CarrierResolver identifies the subscription carrier and returns a canonical carrier Id * and a user friendly carrier name. CarrierResolver reads subscription info and check against * all carrier matching rules stored in CarrierIdProvider. It is msim aware, each phone has a * dedicated CarrierResolver. */ public class CarrierResolver extends Handler { private static final String LOG_TAG = CarrierResolver.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 ICC_CHANGED_EVENT = 2; private static final int PREFER_APN_UPDATE_EVENT = 3; private static final int CARRIER_ID_DB_UPDATE_EVENT = 4; private static final Uri CONTENT_URL_PREFER_APN = Uri.withAppendedPath( Telephony.Carriers.CONTENT_URI, "preferapn"); // Test purpose only. private static final String TEST_ACTION = "com.android.internal.telephony" + ".ACTION_TEST_OVERRIDE_CARRIER_ID"; // cached version of the carrier list, so that we don't need to re-query it every time. private Integer mCarrierListVersion; // cached matching rules based mccmnc to speed up resolution private List mCarrierMatchingRulesOnMccMnc = new ArrayList<>(); // cached carrier Id private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; // cached specific carrier Id private int mSpecificCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; // cached MNO carrier Id. mno carrier shares the same mccmnc as cid and can be solely // identified by mccmnc only. If there is no such mno carrier, mno carrier id equals to // the cid. private int mMnoCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; // cached carrier name private String mCarrierName; private String mSpecificCarrierName; // cached preferapn name private String mPreferApn; // override for testing purpose private String mTestOverrideApn; private String mTestOverrideCarrierPriviledgeRule; // 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(16); private final TelephonyManager mTelephonyMgr; private final ContentObserver mContentObserver = new ContentObserver(this) { @Override public void onChange(boolean selfChange, Uri uri) { if (Telephony.Carriers.CONTENT_URI.equals(uri)) { 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); } } }; /** * A broadcast receiver used for overriding carrier id for testing. There are six parameters, * only override_carrier_id is required, the others are options. * * To override carrier id by adb command, e.g.: * adb shell am broadcast -a com.android.internal.telephony.ACTION_TEST_OVERRIDE_CARRIER_ID \ * --ei override_carrier_id 1 * --ei override_specific_carrier_id 1 * --ei override_mno_carrier_id 1 * --es override_carrier_name test * --es override_specific_carrier_name test * --ei sub_id 1 */ private final BroadcastReceiver mCarrierIdTestReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int phoneId = mPhone.getPhoneId(); int carrierId = intent.getIntExtra("override_carrier_id", TelephonyManager.UNKNOWN_CARRIER_ID); int specificCarrierId = intent.getIntExtra("override_specific_carrier_id", carrierId); int mnoCarrierId = intent.getIntExtra("override_mno_carrier_id", carrierId); String carrierName = intent.getStringExtra("override_carrier_name"); String specificCarrierName = intent.getStringExtra("override_specific_carrier_name"); int subId = intent.getIntExtra("sub_id", SubscriptionManager.getDefaultSubscriptionId()); if (carrierId <= 0) { logd("Override carrier id must be greater than 0.", phoneId); return; } else if (subId != mPhone.getSubId()) { logd("Override carrier id failed. The sub id doesn't same as phone's sub id.", phoneId); return; } else { logd("Override carrier id to: " + carrierId, phoneId); logd("Override specific carrier id to: " + specificCarrierId, phoneId); logd("Override mno carrier id to: " + mnoCarrierId, phoneId); logd("Override carrier name to: " + carrierName, phoneId); logd("Override specific carrier name to: " + specificCarrierName, phoneId); updateCarrierIdAndName( carrierId, carrierName != null ? carrierName : "", specificCarrierId, specificCarrierName != null ? carrierName : "", mnoCarrierId, false); } } }; public CarrierResolver(Phone phone) { logd("Creating CarrierResolver[" + 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); UiccController.getInstance().registerForIccChanged(this, ICC_CHANGED_EVENT, null); if (TelephonyUtils.IS_DEBUGGABLE) { IntentFilter filter = new IntentFilter(); filter.addAction(TEST_ACTION); mContext.registerReceiver(mCarrierIdTestReceiver, filter); } } /** * This is triggered from UiccController after sim state change. * The sequence of sim loading would be * 1. OnSubscriptionsChangedListener * 2. ACTION_SIM_STATE_CHANGED/ACTION_SIM_CARD_STATE_CHANGED * /ACTION_SIM_APPLICATION_STATE_CHANGED * 3. ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED * * For SIM refresh either reset or init refresh type, UiccController will re-trigger * carrier identification with sim loaded state. Framework today silently handle single file * refresh type. * TODO: check fileId from single file refresh, if the refresh file is IMSI, gid1 or other * records which might change carrier id, framework should trigger sim loaded state just like * other refresh events: INIT or RESET and which will ultimately trigger carrier * re-identification. */ public void resolveSubscriptionCarrierId(String simState) { logd("[resolveSubscriptionCarrierId] simState: " + simState); switch (simState) { case IccCardConstants.INTENT_VALUE_ICC_ABSENT: case IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR: // only clear carrier id on absent to avoid transition to unknown carrier id during // intermediate states of sim refresh handleSimAbsent(); break; case IccCardConstants.INTENT_VALUE_ICC_LOADED: handleSimLoaded(false); break; } } private void handleSimLoaded(boolean isSimOverride) { if (mIccRecords != null) { /** * returns empty string to be consistent with * {@link TelephonyManager#getSimOperatorName()} */ mSpn = (mIccRecords.getServiceProviderName() == null) ? "" : mIccRecords.getServiceProviderName(); } else { loge("mIccRecords is null on SIM_LOAD_EVENT, could not get SPN"); } mPreferApn = getPreferApn(); loadCarrierMatchingRulesOnMccMnc( false /* update carrier config */, isSimOverride); } private void handleSimAbsent() { mCarrierMatchingRulesOnMccMnc.clear(); mSpn = null; mPreferApn = null; updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null, TelephonyManager.UNKNOWN_CARRIER_ID, null, TelephonyManager.UNKNOWN_CARRIER_ID, false); } private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String ignored) { } }; /** * 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. 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. 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 (DBG) logd("handleMessage: " + msg.what); switch (msg.what) { case SIM_LOAD_EVENT: AsyncResult result = (AsyncResult) msg.obj; boolean isSimOverride = false; if (result != null) { isSimOverride = result.userObj instanceof Boolean && (Boolean) result.userObj; } handleSimLoaded(isSimOverride); break; case CARRIER_ID_DB_UPDATE_EVENT: // clean the cached carrier list version, so that a new one will be queried. mCarrierListVersion = null; loadCarrierMatchingRulesOnMccMnc(true /* update carrier config*/, false); break; case PREFER_APN_UPDATE_EVENT: String preferApn = getPreferApn(); if (!equals(mPreferApn, preferApn, true)) { logd("[updatePreferApn] from:" + mPreferApn + " to:" + preferApn); mPreferApn = preferApn; matchSubscriptionCarrier(true /* update carrier config*/, false); } break; case ICC_CHANGED_EVENT: // all records used for carrier identification are from SimRecord. final IccRecords newIccRecords = UiccController.getInstance().getIccRecords( mPhone.getPhoneId(), UiccController.APP_FAM_3GPP); if (mIccRecords != newIccRecords) { if (mIccRecords != null) { logd("Removing stale icc objects."); mIccRecords.unregisterForRecordsOverride(this); mIccRecords = null; } if (newIccRecords != null) { logd("new Icc object"); newIccRecords.registerForRecordsOverride(this, SIM_LOAD_EVENT, /* is sim override*/true); mIccRecords = newIccRecords; } } break; default: loge("invalid msg: " + msg.what); break; } } private void loadCarrierMatchingRulesOnMccMnc( boolean updateCarrierConfig, boolean isSimOverride) { 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)); } matchSubscriptionCarrier(updateCarrierConfig, isSimOverride); // Generate metrics related to carrier ID table version. CarrierIdMatchStats.sendCarrierIdTableVersion(getCarrierListVersion()); } } finally { if (cursor != null) { cursor.close(); } } } catch (Exception ex) { loge("[loadCarrierMatchingRules]- ex: " + ex); } } private String getCarrierNameFromId(int cid) { try { Cursor cursor = mContext.getContentResolver().query( CarrierId.All.CONTENT_URI, /* projection */ null, /* selection */ CarrierId.CARRIER_ID + "=?", /* selectionArgs */ new String[]{cid + ""}, null); try { if (cursor != null) { if (VDBG) { logd("[getCarrierNameFromId]- " + cursor.getCount() + " Records(s) in DB" + " cid: " + cid); } while (cursor.moveToNext()) { return cursor.getString(cursor.getColumnIndex(CarrierId.CARRIER_NAME)); } } } finally { if (cursor != null) { cursor.close(); } } } catch (Exception ex) { loge("[getCarrierNameFromId]- ex: " + ex); } return null; } private static List getCarrierMatchingRulesFromMccMnc( @NonNull Context context, String mccmnc) { List rules = new ArrayList<>(); try { Cursor cursor = context.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); } rules.clear(); while (cursor.moveToNext()) { rules.add(makeCarrierMatchingRule(cursor)); } } } finally { if (cursor != null) { cursor.close(); } } } catch (Exception ex) { loge("[loadCarrierMatchingRules]- ex: " + ex); } return rules; } private String getPreferApn() { // return test overrides if present if (!TextUtils.isEmpty(mTestOverrideApn)) { logd("[getPreferApn]- " + mTestOverrideApn + " test override"); return mTestOverrideApn; } 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 boolean isPreferApnUserEdited(@NonNull String preferApn) { try (Cursor cursor = mContext.getContentResolver().query( Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "preferapn/subId/" + mPhone.getSubId()), /* projection */ new String[]{Telephony.Carriers.EDITED_STATUS}, /* selection */ Telephony.Carriers.APN + "=?", /* selectionArgs */ new String[]{preferApn}, /* sortOrder */ null) ) { if (cursor != null && cursor.moveToFirst()) { return cursor.getInt(cursor.getColumnIndexOrThrow( Telephony.Carriers.EDITED_STATUS)) == Telephony.Carriers.USER_EDITED; } } catch (Exception ex) { loge("[isPreferApnUserEdited]- exception: " + ex); } return false; } public void setTestOverrideApn(String apn) { logd("[setTestOverrideApn]: " + apn); mTestOverrideApn = apn; } public void setTestOverrideCarrierPriviledgeRule(String rule) { logd("[setTestOverrideCarrierPriviledgeRule]: " + rule); mTestOverrideCarrierPriviledgeRule = rule; } private void updateCarrierIdAndName(int cid, String name, int specificCarrierId, String specificCarrierName, int mnoCid, boolean isSimOverride) { boolean update = false; if (specificCarrierId != mSpecificCarrierId) { logd("[updateSpecificCarrierId] from:" + mSpecificCarrierId + " to:" + specificCarrierId); mSpecificCarrierId = specificCarrierId; update = true; } if (specificCarrierName != mSpecificCarrierName) { logd("[updateSpecificCarrierName] from:" + mSpecificCarrierName + " to:" + specificCarrierName); mSpecificCarrierName = specificCarrierName; update = true; } if (update) { mCarrierIdLocalLog.log("[updateSpecificCarrierIdAndName] cid:" + mSpecificCarrierId + " name:" + mSpecificCarrierName); final Intent intent = new Intent(TelephonyManager .ACTION_SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED); intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID, mSpecificCarrierId); intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_NAME, mSpecificCarrierName); intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId()); mContext.sendBroadcast(intent); // notify content observers for specific carrier id change event. ContentValues cv = new ContentValues(); cv.put(CarrierId.SPECIFIC_CARRIER_ID, mSpecificCarrierId); cv.put(CarrierId.SPECIFIC_CARRIER_ID_NAME, mSpecificCarrierName); mContext.getContentResolver().update( Telephony.CarrierId.getSpecificCarrierIdUriForSubscriptionId(mPhone.getSubId()), cv, null, null); } 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 (mnoCid != mMnoCarrierId) { logd("[updateMnoCarrierId] from:" + mMnoCarrierId + " to:" + mnoCid); mMnoCarrierId = mnoCid; update = true; } if (update) { mCarrierIdLocalLog.log("[updateCarrierIdAndName] cid:" + mCarrierId + " name:" + mCarrierName + " mnoCid:" + mMnoCarrierId); 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); // notify content observers for carrier id change event ContentValues cv = new ContentValues(); cv.put(CarrierId.CARRIER_ID, mCarrierId); cv.put(CarrierId.CARRIER_NAME, mCarrierName); mContext.getContentResolver().update( Telephony.CarrierId.getUriForSubscriptionId(mPhone.getSubId()), cv, null, null); } // during esim profile switch, there is no sim absent thus carrier id will persist and // might not trigger an update if switch profiles for the same carrier. thus always update // subscriptioninfo db to make sure we have correct carrier id set. if (SubscriptionManager.isValidSubscriptionId(mPhone.getSubId()) && !isSimOverride) { // only persist carrier id to simInfo db when subId is valid. SubscriptionManagerService.getInstance().setCarrierId(mPhone.getSubId(), mCarrierId); } } private static CarrierMatchingRule makeCarrierMatchingRule(Cursor cursor) { String certs = cursor.getString( cursor.getColumnIndexOrThrow(CarrierId.All.PRIVILEGE_ACCESS_RULE)); 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)), (TextUtils.isEmpty(certs) ? null : new ArrayList<>(Arrays.asList(certs))), cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_ID)), cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_NAME)), cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.PARENT_CARRIER_ID))); } /** * carrier matching attributes with corresponding cid */ public 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 << 8; private static final int SCORE_IMSI_PREFIX = 1 << 7; private static final int SCORE_ICCID_PREFIX = 1 << 6; private static final int SCORE_GID1 = 1 << 5; private static final int SCORE_GID2 = 1 << 4; private static final int SCORE_PLMN = 1 << 3; private static final int SCORE_PRIVILEGE_ACCESS_RULE = 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 public final String mccMnc; public final String imsiPrefixPattern; public final String iccidPrefix; public final String gid1; public final String gid2; public final String plmn; public final String spn; public final String apn; // there can be multiple certs configured in the UICC public final List privilegeAccessRule; // user-facing carrier name private String mName; // unique carrier id private int mCid; // unique parent carrier id private int mParentCid; private int mScore = 0; @VisibleForTesting public CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String iccidPrefix, String gid1, String gid2, String plmn, String spn, String apn, List privilegeAccessRule, int cid, String name, int parentCid) { mccMnc = mccmnc; this.imsiPrefixPattern = imsiPrefixPattern; this.iccidPrefix = iccidPrefix; this.gid1 = gid1; this.gid2 = gid2; this.plmn = plmn; this.spn = spn; this.apn = apn; this.privilegeAccessRule = privilegeAccessRule; mCid = cid; mName = name; mParentCid = parentCid; } private CarrierMatchingRule(CarrierMatchingRule rule) { mccMnc = rule.mccMnc; imsiPrefixPattern = rule.imsiPrefixPattern; iccidPrefix = rule.iccidPrefix; gid1 = rule.gid1; gid2 = rule.gid2; plmn = rule.plmn; spn = rule.spn; apn = rule.apn; privilegeAccessRule = rule.privilegeAccessRule; mCid = rule.mCid; mName = rule.mName; mParentCid = rule.mParentCid; } // 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 (mccMnc != null) { if (!CarrierResolver.equals(subscriptionRule.mccMnc, mccMnc, false)) { mScore = SCORE_INVALID; return; } mScore += SCORE_MCCMNC; } if (imsiPrefixPattern != null) { if (!imsiPrefixMatch(subscriptionRule.imsiPrefixPattern, imsiPrefixPattern)) { mScore = SCORE_INVALID; return; } mScore += SCORE_IMSI_PREFIX; } if (iccidPrefix != null) { if (!iccidPrefixMatch(subscriptionRule.iccidPrefix, iccidPrefix)) { mScore = SCORE_INVALID; return; } mScore += SCORE_ICCID_PREFIX; } if (gid1 != null) { if (!gidMatch(subscriptionRule.gid1, gid1)) { mScore = SCORE_INVALID; return; } mScore += SCORE_GID1; } if (gid2 != null) { if (!gidMatch(subscriptionRule.gid2, gid2)) { mScore = SCORE_INVALID; return; } mScore += SCORE_GID2; } if (plmn != null) { if (!CarrierResolver.equals(subscriptionRule.plmn, plmn, true)) { mScore = SCORE_INVALID; return; } mScore += SCORE_PLMN; } if (spn != null) { if (!CarrierResolver.equals(subscriptionRule.spn, spn, true)) { mScore = SCORE_INVALID; return; } mScore += SCORE_SPN; } if (privilegeAccessRule != null && !privilegeAccessRule.isEmpty()) { if (!carrierPrivilegeRulesMatch(subscriptionRule.privilegeAccessRule, privilegeAccessRule)) { mScore = SCORE_INVALID; return; } mScore += SCORE_PRIVILEGE_ACCESS_RULE; } if (apn != null) { if (!CarrierResolver.equals(subscriptionRule.apn, apn, 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); } // We are doing prefix and case insensitive match. // Ideally we should do full string match. However due to SIM manufacture issues // gid from some SIM might has garbage tail. private boolean gidMatch(String gidFromSim, String gid) { return (gidFromSim != null) && gidFromSim.toLowerCase(Locale.ROOT) .startsWith(gid.toLowerCase(Locale.ROOT)); } private boolean carrierPrivilegeRulesMatch(List certsFromSubscription, List certs) { if (certsFromSubscription == null || certsFromSubscription.isEmpty()) { return false; } for (String cert : certs) { for (String certFromSubscription : certsFromSubscription) { if (!TextUtils.isEmpty(cert) && cert.equalsIgnoreCase(certFromSubscription)) { return true; } } } return false; } public String toString() { return "[CarrierMatchingRule] -" + " mccmnc: " + mccMnc + " gid1: " + gid1 + " gid2: " + gid2 + " plmn: " + plmn + " imsi_prefix: " + imsiPrefixPattern + " iccid_prefix" + iccidPrefix + " spn: " + spn + " privilege_access_rule: " + privilegeAccessRule + " apn: " + apn + " name: " + mName + " cid: " + mCid + " score: " + mScore; } } private CarrierMatchingRule getSubscriptionMatchingRule() { 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; List accessRules; // check if test override present if (!TextUtils.isEmpty(mTestOverrideCarrierPriviledgeRule)) { accessRules = new ArrayList<>(Arrays.asList(mTestOverrideCarrierPriviledgeRule)); } else { accessRules = mTelephonyMgr.createForSubscriptionId(mPhone.getSubId()) .getCertsFromCarrierPrivilegeAccessRules(); } if (VDBG) { logd("[matchSubscriptionCarrier]" + " mnnmnc:" + mccmnc + " gid1: " + gid1 + " gid2: " + gid2 + " imsi: " + Rlog.pii(LOG_TAG, imsi) + " iccid: " + Rlog.pii(LOG_TAG, iccid) + " plmn: " + plmn + " spn: " + spn + " apn: " + apn + " accessRules: " + ((accessRules != null) ? accessRules : null)); } return new CarrierMatchingRule( mccmnc, imsi, iccid, gid1, gid2, plmn, spn, apn, accessRules, TelephonyManager.UNKNOWN_CARRIER_ID, null, TelephonyManager.UNKNOWN_CARRIER_ID); } private void updateCarrierConfig() { IccCard iccCard = mPhone.getIccCard(); IccCardConstants.State simState = IccCardConstants.State.UNKNOWN; if (iccCard != null) { simState = iccCard.getState(); } CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); configManager.updateConfigForPhoneId(mPhone.getPhoneId(), UiccController.getIccStateIntentString(simState)); } /** * find the best matching carrier from candidates with matched subscription MCCMNC. */ private void matchSubscriptionCarrier(boolean updateCarrierConfig, boolean isSimOverride) { if (!SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) { logd("[matchSubscriptionCarrier]" + "skip before sim records loaded"); return; } int maxScore = CarrierMatchingRule.SCORE_INVALID; /** * For child-parent relationship. either child and parent have the same matching * score, or child's matching score > parents' matching score. */ CarrierMatchingRule maxRule = null; CarrierMatchingRule maxRuleParent = null; /** * matching rule with mccmnc only. If mnoRule is found, then mno carrier id equals to the * cid from mnoRule. otherwise, mno carrier id is same as cid. */ CarrierMatchingRule mnoRule = null; CarrierMatchingRule subscriptionRule = getSubscriptionMatchingRule(); for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) { rule.match(subscriptionRule); if (rule.mScore > maxScore) { maxScore = rule.mScore; maxRule = rule; maxRuleParent = rule; } else if (maxScore > CarrierMatchingRule.SCORE_INVALID && rule.mScore == maxScore) { // to handle the case that child parent has the same matching score, we need to // differentiate who is child who is parent. if (rule.mParentCid == maxRule.mCid) { maxRule = rule; } else if (maxRule.mParentCid == rule.mCid) { maxRuleParent = rule; } } if (rule.mScore == CarrierMatchingRule.SCORE_MCCMNC) { mnoRule = rule; } } if (maxScore == CarrierMatchingRule.SCORE_INVALID) { logd("[matchSubscriptionCarrier - no match] cid: " + TelephonyManager.UNKNOWN_CARRIER_ID + " name: " + null); updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null, TelephonyManager.UNKNOWN_CARRIER_ID, null, TelephonyManager.UNKNOWN_CARRIER_ID, isSimOverride); } else { // if there is a single matching result, check if this rule has parent cid assigned. if ((maxRule == maxRuleParent) && maxRule.mParentCid != TelephonyManager.UNKNOWN_CARRIER_ID) { maxRuleParent = new CarrierMatchingRule(maxRule); maxRuleParent.mCid = maxRuleParent.mParentCid; maxRuleParent.mName = getCarrierNameFromId(maxRuleParent.mCid); } logd("[matchSubscriptionCarrier] specific cid: " + maxRule.mCid + " specific name: " + maxRule.mName +" cid: " + maxRuleParent.mCid + " name: " + maxRuleParent.mName); updateCarrierIdAndName(maxRuleParent.mCid, maxRuleParent.mName, maxRule.mCid, maxRule.mName, (mnoRule == null) ? maxRule.mCid : mnoRule.mCid, isSimOverride); if (updateCarrierConfig) { logd("[matchSubscriptionCarrier] - Calling updateCarrierConfig()"); updateCarrierConfig(); } } /* * 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.gid1)) ? subscriptionRule.gid1 : null; String unknownMccmncToLog = ((maxScore == CarrierMatchingRule.SCORE_INVALID || (maxScore & CarrierMatchingRule.SCORE_GID1) == 0) && !TextUtils.isEmpty(subscriptionRule.mccMnc)) ? subscriptionRule.mccMnc : null; // pass subscription rule to metrics. scrub all possible PII before uploading. // only log apn if not user edited. String apn = (subscriptionRule.apn != null && !isPreferApnUserEdited(subscriptionRule.apn)) ? subscriptionRule.apn : null; // only log first 7 bits of iccid String iccidPrefix = (subscriptionRule.iccidPrefix != null) && (subscriptionRule.iccidPrefix.length() >= 7) ? subscriptionRule.iccidPrefix.substring(0, 7) : subscriptionRule.iccidPrefix; // only log first 8 bits of imsi String imsiPrefix = (subscriptionRule.imsiPrefixPattern != null) && (subscriptionRule.imsiPrefixPattern.length() >= 8) ? subscriptionRule.imsiPrefixPattern.substring(0, 8) : subscriptionRule.imsiPrefixPattern; CarrierMatchingRule simInfo = new CarrierMatchingRule( subscriptionRule.mccMnc, imsiPrefix, iccidPrefix, subscriptionRule.gid1, subscriptionRule.gid2, subscriptionRule.plmn, subscriptionRule.spn, apn, subscriptionRule.privilegeAccessRule, -1, null, -1); TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent( mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId, unknownMccmncToLog, unknownGid1ToLog, simInfo); // Generate statsd metrics only when MCC/MNC is unknown or there is no match for GID1. if (unknownMccmncToLog != null || unknownGid1ToLog != null) { // Pass the PNN value to metrics only if the SPN is empty String pnn = TextUtils.isEmpty(subscriptionRule.spn) ? subscriptionRule.plmn : ""; CarrierIdMatchStats.onCarrierIdMismatch( mCarrierId, unknownMccmncToLog, unknownGid1ToLog, subscriptionRule.spn, pnn); } } public int getCarrierListVersion() { // Use the cached value if it exists, otherwise retrieve it. if (mCarrierListVersion == null) { // The auto closeable cursor will be closed after exiting try-block. try (Cursor cursor = mContext.getContentResolver().query( Uri.withAppendedPath(CarrierId.All.CONTENT_URI, "get_version"), null, null, null)) { cursor.moveToFirst(); mCarrierListVersion = cursor.getInt(0); } } return mCarrierListVersion; } public int getCarrierId() { return mCarrierId; } /** * Returns fine-grained carrier id of the current subscription. Carrier ids with a valid parent * id are specific carrier ids. * * A specific carrier ID can represent the fact that a carrier may be in effect an aggregation * of other carriers (ie in an MVNO type scenario) where each of these specific carriers which * are used to make up the actual carrier service may have different carrier configurations. * A specific carrier ID could also be used, for example, in a scenario where a carrier requires * different carrier configuration for different service offering such as a prepaid plan. * e.g, {@link #getCarrierId()} will always return Tracfone (id 2022) for a Tracfone SIM, while * {@link #getSpecificCarrierId()} can return Tracfone AT&T or Tracfone T-Mobile based on the * IMSI from the current subscription. * * For carriers without any fine-grained carrier ids, return {@link #getCarrierId()} */ public int getSpecificCarrierId() { return mSpecificCarrierId; } public String getCarrierName() { return mCarrierName; } public String getSpecificCarrierName() { return mSpecificCarrierName; } public int getMnoCarrierId() { return mMnoCarrierId; } /** * a util function to convert carrierIdentifier to the best matching carrier id. * * @return the best matching carrier id. */ public static int getCarrierIdFromIdentifier(@NonNull Context context, @NonNull CarrierIdentifier carrierIdentifier) { final String mccmnc = carrierIdentifier.getMcc() + carrierIdentifier.getMnc(); final String gid1 = carrierIdentifier.getGid1(); final String gid2 = carrierIdentifier.getGid2(); final String imsi = carrierIdentifier.getImsi(); final String spn = carrierIdentifier.getSpn(); if (VDBG) { logd("[getCarrierIdFromIdentifier]" + " mnnmnc:" + mccmnc + " gid1: " + gid1 + " gid2: " + gid2 + " imsi: " + Rlog.pii(LOG_TAG, imsi) + " spn: " + spn); } // assign null to other fields which are not supported by carrierIdentifier. CarrierMatchingRule targetRule = new CarrierMatchingRule(mccmnc, imsi, null, gid1, gid2, null, spn, null, null, TelephonyManager.UNKNOWN_CARRIER_ID_LIST_VERSION, null, TelephonyManager.UNKNOWN_CARRIER_ID); int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID; int maxScore = CarrierMatchingRule.SCORE_INVALID; List rules = getCarrierMatchingRulesFromMccMnc( context, targetRule.mccMnc); for (CarrierMatchingRule rule : rules) { rule.match(targetRule); if (rule.mScore > maxScore) { maxScore = rule.mScore; carrierId = rule.mCid; } } return carrierId; } /** * a util function to convert {mccmnc, mvno_type, mvno_data} to all matching carrier ids. * * @return a list of id with matching {mccmnc, mvno_type, mvno_data} */ public static List getCarrierIdsFromApnQuery(@NonNull Context context, String mccmnc, String mvnoCase, String mvnoData) { String selection = CarrierId.All.MCCMNC + "=" + mccmnc; // build the proper query if ("spn".equals(mvnoCase) && mvnoData != null) { selection += " AND " + CarrierId.All.SPN + "='" + mvnoData + "'"; } else if ("imsi".equals(mvnoCase) && mvnoData != null) { selection += " AND " + CarrierId.All.IMSI_PREFIX_XPATTERN + "='" + mvnoData + "'"; } else if ("gid1".equals(mvnoCase) && mvnoData != null) { selection += " AND " + CarrierId.All.GID1 + "='" + mvnoData + "'"; } else if ("gid2".equals(mvnoCase) && mvnoData != null) { selection += " AND " + CarrierId.All.GID2 + "='" + mvnoData +"'"; } else { logd("mvno case empty or other invalid values"); } List ids = new ArrayList<>(); try { Cursor cursor = context.getContentResolver().query( CarrierId.All.CONTENT_URI, /* projection */ null, /* selection */ selection, /* selectionArgs */ null, null); try { if (cursor != null) { if (VDBG) { logd("[getCarrierIdsFromApnQuery]- " + cursor.getCount() + " Records(s) in DB"); } while (cursor.moveToNext()) { int cid = cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID)); if (!ids.contains(cid)) { ids.add(cid); } } } } finally { if (cursor != null) { cursor.close(); } } } catch (Exception ex) { loge("[getCarrierIdsFromApnQuery]- ex: " + ex); } logd(selection + " " + ids); return ids; } // static helper function to get carrier id from mccmnc public static int getCarrierIdFromMccMnc(@NonNull Context context, String mccmnc) { try (Cursor cursor = getCursorForMccMnc(context, mccmnc)) { if (cursor == null || !cursor.moveToNext()) return TelephonyManager.UNKNOWN_CARRIER_ID; if (VDBG) { logd("[getCarrierIdFromMccMnc]- " + cursor.getCount() + " Records(s) in DB" + " mccmnc: " + mccmnc); } return cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID)); } catch (Exception ex) { loge("[getCarrierIdFromMccMnc]- ex: " + ex); } return TelephonyManager.UNKNOWN_CARRIER_ID; } /** * Static helper function to get carrier name from mccmnc * @param context Context * @param mccmnc PLMN * @return Carrier name string given mccmnc/PLMN * * @hide */ @Nullable public static String getCarrierNameFromMccMnc(@NonNull Context context, String mccmnc) { try (Cursor cursor = getCursorForMccMnc(context, mccmnc)) { if (cursor == null || !cursor.moveToNext()) return null; if (VDBG) { logd("[getCarrierNameFromMccMnc]- " + cursor.getCount() + " Records(s) in DB" + " mccmnc: " + mccmnc); } return cursor.getString(cursor.getColumnIndex(CarrierId.CARRIER_NAME)); } catch (Exception ex) { loge("[getCarrierNameFromMccMnc]- ex: " + ex); } return null; } @Nullable private static Cursor getCursorForMccMnc(@NonNull Context context, String mccmnc) { try { Cursor cursor = context.getContentResolver().query( CarrierId.All.CONTENT_URI, /* projection */ null, /* selection */ CarrierId.All.MCCMNC + "=? AND " + CarrierId.All.GID1 + " is NULL AND " + CarrierId.All.GID2 + " is NULL AND " + CarrierId.All.IMSI_PREFIX_XPATTERN + " is NULL AND " + CarrierId.All.SPN + " is NULL AND " + CarrierId.All.ICCID_PREFIX + " is NULL AND " + CarrierId.All.PLMN + " is NULL AND " + CarrierId.All.PRIVILEGE_ACCESS_RULE + " is NULL AND " + CarrierId.All.APN + " is NULL", /* selectionArgs */ new String[]{mccmnc}, null); return cursor; } catch (Exception ex) { loge("[getCursorForMccMnc]- ex: " + ex); return null; } } 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); } private static void logd(String str, int phoneId) { Rlog.d(LOG_TAG + "[" + phoneId + "]", str); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); ipw.println("mCarrierResolverLocalLogs:"); ipw.increaseIndent(); mCarrierIdLocalLog.dump(fd, pw, args); ipw.decreaseIndent(); ipw.println("mCarrierId: " + mCarrierId); ipw.println("mSpecificCarrierId: " + mSpecificCarrierId); ipw.println("mMnoCarrierId: " + mMnoCarrierId); ipw.println("mCarrierName: " + mCarrierName); ipw.println("mSpecificCarrierName: " + mSpecificCarrierName); ipw.println("carrier_list_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(); } }