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

src.com.android.internal.telephony.uicc.UiccSlot 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.uicc;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.UserHandle;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.view.WindowManager;

import com.android.internal.R;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.uicc.IccCardStatus.CardState;
import com.android.internal.telephony.uicc.euicc.EuiccCard;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.telephony.Rlog;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * This class represents a physical slot on the device.
 */
public class UiccSlot extends Handler {
    private static final String TAG = "UiccSlot";
    private static final boolean DBG = true;

    public static final String EXTRA_ICC_CARD_ADDED =
            "com.android.internal.telephony.uicc.ICC_CARD_ADDED";
    public static final int INVALID_PHONE_ID = -1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            prefix = {"VOLTAGE_CLASS_"},
            value = {VOLTAGE_CLASS_UNKNOWN, VOLTAGE_CLASS_A, VOLTAGE_CLASS_B, VOLTAGE_CLASS_C})
    public @interface VoltageClass {}

    public static final int VOLTAGE_CLASS_UNKNOWN = 0;
    public static final int VOLTAGE_CLASS_A = 1;
    public static final int VOLTAGE_CLASS_B = 2;
    public static final int VOLTAGE_CLASS_C = 3;

    private final Object mLock = new Object();
    private boolean mActive;
    private boolean mStateIsUnknown = true;
    private CardState mCardState;
    private Context mContext;
    private UiccCard mUiccCard;
    private boolean mIsEuicc;
    private @VoltageClass int mMinimumVoltageClass;
    private String mEid;
    private AnswerToReset mAtr;
    private boolean mIsRemovable;
    // Map each available portIdx to phoneId
    private HashMap mPortIdxToPhoneId = new HashMap<>();
    //Map each available portIdx with old radio state for state checking
    private HashMap mLastRadioState = new HashMap<>();
    // Store iccId of each port.
    private HashMap mIccIds = new HashMap<>();

    private static final int EVENT_CARD_REMOVED = 13;
    private static final int EVENT_CARD_ADDED = 14;

    public UiccSlot(Context c, boolean isActive) {
        if (DBG) log("Creating");
        mContext = c;
        mActive = isActive;
        mCardState = null;
    }

    /**
     * Update slot. The main trigger for this is a change in the ICC Card status.
     */
    public void update(CommandsInterface ci, IccCardStatus ics, int phoneId, int slotIndex) {
        if (DBG) log("cardStatus update: " + ics.toString());
        synchronized (mLock) {
            mPortIdxToPhoneId.put(ics.mSlotPortMapping.mPortIndex, phoneId);
            CardState oldState = mCardState;
            mCardState = ics.mCardState;
            mIccIds.put(ics.mSlotPortMapping.mPortIndex, ics.iccid);
            parseAtr(ics.atr);
            mIsRemovable = isSlotRemovable(slotIndex);

            int radioState = ci.getRadioState();
            if (DBG) {
                log("update: radioState=" + radioState + " mLastRadioState=" + mLastRadioState);
            }

            if (absentStateUpdateNeeded(oldState)) {
                updateCardStateAbsent(ci.getRadioState(), phoneId,
                        ics.mSlotPortMapping.mPortIndex);
            // Because mUiccCard may be updated in both IccCardStatus and IccSlotStatus, we need to
            // create a new UiccCard instance in two scenarios:
            //   1. mCardState is changing from ABSENT to non ABSENT.
            //   2. The latest mCardState is not ABSENT, but there is no UiccCard instance.
            } else if ((oldState == null || oldState == CardState.CARDSTATE_ABSENT
                    || mUiccCard == null) && mCardState != CardState.CARDSTATE_ABSENT) {
                // No notification while we are just powering up
                if (radioState != TelephonyManager.RADIO_POWER_UNAVAILABLE
                        && mLastRadioState.getOrDefault(ics.mSlotPortMapping.mPortIndex,
                        TelephonyManager.RADIO_POWER_UNAVAILABLE)
                        != TelephonyManager.RADIO_POWER_UNAVAILABLE) {
                    if (DBG) log("update: notify card added");
                    sendMessage(obtainMessage(EVENT_CARD_ADDED, null));
                }

                // card is present in the slot now; create new mUiccCard
                if (mUiccCard != null) {
                    loge("update: mUiccCard != null when card was present; disposing it now");
                    mUiccCard.dispose();
                }

                if (!mIsEuicc) {
                    // Uicc does not support MEP, passing false by default.
                    mUiccCard = new UiccCard(mContext, ci, ics, phoneId, mLock, false);
                } else {
                    // The EID should be reported with the card status, but in case it's not we want
                    // to catch that here
                    if (TextUtils.isEmpty(ics.eid)) {
                        loge("update: eid is missing. ics.eid="
                                + Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, ics.eid));
                    }
                    mUiccCard = new EuiccCard(mContext, ci, ics, phoneId, mLock,
                            isMultipleEnabledProfileSupported());
                }
            } else {
                if (mUiccCard != null) {
                    mUiccCard.update(mContext, ci, ics, phoneId);
                }
            }
            mLastRadioState.put(ics.mSlotPortMapping.mPortIndex, radioState);
        }
    }

    /**
     * Update slot based on IccSlotStatus.
     */
    public void update(CommandsInterface[] ci, IccSlotStatus iss, int slotIndex) {
        if (DBG) log("slotStatus update: " + iss.toString());
        synchronized (mLock) {
            IccSimPortInfo[] simPortInfos = iss.mSimPortInfos;
            CardState oldState = mCardState;
            parseAtr(iss.atr);
            mCardState = iss.cardState;
            mEid = iss.eid;
            mIsRemovable = isSlotRemovable(slotIndex);

            for (int i = 0; i < simPortInfos.length; i++) {
                int phoneId = iss.mSimPortInfos[i].mLogicalSlotIndex;
                mIccIds.put(i, simPortInfos[i].mIccId);
                if (!iss.mSimPortInfos[i].mPortActive) {
                    // TODO: (b/79432584) evaluate whether should broadcast card state change
                    // even if it's inactive.
                    UiccController.updateInternalIccStateForInactivePort(mContext,
                            mPortIdxToPhoneId.getOrDefault(i, INVALID_PHONE_ID),
                            iss.mSimPortInfos[i].mIccId);
                    mLastRadioState.put(i, TelephonyManager.RADIO_POWER_UNAVAILABLE);
                    if (mUiccCard != null) {
                        // Dispose the port
                        mUiccCard.disposePort(i);
                    }
                } else {
                    if (absentStateUpdateNeeded(oldState)) {
                        int radioState = SubscriptionManager.isValidPhoneId(phoneId) ?
                                ci[phoneId].getRadioState() :
                                TelephonyManager.RADIO_POWER_UNAVAILABLE;
                        updateCardStateAbsent(radioState, phoneId, i);
                    }
                    // TODO: (b/79432584) Create UiccCard or EuiccCard object here.
                    // Right now It's OK not creating it because Card status update will do it.
                    // But we should really make them symmetric.
                }
            }
            // From MEP, Card can have multiple ports. So dispose UiccCard only when all the
            // ports are inactive.
            if (!hasActivePort(simPortInfos)) {
                if (mActive) {
                    mActive = false;
                    nullifyUiccCard(true /* sim state is unknown */);
                }
            } else {
                mActive = true;
            }
            mPortIdxToPhoneId.clear();
            for (int i = 0; i < simPortInfos.length; i++) {
                // If port is not active, update with invalid phone id(i.e. -1)
                mPortIdxToPhoneId.put(i, simPortInfos[i].mPortActive ?
                        simPortInfos[i].mLogicalSlotIndex : INVALID_PHONE_ID);
            }
            // Since the MEP capability is related with number ports reported, thus need to
            // update the flag after UiccCard creation.
            if (mUiccCard != null) {
                mUiccCard.updateSupportMultipleEnabledProfile(isMultipleEnabledProfileSupported());
            }
        }
    }

    private boolean hasActivePort(IccSimPortInfo[] simPortInfos) {
        for (IccSimPortInfo simPortInfo : simPortInfos) {
            if (simPortInfo.mPortActive) {
                return true;
            }
        }
        return false;
    }

    /* Return valid phoneId if possible from the portIdx mapping*/
    private int getAnyValidPhoneId() {
        for (int phoneId : mPortIdxToPhoneId.values()) {
            if (SubscriptionManager.isValidPhoneId(phoneId)) {
                return phoneId;
            }
        }
        return INVALID_PHONE_ID;
    }

    @NonNull
    public int[] getPortList() {
        synchronized (mLock) {
            return mPortIdxToPhoneId.keySet().stream().mapToInt(Integer::valueOf).toArray();
        }
    }

    /** Return whether the passing portIndex belong to this physical slot */
    public boolean isValidPortIndex(int portIndex) {
        return mPortIdxToPhoneId.containsKey(portIndex);
    }

    public int getPortIndexFromPhoneId(int phoneId) {
        synchronized (mLock) {
            for (Map.Entry entry : mPortIdxToPhoneId.entrySet()) {
                if (entry.getValue() == phoneId) {
                    return entry.getKey();
                }
            }
            return TelephonyManager.DEFAULT_PORT_INDEX;
        }
    }

    public int getPortIndexFromIccId(String iccId) {
        synchronized (mLock) {
            for (Map.Entry entry : mIccIds.entrySet()) {
                if (IccUtils.compareIgnoreTrailingFs(entry.getValue(), iccId)) {
                    return entry.getKey();
                }
            }
            // If iccId is not found, return invalid port index.
            return TelephonyManager.INVALID_PORT_INDEX;
        }
    }

    public int getPhoneIdFromPortIndex(int portIndex) {
        synchronized (mLock) {
            return mPortIdxToPhoneId.getOrDefault(portIndex, INVALID_PHONE_ID);
        }
    }

    public boolean isPortActive(int portIdx) {
        synchronized (mLock) {
            return SubscriptionManager.isValidPhoneId(
                    mPortIdxToPhoneId.getOrDefault(portIdx, INVALID_PHONE_ID));
        }
    }

    /* Returns true if multiple enabled profiles are supported */
    public boolean isMultipleEnabledProfileSupported() {
        // even ATR suggest UICC supports multiple enabled profiles, MEP can be disabled per
        // carrier restrictions, so checking the real number of ports reported from modem is
        // necessary.
        return mPortIdxToPhoneId.size() > 1 && mAtr != null &&
                mAtr.isMultipleEnabledProfilesSupported();
    }

    private boolean absentStateUpdateNeeded(CardState oldState) {
        return (oldState != CardState.CARDSTATE_ABSENT || mUiccCard != null)
                && mCardState == CardState.CARDSTATE_ABSENT;
    }

    private void updateCardStateAbsent(int radioState, int phoneId, int portIndex) {
        // No notification while we are just powering up
        if (radioState != TelephonyManager.RADIO_POWER_UNAVAILABLE
                && mLastRadioState.getOrDefault(
                        portIndex, TelephonyManager.RADIO_POWER_UNAVAILABLE)
                != TelephonyManager.RADIO_POWER_UNAVAILABLE) {
            if (DBG) log("update: notify card removed");
            sendMessage(obtainMessage(EVENT_CARD_REMOVED, null));
        }
        UiccController.updateInternalIccState(mContext, IccCardConstants.State.ABSENT,
                null, phoneId);
        // no card present in the slot now; dispose card and make mUiccCard null
        nullifyUiccCard(false /* sim state is not unknown */);
        mLastRadioState.put(portIndex, TelephonyManager.RADIO_POWER_UNAVAILABLE);
    }

    // whenever we set mUiccCard to null, we lose the ability to differentiate between absent and
    // unknown states. To mitigate this, we will us mStateIsUnknown to keep track. The sim is only
    // unknown if we haven't heard from the radio or if the radio has become unavailable.
    private void nullifyUiccCard(boolean stateUnknown) {
        if (mUiccCard != null) {
            mUiccCard.dispose();
        }
        mStateIsUnknown = stateUnknown;
        mUiccCard = null;
    }

    public boolean isStateUnknown() {
        if (mCardState == null || mCardState == CardState.CARDSTATE_ABSENT) {
            // mStateIsUnknown is valid only in this scenario.
            return mStateIsUnknown;
        }
        // if mUiccCard is null, assume the state to be UNKNOWN for now.
        // The state may be known but since the actual card object is not available,
        // it is safer to return UNKNOWN.
        return mUiccCard == null;
    }

    // Return true if a slot index is for removable UICCs or eUICCs
    private boolean isSlotRemovable(int slotIndex) {
        int[] euiccSlots = mContext.getResources()
                .getIntArray(com.android.internal.R.array.non_removable_euicc_slots);
        if (euiccSlots == null) {
            return true;
        }
        for (int euiccSlot : euiccSlots) {
            if (euiccSlot == slotIndex) {
                return false;
            }
        }

        return true;
    }

    private void checkIsEuiccSupported() {
        if (mAtr == null) {
            mIsEuicc = false;
            return;
        }
        mIsEuicc = mAtr.isEuiccSupported();
        log(" checkIsEuiccSupported : " + mIsEuicc);
    }

    private void checkMinimumVoltageClass() {
        mMinimumVoltageClass = VOLTAGE_CLASS_UNKNOWN;
        if (mAtr == null) {
            return;
        }
        // Supported voltage classes are stored in the 5 least significant bits of the TA byte for
        // global interface.
        List interfaceBytes = mAtr.getInterfaceBytes();
        for (int i = 0; i < interfaceBytes.size() - 1; i++) {
            if (interfaceBytes.get(i).getTD() != null
                    && (interfaceBytes.get(i).getTD() & AnswerToReset.T_MASK)
                            == AnswerToReset.T_VALUE_FOR_GLOBAL_INTERFACE
                    && interfaceBytes.get(i + 1).getTA() != null) {
                byte ta = interfaceBytes.get(i + 1).getTA();
                if ((ta & 0x01) != 0) {
                    mMinimumVoltageClass = VOLTAGE_CLASS_A;
                }
                if ((ta & 0x02) != 0) {
                    mMinimumVoltageClass = VOLTAGE_CLASS_B;
                }
                if ((ta & 0x04) != 0) {
                    mMinimumVoltageClass = VOLTAGE_CLASS_C;
                }
                return;
            }
        }
        // Use default value - only class A
        mMinimumVoltageClass = VOLTAGE_CLASS_A;
    }

    private void parseAtr(String atr) {
        mAtr = AnswerToReset.parseAtr(atr);
        checkIsEuiccSupported();
        checkMinimumVoltageClass();
    }

    public boolean isEuicc() {
        return mIsEuicc;
    }

    @VoltageClass
    public int getMinimumVoltageClass() {
        return mMinimumVoltageClass;
    }

    public boolean isActive() {
        return mActive;
    }

    public boolean isRemovable() {
        return mIsRemovable;
    }

    /**
     *  Returns the iccId specific to the port index.
     *  Always use {@link com.android.internal.telephony.uicc.UiccPort#getIccId} to get the iccId.
     *  Use this API to get the iccId of the inactive port only.
     */
    public String getIccId(int portIdx) {
        return mIccIds.get(portIdx);
    }

    public String getEid() {
        return mEid;
    }

    public boolean isExtendedApduSupported() {
        return  (mAtr != null && mAtr.isExtendedApduSupported());
    }

    @Override
    protected void finalize() {
        if (DBG) log("UiccSlot finalized");
    }

    private void onIccSwap(boolean isAdded) {

        boolean isHotSwapSupported = mContext.getResources().getBoolean(
                R.bool.config_hotswapCapable);

        if (isHotSwapSupported) {
            log("onIccSwap: isHotSwapSupported is true, don't prompt for rebooting");
            return;
        }
        // As this check is for shutdown status check, use any phoneId
        Phone phone = PhoneFactory.getPhone(getAnyValidPhoneId());
        if (phone != null && phone.isShuttingDown()) {
            log("onIccSwap: already doing shutdown, no need to prompt");
            return;
        }

        log("onIccSwap: isHotSwapSupported is false, prompt for rebooting");

        promptForRestart(isAdded);
    }

    private void promptForRestart(boolean isAdded) {
        synchronized (mLock) {
            final Resources res = mContext.getResources();
            final ComponentName dialogComponent = ComponentName.unflattenFromString(
                    res.getString(R.string.config_iccHotswapPromptForRestartDialogComponent));
            if (dialogComponent != null) {
                Intent intent = new Intent().setComponent(dialogComponent)
                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        .putExtra(EXTRA_ICC_CARD_ADDED, isAdded);
                try {
                    mContext.startActivityAsUser(intent, UserHandle.CURRENT);
                    return;
                } catch (ActivityNotFoundException e) {
                    loge("Unable to find ICC hotswap prompt for restart activity: " + e);
                }
            }

            // TODO: Here we assume the device can't handle SIM hot-swap
            //      and has to reboot. We may want to add a property,
            //      e.g. REBOOT_ON_SIM_SWAP, to indicate if modem support
            //      hot-swap.
            DialogInterface.OnClickListener listener = null;


            // TODO: SimRecords is not reset while SIM ABSENT (only reset while
            //       Radio_off_or_not_available). Have to reset in both both
            //       added or removed situation.
            listener = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    synchronized (mLock) {
                        if (which == DialogInterface.BUTTON_POSITIVE) {
                            if (DBG) log("Reboot due to SIM swap");
                            PowerManager pm = (PowerManager) mContext
                                    .getSystemService(Context.POWER_SERVICE);
                            pm.reboot("SIM is added.");
                        }
                    }
                }

            };

            Resources r = Resources.getSystem();

            String title = (isAdded) ? r.getString(R.string.sim_added_title) :
                    r.getString(R.string.sim_removed_title);
            String message = (isAdded) ? r.getString(R.string.sim_added_message) :
                    r.getString(R.string.sim_removed_message);
            String buttonTxt = r.getString(R.string.sim_restart_button);

            AlertDialog dialog = new AlertDialog.Builder(mContext)
                    .setTitle(title)
                    .setMessage(message)
                    .setPositiveButton(buttonTxt, listener)
                    .create();
            dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
            dialog.show();
        }
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case EVENT_CARD_REMOVED:
                onIccSwap(false);
                break;
            case EVENT_CARD_ADDED:
                onIccSwap(true);
                break;
            default:
                loge("Unknown Event " + msg.what);
        }
    }

    /**
     * Returns the state of the UiccCard in the slot.
     * @return
     */
    public CardState getCardState() {
        synchronized (mLock) {
            if (mCardState == null) {
                return CardState.CARDSTATE_ABSENT;
            } else {
                return mCardState;
            }
        }
    }

    /**
     * Returns the UiccCard in the slot.
     */
    public UiccCard getUiccCard() {
        synchronized (mLock) {
            return mUiccCard;
        }
    }

    /**
     * Processes radio state unavailable event
     */
    public void onRadioStateUnavailable(int phoneId) {
        nullifyUiccCard(true /* sim state is unknown */);

        if (phoneId != INVALID_PHONE_ID) {
            UiccController.updateInternalIccState(
                    mContext, IccCardConstants.State.UNKNOWN, null, phoneId);
            mLastRadioState.put(getPortIndexFromPhoneId(phoneId),
                    TelephonyManager.RADIO_POWER_UNAVAILABLE);
        }

        mCardState = null;
    }

    private void log(String msg) {
        Rlog.d(TAG, msg);
    }

    private void loge(String msg) {
        Rlog.e(TAG, msg);
    }

    private Map getPrintableIccIds() {
        Map printableIccIds = mIccIds.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey,
                        e -> SubscriptionInfo.givePrintableIccid(e.getValue())));
        return printableIccIds;
    }

    /**
     * Dump
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("UiccSlot:");
        pw.println(" mActive=" + mActive);
        pw.println(" mIsEuicc=" + mIsEuicc);
        pw.println(" isEuiccSupportsMultipleEnabledProfiles="
                + isMultipleEnabledProfileSupported());
        pw.println(" mIsRemovable=" + mIsRemovable);
        pw.println(" mLastRadioState=" + mLastRadioState);
        pw.println(" mIccIds=" + getPrintableIccIds());
        pw.println(" mPortIdxToPhoneId=" + mPortIdxToPhoneId);
        pw.println(" mEid=" + Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mEid));
        pw.println(" mCardState=" + mCardState);
        if (mUiccCard != null) {
            pw.println(" mUiccCard=" + mUiccCard);
            mUiccCard.dump(fd, pw, args);
        } else {
            pw.println(" mUiccCard=null");
        }
        pw.println();
        pw.flush();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy