Add SimultaneousCallingTracker

This CL implements SimultaneousCallingTracker which will be used to aggreagte the current status of support for simultaneous calling over multiple subscriptions (DSDA).

Bug: 320569342
Test: atest SimultaneousCallingTrackerTest
Change-Id: I887131ffdeaace8c392582fe87c7b1dce3b385b3
diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java
index 3b47670..f34ba48 100644
--- a/src/java/com/android/internal/telephony/Phone.java
+++ b/src/java/com/android/internal/telephony/Phone.java
@@ -17,6 +17,7 @@
 package com.android.internal.telephony;
 
 import static android.telephony.TelephonyManager.HAL_SERVICE_RADIO;
+import static android.telephony.ims.ImsService.CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -4620,6 +4621,20 @@
         }
     }
 
+    public boolean isImsServiceSimultaneousCallingSupportCapable(Context context) {
+        if (!mFeatureFlags.simultaneousCallingIndications()) return false;
+        boolean capable = false;
+        ImsManager imsManager = ImsManager.getInstance(context, mPhoneId);
+        if (imsManager != null) {
+            try {
+                capable = imsManager.isCapable(CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING);
+            } catch (ImsException e) {
+                loge("initializeTerminalBasedCallWaiting : exception " + e);
+            }
+        }
+        return capable;
+    }
+
     public void startRingbackTone() {
     }
 
diff --git a/src/java/com/android/internal/telephony/PhoneConfigurationManager.java b/src/java/com/android/internal/telephony/PhoneConfigurationManager.java
index 7141f37..49e1e38 100644
--- a/src/java/com/android/internal/telephony/PhoneConfigurationManager.java
+++ b/src/java/com/android/internal/telephony/PhoneConfigurationManager.java
@@ -49,6 +49,7 @@
 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;
 
@@ -74,6 +75,25 @@
     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;
@@ -95,6 +115,8 @@
     @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.
@@ -156,6 +178,24 @@
     }
 
     /**
+     * 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.
      */
@@ -305,6 +345,9 @@
                     if (ar != null && ar.exception == null) {
                         mStaticCapability = (PhoneCapability) ar.result;
                         notifyCapabilityChanged();
+                        for (Listener l : mListeners) {
+                            l.onPhoneCapabilityChanged();
+                        }
                         maybeEnableCellularDSDASupport();
                     } else {
                         log(msg.what + " failure. Not getting phone capability." + ar.exception);
@@ -317,6 +360,9 @@
                         log("EVENT_DEVICE_CONFIG_CHANGED: from " + mVirtualDsdaEnabled + " to "
                                 + isVirtualDsdaEnabled);
                         mVirtualDsdaEnabled = isVirtualDsdaEnabled;
+                        for (Listener l : mListeners) {
+                            l.onDeviceConfigChanged();
+                        }
                     }
                     break;
                 case EVENT_SIMULTANEOUS_CALLING_SUPPORT_CHANGED:
@@ -463,7 +509,10 @@
         return mTelephonyManager.getActiveModemCount();
     }
 
