Add DomainSelectionService for domain selection
Bug: 243878608
Test: atest TelephonyDomainSelectionServiceTest
Test: atest DomainSelectorBaseTest
Change-Id: I1bac0dcc79e50d142d609bd1419e862e5fed460b
diff --git a/src/com/android/services/telephony/domainselection/DomainSelectorBase.java b/src/com/android/services/telephony/domainselection/DomainSelectorBase.java
new file mode 100644
index 0000000..1a7c992
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/DomainSelectorBase.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.DomainSelector;
+import android.telephony.TransportSelectorCallback;
+import android.telephony.WwanSelectorCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+
+import java.io.PrintWriter;
+
+/**
+ * An abstract base class to implement domain selector for a specific use case.
+ */
+@Keep
+public abstract class DomainSelectorBase extends Handler implements DomainSelector {
+ /**
+ * A listener used to inform the DomainSelectorService that this DomainSelector has been
+ * destroyed.
+ */
+ public interface DestroyListener {
+ /**
+ * Called when the specified domain selector is being destroyed.
+ * This MUST be called when this domain selector is no longer available after
+ * {@link DomainSelector#finishSelection} called.
+ */
+ void onDomainSelectorDestroyed(DomainSelectorBase selector);
+ }
+
+ // Persistent Logging
+ protected final LocalLog mEventLog = new LocalLog(30);
+ protected final Context mContext;
+ protected final ImsStateTracker mImsStateTracker;
+ protected SelectionAttributes mSelectionAttributes;
+ protected TransportSelectorCallback mTransportSelectorCallback;
+ protected WwanSelectorCallback mWwanSelectorCallback;
+ private final int mSlotId;
+ private final int mSubId;
+ private final DestroyListener mDestroyListener;
+ private final String mLogTag;
+
+ public DomainSelectorBase(Context context, int slotId, int subId, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker, @NonNull DestroyListener destroyListener,
+ String logTag) {
+ super(looper);
+ mContext = context;
+ mImsStateTracker = imsStateTracker;
+ mSlotId = slotId;
+ mSubId = subId;
+ mDestroyListener = destroyListener;
+ mLogTag = logTag;
+ }
+
+ /**
+ * Selects a domain for the specified attributes and callback.
+ *
+ * @param attr The attributes required to determine the domain.
+ * @param callback The callback called when the transport selection is completed.
+ */
+ public abstract void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback);
+
+ /**
+ * Destroys this domain selector.
+ */
+ protected void destroy() {
+ removeCallbacksAndMessages(null);
+ notifyDomainSelectorDestroyed();
+ }
+
+ /**
+ * Notifies the application that this domain selector is being destroyed.
+ */
+ protected void notifyDomainSelectorDestroyed() {
+ if (mDestroyListener != null) {
+ mDestroyListener.onDomainSelectorDestroyed(this);
+ }
+ }
+
+ /**
+ * Returns the slot index for this domain selector.
+ */
+ protected int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * Returns the subscription index for this domain selector.
+ */
+ protected int getSubId() {
+ return mSubId;
+ }
+
+ /**
+ * Dumps this instance into a readable format for dumpsys usage.
+ */
+ protected void dump(@NonNull PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println(mLogTag + ":");
+ ipw.increaseIndent();
+ ipw.println("SlotId: " + getSlotId());
+ ipw.println("SubId: " + getSubId());
+ mEventLog.dump(ipw);
+ ipw.decreaseIndent();
+ }
+
+ protected void logd(String s) {
+ Log.d(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ protected void logi(String s) {
+ Log.i(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ protected void loge(String s) {
+ Log.e(mLogTag, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/ImsStateTracker.java b/src/com/android/services/telephony/domainselection/ImsStateTracker.java
new file mode 100644
index 0000000..e1d0d31
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/ImsStateTracker.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ImsStateCallback;
+import android.telephony.ims.ImsStateCallback.DisconnectedReason;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.annotations.Keep;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class for tracking the IMS related information like IMS registration state, MMTEL capabilities.
+ * And, it also tracks the {@link ServiceState} and {@link BarringInfo} to identify the current
+ * network state to which the device is attached.
+ */
+@Keep
+public class ImsStateTracker {
+ /**
+ * A listener used to be notified of the {@link ServiceState} change.
+ */
+ public interface ServiceStateListener {
+ /**
+ * Called when the {@link ServiceState} is updated.
+ */
+ void onServiceStateUpdated(ServiceState serviceState);
+ }
+
+ /**
+ * A listener used to be notified of the {@link BarringInfo} change.
+ */
+ public interface BarringInfoListener {
+ /**
+ * Called when the {@link BarringInfo} is updated.
+ */
+ void onBarringInfoUpdated(BarringInfo barringInfo);
+ }
+
+ /**
+ * A listener used to be notified of the change for MMTEL connection state, IMS registration
+ * state, and MMTEL capabilities.
+ */
+ public interface ImsStateListener {
+ /**
+ * Called when MMTEL feature connection state is changed.
+ */
+ void onImsMmTelFeatureAvailableChanged();
+
+ /**
+ * Called when IMS registration state is changed.
+ */
+ void onImsRegistrationStateChanged();
+
+ /**
+ * Called when MMTEL capability is changed - IMS is registered
+ * and the service is currently available over IMS.
+ */
+ void onImsMmTelCapabilitiesChanged();
+ }
+
+ private static final String TAG = ImsStateTracker.class.getSimpleName();
+ /**
+ * When MMTEL feature connection is unavailable temporarily,
+ * the IMS state will be set to unavailable after waiting for this time.
+ */
+ @VisibleForTesting
+ protected static final long MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS = 1000; // 1 seconds
+
+ // Persistent Logging
+ private final LocalLog mEventLog = new LocalLog(30);
+ private final Context mContext;
+ private final int mSlotId;
+ private final Handler mHandler;
+ private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+ /** For tracking the ServiceState and its related listeners. */
+ private ServiceState mServiceState;
+ private final Set<ServiceStateListener> mServiceStateListeners = new ArraySet<>(2);
+
+ /** For tracking the BarringInfo and its related listeners. */
+ private BarringInfo mBarringInfo;
+ private final Set<BarringInfoListener> mBarringInfoListeners = new ArraySet<>(2);
+
+ /** For tracking IMS states and callbacks. */
+ private final Set<ImsStateListener> mImsStateListeners = new ArraySet<>(5);
+ private ImsMmTelManager mMmTelManager;
+ private ImsStateCallback mImsStateCallback;
+ private RegistrationManager.RegistrationCallback mImsRegistrationCallback;
+ private ImsMmTelManager.CapabilityCallback mMmTelCapabilityCallback;
+ /** The availability of MmTelFeature. */
+ private Boolean mMmTelFeatureAvailable;
+ /** The IMS registration state and the network type that performed IMS registration. */
+ private Boolean mImsRegistered;
+ private @RadioAccessNetworkType int mImsAccessNetworkType = AccessNetworkType.UNKNOWN;
+ /** The MMTEL capabilities - Voice, Video, SMS, and Ut. */
+ private MmTelCapabilities mMmTelCapabilities;
+ private final Runnable mMmTelFeatureUnavailableRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setImsStateAsUnavailable();
+ notifyImsMmTelFeatureAvailableChanged();
+ }
+ };
+
+ public ImsStateTracker(@NonNull Context context, int slotId, @NonNull Looper looper) {
+ mContext = context;
+ mSlotId = slotId;
+ mHandler = new Handler(looper);
+ }
+
+ /**
+ * Destroys this tracker.
+ */
+ public void destroy() {
+ stopListeningForImsState();
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ /**
+ * Returns the slot index for this tracker.
+ */
+ public int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * Returns the current subscription index for this tracker.
+ */
+ public int getSubId() {
+ return mSubId;
+ }
+
+ /**
+ * Returns the Handler instance of this tracker.
+ */
+ @VisibleForTesting
+ public @NonNull Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Starts monitoring the IMS states with the specified subscription.
+ * This method will be called whenever the subscription index for this tracker is changed.
+ * If the subscription index for this tracker is same as previously set, it will be ignored.
+ *
+ * @param subId The subscription index to be started.
+ */
+ public void start(int subId) {
+ if (mSubId == subId) {
+ if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
+ setImsStateAsUnavailable();
+ return;
+ } else if (mImsStateCallback != null) {
+ // If start() is called with the same subscription index and the ImsStateCallback
+ // was already registered, we don't need to unregister and register this callback
+ // again. So, this request should be ignored if the subscription index is same.
+ logd("start: ignored for same subscription(" + mSubId + ")");
+ return;
+ }
+ } else {
+ logi("start: subscription changed from " + mSubId + " to " + subId);
+ mSubId = subId;
+ }
+
+ stopListeningForImsState();
+ startListeningForImsState();
+ }
+
+ /**
+ * Updates the service state of the network to which the device is currently attached.
+ * This method should be run on the same thread as the Handler.
+ *
+ * @param serviceState The {@link ServiceState} to be updated.
+ */
+ public void updateServiceState(ServiceState serviceState) {
+ mServiceState = serviceState;
+
+ for (ServiceStateListener listener : mServiceStateListeners) {
+ listener.onServiceStateUpdated(serviceState);
+ }
+ }
+
+ /**
+ * Adds a listener to be notified of the {@link ServiceState} change.
+ * The newly added listener is notified if the current {@link ServiceState} is present.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addServiceStateListener(@NonNull ServiceStateListener listener) {
+ mServiceStateListeners.add(listener);
+
+ final ServiceState serviceState = mServiceState;
+ if (serviceState != null) {
+ mHandler.post(() -> notifyServiceStateUpdated(listener, serviceState));
+ }
+ }
+
+ /**
+ * Removes a listener to be notified of the {@link ServiceState} change.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeServiceStateListener(@NonNull ServiceStateListener listener) {
+ mServiceStateListeners.remove(listener);
+ }
+
+ /**
+ * Notifies the specified listener of a change to {@link ServiceState}.
+ *
+ * @param listener The listener to be notified.
+ * @param serviceState The {@link ServiceState} to be reported.
+ */
+ private void notifyServiceStateUpdated(ServiceStateListener listener,
+ ServiceState serviceState) {
+ if (!mServiceStateListeners.contains(listener)) {
+ return;
+ }
+ listener.onServiceStateUpdated(serviceState);
+ }
+
+ /**
+ * Updates the barring information received from the network to which the device is currently
+ * attached.
+ * This method should be run on the same thread as the Handler.
+ *
+ * @param barringInfo The {@link BarringInfo} to be updated.
+ */
+ public void updateBarringInfo(BarringInfo barringInfo) {
+ mBarringInfo = barringInfo;
+
+ for (BarringInfoListener listener : mBarringInfoListeners) {
+ listener.onBarringInfoUpdated(barringInfo);
+ }
+ }
+
+ /**
+ * Adds a listener to be notified of the {@link BarringInfo} change.
+ * The newly added listener is notified if the current {@link BarringInfo} is present.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addBarringInfoListener(@NonNull BarringInfoListener listener) {
+ mBarringInfoListeners.add(listener);
+
+ final BarringInfo barringInfo = mBarringInfo;
+ if (barringInfo != null) {
+ mHandler.post(() -> notifyBarringInfoUpdated(listener, barringInfo));
+ }
+ }
+
+ /**
+ * Removes a listener to be notified of the {@link BarringInfo} change.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeBarringInfoListener(@NonNull BarringInfoListener listener) {
+ mBarringInfoListeners.remove(listener);
+ }
+
+ /**
+ * Notifies the specified listener of a change to {@link BarringInfo}.
+ *
+ * @param listener The listener to be notified.
+ * @param barringInfo The {@link BarringInfo} to be reported.
+ */
+ private void notifyBarringInfoUpdated(BarringInfoListener listener, BarringInfo barringInfo) {
+ if (!mBarringInfoListeners.contains(listener)) {
+ return;
+ }
+ listener.onBarringInfoUpdated(barringInfo);
+ }
+
+ /**
+ * Adds a listener to be notified of the IMS state change.
+ * If each state was already received from the IMS service, the newly added listener
+ * is notified once.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addImsStateListener(@NonNull ImsStateListener listener) {
+ mImsStateListeners.add(listener);
+ mHandler.post(() -> notifyImsStateChangeIfValid(listener));
+ }
+
+ /**
+ * Removes a listener to be notified of the IMS state change.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeImsStateListener(@NonNull ImsStateListener listener) {
+ mImsStateListeners.remove(listener);
+ }
+
+ /**
+ * Returns {@code true} if all IMS states are ready, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ public boolean isImsStateReady() {
+ return mMmTelFeatureAvailable != null
+ && mImsRegistered != null
+ && mMmTelCapabilities != null;
+ }
+
+ /**
+ * Returns {@code true} if MMTEL feature connection is available, {@code false} otherwise.
+ */
+ public boolean isMmTelFeatureAvailable() {
+ return mMmTelFeatureAvailable != null && mMmTelFeatureAvailable;
+ }
+
+ /**
+ * Returns {@code true} if IMS is registered, {@code false} otherwise.
+ */
+ public boolean isImsRegistered() {
+ return mImsRegistered != null && mImsRegistered;
+ }
+
+ /**
+ * Returns {@code true} if IMS is registered over Wi-Fi (IWLAN), {@code false} otherwise.
+ */
+ public boolean isImsRegisteredOverWlan() {
+ return mImsAccessNetworkType == AccessNetworkType.IWLAN;
+ }
+
+ /**
+ * Returns {@code true} if IMS voice call is capable, {@code false} otherwise.
+ */
+ public boolean isImsVoiceCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+ }
+
+ /**
+ * Returns {@code true} if IMS video call is capable, {@code false} otherwise.
+ */
+ public boolean isImsVideoCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+ }
+
+ /**
+ * Returns {@code true} if IMS SMS is capable, {@code false} otherwise.
+ */
+ public boolean isImsSmsCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS);
+ }
+
+ /**
+ * Returns {@code true} if IMS UT is capable, {@code false} otherwise.
+ */
+ public boolean isImsUtCapable() {
+ return mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT);
+ }
+
+ /**
+ * Returns the access network type to which IMS is registered.
+ */
+ public @RadioAccessNetworkType int getImsAccessNetworkType() {
+ return mImsAccessNetworkType;
+ }
+
+ /**
+ * Sets the IMS states to the initial values.
+ */
+ private void initImsState() {
+ mMmTelFeatureAvailable = null;
+ mImsRegistered = null;
+ mImsAccessNetworkType = AccessNetworkType.UNKNOWN;
+ mMmTelCapabilities = null;
+ }
+
+ /**
+ * Sets the IMS states to unavailable to notify the readiness of the IMS state
+ * when the subscription is not valid.
+ */
+ private void setImsStateAsUnavailable() {
+ logd("setImsStateAsUnavailable");
+ setMmTelFeatureAvailable(false);
+ setImsRegistered(false);
+ setImsAccessNetworkType(AccessNetworkType.UNKNOWN);
+ setMmTelCapabilities(new MmTelCapabilities());
+ }
+
+ private void setMmTelFeatureAvailable(boolean available) {
+ if (!Objects.equals(mMmTelFeatureAvailable, Boolean.valueOf(available))) {
+ logi("setMmTelFeatureAvailable: " + mMmTelFeatureAvailable + " >> " + available);
+ mMmTelFeatureAvailable = Boolean.valueOf(available);
+ }
+ }
+
+ private void setImsRegistered(boolean registered) {
+ if (!Objects.equals(mImsRegistered, Boolean.valueOf(registered))) {
+ logi("setImsRegistered: " + mImsRegistered + " >> " + registered);
+ mImsRegistered = Boolean.valueOf(registered);
+ }
+ }
+
+ private void setImsAccessNetworkType(int accessNetworkType) {
+ if (mImsAccessNetworkType != accessNetworkType) {
+ logi("setImsAccessNetworkType: " + accessNetworkTypeToString(mImsAccessNetworkType)
+ + " >> " + accessNetworkTypeToString(accessNetworkType));
+ mImsAccessNetworkType = accessNetworkType;
+ }
+ }
+
+ private void setMmTelCapabilities(@NonNull MmTelCapabilities capabilities) {
+ if (!Objects.equals(mMmTelCapabilities, capabilities)) {
+ logi("MMTEL capabilities: " + mMmTelCapabilities + " >> " + capabilities);
+ mMmTelCapabilities = capabilities;
+ }
+ }
+
+ /**
+ * Notifies the specified listener of the current IMS state if it's valid.
+ *
+ * @param listener The {@link ImsStateListener} to be notified.
+ */
+ private void notifyImsStateChangeIfValid(@NonNull ImsStateListener listener) {
+ if (!mImsStateListeners.contains(listener)) {
+ return;
+ }
+
+ if (mMmTelFeatureAvailable != null) {
+ listener.onImsMmTelFeatureAvailableChanged();
+ }
+
+ if (mImsRegistered != null) {
+ listener.onImsRegistrationStateChanged();
+ }
+
+ if (mMmTelCapabilities != null) {
+ listener.onImsMmTelCapabilitiesChanged();
+ }
+ }
+
+ /**
+ * Notifies the application that MMTEL feature connection state is changed.
+ */
+ private void notifyImsMmTelFeatureAvailableChanged() {
+ for (ImsStateListener l : mImsStateListeners) {
+ l.onImsMmTelFeatureAvailableChanged();
+ }
+ }
+
+ /**
+ * Notifies the application that IMS registration state is changed.
+ */
+ private void notifyImsRegistrationStateChanged() {
+ logi("ImsState: " + imsStateToString());
+ for (ImsStateListener l : mImsStateListeners) {
+ l.onImsRegistrationStateChanged();
+ }
+ }
+
+ /**
+ * Notifies the application that MMTEL capabilities is changed.
+ */
+ private void notifyImsMmTelCapabilitiesChanged() {
+ logi("ImsState: " + imsStateToString());
+ for (ImsStateListener l : mImsStateListeners) {
+ l.onImsMmTelCapabilitiesChanged();
+ }
+ }
+
+ /**
+ * Called when MMTEL feature connection state is available.
+ */
+ private void onMmTelFeatureAvailable() {
+ logd("onMmTelFeatureAvailable");
+ mHandler.removeCallbacks(mMmTelFeatureUnavailableRunnable);
+ setMmTelFeatureAvailable(true);
+ registerImsRegistrationCallback();
+ registerMmTelCapabilityCallback();
+ notifyImsMmTelFeatureAvailableChanged();
+ }
+
+ /**
+ * Called when MMTEL feature connection state is unavailable.
+ */
+ private void onMmTelFeatureUnavailable(@DisconnectedReason int reason) {
+ logd("onMmTelFeatureUnavailable: reason=" + disconnectedCauseToString(reason));
+
+ if (reason == ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR
+ || reason == ImsStateCallback.REASON_IMS_SERVICE_NOT_READY) {
+ // Wait for onAvailable for some times and
+ // if it's not available, the IMS state will be set to unavailable.
+ initImsState();
+ setMmTelFeatureAvailable(false);
+ mHandler.postDelayed(mMmTelFeatureUnavailableRunnable,
+ MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS);
+ } else if (reason == ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR
+ || reason == ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED) {
+ // Permanently blocked for this subscription.
+ setImsStateAsUnavailable();
+ notifyImsMmTelFeatureAvailableChanged();
+ } else if (reason == ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED) {
+ // Wait for onAvailable for some times and
+ // if it's not available, the IMS state will be set to unavailable.
+ initImsState();
+ setMmTelFeatureAvailable(false);
+ unregisterImsRegistrationCallback();
+ unregisterMmTelCapabilityCallback();
+ mHandler.postDelayed(mMmTelFeatureUnavailableRunnable,
+ MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS);
+ } else if (reason == ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE) {
+ // The {@link TelephonyDomainSelectionService} will call ImsStateTracker#start
+ // when the subscription changes to register new callbacks.
+ setImsStateAsUnavailable();
+ unregisterImsRegistrationCallback();
+ unregisterMmTelCapabilityCallback();
+ notifyImsMmTelFeatureAvailableChanged();
+ } else {
+ logw("onMmTelFeatureUnavailable: unexpected reason=" + reason);
+ }
+ }
+
+ /**
+ * Called when IMS is registered to the IMS network.
+ */
+ private void onImsRegistered(@NonNull ImsRegistrationAttributes attributes) {
+ logd("onImsRegistered: " + attributes);
+
+ setImsRegistered(true);
+ setImsAccessNetworkType(
+ imsRegTechToAccessNetworkType(attributes.getRegistrationTechnology()));
+ notifyImsRegistrationStateChanged();
+ }
+
+ /**
+ * Called when IMS is unregistered from the IMS network.
+ */
+ private void onImsUnregistered(@NonNull ImsReasonInfo info) {
+ logd("onImsUnregistered: " + info);
+ setImsRegistered(false);
+ setImsAccessNetworkType(AccessNetworkType.UNKNOWN);
+ setMmTelCapabilities(new MmTelCapabilities());
+ notifyImsRegistrationStateChanged();
+ }
+
+ /**
+ * Called when MMTEL capability is changed - IMS is registered
+ * and the service is currently available over IMS.
+ */
+ private void onMmTelCapabilitiesChanged(@NonNull MmTelCapabilities capabilities) {
+ logd("onMmTelCapabilitiesChanged: " + capabilities);
+ setMmTelCapabilities(capabilities);
+ notifyImsMmTelCapabilitiesChanged();
+ }
+
+ /**
+ * Starts listening to monitor the IMS states -
+ * connection state, IMS registration state, and MMTEL capabilities.
+ */
+ private void startListeningForImsState() {
+ if (!SubscriptionManager.isValidSubscriptionId(getSubId())) {
+ setImsStateAsUnavailable();
+ return;
+ }
+
+ ImsManager imsMngr = mContext.getSystemService(ImsManager.class);
+ mMmTelManager = imsMngr.getImsMmTelManager(getSubId());
+ initImsState();
+ registerImsStateCallback();
+ }
+
+ /**
+ * Stops listening to monitor the IMS states -
+ * connection state, IMS registration state, and MMTEL capabilities.
+ */
+ private void stopListeningForImsState() {
+ mHandler.removeCallbacks(mMmTelFeatureUnavailableRunnable);
+
+ if (mMmTelManager != null) {
+ unregisterMmTelCapabilityCallback();
+ unregisterImsRegistrationCallback();
+ unregisterImsStateCallback();
+ mMmTelManager = null;
+ }
+ }
+
+ private void registerImsStateCallback() {
+ if (mImsStateCallback != null) {
+ loge("ImsStateCallback is already registered for sub-" + getSubId());
+ return;
+ }
+ /**
+ * Listens to the IMS connection state change.
+ */
+ mImsStateCallback = new ImsStateCallback() {
+ @Override
+ public void onUnavailable(@DisconnectedReason int reason) {
+ onMmTelFeatureUnavailable(reason);
+ }
+
+ @Override
+ public void onAvailable() {
+ onMmTelFeatureAvailable();
+ }
+
+ @Override
+ public void onError() {
+ // This case will not be happened because this domain selection service
+ // is running on the Telephony service.
+ }
+ };
+
+ try {
+ mMmTelManager.registerImsStateCallback(mHandler::post, mImsStateCallback);
+ } catch (ImsException e) {
+ loge("Exception when registering ImsStateCallback: " + e);
+ mImsStateCallback = null;
+ }
+ }
+
+ private void unregisterImsStateCallback() {
+ if (mImsStateCallback != null) {
+ try {
+ mMmTelManager.unregisterImsStateCallback(mImsStateCallback);
+ } catch (Exception ignored) {
+ // Ignore the runtime exception while unregistering callback.
+ logd("Exception when unregistering ImsStateCallback: " + ignored);
+ }
+ mImsStateCallback = null;
+ }
+ }
+
+ private void registerImsRegistrationCallback() {
+ if (mImsRegistrationCallback != null) {
+ logd("RegistrationCallback is already registered for sub-" + getSubId());
+ return;
+ }
+ /**
+ * Listens to the IMS registration state change.
+ */
+ mImsRegistrationCallback = new RegistrationManager.RegistrationCallback() {
+ @Override
+ public void onRegistered(@NonNull ImsRegistrationAttributes attributes) {
+ onImsRegistered(attributes);
+ }
+
+ @Override
+ public void onUnregistered(@NonNull ImsReasonInfo info) {
+ onImsUnregistered(info);
+ }
+ };
+
+ try {
+ mMmTelManager.registerImsRegistrationCallback(mHandler::post, mImsRegistrationCallback);
+ } catch (ImsException e) {
+ loge("Exception when registering RegistrationCallback: " + e);
+ mImsRegistrationCallback = null;
+ }
+ }
+
+ private void unregisterImsRegistrationCallback() {
+ if (mImsRegistrationCallback != null) {
+ try {
+ mMmTelManager.unregisterImsRegistrationCallback(mImsRegistrationCallback);
+ } catch (Exception ignored) {
+ // Ignore the runtime exception while unregistering callback.
+ logd("Exception when unregistering RegistrationCallback: " + ignored);
+ }
+ mImsRegistrationCallback = null;
+ }
+ }
+
+ private void registerMmTelCapabilityCallback() {
+ if (mMmTelCapabilityCallback != null) {
+ logd("CapabilityCallback is already registered for sub-" + getSubId());
+ return;
+ }
+ /**
+ * Listens to the MmTel feature capabilities change.
+ */
+ mMmTelCapabilityCallback = new ImsMmTelManager.CapabilityCallback() {
+ @Override
+ public void onCapabilitiesStatusChanged(@NonNull MmTelCapabilities capabilities) {
+ onMmTelCapabilitiesChanged(capabilities);
+ }
+ };
+
+ try {
+ mMmTelManager.registerMmTelCapabilityCallback(mHandler::post, mMmTelCapabilityCallback);
+ } catch (ImsException e) {
+ loge("Exception when registering CapabilityCallback: " + e);
+ mMmTelCapabilityCallback = null;
+ }
+ }
+
+ private void unregisterMmTelCapabilityCallback() {
+ if (mMmTelCapabilityCallback != null) {
+ try {
+ mMmTelManager.unregisterMmTelCapabilityCallback(mMmTelCapabilityCallback);
+ } catch (Exception ignored) {
+ // Ignore the runtime exception while unregistering callback.
+ logd("Exception when unregistering CapabilityCallback: " + ignored);
+ }
+ mMmTelCapabilityCallback = null;
+ }
+ }
+
+ /** Returns a string representation of IMS states. */
+ public String imsStateToString() {
+ StringBuilder sb = new StringBuilder("{ ");
+ sb.append("MMTEL: featureAvailable=").append(booleanToString(mMmTelFeatureAvailable));
+ sb.append(", registered=").append(booleanToString(mImsRegistered));
+ sb.append(", accessNetworkType=").append(accessNetworkTypeToString(mImsAccessNetworkType));
+ sb.append(", capabilities=").append(mmTelCapabilitiesToString(mMmTelCapabilities));
+ sb.append(" }");
+ return sb.toString();
+ }
+
+ protected static String accessNetworkTypeToString(
+ @RadioAccessNetworkType int accessNetworkType) {
+ switch (accessNetworkType) {
+ case AccessNetworkType.UNKNOWN: return "UNKNOWN";
+ case AccessNetworkType.GERAN: return "GERAN";
+ case AccessNetworkType.UTRAN: return "UTRAN";
+ case AccessNetworkType.EUTRAN: return "EUTRAN";
+ case AccessNetworkType.CDMA2000: return "CDMA2000";
+ case AccessNetworkType.IWLAN: return "IWLAN";
+ case AccessNetworkType.NGRAN: return "NGRAN";
+ default: return Integer.toString(accessNetworkType);
+ }
+ }
+
+ /** Converts the IMS registration technology to the access network type. */
+ private static @RadioAccessNetworkType int imsRegTechToAccessNetworkType(
+ @ImsRegistrationImplBase.ImsRegistrationTech int imsRegTech) {
+ switch (imsRegTech) {
+ case ImsRegistrationImplBase.REGISTRATION_TECH_LTE:
+ return AccessNetworkType.EUTRAN;
+ case ImsRegistrationImplBase.REGISTRATION_TECH_NR:
+ return AccessNetworkType.NGRAN;
+ case ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN:
+ return AccessNetworkType.IWLAN;
+ default:
+ return AccessNetworkType.UNKNOWN;
+ }
+ }
+
+ private static String booleanToString(Boolean b) {
+ return b == null ? "null" : b.toString();
+ }
+
+ private static String mmTelCapabilitiesToString(MmTelCapabilities c) {
+ if (c == null) {
+ return "null";
+ }
+ StringBuilder sb = new StringBuilder("[");
+ sb.append("voice=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE));
+ sb.append(", video=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO));
+ sb.append(", ut=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT));
+ sb.append(", sms=").append(c.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS));
+ sb.append("]");
+ return sb.toString();
+ }
+
+ private static String disconnectedCauseToString(@DisconnectedReason int reason) {
+ switch (reason) {
+ case ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR:
+ return "UNKNOWN_TEMPORARY_ERROR";
+ case ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR:
+ return "UNKNOWN_PERMANENT_ERROR";
+ case ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED:
+ return "IMS_SERVICE_DISCONNECTED";
+ case ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED:
+ return "NO_IMS_SERVICE_CONFIGURED";
+ case ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE:
+ return "SUBSCRIPTION_INACTIVE";
+ case ImsStateCallback.REASON_IMS_SERVICE_NOT_READY:
+ return "IMS_SERVICE_NOT_READY";
+ default:
+ return Integer.toString(reason);
+ }
+ }
+
+ /**
+ * Dumps this instance into a readable format for dumpsys usage.
+ */
+ public void dump(@NonNull PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println("ImsStateTracker:");
+ ipw.increaseIndent();
+ ipw.println("SlotId: " + getSlotId());
+ ipw.println("SubId: " + getSubId());
+ ipw.println("ServiceState: " + mServiceState);
+ ipw.println("BarringInfo: " + mBarringInfo);
+ ipw.println("ImsState: " + imsStateToString());
+ ipw.println("Event Log:");
+ ipw.increaseIndent();
+ mEventLog.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.decreaseIndent();
+ }
+
+ private void logd(String s) {
+ Log.d(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ private void logi(String s) {
+ Log.i(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ private void loge(String s) {
+ Log.e(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+
+ private void logw(String s) {
+ Log.w(TAG, "[" + getSlotId() + "|" + getSubId() + "] " + s);
+ mEventLog.log("[" + getSlotId() + "|" + getSubId() + "] " + s);
+ }
+}
diff --git a/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
new file mode 100644
index 0000000..56b9b68
--- /dev/null
+++ b/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionService.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.BarringInfo;
+import android.telephony.DisconnectCause;
+import android.telephony.DomainSelectionService;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.telephony.TransportSelectorCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements the telephony domain selection for various telephony features.
+ */
+public class TelephonyDomainSelectionService extends DomainSelectionService {
+ /**
+ * Testing interface for injecting mock ImsStateTracker.
+ */
+ @VisibleForTesting
+ public interface ImsStateTrackerFactory {
+ /**
+ * @return The {@link ImsStateTracker} created for the specified slot.
+ */
+ ImsStateTracker create(Context context, int slotId, @NonNull Looper looper);
+ }
+
+ /**
+ * Testing interface for injecting mock DomainSelector.
+ */
+ @VisibleForTesting
+ public interface DomainSelectorFactory {
+ /**
+ * @return The {@link DomainSelectorBase} created using the specified arguments.
+ */
+ DomainSelectorBase create(Context context, int slotId, int subId,
+ @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener);
+ }
+
+ private static final class DefaultDomainSelectorFactory implements DomainSelectorFactory {
+ @Override
+ public DomainSelectorBase create(Context context, int slotId, int subId,
+ @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper,
+ @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener) {
+ DomainSelectorBase selector = null;
+
+ logi("create-DomainSelector: slotId=" + slotId + ", subId=" + subId
+ + ", selectorType=" + selectorTypeToString(selectorType)
+ + ", emergency=" + isEmergency);
+
+ switch (selectorType) {
+ case SELECTOR_TYPE_CALLING:
+ if (isEmergency) {
+ // TODO(ag/19990283) uncomment when emergency call domain selector is ready.
+ /*selector = new EmergencyCallDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);*/
+ } else {
+ // TODO(ag/20024470) uncomment when normal call domain selector is ready.
+ /*selector = new NormalCallDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);*/
+ }
+ break;
+ case SELECTOR_TYPE_SMS:
+ // TODO(ag/20075167) uncomment when SMS domain selector is ready.
+ /*if (isEmergency) {
+ selector = new EmergencySmsDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);
+ } else {
+ selector = new SmsDomainSelector(context, slotId, subId, looper,
+ imsStateTracker, listener);
+ }*/
+ break;
+ default:
+ // Not reachable.
+ break;
+ }
+
+ return selector;
+ }
+ };
+
+ /**
+ * A container class to manage the domain selector per a slot and selector type.
+ * If the domain selector is not null and reusable, the same domain selector will be used
+ * for the specific slot.
+ */
+ private static final class DomainSelectorContainer {
+ private final int mSlotId;
+ private final @SelectorType int mSelectorType;
+ private final boolean mIsEmergency;
+ private final @NonNull DomainSelectorBase mSelector;
+
+ DomainSelectorContainer(int slotId, @SelectorType int selectorType, boolean isEmergency,
+ @NonNull DomainSelectorBase selector) {
+ mSlotId = slotId;
+ mSelectorType = selectorType;
+ mIsEmergency = isEmergency;
+ mSelector = selector;
+ }
+
+ public int getSlotId() {
+ return mSlotId;
+ }
+
+ public @SelectorType int getSelectorType() {
+ return mSelectorType;
+ }
+
+ public DomainSelectorBase getDomainSelector() {
+ return mSelector;
+ }
+
+ public boolean isEmergency() {
+ return mIsEmergency;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{ ")
+ .append("slotId=").append(mSlotId)
+ .append(", selectorType=").append(selectorTypeToString(mSelectorType))
+ .append(", isEmergency=").append(mIsEmergency)
+ .append(", selector=").append(mSelector)
+ .append(" }").toString();
+ }
+ }
+
+ private final DomainSelectorBase.DestroyListener mDestroyListener =
+ new DomainSelectorBase.DestroyListener() {
+ @Override
+ public void onDomainSelectorDestroyed(DomainSelectorBase selector) {
+ logd("DomainSelector destroyed: " + selector);
+ removeDomainSelector(selector);
+ }
+ };
+
+ /**
+ * A class to listen for the subscription change for starting {@link ImsStateTracker}
+ * to monitor the IMS states.
+ */
+ private final OnSubscriptionsChangedListener mSubscriptionsChangedListener =
+ new OnSubscriptionsChangedListener() {
+ @Override
+ public void onSubscriptionsChanged() {
+ handleSubscriptionsChanged();
+ }
+ };
+
+ private static final String TAG = TelephonyDomainSelectionService.class.getSimpleName();
+
+ // Persistent Logging
+ private static final LocalLog sEventLog = new LocalLog(20);
+ private final Context mContext;
+ // Map of slotId -> ImsStateTracker
+ private final SparseArray<ImsStateTracker> mImsStateTrackers = new SparseArray<>(2);
+ private final List<DomainSelectorContainer> mDomainSelectorContainers = new ArrayList<>();
+ private final ImsStateTrackerFactory mImsStateTrackerFactory;
+ private final DomainSelectorFactory mDomainSelectorFactory;
+ private Handler mServiceHandler;
+
+ public TelephonyDomainSelectionService(Context context) {
+ this(context, ImsStateTracker::new, new DefaultDomainSelectorFactory());
+ }
+
+ @VisibleForTesting
+ public TelephonyDomainSelectionService(Context context,
+ @NonNull ImsStateTrackerFactory imsStateTrackerFactory,
+ @NonNull DomainSelectorFactory domainSelectorFactory) {
+ mContext = context;
+ mImsStateTrackerFactory = imsStateTrackerFactory;
+ mDomainSelectorFactory = domainSelectorFactory;
+
+ // Create a worker thread for this domain selection service.
+ getExecutor();
+
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ int activeModemCount = (tm != null) ? tm.getActiveModemCount() : 1;
+ for (int i = 0; i < activeModemCount; ++i) {
+ mImsStateTrackers.put(i, mImsStateTrackerFactory.create(mContext, i, getLooper()));
+ }
+
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ if (sm != null) {
+ sm.addOnSubscriptionsChangedListener(getExecutor(), mSubscriptionsChangedListener);
+ } else {
+ loge("Adding OnSubscriptionChangedListener failed");
+ }
+
+ logi("TelephonyDomainSelectionService created");
+ }
+
+ @Override
+ public void onDestroy() {
+ logd("onDestroy");
+
+ List<DomainSelectorContainer> domainSelectorContainers;
+
+ synchronized (mDomainSelectorContainers) {
+ domainSelectorContainers = new ArrayList<>(mDomainSelectorContainers);
+ mDomainSelectorContainers.clear();
+ }
+
+ for (DomainSelectorContainer dsc : domainSelectorContainers) {
+ DomainSelectorBase selector = dsc.getDomainSelector();
+ if (selector != null) {
+ selector.destroy();
+ }
+ }
+ domainSelectorContainers.clear();
+
+ synchronized (mImsStateTrackers) {
+ for (int i = 0; i < mImsStateTrackers.size(); ++i) {
+ ImsStateTracker ist = mImsStateTrackers.get(i);
+ if (ist != null) {
+ ist.destroy();
+ }
+ }
+ mImsStateTrackers.clear();
+ }
+
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ if (sm != null) {
+ sm.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener);
+ }
+
+ if (mServiceHandler != null) {
+ mServiceHandler.getLooper().quit();
+ mServiceHandler = null;
+ }
+ }
+
+ /**
+ * Selects a domain for the given attributes and callback.
+ *
+ * @param attr required to determine the domain.
+ * @param callback the callback instance being registered.
+ */
+ @Override
+ public void onDomainSelection(@NonNull SelectionAttributes attr,
+ @NonNull TransportSelectorCallback callback) {
+ final int slotId = attr.getSlotId();
+ final int subId = attr.getSubId();
+ final int selectorType = attr.getSelectorType();
+ final boolean isEmergency = attr.isEmergency();
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ DomainSelectorBase selector = mDomainSelectorFactory.create(mContext, slotId, subId,
+ selectorType, isEmergency, getLooper(), ist, mDestroyListener);
+
+ if (selector != null) {
+ // Ensures that ImsStateTracker is started before selecting the domain if not started
+ // for the specified subscription index.
+ ist.start(subId);
+ addDomainSelector(slotId, selectorType, isEmergency, selector);
+ } else {
+ loge("No proper domain selector: " + selectorTypeToString(selectorType));
+ callback.onSelectionTerminated(DisconnectCause.ERROR_UNSPECIFIED);
+ return;
+ }
+
+ // Notify the caller that the domain selector is created.
+ callback.onCreated(selector);
+
+ // Performs the domain selection.
+ selector.selectDomain(attr, callback);
+ }
+
+ /**
+ * Called when the {@link ServiceState} needs to be updated for the specified slot and
+ * subcription index.
+ *
+ * @param slotId for which the service state changed.
+ * @param subId The current subscription for a specified slot.
+ * @param serviceState The {@link ServiceState} to be updated.
+ */
+ @Override
+ public void onServiceStateUpdated(int slotId, int subId, @NonNull ServiceState serviceState) {
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ if (ist != null) {
+ ist.updateServiceState(serviceState);
+ }
+ }
+
+ /**
+ * Called when the {@link BarringInfo} needs to be updated for the specified slot and
+ * subscription index.
+ *
+ * @param slotId The slot the BarringInfo is updated for.
+ * @param subId The current subscription for a specified slot.
+ * @param barringInfo The {@link BarringInfo} to be updated.
+ */
+ @Override
+ public void onBarringInfoUpdated(int slotId, int subId, @NonNull BarringInfo barringInfo) {
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ if (ist != null) {
+ ist.updateBarringInfo(barringInfo);
+ }
+ }
+
+ /**
+ * Returns an Executor used to execute methods called remotely by the framework.
+ */
+ @SuppressLint("OnNameExpected")
+ @Override
+ public @NonNull Executor getExecutor() {
+ if (mServiceHandler == null) {
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mServiceHandler = new Handler(handlerThread.getLooper());
+ }
+
+ return mServiceHandler::post;
+ }
+
+ /**
+ * Returns a Looper instance.
+ */
+ @VisibleForTesting
+ public Looper getLooper() {
+ getExecutor();
+ return mServiceHandler.getLooper();
+ }
+
+ /**
+ * Handles the subscriptions change.
+ */
+ private void handleSubscriptionsChanged() {
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ List<SubscriptionInfo> subsInfoList =
+ (sm != null) ? sm.getActiveSubscriptionInfoList() : null;
+
+ if (subsInfoList == null || subsInfoList.isEmpty()) {
+ logd("handleSubscriptionsChanged: No valid SubscriptionInfo");
+ return;
+ }
+
+ for (int i = 0; i < subsInfoList.size(); ++i) {
+ SubscriptionInfo subsInfo = subsInfoList.get(i);
+ int slotId = subsInfo.getSimSlotIndex();
+
+ if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ logd("handleSubscriptionsChanged: slotId=" + slotId);
+ ImsStateTracker ist = getImsStateTracker(slotId);
+ ist.start(subsInfo.getSubscriptionId());
+ }
+ }
+ }
+
+ /**
+ * Adds the {@link DomainSelectorBase} to the list of domain selector container.
+ */
+ private void addDomainSelector(int slotId, @SelectorType int selectorType,
+ boolean isEmergency, @NonNull DomainSelectorBase selector) {
+ synchronized (mDomainSelectorContainers) {
+ // If the domain selector already exists, remove the previous one first.
+ for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+ DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+
+ if (dsc.getSlotId() == slotId
+ && dsc.getSelectorType() == selectorType
+ && dsc.isEmergency() == isEmergency) {
+ mDomainSelectorContainers.remove(i);
+ DomainSelectorBase oldSelector = dsc.getDomainSelector();
+ if (oldSelector != null) {
+ logw("DomainSelector destroyed by new domain selection request: " + dsc);
+ oldSelector.destroy();
+ }
+ break;
+ }
+ }
+
+ DomainSelectorContainer dsc =
+ new DomainSelectorContainer(slotId, selectorType, isEmergency, selector);
+ mDomainSelectorContainers.add(dsc);
+
+ logi("DomainSelector added: " + dsc + ", count=" + mDomainSelectorContainers.size());
+ }
+ }
+
+ /**
+ * Removes the domain selector container that matches with the specified
+ * {@link DomainSelectorBase}.
+ */
+ private void removeDomainSelector(@NonNull DomainSelectorBase selector) {
+ synchronized (mDomainSelectorContainers) {
+ for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+ DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+
+ if (dsc.getDomainSelector() == selector) {
+ mDomainSelectorContainers.remove(i);
+ logi("DomainSelector removed: " + dsc
+ + ", count=" + mDomainSelectorContainers.size());
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link ImsStateTracker} instance for the specified slot.
+ * If the {@link ImsStateTracker} does not exist for the slot, it creates new instance
+ * and returns.
+ */
+ private ImsStateTracker getImsStateTracker(int slotId) {
+ synchronized (mImsStateTrackers) {
+ ImsStateTracker ist = mImsStateTrackers.get(slotId);
+
+ if (ist == null) {
+ ist = mImsStateTrackerFactory.create(mContext, slotId, getLooper());
+ mImsStateTrackers.put(slotId, ist);
+ }
+
+ return ist;
+ }
+ }
+
+ private static String selectorTypeToString(@SelectorType int selectorType) {
+ switch (selectorType) {
+ case SELECTOR_TYPE_CALLING: return "CALLING";
+ case SELECTOR_TYPE_SMS: return "SMS";
+ case SELECTOR_TYPE_UT: return "UT";
+ default: return Integer.toString(selectorType);
+ }
+ }
+
+ private static void logd(String s) {
+ Log.d(TAG, s);
+ }
+
+ private static void logi(String s) {
+ Log.i(TAG, s);
+ sEventLog.log(s);
+ }
+
+ private static void loge(String s) {
+ Log.e(TAG, s);
+ sEventLog.log(s);
+ }
+
+ private static void logw(String s) {
+ Log.w(TAG, s);
+ sEventLog.log(s);
+ }
+
+ /**
+ * Dumps this instance into a readable format for dumpsys usage.
+ */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println("TelephonyDomainSelectionService:");
+ ipw.increaseIndent();
+ ipw.println("ImsStateTrackers:");
+ synchronized (mImsStateTrackers) {
+ for (int i = 0; i < mImsStateTrackers.size(); ++i) {
+ ImsStateTracker ist = mImsStateTrackers.valueAt(i);
+ ist.dump(ipw);
+ }
+ }
+ ipw.decreaseIndent();
+ ipw.increaseIndent();
+ synchronized (mDomainSelectorContainers) {
+ for (int i = 0; i < mDomainSelectorContainers.size(); ++i) {
+ DomainSelectorContainer dsc = mDomainSelectorContainers.get(i);
+ ipw.println("DomainSelector: " + dsc.toString());
+ ipw.increaseIndent();
+ DomainSelectorBase selector = dsc.getDomainSelector();
+ if (selector != null) {
+ selector.dump(ipw);
+ }
+ ipw.decreaseIndent();
+ }
+ }
+ ipw.decreaseIndent();
+ ipw.increaseIndent();
+ ipw.println("Event Log:");
+ ipw.increaseIndent();
+ sEventLog.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.decreaseIndent();
+ ipw.println("________________________________");
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java b/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java
new file mode 100644
index 0000000..74c3311
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/DomainSelectorBaseTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.TransportSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for DomainSelectorBase.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DomainSelectorBaseTest {
+ public class TestDomainSelectorBase extends DomainSelectorBase {
+ public TestDomainSelectorBase(Context context, int slotId, int subId,
+ @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener, String logTag) {
+ super(context, slotId, subId, looper, imsStateTracker, listener, logTag);
+ }
+
+ @Override
+ public void cancelSelection() {
+ // No operations.
+ }
+
+ @Override
+ public void reselectDomain(@NonNull SelectionAttributes attr) {
+ // No operations.
+ }
+
+ @Override
+ public void finishSelection() {
+ // No operations.
+ }
+
+ @Override
+ public void selectDomain(SelectionAttributes attr, TransportSelectorCallback callback) {
+ // No operations.
+ }
+ }
+
+ private static final String TAG = DomainSelectorBaseTest.class.getSimpleName();
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+
+ @Mock private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+ @Mock private ImsStateTracker mImsStateTracker;
+
+ private Context mContext;
+ private Looper mLooper;
+ private TestDomainSelectorBase mDomainSelector;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext();
+
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ mDomainSelector = new TestDomainSelectorBase(mContext, SLOT_0, SUB_1, mLooper,
+ mImsStateTracker, mDomainSelectorDestroyListener, TAG);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mDomainSelector != null) {
+ mDomainSelector.destroy();
+ mDomainSelector = null;
+ }
+
+ if (mLooper != null) {
+ mLooper.quit();
+ mLooper = null;
+ }
+
+ mDomainSelectorDestroyListener = null;
+ mImsStateTracker = null;
+ mContext = null;
+ }
+
+ @Test
+ @SmallTest
+ public void testInit() {
+ assertEquals(SLOT_0, mDomainSelector.getSlotId());
+ assertEquals(SUB_1, mDomainSelector.getSubId());
+ }
+
+ @Test
+ @SmallTest
+ public void testDestroy() {
+ mDomainSelector.destroy();
+ verify(mDomainSelectorDestroyListener).onDomainSelectorDestroyed(eq(mDomainSelector));
+ mDomainSelector = null;
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java b/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java
new file mode 100644
index 0000000..b00926f
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/ImsStateTrackerTest.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.BarringInfo;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ImsStateCallback;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for ImsStateTracker.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ImsStateTrackerTest {
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+ private static final int SUB_2 = 2;
+ private static final long TIMEOUT_MS = 100;
+
+ @Mock private ImsMmTelManager mMmTelManager;
+ @Mock private ImsMmTelManager mMmTelManager2;
+ @Mock private ImsStateTracker.BarringInfoListener mBarringInfoListener;
+ @Mock private ImsStateTracker.ServiceStateListener mServiceStateListener;
+ @Mock private ImsStateTracker.ImsStateListener mImsStateListener;
+ @Mock private ServiceState mServiceState;
+
+ private Context mContext;
+ private Looper mLooper;
+ private BarringInfo mBarringInfo = new BarringInfo();
+ private ImsStateTracker mImsStateTracker;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext() {
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (serviceClass == ImsManager.class) {
+ return Context.TELEPHONY_IMS_SERVICE;
+ }
+ return super.getSystemServiceName(serviceClass);
+ }
+ };
+
+ HandlerThread handlerThread = new HandlerThread(
+ ImsStateTrackerTest.class.getSimpleName());
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ mImsStateTracker = new ImsStateTracker(mContext, SLOT_0, mLooper);
+
+ ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+ when(imsManager.getImsMmTelManager(eq(SUB_1))).thenReturn(mMmTelManager);
+ when(imsManager.getImsMmTelManager(eq(SUB_2))).thenReturn(mMmTelManager2);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mImsStateTracker.destroy();
+ mImsStateTracker = null;
+ mMmTelManager = null;
+
+ if (mLooper != null) {
+ mLooper.quit();
+ mLooper = null;
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testInit() {
+ assertEquals(SLOT_0, mImsStateTracker.getSlotId());
+ assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, mImsStateTracker.getSubId());
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWithInvalidSubId() {
+ mImsStateTracker.start(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+ assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, mImsStateTracker.getSubId());
+ assertTrue(isImsStateUnavailable());
+ }
+
+ @Test
+ @SmallTest
+ public void testStart() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWithDifferentSubId() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+
+ mImsStateTracker.start(SUB_2);
+
+ assertEquals(SUB_2, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ verify(mMmTelManager).unregisterImsStateCallback(
+ any(ImsStateCallback.class));
+ verify(mMmTelManager2).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWithSameSubId() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertTrue(isImsStateInit());
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+ verify(mMmTelManager, never()).unregisterImsStateCallback(
+ any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testStartWhenRegisteringCallbacksThrowException() throws ImsException {
+ doAnswer((invocation) -> {
+ throw new ImsException("Intended exception for ImsStateCallback.");
+ }).when(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), any(ImsStateCallback.class));
+
+ mImsStateTracker.start(SUB_1);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+
+ mImsStateTracker.start(SUB_2);
+
+ assertEquals(SUB_2, mImsStateTracker.getSubId());
+
+ verify(mMmTelManager, never()).unregisterImsStateCallback(
+ any(ImsStateCallback.class));
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateServiceStateBeforeAddingListener() {
+ mImsStateTracker.updateServiceState(mServiceState);
+ mImsStateTracker.addServiceStateListener(mServiceStateListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mServiceStateListener).onServiceStateUpdated(eq(mServiceState));
+
+ mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+ ServiceState ss = Mockito.mock(ServiceState.class);
+ mImsStateTracker.updateServiceState(ss);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mServiceStateListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateServiceStateAfterAddingListener() {
+ mImsStateTracker.addServiceStateListener(mServiceStateListener);
+ mImsStateTracker.updateServiceState(mServiceState);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mServiceStateListener).onServiceStateUpdated(eq(mServiceState));
+
+ mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+ ServiceState ss = Mockito.mock(ServiceState.class);
+ mImsStateTracker.updateServiceState(ss);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mServiceStateListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testAddAndRemoveServiceStateListener() {
+ mImsStateTracker.updateServiceState(mServiceState);
+ mImsStateTracker.addServiceStateListener(mServiceStateListener);
+ mImsStateTracker.removeServiceStateListener(mServiceStateListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mServiceStateListener, never()).onServiceStateUpdated(eq(mServiceState));
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateBarringInfoBeforeAddingListener() {
+ mImsStateTracker.updateBarringInfo(mBarringInfo);
+ mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mBarringInfoListener).onBarringInfoUpdated(eq(mBarringInfo));
+
+ mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+ BarringInfo bi = new BarringInfo();
+ mImsStateTracker.updateBarringInfo(bi);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mBarringInfoListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateBarringInfoAfterAddingListener() {
+ mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+ mImsStateTracker.updateBarringInfo(mBarringInfo);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mBarringInfoListener).onBarringInfoUpdated(eq(mBarringInfo));
+
+ mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+ BarringInfo bi = new BarringInfo();
+ mImsStateTracker.updateBarringInfo(bi);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verifyNoMoreInteractions(mBarringInfoListener);
+ }
+
+ @Test
+ @SmallTest
+ public void testAddAndRemoveBarringInfoListener() {
+ mImsStateTracker.updateBarringInfo(mBarringInfo);
+ mImsStateTracker.addBarringInfoListener(mBarringInfoListener);
+ mImsStateTracker.removeBarringInfoListener(mBarringInfoListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(mBarringInfoListener, never()).onBarringInfoUpdated(eq(mBarringInfo));
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnAvailable() throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onAvailable();
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonUnknownPermanentError()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR);
+
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonNoImsServiceConfigured()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED);
+
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonUnknownTemporaryError()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonImsServiceNotReady()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_NOT_READY);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonImsServiceDisconnected()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnUnavailableWithReasonSubscriptionInactive()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnAvailableUnavailableWithReasonImsServiceDisconnected()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onAvailable();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mMmTelManager).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener, times(2)).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ public void testNotifyImsStateCallbackOnUnavailableAvailableWithReasonImsServiceDisconnected()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onUnavailable(ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED);
+ callback.onAvailable();
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+
+ waitForHandlerActionDelayed(mImsStateTracker.getHandler(),
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS,
+ ImsStateTracker.MMTEL_FEATURE_AVAILABLE_WAIT_TIME_MILLIS + TIMEOUT_MS);
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ assertFalse(isImsStateUnavailable());
+ assertFalse(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mMmTelManager, never()).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager, never()).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsStateCallbackOnAvailableUnavailableWithReasonSubscriptionInactive()
+ throws ImsException {
+ ImsStateCallback callback = setUpImsStateCallback();
+ callback.onAvailable();
+ callback.onUnavailable(ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE);
+
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ assertTrue(isImsStateUnavailable());
+ assertTrue(mImsStateTracker.isImsStateReady());
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mMmTelManager).unregisterImsRegistrationCallback(
+ any(RegistrationManager.RegistrationCallback.class));
+ verify(mMmTelManager).unregisterMmTelCapabilityCallback(
+ any(ImsMmTelManager.CapabilityCallback.class));
+ verify(mImsStateListener, times(2)).onImsMmTelFeatureAvailableChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsRegistrationCallbackOnRegistered() throws ImsException {
+ RegistrationManager.RegistrationCallback callback = setUpImsRegistrationCallback();
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+ // It's false because the MMTEL capabilities are not updated yet.
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR).build());
+
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.NGRAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN).build());
+
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertTrue(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.IWLAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_NONE).build());
+
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.UNKNOWN, mImsStateTracker.getImsAccessNetworkType());
+
+ verify(mImsStateListener, times(4)).onImsRegistrationStateChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyImsRegistrationCallbackOnUnregistered() throws ImsException {
+ RegistrationManager.RegistrationCallback callback = setUpImsRegistrationCallback();
+ callback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+ // It's false because the MMTEL capabilities are not updated yet.
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+
+ callback.onUnregistered(new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 0, null));
+
+ // When IMS is unregistered, the MMTEL capability is also reset.
+ assertTrue(mImsStateTracker.isImsStateReady());
+ assertFalse(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.UNKNOWN, mImsStateTracker.getImsAccessNetworkType());
+
+ verify(mImsStateListener, times(2)).onImsRegistrationStateChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotifyMmTelCapabilityCallbackOnCapabilitiesStatusChanged() throws ImsException {
+ ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+ assertFalse(mImsStateTracker.isImsVoiceCapable());
+ assertFalse(mImsStateTracker.isImsVideoCapable());
+ assertFalse(mImsStateTracker.isImsSmsCapable());
+ assertFalse(mImsStateTracker.isImsUtCapable());
+
+ MmTelCapabilities capabilities = new MmTelCapabilities(
+ MmTelCapabilities.CAPABILITY_TYPE_VOICE
+ | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+ | MmTelCapabilities.CAPABILITY_TYPE_SMS
+ | MmTelCapabilities.CAPABILITY_TYPE_UT
+ );
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ assertTrue(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsVoiceCapable());
+ assertTrue(mImsStateTracker.isImsVideoCapable());
+ assertTrue(mImsStateTracker.isImsSmsCapable());
+ assertTrue(mImsStateTracker.isImsUtCapable());
+
+ capabilities = new MmTelCapabilities();
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ assertTrue(mImsStateTracker.isImsStateReady());
+ assertFalse(mImsStateTracker.isImsVoiceCapable());
+ assertFalse(mImsStateTracker.isImsVideoCapable());
+ assertFalse(mImsStateTracker.isImsSmsCapable());
+ assertFalse(mImsStateTracker.isImsUtCapable());
+
+ verify(mImsStateListener, times(2)).onImsMmTelCapabilitiesChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testAddImsStateListenerWhenImsStateReady() throws ImsException {
+ ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+ MmTelCapabilities capabilities = new MmTelCapabilities(
+ MmTelCapabilities.CAPABILITY_TYPE_VOICE
+ | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+ | MmTelCapabilities.CAPABILITY_TYPE_SMS
+ | MmTelCapabilities.CAPABILITY_TYPE_UT
+ );
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ ImsStateTracker.ImsStateListener listener =
+ Mockito.mock(ImsStateTracker.ImsStateListener.class);
+ mImsStateTracker.addImsStateListener(listener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(listener).onImsMmTelFeatureAvailableChanged();
+ verify(listener).onImsRegistrationStateChanged();
+ verify(listener).onImsMmTelCapabilitiesChanged();
+ }
+
+ @Test
+ @SmallTest
+ public void testAddAndRemoveImsStateListenerWhenImsStateReady() throws ImsException {
+ ImsMmTelManager.CapabilityCallback callback = setUpMmTelCapabilityCallback();
+
+ MmTelCapabilities capabilities = new MmTelCapabilities(
+ MmTelCapabilities.CAPABILITY_TYPE_VOICE
+ | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+ | MmTelCapabilities.CAPABILITY_TYPE_SMS
+ | MmTelCapabilities.CAPABILITY_TYPE_UT
+ );
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ Handler handler = new Handler(mLooper);
+ ImsStateTracker.ImsStateListener listener =
+ Mockito.mock(ImsStateTracker.ImsStateListener.class);
+ handler.post(() -> {
+ mImsStateTracker.addImsStateListener(listener);
+ mImsStateTracker.removeImsStateListener(listener);
+ });
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ verify(listener, never()).onImsMmTelFeatureAvailableChanged();
+ verify(listener, never()).onImsRegistrationStateChanged();
+ verify(listener, never()).onImsMmTelCapabilitiesChanged();
+ }
+
+ private ImsStateCallback setUpImsStateCallback() throws ImsException {
+ mImsStateTracker.start(SUB_1);
+ mImsStateTracker.addImsStateListener(mImsStateListener);
+ waitForHandlerAction(mImsStateTracker.getHandler(), TIMEOUT_MS);
+
+ assertEquals(SUB_1, mImsStateTracker.getSubId());
+ assertFalse(mImsStateTracker.isMmTelFeatureAvailable());
+ ArgumentCaptor<ImsStateCallback> callbackCaptor =
+ ArgumentCaptor.forClass(ImsStateCallback.class);
+ verify(mMmTelManager).registerImsStateCallback(
+ any(Executor.class), callbackCaptor.capture());
+
+ ImsStateCallback imsStateCallback = callbackCaptor.getValue();
+ assertNotNull(imsStateCallback);
+ return imsStateCallback;
+ }
+
+ private RegistrationManager.RegistrationCallback setUpImsRegistrationCallback()
+ throws ImsException {
+ ImsStateCallback imsStateCallback = setUpImsStateCallback();
+ imsStateCallback.onAvailable();
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ ArgumentCaptor<RegistrationManager.RegistrationCallback> callbackCaptor =
+ ArgumentCaptor.forClass(RegistrationManager.RegistrationCallback.class);
+ verify(mMmTelManager).registerImsRegistrationCallback(
+ any(Executor.class), callbackCaptor.capture());
+
+ RegistrationManager.RegistrationCallback registrationCallback = callbackCaptor.getValue();
+ assertNotNull(registrationCallback);
+ return registrationCallback;
+ }
+
+ private ImsMmTelManager.CapabilityCallback setUpMmTelCapabilityCallback()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ setUpImsRegistrationCallback();
+ registrationCallback.onRegistered(new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build());
+
+ assertTrue(mImsStateTracker.isMmTelFeatureAvailable());
+ // It's false because the MMTEL capabilities are not updated.
+ assertFalse(mImsStateTracker.isImsStateReady());
+ assertTrue(mImsStateTracker.isImsRegistered());
+ assertFalse(mImsStateTracker.isImsRegisteredOverWlan());
+ assertEquals(AccessNetworkType.EUTRAN, mImsStateTracker.getImsAccessNetworkType());
+ ArgumentCaptor<ImsMmTelManager.CapabilityCallback> callbackCaptor =
+ ArgumentCaptor.forClass(ImsMmTelManager.CapabilityCallback.class);
+ verify(mMmTelManager).registerMmTelCapabilityCallback(
+ any(Executor.class), callbackCaptor.capture());
+
+ ImsMmTelManager.CapabilityCallback capabilityCallback = callbackCaptor.getValue();
+ assertNotNull(capabilityCallback);
+ return capabilityCallback;
+ }
+
+ private boolean isImsStateUnavailable() {
+ return mImsStateTracker.isImsStateReady()
+ && !mImsStateTracker.isImsRegistered()
+ && !mImsStateTracker.isMmTelFeatureAvailable()
+ && !mImsStateTracker.isImsVoiceCapable()
+ && !mImsStateTracker.isImsVideoCapable()
+ && !mImsStateTracker.isImsSmsCapable()
+ && !mImsStateTracker.isImsUtCapable()
+ && (AccessNetworkType.UNKNOWN == mImsStateTracker.getImsAccessNetworkType());
+ }
+
+ private boolean isImsStateInit() {
+ return !mImsStateTracker.isImsStateReady()
+ && !mImsStateTracker.isImsRegistered()
+ && !mImsStateTracker.isMmTelFeatureAvailable()
+ && !mImsStateTracker.isImsVoiceCapable()
+ && !mImsStateTracker.isImsVideoCapable()
+ && !mImsStateTracker.isImsSmsCapable()
+ && !mImsStateTracker.isImsUtCapable()
+ && (AccessNetworkType.UNKNOWN == mImsStateTracker.getImsAccessNetworkType());
+ }
+
+ private void waitForHandlerAction(Handler h, long timeoutMillis) {
+ waitForHandlerActionDelayed(h, 0, timeoutMillis);
+ }
+
+ private void waitForHandlerActionDelayed(Handler h, long delayMillis, long timeoutMillis) {
+ final CountDownLatch lock = new CountDownLatch(1);
+ h.postDelayed(lock::countDown, delayMillis);
+ while (lock.getCount() > 0) {
+ try {
+ lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java b/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java
new file mode 100644
index 0000000..ace59e3
--- /dev/null
+++ b/tests/src/com/android/services/telephony/domainselection/TelephonyDomainSelectionServiceTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2022 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.services.telephony.domainselection;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.BarringInfo;
+import android.telephony.DomainSelectionService;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.DomainSelectionService.SelectorType;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TransportSelectorCallback;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TestContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for TelephonyDomainSelectionService.
+ */
+@RunWith(AndroidJUnit4.class)
+public class TelephonyDomainSelectionServiceTest {
+ private TelephonyDomainSelectionService.ImsStateTrackerFactory mImsStateTrackerFactory =
+ new TelephonyDomainSelectionService.ImsStateTrackerFactory() {
+ @Override
+ public ImsStateTracker create(Context context, int slotId,
+ @NonNull Looper looper) {
+ return mImsStateTracker;
+ }
+ };
+ private TelephonyDomainSelectionService.DomainSelectorFactory mDomainSelectorFactory =
+ new TelephonyDomainSelectionService.DomainSelectorFactory() {
+ @Override
+ public DomainSelectorBase create(Context context, int slotId, int subId,
+ @SelectorType int selectorType, boolean isEmergency,
+ @NonNull Looper looper, @NonNull ImsStateTracker imsStateTracker,
+ @NonNull DomainSelectorBase.DestroyListener listener) {
+ switch (selectorType) {
+ case DomainSelectionService.SELECTOR_TYPE_CALLING: // fallthrough
+ case DomainSelectionService.SELECTOR_TYPE_SMS: // fallthrough
+ case DomainSelectionService.SELECTOR_TYPE_UT:
+ mDomainSelectorDestroyListener = listener;
+ if (subId == SUB_1) {
+ return mDomainSelectorBase1;
+ } else {
+ return mDomainSelectorBase2;
+ }
+ default:
+ return null;
+ }
+ }
+ };
+ private static final int SLOT_0 = 0;
+ private static final int SUB_1 = 1;
+ private static final int SUB_2 = 2;
+ private static final String CALL_ID = "Call_1";
+ private static final @SelectorType int TEST_SELECTOR_TYPE =
+ DomainSelectionService.SELECTOR_TYPE_CALLING;
+ private static final @SelectorType int INVALID_SELECTOR_TYPE = -1;
+
+ @Mock private DomainSelectorBase mDomainSelectorBase1;
+ @Mock private DomainSelectorBase mDomainSelectorBase2;
+ @Mock private TransportSelectorCallback mSelectorCallback1;
+ @Mock private TransportSelectorCallback mSelectorCallback2;
+ @Mock private ImsStateTracker mImsStateTracker;
+
+ private final ServiceState mServiceState = new ServiceState();
+ private final BarringInfo mBarringInfo = new BarringInfo();
+ private Context mContext;
+ private Handler mServiceHandler;
+ private TestableLooper mTestableLooper;
+ private SubscriptionManager mSubscriptionManager;
+ private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
+ private DomainSelectorBase.DestroyListener mDomainSelectorDestroyListener;
+ private TelephonyDomainSelectionService mDomainSelectionService;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mContext = new TestContext();
+ mDomainSelectionService = new TelephonyDomainSelectionService(mContext,
+ mImsStateTrackerFactory, mDomainSelectorFactory);
+ mServiceHandler = new Handler(mDomainSelectionService.getLooper());
+ mTestableLooper = new TestableLooper(mDomainSelectionService.getLooper());
+
+ mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+ ArgumentCaptor<OnSubscriptionsChangedListener> listenerCaptor =
+ ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+ verify(mSubscriptionManager).addOnSubscriptionsChangedListener(
+ any(Executor.class), listenerCaptor.capture());
+ mOnSubscriptionsChangedListener = listenerCaptor.getValue();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mTestableLooper != null) {
+ mTestableLooper.destroy();
+ mTestableLooper = null;
+ }
+ mServiceHandler = null;
+
+ if (mDomainSelectionService != null) {
+ mDomainSelectionService.onDestroy();
+ mDomainSelectionService = null;
+ }
+
+ mDomainSelectorBase1 = null;
+ mDomainSelectorBase2 = null;
+ mSelectorCallback1 = null;
+ mSelectorCallback2 = null;
+ mImsStateTracker = null;
+ mSubscriptionManager = null;
+ mOnSubscriptionsChangedListener = null;
+ mDomainSelectorDestroyListener = null;
+ }
+
+ @Test
+ @SmallTest
+ public void testGetExecutor() {
+ assertNotNull(mDomainSelectionService.getExecutor());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelection() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_1));
+ verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelectionWithInvalidSelectorType() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, INVALID_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker, never()).start(anyInt());
+ verify(mSelectorCallback1).onSelectionTerminated(anyInt());
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1, never()).selectDomain(eq(attr1), eq(mSelectorCallback1));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelectionTwiceWithDestroy() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_1));
+ verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+
+ // Notify the domain selection service that this domain selector is destroyed.
+ mDomainSelectorDestroyListener.onDomainSelectorDestroyed(mDomainSelectorBase1);
+
+ SelectionAttributes attr2 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_2, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr2, mSelectorCallback2);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_2));
+ verify(mSelectorCallback2).onCreated(eq(mDomainSelectorBase2));
+ verifyNoMoreInteractions(mSelectorCallback2);
+ verify(mDomainSelectorBase2).selectDomain(eq(attr2), eq(mSelectorCallback2));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDomainSelectionTwiceWithoutDestroy() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_1));
+ verify(mSelectorCallback1).onCreated(eq(mDomainSelectorBase1));
+ verifyNoMoreInteractions(mSelectorCallback1);
+ verify(mDomainSelectorBase1).selectDomain(eq(attr1), eq(mSelectorCallback1));
+
+ SelectionAttributes attr2 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_2, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr2, mSelectorCallback2);
+ });
+ processAllMessages();
+
+ verify(mImsStateTracker).start(eq(SUB_2));
+ verify(mSelectorCallback2).onCreated(eq(mDomainSelectorBase2));
+ verifyNoMoreInteractions(mSelectorCallback2);
+ verify(mDomainSelectorBase2).selectDomain(eq(attr2), eq(mSelectorCallback2));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnServiceStateUpdated() {
+ mDomainSelectionService.onServiceStateUpdated(SLOT_0, SUB_1, mServiceState);
+
+ verify(mImsStateTracker).updateServiceState(eq(mServiceState));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnBarringInfoUpdated() {
+ mDomainSelectionService.onBarringInfoUpdated(SLOT_0, SUB_1, mBarringInfo);
+
+ verify(mImsStateTracker).updateBarringInfo(eq(mBarringInfo));
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDestroy() {
+ SelectionAttributes attr1 = new SelectionAttributes.Builder(
+ SLOT_0, SUB_1, TEST_SELECTOR_TYPE)
+ .setCallId(CALL_ID)
+ .setEmergency(true)
+ .build();
+ mServiceHandler.post(() -> {
+ mDomainSelectionService.onDomainSelection(attr1, mSelectorCallback1);
+ });
+ processAllMessages();
+
+ mDomainSelectionService.onDestroy();
+
+ verify(mImsStateTracker).destroy();
+ verify(mDomainSelectorBase1).destroy();
+ verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleSubscriptionsChangedWithEmptySubscriptionInfo() {
+ when(mSubscriptionManager.getActiveSubscriptionInfoList())
+ .thenReturn(null, new ArrayList<SubscriptionInfo>());
+
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ verify(mImsStateTracker, never()).start(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleSubscriptionsChangedWithActiveSubscriptionInfoAndInvalidSlotIndex() {
+ SubscriptionInfo subsInfo = Mockito.mock(SubscriptionInfo.class);
+ List<SubscriptionInfo> subsInfoList = new ArrayList<>();
+ subsInfoList.add(subsInfo);
+ when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subsInfoList);
+ when(subsInfo.getSimSlotIndex()).thenReturn(SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ verify(mImsStateTracker, never()).start(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleSubscriptionsChangedWithActiveSubscriptionInfo() {
+ SubscriptionInfo subsInfo = Mockito.mock(SubscriptionInfo.class);
+ List<SubscriptionInfo> subsInfoList = new ArrayList<>();
+ subsInfoList.add(subsInfo);
+ when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(subsInfoList);
+ when(subsInfo.getSubscriptionId())
+ .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID, SUB_1);
+ when(subsInfo.getSimSlotIndex()).thenReturn(SLOT_0);
+
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+ mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ verify(mImsStateTracker).start(eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID));
+ verify(mImsStateTracker).start(eq(SUB_1));
+ }
+
+ private void processAllMessages() {
+ while (!mTestableLooper.getLooper().getQueue().isIdle()) {
+ mTestableLooper.processAllMessages();
+ }
+ }
+}