Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 50dd796f authored by Bonian Chen's avatar Bonian Chen Committed by Android (Google) Code Review
Browse files

Merge "Initial setup for slot change receiver migration"

parents efa2a71b 838aa43c
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -3571,6 +3571,15 @@
                       android:value="com.android.settings.sound.MediaControlsSettings" />
        </activity>

        <receiver android:name=".sim.receivers.SimSlotChangeReceiver"
                  android:permission="com.qualcomm.permission.USE_QCRIL_MSG_TUNNEL"
                  android:exported="true">
            <intent-filter>
                <action android:name="qualcomm.intent.action.ACTION_SLOT_STATUS_CHANGED_IND" />
                <action android:name="android.telephony.action.SIM_SLOT_STATUS_CHANGED" />
            </intent-filter>
        </receiver>

        <!-- This is the longest AndroidManifest.xml ever. -->
    </application>
</manifest>
+224 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.settings.sim.receivers;

import static android.content.Context.MODE_PRIVATE;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Looper;
import android.os.SystemProperties;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.UiccSlotInfo;
import android.util.Log;

import com.android.settings.network.SubscriptionUtil;

import com.google.common.collect.ImmutableList;

import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

/** Perform actions after a slot change event is triggered. */
public class SimSlotChangeHandler {
    private static final String TAG = "SimSlotChangeHandler";

    private static final String SYSTEM_PROPERTY_DEVICE_PROVISIONED =
            "persist.sys.device_provisioned";

    private static final String EUICC_PREFS = "euicc_prefs";
    private static final String KEY_REMOVABLE_SLOT_STATE = "removable_slot_state";

    private static volatile SimSlotChangeHandler sSlotChangeHandler;

    /** Returns a SIM slot change handler singleton. */
    public static SimSlotChangeHandler get() {
        if (sSlotChangeHandler == null) {
            synchronized (SimSlotChangeHandler.class) {
                if (sSlotChangeHandler == null) {
                    sSlotChangeHandler = new SimSlotChangeHandler();
                }
            }
        }
        return sSlotChangeHandler;
    }

    private SubscriptionManager mSubMgr;
    private TelephonyManager mTelMgr;
    private Context mContext;
    private boolean mNotificationEnabled = true;

    void onSlotsStatusChange(Context context) {
        init(context);

        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new IllegalStateException("Cannot be called from main thread.");
        }

        if (mTelMgr.getActiveModemCount() > 1) {
            Log.i(TAG, "The device is already in DSDS mode. Do nothing.");
            return;
        }

        UiccSlotInfo removableSlotInfo = getRemovableUiccSlotInfo();
        if (removableSlotInfo == null) {
            Log.e(TAG, "Unable to find the removable slot. Do nothing.");
            return;
        }

        int lastRemovableSlotState = getLastRemovableSimSlotState(mContext);
        int currentRemovableSlotState = removableSlotInfo.getCardStateInfo();

        // Sets the current removable slot state.
        setRemovableSimSlotState(mContext, currentRemovableSlotState);

        if (lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT
                && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT) {
            handleSimInsert(removableSlotInfo);
            return;
        }
        if (lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT
                && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT) {
            handleSimRemove(removableSlotInfo);
            return;
        }
        Log.i(TAG, "Do nothing on slot status changes.");
    }

    private void init(Context context) {
        mSubMgr =
                (SubscriptionManager)
                        context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
        mTelMgr = context.getSystemService(TelephonyManager.class);
        mContext = context;
    }

    private void handleSimInsert(UiccSlotInfo removableSlotInfo) {
        Log.i(TAG, "Detect SIM inserted.");

        if (!isSuwFinished()) {
            // TODO: Store the action and handle it after SUW is finished.
            Log.i(TAG, "Still in SUW. Handle SIM insertion after SUW is finished");
            return;
        }

        if (removableSlotInfo.getIsActive()) {
            Log.i(TAG, "The removable slot is already active. Do nothing.");
            return;
        }

        if (!hasActiveEsimSubscription()) {
            if (mTelMgr.isMultiSimEnabled()) {
                Log.i(TAG, "Enabled profile exists. DSDS condition satisfied.");
                // TODO Display DSDS dialog to ask users whether to enable DSDS.
            } else {
                Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.");
                // TODO Display Choose a number to use screen for subscription selection.
            }
            return;
        }

        Log.i(
                TAG,
                "No enabled eSIM profile. Ready to switch to removable slot and show"
                        + " notification.");
        // TODO Switch the slot to the removebale slot and show the notification.
    }

    private void handleSimRemove(UiccSlotInfo removableSlotInfo) {
        Log.i(TAG, "Detect SIM removed.");

        if (!isSuwFinished()) {
            // TODO: Store the action and handle it after SUW is finished.
            Log.i(TAG, "Still in SUW. Handle SIM removal after SUW is finished");
            return;
        }

        List<SubscriptionInfo> groupedEmbeddedSubscriptions = getGroupedEmbeddedSubscriptions();

        if (groupedEmbeddedSubscriptions.size() == 0 || !removableSlotInfo.getIsActive()) {
            Log.i(TAG, "eSIM slot is active or no subscriptions exist. Do nothing.");
            return;
        }

        // If there is only 1 eSIM profile exists, we ask the user if they want to switch to that
        // profile.
        if (groupedEmbeddedSubscriptions.size() == 1) {
            Log.i(TAG, "Only 1 eSIM profile found. Ask user's consent to switch.");
            // TODO Display a dialog to ask users to switch.
            return;
        }

        // If there are more than 1 eSIM profiles installed, we show a screen to let users to choose
        // the number they want to use.
        Log.i(TAG, "Multiple eSIM profiles found. Ask user which subscription to use.");
        // TODO Display a dialog to ask user which SIM to switch.
    }

    private int getLastRemovableSimSlotState(Context context) {
        final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE);
        return prefs.getInt(KEY_REMOVABLE_SLOT_STATE, UiccSlotInfo.CARD_STATE_INFO_ABSENT);
    }

    private void setRemovableSimSlotState(Context context, int state) {
        final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE);
        prefs.edit().putInt(KEY_REMOVABLE_SLOT_STATE, state).apply();
    }

    @Nullable
    private UiccSlotInfo getRemovableUiccSlotInfo() {
        UiccSlotInfo[] slotInfos = mTelMgr.getUiccSlotsInfo();
        if (slotInfos == null) {
            Log.e(TAG, "slotInfos is null. Unable to get slot infos.");
            return null;
        }
        for (UiccSlotInfo slotInfo : slotInfos) {
            if (slotInfo != null && slotInfo.isRemovable()) {

                return slotInfo;
            }
        }
        return null;
    }

    private boolean isSuwFinished() {
        return "1".equals(SystemProperties.get(SYSTEM_PROPERTY_DEVICE_PROVISIONED, "0"));
    }

    private boolean hasActiveEsimSubscription() {
        List<SubscriptionInfo> activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr);
        return activeSubs.stream().anyMatch(SubscriptionInfo::isEmbedded);
    }

    private List<SubscriptionInfo> getGroupedEmbeddedSubscriptions() {
        List<SubscriptionInfo> groupedSubscriptions =
                SubscriptionUtil.getSelectableSubscriptionInfoList(mContext);
        if (groupedSubscriptions == null) {
            return ImmutableList.of();
        }
        return ImmutableList.copyOf(
                groupedSubscriptions.stream()
                        .filter(sub -> sub.isEmbedded())
                        .collect(Collectors.toList()));
    }

    private SimSlotChangeHandler() {}
}
+143 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.settings.sim.receivers;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.telephony.UiccCardInfo;
import android.telephony.UiccSlotInfo;
import android.telephony.euicc.EuiccManager;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;

