All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.internal.telephony.CarrierResolver Maven / Gradle / Ivy

/*
 * 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.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.provider.Telephony;
import android.service.carrier.CarrierIdentifier;
import android.telephony.PhoneStateListener;
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.annotations.VisibleForTesting;
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.Arrays;
import java.util.List;

/**
 * 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");

    // 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(20);
    private final TelephonyManager mTelephonyMgr;

    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);
            }
        }
    };

    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);
    }

    /**
     * This is triggered from SubscriptionInfoUpdater after sim state change.
     * The sequence of sim loading would be
     *  1. ACTION_SUBINFO_CONTENT_CHANGE
     *  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, SubscriptionInfoUpdater 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();
                break;
        }
    }

    private void handleSimLoaded() {
        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();
    }

    private void handleSimAbsent() {
        mCarrierMatchingRulesOnMccMnc.clear();
        mSpn = null;
        mPreferApn = null;
        updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null,
                TelephonyManager.UNKNOWN_CARRIER_ID, null,
                TelephonyManager.UNKNOWN_CARRIER_ID);
    }

    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:
                handleSimLoaded();
                break;
            case CARRIER_ID_DB_UPDATE_EVENT:
                loadCarrierMatchingRulesOnMccMnc();
                break;
            case PREFER_APN_UPDATE_EVENT:
                String preferApn = getPreferApn();
                if (!equals(mPreferApn, preferApn, true)) {
                    logd("[updatePreferApn] from:" + mPreferApn + " to:" + preferApn);
                    mPreferApn = preferApn;
                    matchSubscriptionCarrier();
                }
                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, 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));
                    }
                    matchSubscriptionCarrier();
                }
            } 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 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())) {
            // only persist carrier id to simInfo db when subId is valid.
            SubscriptionController.getInstance().setCarrierId(mCarrierId, mPhone.getSubId());
        }
    }

    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().startsWith(gid.toLowerCase());
        }

        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);
    }

    /**
     * find the best matching carrier from candidates with matched subscription MCCMNC.
     */
    private void matchSubscriptionCarrier() {
        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);
        } 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);
        }

        /*
         * 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);
    }

    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;
    }
    /**
     * 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 = 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);
            try {
                if (cursor != null) {
                    if (VDBG) {
                        logd("[getCarrierIdFromMccMnc]- " + cursor.getCount()
                                + " Records(s) in DB" + " mccmnc: " + mccmnc);
                    }
                    while (cursor.moveToNext()) {
                        return cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID));
                    }
                }
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        } catch (Exception ex) {
            loge("[getCarrierIdFromMccMnc]- ex: " + ex);
        }
        return TelephonyManager.UNKNOWN_CARRIER_ID;
    }

    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("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();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy