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

Commit 610060c8 authored by Jiashen Wang's avatar Jiashen Wang
Browse files

Initial setup for slot change receiver migration

Initial setup for migrating SlotChangehandler from LPA to Settings
Bug: 160819212
Bug: 153811431
Test: Manually tested pSIM insertion and removal

Change-Id: I3009182db4c6e1863dc9b619e21a17b1297743ed
parent b6bba3e9
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -3635,6 +3635,14 @@
            </intent-filter>/>
        </receiver>

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

        <!-- This is the longest AndroidManifest.xml ever. -->
    </application>
</manifest>
+229 −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.provider.Settings;
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 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;

    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(mContext)) {
            // TODO(b/170508680): 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(b/170508680): Display DSDS dialog to ask users whether to enable DSDS.
            } else {
                Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.");
                // TODO(b/170508680): 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(b/170508680): Switch the slot to the removebale slot and show the notification.
    }

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

        if (!isSuwFinished(mContext)) {
            // TODO(b/170508680): 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(b/170508680): 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(b/170508680): 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 static boolean isSuwFinished(Context context) {
        try {
            // DEVICE_PROVISIONED is 0 if still in setup wizard. 1 if setup completed.
            return Settings.Global.getInt(
                            context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED)
                    == 1;
        } catch (Settings.SettingNotFoundException e) {
            Log.e(TAG, "Cannot get DEVICE_PROVISIONED from the device.", e);
            return false;
        }
    }

    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() {}
}
+139 −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 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)) {
            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);
    }
}