import com.android.settingslib.utils.ThreadUtils;

import java.util.List;

/** The receiver when the slot status changes. */
public class SimSlotChangeReceiver extends BroadcastReceiver {
    private static final String TAG = "SlotChangeReceiver";

    private static final String SLOT_CHANGE_ACTION_QC =
            "qualcomm.intent.action.ACTION_SLOT_STATUS_CHANGED_IND";

    private final SimSlotChangeHandler mSlotChangeHandler = SimSlotChangeHandler.get();
    private final Object mLock = new Object();

    @Override
    public void onReceive(Context context, Intent intent) {

        String action = intent.getAction();
        if (!TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED.equals(action)
                && !SLOT_CHANGE_ACTION_QC.equals(action)) {
            Log.e(TAG, "Ignore slot changes due to unexpected action: " + action);
            return;
        }

        ThreadUtils.postOnBackgroundThread(
                () -> {
                    synchronized (mLock) {
                        if (!shouldHandleSlotChange(context)) {
                            return;
                        }
                        mSlotChangeHandler.onSlotsStatusChange(context);
                    }
                });
    }

    // Checks whether the slot event should be handled.
    private boolean shouldHandleSlotChange(Context context) {
        final EuiccManager euiccManager = context.getSystemService(EuiccManager.class);
        if (euiccManager == null || !euiccManager.isEnabled()) {
            Log.i(TAG, "Ignore slot changes because EuiccManager is disabled.");
            return false;
        }

        if (euiccManager.getOtaStatus() == EuiccManager.EUICC_OTA_IN_PROGRESS) {
            Log.i(TAG, "Ignore slot changes because eSIM OTA is in progress.");
            return false;
        }

        if (!isSimSlotStateValid(context)) {
            Log.i(TAG, "Ignore slot changes because SIM states are not valid.");
            return false;
        }

        return true;
    }

    // Checks whether the SIM slot state is valid for slot change event.
    private boolean isSimSlotStateValid(Context context) {
        final TelephonyManager telMgr = context.getSystemService(TelephonyManager.class);
        UiccSlotInfo[] slotInfos = telMgr.getUiccSlotsInfo();
        if (slotInfos == null) {
            Log.e(TAG, "slotInfos is null. Unable to get slot infos.");
            return false;
        }

        boolean isAllCardStringsEmpty = true;
        for (int i = 0; i < slotInfos.length; i++) {
            UiccSlotInfo slotInfo = slotInfos[i];

            if (slotInfo == null) {
                return false;
            }

            // After pSIM is inserted, there might be a short period that the status of both slots
            // are not accurate. We drop the event if any of sim presence state is ERROR or
            // RESTRICTED.
            if (slotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_ERROR
                    || slotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_RESTRICTED) {
                Log.i(TAG, "The SIM state is in an error. Drop the event. SIM info: " + slotInfo);
                return false;
            }

            UiccCardInfo cardInfo = findUiccCardInfoBySlot(telMgr, i);
            if (cardInfo == null) {
                continue;
            }
            if (!TextUtils.isEmpty(slotInfo.getCardId())
                    || !TextUtils.isEmpty(cardInfo.getIccId())) {
                isAllCardStringsEmpty = false;
            }
        }

        // We also drop the event if both the card strings are empty, which usually means it's
        // between SIM slots switch the slot status is not stable at this moment.
        if (isAllCardStringsEmpty) {
            Log.i(TAG, "All UICC card strings are empty. Drop this event.");
            return false;
        }

        return true;
    }

    @Nullable
    private UiccCardInfo findUiccCardInfoBySlot(TelephonyManager telMgr, int physicalSlotIndex) {
        List<UiccCardInfo> cardInfos = telMgr.getUiccCardsInfo();
        if (cardInfos == null) {
            return null;
        }
        return cardInfos.stream()
                .filter(info -> info.getSlotIndex() == physicalSlotIndex)
                .findFirst()
                .orElse(null);
    }
}