-    @VisibleForTesting
+    /**
+     * @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;
     }
@@ -510,6 +559,14 @@
         return mStaticCapability.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
diff --git a/src/java/com/android/internal/telephony/PhoneFactory.java b/src/java/com/android/internal/telephony/PhoneFactory.java
index b1ff500..c5bc428 100644
--- a/src/java/com/android/internal/telephony/PhoneFactory.java
+++ b/src/java/com/android/internal/telephony/PhoneFactory.java
@@ -98,6 +98,7 @@
     @UnsupportedAppUsage
     static private Context sContext;
     static private PhoneConfigurationManager sPhoneConfigurationManager;
+    static private SimultaneousCallingTracker sSimultaneousCallingTracker;
     static private PhoneSwitcher sPhoneSwitcher;
     static private TelephonyNetworkFactory[] sTelephonyNetworkFactories;
     static private NotificationChannelController sNotificationChannelController;
@@ -257,6 +258,10 @@
                 }
 
                 sPhoneConfigurationManager = PhoneConfigurationManager.init(sContext, featureFlags);
+                if (featureFlags.simultaneousCallingIndications()) {
+                    sSimultaneousCallingTracker =
+                            SimultaneousCallingTracker.init(sContext, featureFlags);
+                }
 
                 sCellularNetworkValidator = CellularNetworkValidator.make(sContext);
 
diff --git a/src/java/com/android/internal/telephony/SimultaneousCallingTracker.java b/src/java/com/android/internal/telephony/SimultaneousCallingTracker.java
new file mode 100644
index 0000000..0a14ccd
--- /dev/null
+++ b/src/java/com/android/internal/telephony/SimultaneousCallingTracker.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2024 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 android.annotation.NonNull;
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.Message;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyRegistryManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.subscription.SubscriptionManagerService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.stream.Collectors;
+
+public class SimultaneousCallingTracker {
+    private static SimultaneousCallingTracker sInstance = null;
+    private final Context mContext;
+
+    /**
+     * A dynamic map of all voice capable {@link Phone} objects mapped to the set of {@link Phone}
+     * objects each {@link Phone} has a compatible user association with. To be considered
+     * compatible based on user association, both must be associated with the same
+     * {@link android.os.UserHandle} or both must be unassociated.
+     */
+    private Map<Phone, Set<Phone>> mVoiceCapablePhoneMap = new HashMap<>();
+
+    @VisibleForTesting
+    public boolean isDeviceSimultaneousCallingCapable = false;
+    public Set<Listener> mListeners = new CopyOnWriteArraySet<>();
+    private final PhoneConfigurationManager mPhoneConfigurationManager;
+    private final Handler mHandler;
+
+    /**
+     * A dynamic map of all the Phone IDs mapped to the set of {@link Phone} objects each
+     * {@link Phone} supports simultaneous calling (DSDA) with.
+     */
+    private Map<Integer, Set<Phone>> mSimultaneousCallPhoneSupportMap = new HashMap<>();
+    private static final String LOG_TAG = "SimultaneousCallingTracker";
+    protected static final int EVENT_SUBSCRIPTION_CHANGED         = 101;
+    protected static final int EVENT_PHONE_CAPABILITY_CHANGED     = 102;
+    protected static final int EVENT_MULTI_SIM_CONFIG_CHANGED     = 103;
+    protected static final int EVENT_DEVICE_CONFIG_CHANGED        = 104;
+    protected static final int EVENT_IMS_REGISTRATION_CHANGED     = 105;
+
+    /** Feature flags */
+    @NonNull
+    private final FeatureFlags mFeatureFlags;
+
+    /**
+     * Init method to instantiate the object
+     * Should only be called once.
+     */
+    public static SimultaneousCallingTracker init(Context context,
+            @NonNull FeatureFlags featureFlags) {
+        if (sInstance == null) {
+            sInstance = new SimultaneousCallingTracker(context, featureFlags);
+        } else {
+            Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Constructor.
+     * @param context context needed to send broadcast.
+     */
+    private SimultaneousCallingTracker(Context context, @NonNull FeatureFlags featureFlags) {
+        mContext = context;
+        mFeatureFlags = featureFlags;
+        mHandler = new ConfigManagerHandler();
+        mPhoneConfigurationManager = PhoneConfigurationManager.getInstance();
+        mPhoneConfigurationManager.addListener(mPhoneConfigurationManagerListener);
+        PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler,
+                EVENT_MULTI_SIM_CONFIG_CHANGED, null);
+        TelephonyRegistryManager telephonyRegistryManager = (TelephonyRegistryManager)
+                context.getSystemService(Context.TELEPHONY_REGISTRY_SERVICE);
+        telephonyRegistryManager.addOnSubscriptionsChangedListener(
+                mSubscriptionsChangedListener, new HandlerExecutor(mHandler));
+    }
+
+    /**
+     * Static method to get instance.
+     */
+    public static SimultaneousCallingTracker 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) {
+            if (!mFeatureFlags.simultaneousCallingIndications()) { return; }
+            Log.v(LOG_TAG, "Received EVENT " + msg.what);
+            switch (msg.what) {
+                case EVENT_PHONE_CAPABILITY_CHANGED -> {
+                    checkSimultaneousCallingDeviceCapability();
+                }
+                case EVENT_SUBSCRIPTION_CHANGED -> {
+                    updatePhoneMapAndSimultaneousCallSupportMap();
+                }
+                case EVENT_MULTI_SIM_CONFIG_CHANGED -> {
+                    int activeModemCount = (int) ((AsyncResult) msg.obj).result;
+                    if (activeModemCount > 1) {
+                        // SSIM --> MSIM: recalculate simultaneous calling supported combinations
+                        updatePhoneMapAndSimultaneousCallSupportMap();
+                    } else {
+                        // MSIM --> SSIM: remove all simultaneous calling supported combinations
+                        disableSimultaneousCallingSupport();
+                        handleSimultaneousCallingSupportChanged();
+                    }
+                }
+                case EVENT_DEVICE_CONFIG_CHANGED, EVENT_IMS_REGISTRATION_CHANGED -> {
+                    updateSimultaneousCallSupportMap();
+                }
+                default -> Log.i(LOG_TAG, "Received unknown event: " + msg.what);
+            }
+        }
+    }
+
+    /**
+     * Listener interface for events related to the {@link SimultaneousCallingTracker}.
+     */
+    public interface Listener {
+        /**
+         * Inform Telecom that the simultaneous calling subscription support map may have changed.
+         *
+         * @param simultaneousCallSubSupportMap Map of all voice capable subscription IDs mapped to
+         *                                      a set containing the subscription IDs which that
+         *                                      subscription is DSDA compatible with.
+         */
+        public void onSimultaneousCallingSupportChanged(Map<Integer,
+                Set<Integer>> simultaneousCallSubSupportMap);
+    }
+
+    /**
+     * Base listener implementation.
+     */
+    public abstract static class ListenerBase implements SimultaneousCallingTracker.Listener {
+        @Override
+        public void onSimultaneousCallingSupportChanged(Map<Integer,
+                Set<Integer>> simultaneousCallSubSupportMap) {}
+    }
+
+    /**
+     * Assign a listener to be notified of state changes.
+     *
+     * @param listener A listener.
+     */
+    public void addListener(Listener listener) {
+        if (mFeatureFlags.simultaneousCallingIndications()) {
+            mListeners.add(listener);
+        }
+    }
+
+    /**
+     * Removes a listener.
+     *
+     * @param listener A listener.
+     */
+    public final void removeListener(Listener listener) {
+        if (mFeatureFlags.simultaneousCallingIndications()) {
+            mListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Listener for listening to events in the {@link android.telephony.TelephonyRegistryManager}
+     */
+    private final SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionsChangedListener =
+            new SubscriptionManager.OnSubscriptionsChangedListener() {
+                @Override
+                public void onSubscriptionsChanged() {
+                    if (!mHandler.hasMessages(EVENT_SUBSCRIPTION_CHANGED)) {
+                        mHandler.sendEmptyMessage(EVENT_SUBSCRIPTION_CHANGED);
+                    }
+                }
+            };
+
+    /**
+     * Listener for listening to events in the {@link PhoneConfigurationManager}.
+     */
+    private final PhoneConfigurationManager.Listener mPhoneConfigurationManagerListener =
+            new PhoneConfigurationManager.Listener() {
+                @Override
+                public void onPhoneCapabilityChanged() {
+                    if (!mHandler.hasMessages(EVENT_PHONE_CAPABILITY_CHANGED)) {
+                        mHandler.sendEmptyMessage(EVENT_PHONE_CAPABILITY_CHANGED);
+                    }
+                }
+                @Override
+                public void onDeviceConfigChanged() {
+                    if (!mHandler.hasMessages(EVENT_DEVICE_CONFIG_CHANGED)) {
+                        mHandler.sendEmptyMessage(EVENT_DEVICE_CONFIG_CHANGED);
+                    }
+                }
+            };
+
+    private void checkSimultaneousCallingDeviceCapability() {
+        if (mPhoneConfigurationManager.getNumberOfModemsWithSimultaneousVoiceConnections() > 1) {
+            isDeviceSimultaneousCallingCapable = true;
+            mPhoneConfigurationManager.registerForSimultaneousCellularCallingSlotsChanged(
+                    this::onSimultaneousCellularCallingSlotsChanged);
+        }
+    }
+
+    /**
+     *
+     * @param subId to get the slots supporting simultaneous calling with
+     * @return the set of subId's that support simultaneous calling with the param subId
+     */
+    public Set<Integer> getSubIdsSupportingSimultaneousCalling(int subId) {
+        if (!isDeviceSimultaneousCallingCapable) {
+            Log.v(LOG_TAG, "Device is not simultaneous calling capable");
+            return Collections.emptySet();
+        }
+        for (int phoneId : mSimultaneousCallPhoneSupportMap.keySet()) {
+            if (PhoneFactory.getPhone(phoneId).getSubId() == subId) {
+                Set<Integer> subIdsSupportingSimultaneousCalling = new HashSet<>();
+                for (Phone phone : mSimultaneousCallPhoneSupportMap.get(phoneId)) {
+                    subIdsSupportingSimultaneousCalling.add(phone.getSubId());
+                }
+                Log.d(LOG_TAG, "getSlotsSupportingSimultaneousCalling for subId=" + subId +
+                        "; subIdsSupportingSimultaneousCalling=[" +
+                        getStringFromSet(subIdsSupportingSimultaneousCalling) + "].");
+                return subIdsSupportingSimultaneousCalling;
+            }
+        }
+        Log.e(LOG_TAG, "getSlotsSupportingSimultaneousCalling: Subscription ID not found in"
+                + " the map of voice capable phones.");
+        return Collections.emptySet();
+    }
+
+    private void updatePhoneMapAndSimultaneousCallSupportMap() {
+        if (!isDeviceSimultaneousCallingCapable) {
+            Log.d(LOG_TAG, "Ignoring updatePhoneMapAndSimultaneousCallSupportMap since device "
+                    + "is not DSDA capable.");
+            return;
+        }
+        unregisterForImsRegistrationChanges(mVoiceCapablePhoneMap);
+        mVoiceCapablePhoneMap = generateVoiceCapablePhoneMapBasedOnUserAssociation();
+        Log.i(LOG_TAG, "updatePhoneMapAndSimultaneousCallSupportMap: mVoiceCapablePhoneMap.size = "
+                + mVoiceCapablePhoneMap.size());
+        registerForImsRegistrationChanges(mVoiceCapablePhoneMap);
+        updateSimultaneousCallSupportMap();
+    }
+
+    private void updateSimultaneousCallSupportMap() {
+        if (!isDeviceSimultaneousCallingCapable) {
+            Log.d(LOG_TAG, "Ignoring updateSimultaneousCallSupportMap since device is not DSDA"
+                    + "capable.");
+            return;
+        }
+        mSimultaneousCallPhoneSupportMap =
+                generateSimultaneousCallSupportMap(mVoiceCapablePhoneMap);
+        handleSimultaneousCallingSupportChanged();
+    }
+
+    /**
+     * The simultaneous cellular calling slots have changed.
+     * @param slotIds The Set of slotIds that have simultaneous cellular calling.
+     */
+    private void onSimultaneousCellularCallingSlotsChanged(Set<Integer> slotIds) {
+        //Cellular calling slots have changed - regenerate simultaneous calling support map:
+        updateSimultaneousCallSupportMap();
+    }
+
+    private void disableSimultaneousCallingSupport() {
+        if (!isDeviceSimultaneousCallingCapable) {
+            Log.d(LOG_TAG, "Ignoring updateSimultaneousCallSupportMap since device is not DSDA"
+                    + "capable.");
+            return;
+        }
+        unregisterForImsRegistrationChanges(mVoiceCapablePhoneMap);
+
+        // In Single-SIM mode, simultaneous calling is not supported at all:
+        mSimultaneousCallPhoneSupportMap.clear();
+        mVoiceCapablePhoneMap.clear();
+    }
+
+    /**
+     * Registers a listener to receive IMS registration changes for all phones in the phoneMap.
+     *
+     * @param phoneMap Map of voice capable phones mapped to the set of phones each has a compatible
+     *                 user association with.
+     */
+    private void registerForImsRegistrationChanges(Map<Phone, Set<Phone>> phoneMap) {
+        for (Phone phone : phoneMap.keySet()) {
+            ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
+            if (imsPhone != null) {
+                Log.v(LOG_TAG, "registerForImsRegistrationChanges: registering phoneId = " +
+                        phone.getPhoneId());
+                imsPhone.registerForImsRegistrationChanges(mHandler,
+                        EVENT_IMS_REGISTRATION_CHANGED, null);
+            } else {
+                Log.v(LOG_TAG, "registerForImsRegistrationChanges: phone not recognized as "
+                        + "ImsPhone: phoneId = " + phone.getPhoneId());
+            }
+        }
+    }
+
+    /**
+     * Unregisters the listener to stop receiving IMS registration changes for all phones in the
+     * phoneMap.
+     *
+     * @param phoneMap Map of voice capable phones mapped to the set of phones each has a compatible
+     *                 user association with.
+     */
+    private void unregisterForImsRegistrationChanges(Map<Phone, Set<Phone>> phoneMap) {
+        for (Phone phone : phoneMap.keySet()) {
+            ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
+            if (imsPhone != null) {
+                imsPhone.unregisterForImsRegistrationChanges(mHandler);
+            }
+        }
+    }
+
+    /**
+     * Generates mVoiceCapablePhoneMap by iterating through {@link PhoneFactory#getPhones()} and
+     * checking whether each {@link Phone} corresponds to a valid and voice capable subscription.
+     * Maps the voice capable phones to the other voice capable phones that have compatible user
+     * associations
+     */
+    private Map<Phone, Set<Phone>> generateVoiceCapablePhoneMapBasedOnUserAssociation() {
+        Map<Phone, Set<Phone>> voiceCapablePhoneMap = new HashMap<>(3);
+
+        // Generate a map of phone slots that corresponds to valid and voice capable subscriptions:
+        Phone[] allPhones = PhoneFactory.getPhones();
+        for (Phone phone : allPhones) {
+            int subId = phone.getSubId();
+            SubscriptionInfo subInfo =
+                    SubscriptionManagerService.getInstance().getSubscriptionInfo(subId);
+
+            if (mFeatureFlags.dataOnlyCellularService() &&
+                    subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID && subInfo != null &&
+                    subInfo.getServiceCapabilities()
+                            .contains(SubscriptionManager.SERVICE_CAPABILITY_VOICE)) {
+                Log.v(LOG_TAG, "generateVoiceCapablePhoneMapBasedOnUserAssociation: adding "
+                        + "phoneId = " + phone.getPhoneId());
+                voiceCapablePhoneMap.put(phone, new HashSet<>(3));
+            }
+        }
+
+        Map<Phone, Set<Phone>> userAssociationPhoneMap = new HashMap<>(3);
+        // Map the voice capable phones to the others that have compatible user associations:
+        for (Phone phone1 : voiceCapablePhoneMap.keySet()) {
+            Set<Phone> phone1UserAssociationCompatiblePhones = new HashSet<>(3);
+            for (Phone phone2 : voiceCapablePhoneMap.keySet()) {
+                if (phone1.getPhoneId() == phone2.getPhoneId()) { continue; }
+                if (phonesHaveSameUserAssociation(phone1, phone2)) {
+                    phone1UserAssociationCompatiblePhones.add(phone2);
+                }
+            }
+            userAssociationPhoneMap.put(phone1, phone1UserAssociationCompatiblePhones);
+        }
+
+        return userAssociationPhoneMap;
+    }
+
+    private Map<Integer, Set<Phone>> generateSimultaneousCallSupportMap(
+            Map<Phone, Set<Phone>> phoneMap) {
+        Map<Integer, Set<Phone>> simultaneousCallSubSupportMap = new HashMap<>(3);
+
+        // Initially populate simultaneousCallSubSupportMap based on the passed in phoneMap:
+        for (Phone phone : phoneMap.keySet()) {
+            simultaneousCallSubSupportMap.put(phone.getPhoneId(),
+                    new HashSet<>(phoneMap.get(phone)));
+        }
+
+        // Remove phone combinations that don't support simultaneous calling from the support map:
+        for (Phone phone : phoneMap.keySet()) {
+            if (phone.isImsRegistered()) {
+                if (mPhoneConfigurationManager.isVirtualDsdaEnabled() ||
+                        phone.isImsServiceSimultaneousCallingSupportCapable(mContext)) {
+                    // Check if the transport types of each phone support simultaneous IMS calling:
+                    int phone1TransportType = ((ImsPhone) phone.getImsPhone()).getTransportType();
+                    if (phone1TransportType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
+                        // The transport type of this phone is WLAN so all combos are supported:
+                        continue;
+                    }
+                    for (Phone phone2 : phoneMap.keySet()) {
+                        if (phone.getPhoneId() == phone2.getPhoneId()) { continue; }
+                        if (!phonesSupportSimultaneousCallingViaCellularOrWlan(phone, phone2)) {
+                            simultaneousCallSubSupportMap.get(phone.getPhoneId()).remove(phone2);
+                        }
+                    }
+                } else {
+                    // IMS is registered, vDSDA is disabled, but IMS is not DSDA capable so
+                    // clear the map for this phone:
+                    simultaneousCallSubSupportMap.get(phone.getPhoneId()).clear();
+                }
+            } else {
+                // Check if this phone supports simultaneous cellular calling with other phones:
+                for (Phone phone2 : phoneMap.keySet()) {
+                    if (phone.getPhoneId() == phone2.getPhoneId()) { continue; }
+                    if (!phonesSupportSimultaneousCallingViaCellularOrWlan(phone, phone2)) {
+                        simultaneousCallSubSupportMap.get(phone.getPhoneId()).remove(phone2);
+                    }
+                }
+            }
+        }
+        Log.v(LOG_TAG, "generateSimultaneousCallSupportMap: returning "
+                + "simultaneousCallSubSupportMap = " +
+                getStringFromMap(simultaneousCallSubSupportMap));
+        return simultaneousCallSubSupportMap;
+    }
+
+    /**
+     * Determines whether the {@link Phone} instances have compatible user associations. To be
+     * considered compatible based on user association, both must be associated with the same
+     * {@link android.os.UserHandle} or both must be unassociated.
+     */
+    private boolean phonesHaveSameUserAssociation(Phone phone1, Phone phone2) {
+        return Objects.equals(phone1.getUserHandle(), phone2.getUserHandle());
+    }
+
+    private boolean phonesSupportCellularSimultaneousCalling(Phone phone1, Phone phone2) {
+        Set<Integer> slotsSupportingSimultaneousCellularCalls =
+                mPhoneConfigurationManager.getSlotsSupportingSimultaneousCellularCalls();
+        Log.v(LOG_TAG, "phonesSupportCellularSimultaneousCalling: modem returned slots = " +
+                getStringFromSet(slotsSupportingSimultaneousCellularCalls));
+        if (slotsSupportingSimultaneousCellularCalls.contains(phone1.getPhoneId()) &&
+                slotsSupportingSimultaneousCellularCalls.contains(phone2.getPhoneId())) {
+            return true;
+        };
+        return false;
+    }
+
+    private boolean phonesSupportSimultaneousCallingViaCellularOrWlan(Phone phone1, Phone phone2) {
+        int phone2TransportType =
+                ((ImsPhone) phone2.getImsPhone()).getTransportType();
+        return phone2TransportType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN ||
+                phonesSupportCellularSimultaneousCalling(phone1, phone2);
+    }
+
+    private void handleSimultaneousCallingSupportChanged() {
+        try {
+            Log.v(LOG_TAG, "handleSimultaneousCallingSupportChanged");
+            // Convert mSimultaneousCallPhoneSupportMap to a map of each subId to a set of the
+            // subIds it supports simultaneous calling with:
+            Map<Integer, Set<Integer>> simultaneousCallSubscriptionIdMap = new HashMap<>();
+            for (Integer phoneId : mSimultaneousCallPhoneSupportMap.keySet()) {
+                Phone phone = PhoneFactory.getPhone(phoneId);
+                if (phone == null) {
+                    Log.wtf(LOG_TAG, "handleSimultaneousCallingSupportChanged: phoneId=" +
+                            phoneId + " not found.");
+                    return;
+                }
+                int subId = phone.getSubId();
+                Set<Integer> supportedSubscriptionIds = new HashSet<>(3);
+                for (Phone p : mSimultaneousCallPhoneSupportMap.get(phoneId)) {
+                    supportedSubscriptionIds.add(p.getSubId());
+                }
+                simultaneousCallSubscriptionIdMap.put(subId, supportedSubscriptionIds);
+            }
+
+            // Notify listeners that simultaneous calling support has changed:
+            for (Listener l : mListeners) {
+                l.onSimultaneousCallingSupportChanged(simultaneousCallSubscriptionIdMap);
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "handleVideoCapabilitiesChanged: Exception = " + e);
+        }
+    }
+
+    private String getStringFromMap(Map<Integer, Set<Phone>> phoneMap) {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<Integer, Set<Phone>> entry : phoneMap.entrySet()) {
+            sb.append("Phone ID=");
+            sb.append(entry.getKey());
+            sb.append(" - Simultaneous calling compatible phone IDs=[");
+            sb.append(entry.getValue().stream().map(Phone::getPhoneId).map(String::valueOf)
+                    .collect(Collectors.joining(", ")));
+            sb.append("]; ");
+        }
+        return sb.toString();
+    }
+
+    private String getStringFromSet(Set<Integer> integerSet) {
+        return integerSet.stream().map(String::valueOf).collect(Collectors.joining(","));
+    }
+}
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
index 9f3ec3b..a9c2fc5 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.telephony.imsphone;
 
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
 import static android.telephony.ims.ImsManager.EXTRA_WFC_REGISTRATION_FAILURE_MESSAGE;
 import static android.telephony.ims.ImsManager.EXTRA_WFC_REGISTRATION_FAILURE_TITLE;
 import static android.telephony.ims.RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED;
