/*
 * Copyright (C) 2018 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.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
import static android.telephony.TelephonyManager.EXTRA_ACTIVE_SIM_SUPPORTED_COUNT;

import android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncResult;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.RegistrantList;
import android.os.SystemProperties;
import android.provider.DeviceConfig;
import android.sysprop.TelephonyProperties;
import android.telephony.PhoneCapability;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyRegistryManager;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.subscription.SubscriptionManagerService;
import com.android.telephony.Rlog;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * This class manages phone's configuration which defines the potential capability (static) of the
 * phone and its current activated capability (current).
 * It gets and monitors static and current phone capability from the modem; send broadcast
 * if they change, and sends commands to modem to enable or disable phones.
 */
public class PhoneConfigurationManager {
    public static final String DSDA = "dsda";
    public static final String DSDS = "dsds";
    public static final String TSTS = "tsts";
    public static final String SSSS = "";
    /** DeviceConfig key for whether Virtual DSDA is enabled. */
    private static final String KEY_ENABLE_VIRTUAL_DSDA = "enable_virtual_dsda";
    private static final String LOG_TAG = "PhoneCfgMgr";
    private static final int EVENT_SWITCH_DSDS_CONFIG_DONE = 100;
    private static final int EVENT_GET_MODEM_STATUS = 101;
    private static final int EVENT_GET_MODEM_STATUS_DONE = 102;
    private static final int EVENT_GET_PHONE_CAPABILITY_DONE = 103;
    private static final int EVENT_DEVICE_CONFIG_CHANGED = 104;
    private static final int EVENT_GET_SIMULTANEOUS_CALLING_SUPPORT_DONE = 105;
    private static final int EVENT_SIMULTANEOUS_CALLING_SUPPORT_CHANGED = 106;

    /**
     * Listener interface for events related to the {@link PhoneConfigurationManager} which should
     * be reported to the {@link SimultaneousCallingTracker}.
     */
    public interface Listener {
        public void onPhoneCapabilityChanged();
        public void onDeviceConfigChanged();
    }

    /**
     * Base listener implementation.
     */
    public abstract static class ListenerBase implements Listener {
        @Override
        public void onPhoneCapabilityChanged() {}
        @Override
        public void onDeviceConfigChanged() {}
    }


    private static PhoneConfigurationManager sInstance = null;
    private final Context mContext;
    // Static capability retrieved from the modem - may be null in the case where no info has been
    // retrieved yet.
    private PhoneCapability mStaticCapability = null;
    private final Set<Integer> mSlotsSupportingSimultaneousCellularCalls = new HashSet<>(3);
    private final Set<Integer> mSubIdsSupportingSimultaneousCellularCalls = new HashSet<>(3);
    private final HashSet<Consumer<Set<Integer>>> mSimultaneousCellularCallingListeners =
            new HashSet<>(1);
    private final RadioConfig mRadioConfig;
    private final Handler mHandler;
    // mPhones is obtained from PhoneFactory and can have phones corresponding to inactive modems as
    // well. That is, the array size can be 2 even if num of active modems is 1.
    private Phone[] mPhones;
    private final Map<Integer, Boolean> mPhoneStatusMap;
    private MockableInterface mMi = new MockableInterface();
    private TelephonyManager mTelephonyManager;

    /** Feature flags */
    @NonNull
    private final FeatureFlags mFeatureFlags;
    private final DefaultPhoneNotifier mNotifier;
    public Set<Listener> mListeners = new CopyOnWriteArraySet<>();

    /**
     * True if 'Virtual DSDA' i.e., in-call IMS connectivity on both subs with only single logical
     * modem, is enabled.
     */
    private boolean mVirtualDsdaEnabled;
    private static final RegistrantList sMultiSimConfigChangeRegistrants = new RegistrantList();
    private static final String ALLOW_MOCK_MODEM_PROPERTY = "persist.radio.allow_mock_modem";
    private static final String BOOT_ALLOW_MOCK_MODEM_PROPERTY = "ro.boot.radio.allow_mock_modem";
    private static final boolean DEBUG = !"user".equals(Build.TYPE);
    /**
     * Init method to instantiate the object
     * Should only be called once.
     */
    public static PhoneConfigurationManager init(Context context,
            @NonNull FeatureFlags featureFlags) {
        synchronized (PhoneConfigurationManager.class) {
            if (sInstance == null) {
                sInstance = new PhoneConfigurationManager(context, featureFlags);
            } else {
                Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
            }
            return sInstance;
        }
    }

