| /* |
| * 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, "handleSimultaneousCallingSupportChanged: 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(",")); |
| } |
| } |