@@ -290,6 +291,8 @@
 
     private final RegistrantList mSilentRedialRegistrants = new RegistrantList();
 
+    private final RegistrantList mImsRegistrationUpdateRegistrants = new RegistrantList();
+
     private final LocalLog mRegLocalLog = new LocalLog(64);
     private TelephonyMetrics mMetrics;
 
@@ -313,6 +316,7 @@
     private @RegistrationManager.SuggestedAction int mImsRegistrationSuggestedAction;
     private @ImsRegistrationImplBase.ImsRegistrationTech int mImsDeregistrationTech =
             REGISTRATION_TECH_NONE;
+    private @AccessNetworkConstants.TransportType int mTransportType = TRANSPORT_TYPE_INVALID;
     private int mImsRegistrationCapabilities;
     private boolean mNotifiedRegisteredState;
 
@@ -1664,6 +1668,14 @@
         }
     }
 
+    public void registerForImsRegistrationChanges(Handler h, int what, Object obj) {
+        mImsRegistrationUpdateRegistrants.addUnique(h, what, obj);
+    }
+
+    public void unregisterForImsRegistrationChanges(Handler h) {
+        mImsRegistrationUpdateRegistrants.remove(h);
+    }
+
     @Override
     public void registerForSilentRedial(Handler h, int what, Object obj) {
         mSilentRedialRegistrants.addUnique(h, what, obj);
@@ -2470,7 +2482,7 @@
         int subId = getSubId();
         if (SubscriptionManager.isValidSubscriptionId(subId)) {
             updateImsRegistrationInfo(REGISTRATION_STATE_NOT_REGISTERED,
-                    REGISTRATION_TECH_NONE, SUGGESTED_ACTION_NONE);
+                    REGISTRATION_TECH_NONE, SUGGESTED_ACTION_NONE, TRANSPORT_TYPE_INVALID);
         }
     }
 