    /**
     * Constructor.
     * @param context context needed to send broadcast.
     */
    private PhoneConfigurationManager(Context context, @NonNull FeatureFlags featureFlags) {
        mContext = context;
        mFeatureFlags = featureFlags;
        // TODO: send commands to modem once interface is ready.
        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        mRadioConfig = RadioConfig.getInstance();
        mHandler = new ConfigManagerHandler();
        mPhoneStatusMap = new HashMap<>();
        mVirtualDsdaEnabled = DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_TELEPHONY, KEY_ENABLE_VIRTUAL_DSDA, false);
        mNotifier = new DefaultPhoneNotifier(mContext, mFeatureFlags);
        DeviceConfig.addOnPropertiesChangedListener(
                DeviceConfig.NAMESPACE_TELEPHONY, Runnable::run,
                properties -> {
                    if (TextUtils.equals(DeviceConfig.NAMESPACE_TELEPHONY,
                            properties.getNamespace())) {
                        mHandler.sendEmptyMessage(EVENT_DEVICE_CONFIG_CHANGED);
                    }
                });

        notifyCapabilityChanged();

        mPhones = PhoneFactory.getPhones();

        for (Phone phone : mPhones) {
            registerForRadioState(phone);
        }
    }

    /**
     * Assign a listener to be notified of state changes.
     *
     * @param listener A listener.
     */
    public void addListener(Listener listener) {
        mListeners.add(listener);
    }

    /**
     * Removes a listener.
     *
     * @param listener A listener.
     */
    public final void removeListener(Listener listener) {
        mListeners.remove(listener);
    }

    /**
     * Updates the mapping between the slot IDs that support simultaneous calling and the
     * associated sub IDs as well as notifies listeners.
     */
    private void updateSimultaneousSubIdsFromPhoneIdMappingAndNotify() {
        if (!mFeatureFlags.simultaneousCallingIndications()) return;
        Set<Integer> slotCandidates = mSlotsSupportingSimultaneousCellularCalls.stream()
                .map(i -> mPhones[i].getSubId())
                .filter(i ->i > SubscriptionManager.INVALID_SUBSCRIPTION_ID)
                        .collect(Collectors.toSet());
        if (mSubIdsSupportingSimultaneousCellularCalls.equals(slotCandidates))  return;
        log("updateSimultaneousSubIdsFromPhoneIdMapping update: "
                + mSubIdsSupportingSimultaneousCellularCalls + " -> " + slotCandidates);
        mSubIdsSupportingSimultaneousCellularCalls.clear();
        mSubIdsSupportingSimultaneousCellularCalls.addAll(slotCandidates);
        mNotifier.notifySimultaneousCellularCallingSubscriptionsChanged(
                mSubIdsSupportingSimultaneousCellularCalls);
    }

    private void registerForRadioState(Phone phone) {
        phone.mCi.registerForAvailable(mHandler, Phone.EVENT_RADIO_AVAILABLE, phone);
    }

    private PhoneCapability getDefaultCapability() {
        if (getPhoneCount() > 1) {
            return PhoneCapability.DEFAULT_DSDS_CAPABILITY;
        } else {
            return PhoneCapability.DEFAULT_SSSS_CAPABILITY;
        }
    }

    /**
     * If virtual DSDA is enabled for this UE, then increase maxActiveVoiceSubscriptions to 2.
     */
    private PhoneCapability maybeOverrideMaxActiveVoiceSubscriptions(
            final PhoneCapability staticCapability) {
        boolean isVDsdaEnabled = staticCapability.getLogicalModemList().size() > 1
                && mVirtualDsdaEnabled;
        boolean isBkwdCompatDsdaEnabled = mFeatureFlags.simultaneousCallingIndications()
                && mMi.getMultiSimProperty().orElse(SSSS).equals(DSDA);
        if (isVDsdaEnabled || isBkwdCompatDsdaEnabled) {
            // Since we already initialized maxActiveVoiceSubscriptions to the count the
            // modem is capable of, we are only able to increase that count via this method. We do
            // not allow a decrease of maxActiveVoiceSubscriptions:
            int updatedMaxActiveVoiceSubscriptions =
                    Math.max(staticCapability.getMaxActiveVoiceSubscriptions(), 2);
            return new PhoneCapability.Builder(staticCapability)
                    .setMaxActiveVoiceSubscriptions(updatedMaxActiveVoiceSubscriptions)
                    .build();
        } else {
            return staticCapability;
        }
    }

    private void maybeEnableCellularDSDASupport() {
        boolean bkwdsCompatDsda = mFeatureFlags.simultaneousCallingIndications()
                && getPhoneCount() > 1
                && mMi.getMultiSimProperty().orElse(SSSS).equals(DSDA);
        boolean halSupportSimulCalling = mRadioConfig != null
                && mRadioConfig.getRadioConfigProxy(null).getVersion().greaterOrEqual(
                        RIL.RADIO_HAL_VERSION_2_2)
                && getPhoneCount() > 1
                && getCellularStaticPhoneCapability().getMaxActiveVoiceSubscriptions() > 1;
        // Register for simultaneous calling support changes in the modem if the HAL supports it
        if (halSupportSimulCalling) {
            updateSimultaneousCallingSupport();
            mRadioConfig.registerForSimultaneousCallingSupportStatusChanged(mHandler,
                    EVENT_SIMULTANEOUS_CALLING_SUPPORT_CHANGED, null);
        } else if (bkwdsCompatDsda) {
            // For older devices that only declare that they support DSDA via modem config,
            // set DSDA as capable now statically.
            log("DSDA modem config detected - setting DSDA enabled");
            for (Phone p : mPhones) {
                mSlotsSupportingSimultaneousCellularCalls.add(p.getPhoneId());
            }
            updateSimultaneousSubIdsFromPhoneIdMappingAndNotify();
            notifySimultaneousCellularCallingSlotsChanged();
        }
        // Register for subId updates to notify listeners when simultaneous calling is configured
        if (mFeatureFlags.simultaneousCallingIndications()
                && (bkwdsCompatDsda || halSupportSimulCalling)) {
            mContext.getSystemService(TelephonyRegistryManager.class)
                    .addOnSubscriptionsChangedListener(
                            new SubscriptionManager.OnSubscriptionsChangedListener() {
                                @Override
                                public void onSubscriptionsChanged() {
                                    updateSimultaneousSubIdsFromPhoneIdMappingAndNotify();
                                }
                            }, mHandler::post);
        }
    }

    /**
     * Static method to get instance.
     */
    public static PhoneConfigurationManager getInstance() {
        if (sInstance == null) {
            Log.wtf(LOG_TAG, "getInstance null");
        }

        return sInstance;
    }

    /**
     * Handler class to handle callbacks
     */
    private final class ConfigManagerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            AsyncResult ar;
            Phone phone = null;
            switch (msg.what) {
                case Phone.EVENT_RADIO_AVAILABLE:
                case Phone.EVENT_RADIO_ON:
                    log("Received EVENT_RADIO_AVAILABLE/EVENT_RADIO_ON");
                    ar = (AsyncResult) msg.obj;
                    if (ar.userObj != null && ar.userObj instanceof Phone) {
                        phone = (Phone) ar.userObj;
                        updatePhoneStatus(phone);
                    } else {
                        // phone is null
                        log("Unable to add phoneStatus to cache. "
                                + "No phone object provided for event " + msg.what);
                    }
                    updateRadioCapability();
                    break;
                case EVENT_SWITCH_DSDS_CONFIG_DONE:
                    ar = (AsyncResult) msg.obj;
                    if (ar != null && ar.exception == null) {
                        int numOfLiveModems = msg.arg1;
                        onMultiSimConfigChanged(numOfLiveModems);
                    } else {
                        log(msg.what + " failure. Not switching multi-sim config." + ar.exception);
                    }
                    break;
                case EVENT_GET_MODEM_STATUS_DONE:
                    ar = (AsyncResult) msg.obj;
                    if (ar != null && ar.exception == null) {
                        int phoneId = msg.arg1;
                        boolean enabled = (boolean) ar.result;
                        // update the cache each time getModemStatus is requested
                        addToPhoneStatusCache(phoneId, enabled);
                    } else {
                        log(msg.what + " failure. Not updating modem status." + ar.exception);
                    }
                    break;
                case EVENT_GET_PHONE_CAPABILITY_DONE:
                    ar = (AsyncResult) msg.obj;
                    if (ar != null && ar.exception == null) {
                        setStaticPhoneCapability((PhoneCapability) ar.result);
                        notifyCapabilityChanged();
                        for (Listener l : mListeners) {
                            l.onPhoneCapabilityChanged();
                        }
                        maybeEnableCellularDSDASupport();
                    } else {
                        log(msg.what + " failure. Not getting phone capability." + ar.exception);
                    }
                    break;
                case EVENT_DEVICE_CONFIG_CHANGED:
                    boolean isVirtualDsdaEnabled = DeviceConfig.getBoolean(
                            DeviceConfig.NAMESPACE_TELEPHONY, KEY_ENABLE_VIRTUAL_DSDA, false);
                    if (isVirtualDsdaEnabled != mVirtualDsdaEnabled) {
                        log("EVENT_DEVICE_CONFIG_CHANGED: from " + mVirtualDsdaEnabled + " to "
                                + isVirtualDsdaEnabled);
                        mVirtualDsdaEnabled = isVirtualDsdaEnabled;
                        for (Listener l : mListeners) {
                            l.onDeviceConfigChanged();
                        }
                    }
                    break;
                case EVENT_SIMULTANEOUS_CALLING_SUPPORT_CHANGED:
                case EVENT_GET_SIMULTANEOUS_CALLING_SUPPORT_DONE:
                    log("Received EVENT_SLOTS_SUPPORTING_SIMULTANEOUS_CALL_CHANGED/DONE");
                    if (getPhoneCount() < 2) {
                        if (!mSlotsSupportingSimultaneousCellularCalls.isEmpty()) {
                            mSlotsSupportingSimultaneousCellularCalls.clear();
                        }
                        break;
                    }
                    ar = (AsyncResult) msg.obj;
                    if (ar != null && ar.exception == null) {
                        List<Integer> returnedArrayList = (List<Integer>) ar.result;
                        if (!mSlotsSupportingSimultaneousCellularCalls.isEmpty()) {
                            mSlotsSupportingSimultaneousCellularCalls.clear();
                        }
                        int maxValidPhoneSlot = getPhoneCount() - 1;
                        for (int i : returnedArrayList) {
                            if (i < 0 || i > maxValidPhoneSlot) {
                                loge("Invalid slot supporting DSDA =" + i + ". Disabling DSDA.");
                                mSlotsSupportingSimultaneousCellularCalls.clear();
                                break;
                            }
                            mSlotsSupportingSimultaneousCellularCalls.add(i);
                        }
                        // Ensure the slots supporting cellular DSDA does not exceed the phone count
                        if (mSlotsSupportingSimultaneousCellularCalls.size() > getPhoneCount()) {
                            loge("Invalid size of DSDA slots. Disabling cellular DSDA.");
                            mSlotsSupportingSimultaneousCellularCalls.clear();
                        }
                    } else {
                        log(msg.what + " failure. Not getting logical slots that support "
                                + "simultaneous calling." + ar.exception);
                        mSlotsSupportingSimultaneousCellularCalls.clear();
                    }
                    if (mFeatureFlags.simultaneousCallingIndications()) {
                        updateSimultaneousSubIdsFromPhoneIdMappingAndNotify();
                        notifySimultaneousCellularCallingSlotsChanged();
                    }
                    break;
                default:
                    log("Unknown event: " + msg.what);
            }
        }
    }

    /**
     * Enable or disable phone
     *
     * @param phone which phone to operate on
     * @param enable true or false
     * @param result the message to sent back when it's done.
     */
    public void enablePhone(Phone phone, boolean enable, Message result) {
        if (phone == null) {
            log("enablePhone failed phone is null");
            return;
        }
        phone.mCi.enableModem(enable, result);
    }

    /**
     * Get phone status (enabled/disabled)
     * first query cache, if the status is not in cache,
     * add it to cache and return a default value true (non-blocking).
     *
     * @param phone which phone to operate on
     */
    public boolean getPhoneStatus(Phone phone) {
        if (phone == null) {
            log("getPhoneStatus failed phone is null");
            return false;
        }

        int phoneId = phone.getPhoneId();

        //use cache if the status has already been updated/queried
        try {
            return getPhoneStatusFromCache(phoneId);
        } catch (NoSuchElementException ex) {
            // Return true if modem status cannot be retrieved. For most cases, modem status
            // is on. And for older version modems, GET_MODEM_STATUS and disable modem are not
            // supported. Modem is always on.
            //TODO: this should be fixed in R to support a third status UNKNOWN b/131631629
            return true;
        } finally {
            //in either case send an asynchronous request to retrieve the phone status
            updatePhoneStatus(phone);
        }
    }

    /**
     * Get phone status (enabled/disabled) directly from modem, and use a result Message object
     * Note: the caller of this method is reponsible to call this in a blocking fashion as well
     * as read the results and handle the error case.
     * (In order to be consistent, in error case, we should return default value of true; refer
     *  to #getPhoneStatus method)
     *
     * @param phone which phone to operate on
     * @param result message that will be updated with result
     */
    public void getPhoneStatusFromModem(Phone phone, Message result) {
        if (phone == null) {
            log("getPhoneStatus failed phone is null");
        }
        phone.mCi.getModemStatus(result);
    }

    /**
     * return modem status from cache, NoSuchElementException if phoneId not in cache
     * @param phoneId
     */
    public boolean getPhoneStatusFromCache(int phoneId) throws NoSuchElementException {
        if (mPhoneStatusMap.containsKey(phoneId)) {
            return mPhoneStatusMap.get(phoneId);
        } else {
            throw new NoSuchElementException("phoneId not found: " + phoneId);
        }
    }

    /**
     * method to call RIL getModemStatus
     */
    private void updatePhoneStatus(Phone phone) {
        Message result = Message.obtain(
                mHandler, EVENT_GET_MODEM_STATUS_DONE, phone.getPhoneId(), 0 /**dummy arg*/);
        phone.mCi.getModemStatus(result);
    }

    /**
     * Add status of the phone to the status HashMap
     * @param phoneId
     * @param status
     */
    public void addToPhoneStatusCache(int phoneId, boolean status) {
        mPhoneStatusMap.put(phoneId, status);
    }

    /**
     * Returns how many phone objects the device supports.
     */
    public int getPhoneCount() {
        return mTelephonyManager.getActiveModemCount();
    }

    /**
     * @return The updated list of logical slots that support simultaneous cellular calling from the
     * modem based on current network conditions.
     */
    public Set<Integer> getSlotsSupportingSimultaneousCellularCalls() {
        return mSlotsSupportingSimultaneousCellularCalls;
    }

    /**
     * Get the current the list of logical slots supporting simultaneous cellular calling from the
     * modem based on current network conditions.
     */
    @VisibleForTesting
    public void updateSimultaneousCallingSupport() {
        log("updateSimultaneousCallingSupport: sending the request for "
                + "getting the list of logical slots supporting simultaneous cellular calling");
        Message callback = Message.obtain(
                mHandler, EVENT_GET_SIMULTANEOUS_CALLING_SUPPORT_DONE);
        mRadioConfig.updateSimultaneousCallingSupport(callback);
        log("updateSimultaneousCallingSupport: "
                + "mSlotsSupportingSimultaneousCellularCalls = " +
                mSlotsSupportingSimultaneousCellularCalls);
    }

    /**
     * @return static overall phone capabilities for all phones, including voice overrides.
     */
    public synchronized PhoneCapability getStaticPhoneCapability() {
        boolean isDefault = mStaticCapability == null;
        PhoneCapability caps = isDefault ? getDefaultCapability() : mStaticCapability;
        caps = maybeOverrideMaxActiveVoiceSubscriptions(caps);
        log("getStaticPhoneCapability: isDefault=" + isDefault + ", caps=" + caps);
        return caps;
    }

    /**
     * @return untouched capabilities returned from the modem
     */
    private synchronized PhoneCapability getCellularStaticPhoneCapability() {
        log("getCellularStaticPhoneCapability: mStaticCapability " + mStaticCapability);
        return mStaticCapability;
    }

    /**
     * Caches the static PhoneCapability returned by the modem
     */
    public synchronized void setStaticPhoneCapability(PhoneCapability capability) {
        log("setStaticPhoneCapability: mStaticCapability " + capability);
        mStaticCapability = capability;
    }

    /**
     * Query the modem to return its static PhoneCapability and cache it
     */
    @VisibleForTesting
    public void updateRadioCapability() {
        log("updateRadioCapability: sending the request for getting PhoneCapability");
        Message callback = Message.obtain(mHandler, EVENT_GET_PHONE_CAPABILITY_DONE);
        mRadioConfig.getPhoneCapability(callback);
    }

    /**
     * get configuration related status of each phone.
     */
    public PhoneCapability getCurrentPhoneCapability() {
        return getStaticPhoneCapability();
    }

    public int getNumberOfModemsWithSimultaneousDataConnections() {
        return getStaticPhoneCapability().getMaxActiveDataSubscriptions();
    }

    public int getNumberOfModemsWithSimultaneousVoiceConnections() {
        return getStaticPhoneCapability().getMaxActiveVoiceSubscriptions();
    }

    public boolean isVirtualDsdaEnabled() {
        return mVirtualDsdaEnabled;
    }

    /**
     * Register to listen to changes in the Phone slots that support simultaneous calling.
     * @param consumer A consumer that will be used to consume the new slots supporting simultaneous
     *                 cellular calling when it changes.
     */
    public void registerForSimultaneousCellularCallingSlotsChanged(
            Consumer<Set<Integer>> consumer) {
        mSimultaneousCellularCallingListeners.add(consumer);
    }

    private void notifySimultaneousCellularCallingSlotsChanged() {
        log("notifying listeners of changes to simultaneous cellular calling - new state:"
                + mSlotsSupportingSimultaneousCellularCalls);
        for (Consumer<Set<Integer>> consumer : mSimultaneousCellularCallingListeners) {
            try {
                consumer.accept(new HashSet<>(mSlotsSupportingSimultaneousCellularCalls));
            } catch (Exception e) {
                log("Unexpected Exception encountered when notifying listener: " + e);
            }
        }
    }

    private void notifyCapabilityChanged() {
        mNotifier.notifyPhoneCapabilityChanged(getStaticPhoneCapability());
    }

    /**
     * Switch configs to enable multi-sim or switch back to single-sim
     * @param numOfSims number of active sims we want to switch to
     */
    public void switchMultiSimConfig(int numOfSims) {
        log("switchMultiSimConfig: with numOfSims = " + numOfSims);
        if (getStaticPhoneCapability().getLogicalModemList().size() < numOfSims) {
            log("switchMultiSimConfig: Phone is not capable of enabling "
                    + numOfSims + " sims, exiting!");
            return;
        }
        if (getPhoneCount() != numOfSims) {
            log("switchMultiSimConfig: sending the request for switching");
            Message callback = Message.obtain(
                    mHandler, EVENT_SWITCH_DSDS_CONFIG_DONE, numOfSims, 0 /**dummy arg*/);
            mRadioConfig.setNumOfLiveModems(numOfSims, callback);
        } else {
            log("switchMultiSimConfig: No need to switch. getNumOfActiveSims is already "
                    + numOfSims);
        }
    }

    /**
     * Get whether reboot is required or not after making changes to modem configurations.
     * Return value defaults to true
     */
    public boolean isRebootRequiredForModemConfigChange() {
        return mMi.isRebootRequiredForModemConfigChange();
    }

    private void onMultiSimConfigChanged(int numOfActiveModems) {
        int oldNumOfActiveModems = getPhoneCount();
        setMultiSimProperties(numOfActiveModems);

        if (isRebootRequiredForModemConfigChange()) {
            log("onMultiSimConfigChanged: Rebooting.");
            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
            pm.reboot("Multi-SIM config changed.");
        } else {
            log("onMultiSimConfigChanged: Rebooting is not required.");
            mMi.notifyPhoneFactoryOnMultiSimConfigChanged(mContext, numOfActiveModems);
            broadcastMultiSimConfigChange(numOfActiveModems);
            boolean subInfoCleared = false;
            // if numOfActiveModems is decreasing, deregister old RILs
            // eg if we are going from 2 phones to 1 phone, we need to deregister RIL for the
            // second phone. This loop does nothing if numOfActiveModems is increasing.
            for (int phoneId = numOfActiveModems; phoneId < oldNumOfActiveModems; phoneId++) {
                SubscriptionManagerService.getInstance().markSubscriptionsInactive(phoneId);
                subInfoCleared = true;
                mPhones[phoneId].mCi.onSlotActiveStatusChange(
                        SubscriptionManager.isValidPhoneId(phoneId));
            }
            if (subInfoCleared) {
                // This triggers update of default subs. This should be done asap after
                // setMultiSimProperties() to avoid (minimize) duration for which default sub can be
                // invalid and can map to a non-existent phone.
                // If forexample someone calls a TelephonyManager API on default sub after
                // setMultiSimProperties() and before onSubscriptionsChanged() below -- they can be
                // using an invalid sub, which can map to a non-existent phone and can cause an
                // exception (see b/163582235).
                MultiSimSettingController.getInstance().onPhoneRemoved();
            }
            // old phone objects are not needed now; mPhones can be updated
            mPhones = PhoneFactory.getPhones();
            // if numOfActiveModems is increasing, register new RILs
            // eg if we are going from 1 phone to 2 phones, we need to register RIL for the second
            // phone. This loop does nothing if numOfActiveModems is decreasing.
            for (int phoneId = oldNumOfActiveModems; phoneId < numOfActiveModems; phoneId++) {
                Phone phone = mPhones[phoneId];
                registerForRadioState(phone);
                phone.mCi.onSlotActiveStatusChange(SubscriptionManager.isValidPhoneId(phoneId));
            }

            if (numOfActiveModems > 1) {
                // Check if cellular DSDA is supported. If it is, then send a request to the
                // modem to refresh the list of SIM slots that currently support DSDA based on
                // current network conditions
                maybeEnableCellularDSDASupport();
            } else {
                // The number of active modems is 0 or 1, disable cellular DSDA:
                mSlotsSupportingSimultaneousCellularCalls.clear();
                if (mFeatureFlags.simultaneousCallingIndications()) {
                    updateSimultaneousSubIdsFromPhoneIdMappingAndNotify();
                    notifySimultaneousCellularCallingSlotsChanged();
                }
            }

            // When the user enables DSDS mode, the default VOICE and SMS subId should be switched
            // to "No Preference".  Doing so will sync the network/sim settings and telephony.
            // (see b/198123192)
            if (numOfActiveModems > oldNumOfActiveModems && numOfActiveModems == 2) {
                Log.i(LOG_TAG, " onMultiSimConfigChanged: DSDS mode enabled; "
                        + "setting VOICE & SMS subId to -1 (No Preference)");

                //Set the default VOICE subId to -1 ("No Preference")
                SubscriptionManagerService.getInstance().setDefaultVoiceSubId(
                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);

                //TODO:: Set the default SMS sub to "No Preference". Tracking this bug (b/227386042)
            } else {
                Log.i(LOG_TAG,
                        "onMultiSimConfigChanged: DSDS mode NOT detected.  NOT setting the "
                                + "default VOICE and SMS subId to -1 (No Preference)");
            }
        }
    }

    /**
     * Helper method to set system properties for setting multi sim configs,
     * as well as doing the phone reboot
     * NOTE: In order to support more than 3 sims, we need to change this method.
     * @param numOfActiveModems number of active sims
     */
    private void setMultiSimProperties(int numOfActiveModems) {
        mMi.setMultiSimProperties(numOfActiveModems);
    }

    @VisibleForTesting
    public static void notifyMultiSimConfigChange(int numOfActiveModems) {
        sMultiSimConfigChangeRegistrants.notifyResult(numOfActiveModems);
    }

    /**
     * Register for multi-SIM configuration change, for example if the devices switched from single
     * SIM to dual-SIM mode.
     *
     * It doesn't trigger callback upon registration as multi-SIM config change is in-frequent.
     */
    public static void registerForMultiSimConfigChange(Handler h, int what, Object obj) {
        sMultiSimConfigChangeRegistrants.addUnique(h, what, obj);
    }

    /**
     * Unregister for multi-SIM configuration change.
     */
    public static void unregisterForMultiSimConfigChange(Handler h) {
        sMultiSimConfigChangeRegistrants.remove(h);
    }

    /**
     * Unregister for all multi-SIM configuration change events.
     */
    public static void unregisterAllMultiSimConfigChangeRegistrants() {
        sMultiSimConfigChangeRegistrants.removeAll();
    }

    private void broadcastMultiSimConfigChange(int numOfActiveModems) {
        log("broadcastSimSlotNumChange numOfActiveModems" + numOfActiveModems);
        // Notify internal registrants first.
        notifyMultiSimConfigChange(numOfActiveModems);

        Intent intent = new Intent(ACTION_MULTI_SIM_CONFIG_CHANGED);
        intent.putExtra(EXTRA_ACTIVE_SIM_SUPPORTED_COUNT, numOfActiveModems);
        mContext.sendBroadcast(intent);
    }
    /**
     * This is invoked from shell commands during CTS testing only.
     * @return true if the modem service is set successfully, false otherwise.
     */
    public boolean setModemService(String serviceName) {
        log("setModemService: " + serviceName);
        boolean statusRadioConfig = false;
        boolean statusRil = false;
        final boolean isAllowed = SystemProperties.getBoolean(ALLOW_MOCK_MODEM_PROPERTY, false);
        final boolean isAllowedForBoot =
                SystemProperties.getBoolean(BOOT_ALLOW_MOCK_MODEM_PROPERTY, false);

        // Check for ALLOW_MOCK_MODEM_PROPERTY and BOOT_ALLOW_MOCK_MODEM_PROPERTY on user builds
        if (isAllowed || isAllowedForBoot || DEBUG) {
            if (mRadioConfig != null) {
                statusRadioConfig = mRadioConfig.setModemService(serviceName);
            }

            if (!statusRadioConfig) {
                loge("setModemService: switching modem service for radioconfig fail");
                return false;
            }

            for (int i = 0; i < getPhoneCount(); i++) {
                if (mPhones[i] != null) {
                    statusRil = mPhones[i].mCi.setModemService(serviceName);
                }

                if (!statusRil) {
                    loge("setModemService: switch modem for radio " + i + " fail");

                    // Disconnect the switched service
                    mRadioConfig.setModemService(null);
                    for (int t = 0; t < i; t++) {
                        mPhones[t].mCi.setModemService(null);
                    }
                    return false;
                }
            }
        } else {
            loge("setModemService is not allowed");
            return false;
        }

        return true;
    }

     /**
     * This is invoked from shell commands to query during CTS testing only.
     * @return the service name of the connected service.
     */
    public String getModemService() {
        if (mPhones[0] == null) {
            return "";
        }

        return mPhones[0].mCi.getModemService();
    }

    /**
     * A wrapper class that wraps some methods so that they can be replaced or mocked in unit-tests.
     *
     * For example, setting or reading system property are static native methods that can't be
     * directly mocked. We can mock it by replacing MockableInterface object with a mock instance
     * in unittest.
     */
    @VisibleForTesting
    public static class MockableInterface {
        /**
         * Wrapper function to decide whether reboot is required for modem config change.
         */
        @VisibleForTesting
        public boolean isRebootRequiredForModemConfigChange() {
            boolean rebootRequired = TelephonyProperties.reboot_on_modem_change().orElse(false);
            log("isRebootRequiredForModemConfigChange: isRebootRequired = " + rebootRequired);
            return rebootRequired;
        }

        /**
         * Wrapper function to call setMultiSimProperties.
         */
        @VisibleForTesting
        public void setMultiSimProperties(int numOfActiveModems) {
            String multiSimConfig;
            switch(numOfActiveModems) {
                case 3:
                    multiSimConfig = TSTS;
                    break;
                case 2:
                    multiSimConfig = DSDS;
                    break;
                default:
                    multiSimConfig = SSSS;
            }

            log("setMultiSimProperties to " + multiSimConfig);
            TelephonyProperties.multi_sim_config(multiSimConfig);
        }

        /**
         * Wrapper function to call PhoneFactory.onMultiSimConfigChanged.
         */
        @VisibleForTesting
        public void notifyPhoneFactoryOnMultiSimConfigChanged(
                Context context, int numOfActiveModems) {
            PhoneFactory.onMultiSimConfigChanged(context, numOfActiveModems);
        }

        /**
         * Wrapper function to query the sysprop for multi_sim_config
         */
        public Optional<String> getMultiSimProperty() {
            return TelephonyProperties.multi_sim_config();
        }
    }

    private static void log(String s) {
        Rlog.d(LOG_TAG, s);
    }

    private static void loge(String s) {
        Rlog.e(LOG_TAG, s);
    }

    private static void loge(String s, Exception ex) {
        Rlog.e(LOG_TAG, s, ex);
    }
}