@@ -2478,13 +2490,13 @@
             ImsRegistrationCallbackHelper.ImsRegistrationUpdate() {
         @Override
         public void handleImsRegistered(@NonNull ImsRegistrationAttributes attributes) {
-            int imsRadioTech = attributes.getTransportType();
+            int imsTransportType = attributes.getTransportType();
             if (DBG) {
-                logd("handleImsRegistered: onImsMmTelConnected imsRadioTech="
-                        + AccessNetworkConstants.transportTypeToString(imsRadioTech));
+                logd("handleImsRegistered: onImsMmTelConnected imsTransportType="
+                        + AccessNetworkConstants.transportTypeToString(imsTransportType));
             }
-            mRegLocalLog.log("handleImsRegistered: onImsMmTelConnected imsRadioTech="
-                    + AccessNetworkConstants.transportTypeToString(imsRadioTech));
+            mRegLocalLog.log("handleImsRegistered: onImsMmTelConnected imsTransportType="
+                    + AccessNetworkConstants.transportTypeToString(imsTransportType));
             setServiceState(ServiceState.STATE_IN_SERVICE);
             getDefaultPhone().setImsRegistrationState(true);
             mMetrics.writeOnImsConnectionState(mPhoneId, ImsConnectionState.State.CONNECTED, null);
@@ -2492,7 +2504,10 @@
             mImsNrSaModeHandler.onImsRegistered(
                     attributes.getRegistrationTechnology(), attributes.getFeatureTags());
             updateImsRegistrationInfo(REGISTRATION_STATE_REGISTERED,
-                    attributes.getRegistrationTechnology(), SUGGESTED_ACTION_NONE);
+                    attributes.getRegistrationTechnology(), SUGGESTED_ACTION_NONE,
+                    imsTransportType);
+            AsyncResult ar = new AsyncResult(null, null, null);
+            mImsRegistrationUpdateRegistrants.notifyRegistrants(ar);
         }
 
         @Override
@@ -2508,6 +2523,8 @@
             mMetrics.writeOnImsConnectionState(mPhoneId, ImsConnectionState.State.PROGRESSING,
                     null);
             mImsStats.onImsRegistering(imsRadioTech);
+            AsyncResult ar = new AsyncResult(null, null, null);
+            mImsRegistrationUpdateRegistrants.notifyRegistrants(ar);
         }
 
         @Override
@@ -2542,13 +2559,15 @@
                 }
             }
             updateImsRegistrationInfo(REGISTRATION_STATE_NOT_REGISTERED,
-                    imsRadioTech, suggestedModemAction);
+                    imsRadioTech, suggestedModemAction, TRANSPORT_TYPE_INVALID);
 
             if (mFeatureFlags.clearCachedImsPhoneNumberWhenDeviceLostImsRegistration()) {
                 // Clear the phone number from P-Associated-Uri
                 setCurrentSubscriberUris(null);
                 clearPhoneNumberForSourceIms();
             }
+            AsyncResult ar = new AsyncResult(null, null, null);
+            mImsRegistrationUpdateRegistrants.notifyRegistrants(ar);
         }
 
         @Override
@@ -2688,6 +2707,11 @@
         return mImsStats;
     }
 
+    /** Returns the {@link AccessNetworkConstants.TransportType} used to register this IMS phone. */
+    public @AccessNetworkConstants.TransportType int getTransportType() {
+        return mTransportType;
+    }
+
     /** Sets the {@link ImsStats} mock for this IMS phone during unit testing. */
     @VisibleForTesting
     public void setImsStats(ImsStats imsStats) {
@@ -2738,7 +2762,8 @@
     private void updateImsRegistrationInfo(
             @RegistrationManager.ImsRegistrationState int regState,
             @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech,
-            @RegistrationManager.SuggestedAction int suggestedAction) {
+            @RegistrationManager.SuggestedAction int suggestedAction,
+            @AccessNetworkConstants.TransportType int transportType) {
 
         if (regState == mImsRegistrationState) {
             // In NOT_REGISTERED state, the current PLMN can be blocked with a suggested action.
@@ -2764,6 +2789,7 @@
                 mDefaultPhone.mCi.updateImsRegistrationInfo(regState, imsRadioTech, 0,
                         mImsRegistrationCapabilities, null);
                 mImsRegistrationTech = imsRadioTech;
+                mTransportType = transportType;
                 mNotifiedRegisteredState = true;
                 return;
             }
@@ -2771,6 +2797,7 @@
 
         mImsRegistrationState = regState;
         mImsRegistrationTech = imsRadioTech;
+        mTransportType = transportType;
         mImsRegistrationSuggestedAction = suggestedAction;
         if (regState == REGISTRATION_STATE_NOT_REGISTERED) {
             mImsDeregistrationTech = imsRadioTech;
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SimultaneousCallingTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/SimultaneousCallingTrackerTest.java
new file mode 100644
index 0000000..d3fde34
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/SimultaneousCallingTrackerTest.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2024 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.ModemInfo;
+import android.telephony.PhoneCapability;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyRegistryManager;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.imsphone.ImsPhone;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class SimultaneousCallingTrackerTest extends TelephonyTest {
+    // Mocked classes
+    Handler mHandler;
+    CommandsInterface mMockCi0;
+    CommandsInterface mMockCi1;
+    CommandsInterface mMockCi2;
+    private Phone mPhone1; // mPhone as phone 0 is already defined in TelephonyTest.
+    private Phone mPhone2;
+    private SubscriptionInfo mSubInfo;
+    private ImsPhone mImsPhone;
+    private ImsPhone mImsPhone1;
+    private ImsPhone mImsPhone2;
+    PhoneConfigurationManager.MockableInterface mMi;
+    private static final int EVENT_MULTI_SIM_CONFIG_CHANGED = 1;
+    private static final PhoneCapability STATIC_DSDA_CAPABILITY;
+    private static final Set<Integer> STATIC_SERVICE_CAPABILITIES;
+
+    static {
+        ModemInfo modemInfo1 = new ModemInfo(0, 0, true, true);
+        ModemInfo modemInfo2 = new ModemInfo(1, 0, true, true);
+
+        List<ModemInfo> logicalModemList = new ArrayList<>();
+        logicalModemList.add(modemInfo1);
+        logicalModemList.add(modemInfo2);
+        int[] deviceNrCapabilities = new int[0];
+
+        STATIC_DSDA_CAPABILITY = new PhoneCapability(2, 1, logicalModemList, false,
+                deviceNrCapabilities);
+
+        STATIC_SERVICE_CAPABILITIES = new HashSet<>(1);
+        STATIC_SERVICE_CAPABILITIES.add(SubscriptionManager.SERVICE_CAPABILITY_VOICE);
+    }
+    PhoneConfigurationManager mPcm;
+    SimultaneousCallingTracker mSct;
+
+    private FeatureFlags mFeatureFlags;
+    private TelephonyRegistryManager mMockRegistryManager;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp(getClass().getSimpleName());
+        mHandler = mock(Handler.class);
+        mMockCi0 = mock(CommandsInterface.class);
+        mMockCi1 = mock(CommandsInterface.class);
+        mMockCi2 = mock(CommandsInterface.class);
+        mFeatureFlags = mock(FeatureFlags.class);
+        mPhone1 = mock(Phone.class);
+        mPhone2 = mock(Phone.class);
+        mImsPhone = mock(ImsPhone.class);
+        mImsPhone1 = mock(ImsPhone.class);
+        mImsPhone2 = mock(ImsPhone.class);
+        mSubInfo = mock(SubscriptionInfo.class);
+        doReturn(mImsPhone).when(mPhone).getImsPhone();
+        doReturn(mImsPhone1).when(mPhone1).getImsPhone();
+        doReturn(mImsPhone2).when(mPhone2).getImsPhone();
+        mMi = mock(PhoneConfigurationManager.MockableInterface.class);
+        mPhone.mCi = mMockCi0;
+        mCT.mCi = mMockCi0;
+        mPhone1.mCi = mMockCi1;
+        mPhone2.mCi = mMockCi2;
+        doReturn(0).when(mPhone).getPhoneId();
+        doReturn(10).when(mPhone).getSubId();
+        doReturn(1).when(mPhone1).getPhoneId();
+        // This will be updated to 11 during each test in order to trigger onSubscriptionChanged:
+        doReturn(110).when(mPhone1).getSubId();
+        doReturn(2).when(mPhone2).getPhoneId();
+        doReturn(12).when(mPhone2).getSubId();
+        doReturn(STATIC_SERVICE_CAPABILITIES).when(mSubInfo).getServiceCapabilities();
+        doReturn(mSubInfo).when(mSubscriptionManagerService)
+                .getSubscriptionInfo(any(Integer.class));
+        doReturn(RIL.RADIO_HAL_VERSION_2_2).when(mMockRadioConfigProxy).getVersion();
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+        doReturn(true).when(mFeatureFlags).dataOnlyCellularService();
+        mMockRegistryManager = mContext.getSystemService(TelephonyRegistryManager.class);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mPcm = null;
+        mSct = null;
+        mPhone1 = null;
+        mPhone2 = null;
+        super.tearDown();
+    }
+
+    /**
+     * @param numOfSim the current number of SIM subscriptions
+     */
+    private void init(int numOfSim) throws Exception {
+        doReturn(numOfSim).when(mTelephonyManager).getActiveModemCount();
+        replaceInstance(SimultaneousCallingTracker.class, "sInstance", null, null);
+        replaceInstance(PhoneConfigurationManager.class, "sInstance", null, null);
+        switch (numOfSim) {
+            case 0 -> mPhones = new Phone[]{};
+            case 1 -> mPhones = new Phone[]{mPhone};
+            case 2 -> mPhones = new Phone[]{mPhone, mPhone1};
+            case 3 -> mPhones = new Phone[]{mPhone, mPhone1, mPhone2};
+        }
+        replaceInstance(PhoneFactory.class, "sPhones", null, mPhones);
+        mPcm = PhoneConfigurationManager.init(mContext, mFeatureFlags);
+        mSct = SimultaneousCallingTracker.init(mContext, mFeatureFlags);
+        replaceInstance(PhoneConfigurationManager.class, "mMi", mPcm, mMi);
+        processAllMessages();
+    }
+
+    private void setRebootRequiredForConfigSwitch(boolean rebootRequired) {
+        doReturn(rebootRequired).when(mMi).isRebootRequiredForModemConfigChange();
+    }
+
+    private void setAndVerifyStaticCapability(PhoneCapability capability) {
+        mPcm.getCurrentPhoneCapability();
+        ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockRadioConfig).getPhoneCapability(captor.capture());
+        Message msg = captor.getValue();
+        AsyncResult.forMessage(msg, capability, null);
+        msg.sendToTarget();
+        processAllMessages();
+
+        assertEquals(capability, mPcm.getStaticPhoneCapability());
+        assertTrue(mSct.isDeviceSimultaneousCallingCapable);
+
+    }
+
+    private void setAndVerifySlotsSupportingSimultaneousCellularCalling(int[] enabledLogicalSlots) {
+        ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockRadioConfig).updateSimultaneousCallingSupport(captor.capture());
+        Message msg = captor.getValue();
+        AsyncResult.forMessage(msg, enabledLogicalSlots, null);
+        msg.sendToTarget();
+        processAllMessages();
+
+        HashSet<Integer> expectedSlots = new HashSet<>();
+        for (int i : enabledLogicalSlots) { expectedSlots.add(i); }
+        assertEquals(expectedSlots, mPcm.getSlotsSupportingSimultaneousCellularCalls());
+    }
+
+    private void updateSubId(Phone phone, int newSubId) {
+        ArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener> cBCaptorList =
+                ArgumentCaptor.forClass(SubscriptionManager.OnSubscriptionsChangedListener.class);
+        verify(mMockRegistryManager, times(2))
+                .addOnSubscriptionsChangedListener(cBCaptorList.capture(), any());
+
+        // Change sub ID mapping
+        doReturn(newSubId).when(phone).getSubId();
+        List<SubscriptionManager.OnSubscriptionsChangedListener> listeners =
+                cBCaptorList.getAllValues();
+        listeners.get(0).onSubscriptionsChanged();
+        listeners.get(1).onSubscriptionsChanged();
+        processAllMessages();
+    }
+
+    /**
+     * Test that simultaneous calling is not supported when the device is only capable of a max
+     * active voice count of 1.
+     */
+    @Test
+    @SmallTest
+    public void testDeviceNotSimultaneousCallingCapable() throws Exception {
+        init(1);
+        assertFalse(mSct.isDeviceSimultaneousCallingCapable);
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(10).size());
+    }
+
+    /**
+     * Test that simultaneous calling is not supported when the subscriptions have different user
+     * associations.
+     */
+    @Test
+    @SmallTest
+    public void testDifferentUserAssociations_SimultaneousCallingDisabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+
+        //Assign each phone to a different user which should disable simultaneous calling:
+        doReturn(new UserHandle(123)).when(mPhone).getUserHandle();
+        doReturn(new UserHandle(321)).when(mPhone1).getUserHandle();
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0, 1};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(10).size());
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(11).size());
+    }
+
+    /**
+     * Test that simultaneous calling is not supported when IMS is not registered and cellular
+     * simultaneous calling is only supported for one SIM subscription.
+     */
+    @Test
+    @SmallTest
+    public void testCellularDSDANotSupported_SimultaneousCallingDisabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        // Have the modem inform telephony that only phone slot 0 supports DSDA:
+        int[] enabledLogicalSlots = {0};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(10).size());
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(11).size());
+    }
+
+    /**
+     * Test that simultaneous calling is supported when IMS is not registered and cellular
+     * simultaneous calling is supported for both SIM subscription.
+     */
+    @Test
+    @SmallTest
+    public void testCellularDSDASupported_SimultaneousCallingEnabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0, 1};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(10).contains(11));
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(11).contains(10));
+    }
+
+    /**
+     * Test that simultaneous calling is supported when IMS is not registered and cellular
+     * simultaneous calling is supported for both SIM subscription. Then test that simultaneous
+     * calling is not supported after a multi SIM config change to single-SIM.
+     */
+    @Test
+    @SmallTest
+    public void testSingleSimSwitch_SimultaneousCallingDisabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0, 1};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(10).contains(11));
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(11).contains(10));
+
+        // Register for multi SIM config change.
+        mPcm.registerForMultiSimConfigChange(mHandler, EVENT_MULTI_SIM_CONFIG_CHANGED, null);
+        verify(mHandler, never()).sendMessageAtTime(any(), anyLong());
+
+        // Switch to single sim.
+        setRebootRequiredForConfigSwitch(false);
+        mPcm.switchMultiSimConfig(1);
+        ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockRadioConfig).setNumOfLiveModems(eq(1), captor.capture());
+
+        // Send message back to indicate switch success.
+        Message message = captor.getValue();
+        AsyncResult.forMessage(message, null, null);
+        message.sendToTarget();
+        processAllMessages();
+
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(10).size());
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(11).size());
+    }
+
+    /**
+     * Test that simultaneous calling is not supported when IMS is registered, vDSDA is disabled,
+     * but ImsService#CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING is not set.
+     */
+    @Test
+    @SmallTest
+    public void testImsDSDANotSupported_SimultaneousCallingDisabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+        doReturn(true).when(mPhone).isImsRegistered();
+        doReturn(true).when(mPhone1).isImsRegistered();
+        doReturn(false).when(mPhone)
+                .isImsServiceSimultaneousCallingSupportCapable(any(Context.class));
+        doReturn(false).when(mPhone1)
+                .isImsServiceSimultaneousCallingSupportCapable(any(Context.class));
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0, 1};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(10).size());
+        assertEquals(0, mSct.getSubIdsSupportingSimultaneousCalling(11).size());
+    }
+
+    /**
+     * Test that simultaneous calling is supported when IMS is registered, vDSDA is enabled,
+     * and the IMS transport type of each SIM subscription is WLAN.
+     */
+    //TODO: Implement a way to set vDSDAEnabled to true and then re-enable this test
+    @Ignore
+    @Test
+    @SmallTest
+    public void testImsVDSDAEnabledTransportTypeWLAN_SimultaneousCallingEnabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+        doReturn(true).when(mPhone).isImsRegistered();
+        doReturn(true).when(mPhone1).isImsRegistered();
+        doReturn(AccessNetworkConstants.TRANSPORT_TYPE_WLAN).when(mImsPhone).getTransportType();
+        doReturn(AccessNetworkConstants.TRANSPORT_TYPE_WLAN).when(mImsPhone1).getTransportType();
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(10).contains(11));
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(11).contains(10));
+    }
+
+    /**
+     * Test that simultaneous calling is supported when IMS is registered, vDSDA is disabled,
+     * ImsService#CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING is set and the IMS transport type of each
+     * SIM subscription is WLAN.
+     */
+    @Test
+    @SmallTest
+    public void testImsVDSDADisabledTransportTypeWLAN_SimultaneousCallingEnabled()
+            throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+        doReturn(true).when(mPhone).isImsRegistered();
+        doReturn(true).when(mPhone1).isImsRegistered();
+        doReturn(true).when(mPhone)
+                .isImsServiceSimultaneousCallingSupportCapable(any(Context.class));
+        doReturn(true).when(mPhone1)
+                .isImsServiceSimultaneousCallingSupportCapable(any(Context.class));
+        doReturn(AccessNetworkConstants.TRANSPORT_TYPE_WLAN).when(mImsPhone).getTransportType();
+        doReturn(AccessNetworkConstants.TRANSPORT_TYPE_WLAN).when(mImsPhone1).getTransportType();
+
+        init(2);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0, 1};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(10).contains(11));
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(11).contains(10));
+    }
+
+    /**
+     * Test that simultaneous calling is supported between all subs in the following 3-SIM case:
+     * SIM A: IMS Unregistered, vDSDA Disabled
+     * SIM B: IMS WWAN Registered, vDSDA Disabled, CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING not set
+     * SIM C: IMS WLAN Registered, vDSDA Disabled, CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING set
+     */
+    @Test
+    @SmallTest
+    public void testThreeSimCase_SimultaneousCallingEnabled() throws Exception {
+        doReturn(true).when(mFeatureFlags).simultaneousCallingIndications();
+        doReturn(false).when(mPhone).isImsRegistered();
+        doReturn(true).when(mPhone1).isImsRegistered();
+        doReturn(true).when(mPhone2).isImsRegistered();
+        doReturn(true).when(mPhone1)
+                .isImsServiceSimultaneousCallingSupportCapable(any(Context.class));
+        doReturn(true).when(mPhone2)
+                .isImsServiceSimultaneousCallingSupportCapable(any(Context.class));
+        doReturn(AccessNetworkConstants.TRANSPORT_TYPE_WWAN).when(mImsPhone1).getTransportType();
+        doReturn(AccessNetworkConstants.TRANSPORT_TYPE_WLAN).when(mImsPhone2).getTransportType();
+
+        init(3);
+        setAndVerifyStaticCapability(STATIC_DSDA_CAPABILITY);
+
+        int[] enabledLogicalSlots = {0, 1};
+        setAndVerifySlotsSupportingSimultaneousCellularCalling(enabledLogicalSlots);
+
+        // Trigger onSubscriptionsChanged by updating the subscription ID of a phone slot:
+        updateSubId(mPhone1, 11);
+
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(10)
+                .containsAll(Arrays.asList(11,12)));
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(11)
+                .containsAll(Arrays.asList(10,12)));
+        assertTrue(mSct.getSubIdsSupportingSimultaneousCalling(12)
+                .containsAll(Arrays.asList(10,11)));
+    }
+}