Refactor TelephonyRcsService to contain RcsFeatureManager
Instead of using ImsPhone as the root object to maintain RCS,
this refactor moves these dependencies into TelephonyRcsService
to remove the messy coupling in ImsPhone and TelephonyRcsService.
Test: atest TeleServiceTests
Test: atest CtsTelephonyTestCases
Change-Id: I82c1681b6fcd5ff9efbbbb05dfbfd71aa2d00101
diff --git a/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index e09c6af..6b1b5e3 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -34,11 +34,12 @@
import android.util.Log;
import com.android.ims.ImsManager;
-import com.android.ims.RcsFeatureManager;
import com.android.internal.telephony.IIntegerConsumer;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.services.telephony.rcs.RcsFeatureController;
import com.android.services.telephony.rcs.TelephonyRcsService;
+import com.android.services.telephony.rcs.UserCapabilityExchangeImpl;
import java.util.List;
@@ -77,16 +78,16 @@
}
/**
- * Register a IImsRegistrationCallback to receive IMS network registration state.
+ * Register a {@link RegistrationManager.RegistrationCallback} to receive IMS network
+ * registration state.
*/
@Override
- public void registerImsRegistrationCallback(int subId, IImsRegistrationCallback callback)
- throws RemoteException {
+ public void registerImsRegistrationCallback(int subId, IImsRegistrationCallback callback) {
enforceReadPrivilegedPermission("registerImsRegistrationCallback");
final long token = Binder.clearCallingIdentity();
try {
- getRcsFeatureManager(subId).registerImsRegistrationCallback(callback);
- } catch (com.android.ims.ImsException e) {
+ getRcsFeatureController(subId).registerImsRegistrationCallback(subId, callback);
+ } catch (ImsException e) {
Log.e(TAG, "registerImsRegistrationCallback: sudId=" + subId + ", " + e.getMessage());
throw new ServiceSpecificException(e.getCode());
} finally {
@@ -95,14 +96,14 @@
}
/**
- * Removes an existing {@link RegistrationCallback}.
+ * Removes an existing {@link RegistrationManager.RegistrationCallback}.
*/
@Override
public void unregisterImsRegistrationCallback(int subId, IImsRegistrationCallback callback) {
enforceReadPrivilegedPermission("unregisterImsRegistrationCallback");
final long token = Binder.clearCallingIdentity();
try {
- getRcsFeatureManager(subId).unregisterImsRegistrationCallback(callback);
+ getRcsFeatureController(subId).unregisterImsRegistrationCallback(subId, callback);
} catch (ServiceSpecificException e) {
Log.e(TAG, "unregisterImsRegistrationCallback: error=" + e.errorCode);
} finally {
@@ -118,7 +119,7 @@
enforceReadPrivilegedPermission("getImsRcsRegistrationState");
final long token = Binder.clearCallingIdentity();
try {
- getImsPhone(subId).getImsRcsRegistrationState(regState -> {
+ getRcsFeatureController(subId).getRegistrationState(regState -> {
try {
consumer.accept((regState == null)
? RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED : regState);
@@ -139,7 +140,7 @@
enforceReadPrivilegedPermission("getImsRcsRegistrationTransportType");
final long token = Binder.clearCallingIdentity();
try {
- getImsPhone(subId).getImsRcsRegistrationTech(regTech -> {
+ getRcsFeatureController(subId).getRegistrationTech(regTech -> {
// Convert registration tech from ImsRegistrationImplBase -> RegistrationManager
int regTechConverted = (regTech == null)
? ImsRegistrationImplBase.REGISTRATION_TECH_NONE : regTech;
@@ -164,13 +165,12 @@
* @param callback The ImsCapabilityCallback to be registered.
*/
@Override
- public void registerRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback)
- throws RemoteException {
+ public void registerRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback) {
enforceReadPrivilegedPermission("registerRcsAvailabilityCallback");
final long token = Binder.clearCallingIdentity();
try {
- getRcsFeatureManager(subId).registerRcsAvailabilityCallback(callback);
- } catch (com.android.ims.ImsException e) {
+ getRcsFeatureController(subId).registerRcsAvailabilityCallback(subId, callback);
+ } catch (ImsException e) {
Log.e(TAG, "registerRcsAvailabilityCallback: sudId=" + subId + ", " + e.getMessage());
throw new ServiceSpecificException(e.getCode());
} finally {
@@ -189,9 +189,7 @@
enforceReadPrivilegedPermission("unregisterRcsAvailabilityCallback");
final long token = Binder.clearCallingIdentity();
try {
- getRcsFeatureManager(subId).unregisterRcsAvailabilityCallback(callback);
- } catch (com.android.ims.ImsException e) {
- Log.e(TAG, "unregisterRcsAvailabilityCallback: sudId=" + subId + "," + e.getMessage());
+ getRcsFeatureController(subId).unregisterRcsAvailabilityCallback(subId, callback);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -212,8 +210,8 @@
enforceReadPrivilegedPermission("isCapable");
final long token = Binder.clearCallingIdentity();
try {
- return getRcsFeatureManager(subId).isCapable(capability, radioTech);
- } catch (com.android.ims.ImsException e) {
+ return getRcsFeatureController(subId).isCapable(capability, radioTech);
+ } catch (ImsException e) {
Log.e(TAG, "isCapable: sudId=" + subId
+ ", capability=" + capability + ", " + e.getMessage());
return false;
@@ -236,8 +234,8 @@
enforceReadPrivilegedPermission("isAvailable");
final long token = Binder.clearCallingIdentity();
try {
- return getRcsFeatureManager(subId).isAvailable(capability);
- } catch (com.android.ims.ImsException e) {
+ return getRcsFeatureController(subId).isAvailable(capability);
+ } catch (ImsException e) {
Log.e(TAG, "isAvailable: sudId=" + subId
+ ", capability=" + capability + ", " + e.getMessage());
return false;
@@ -250,21 +248,35 @@
public void requestCapabilities(int subId, List<Uri> contactNumbers,
IRcsUceControllerCallback c) {
enforceReadPrivilegedPermission("requestCapabilities");
- if (mRcsService == null) {
- throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
- "IMS is not available on device.");
+ final long token = Binder.clearCallingIdentity();
+ try {
+ UserCapabilityExchangeImpl uce = getRcsFeatureController(subId).getFeature(
+ UserCapabilityExchangeImpl.class);
+ if (uce == null) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+ "This subscription does not support UCE.");
+ }
+ uce.requestCapabilities(contactNumbers, c);
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- mRcsService.requestCapabilities(getImsPhone(subId).getPhoneId(), contactNumbers, c);
}
@Override
public int getUcePublishState(int subId) {
enforceReadPrivilegedPermission("getUcePublishState");
- if (mRcsService == null) {
- throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
- "IMS is not available on device.");
+ final long token = Binder.clearCallingIdentity();
+ try {
+ UserCapabilityExchangeImpl uce = getRcsFeatureController(subId).getFeature(
+ UserCapabilityExchangeImpl.class);
+ if (uce == null) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+ "This subscription does not support UCE.");
+ }
+ return uce.getUcePublishState();
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- return mRcsService.getUcePublishState(getImsPhone(subId).getPhoneId());
}
@Override
@@ -332,27 +344,27 @@
* @return The RcsFeatureManager instance
* @throws ServiceSpecificException if getting RcsFeatureManager instance failed.
*/
- private RcsFeatureManager getRcsFeatureManager(int subId) {
+ private RcsFeatureController getRcsFeatureController(int subId) {
if (!ImsManager.isImsSupportedOnDevice(mApp)) {
throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
"IMS is not available on device.");
}
+ if (mRcsService == null) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+ "IMS is not available on device.");
+ }
Phone phone = PhoneGlobals.getPhone(subId);
if (phone == null) {
throw new ServiceSpecificException(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION,
"Invalid subscription Id: " + subId);
}
- ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
- if (imsPhone == null) {
+ int slotId = phone.getPhoneId();
+ RcsFeatureController c = mRcsService.getFeatureController(slotId);
+ if (c == null) {
throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
- "Cannot find ImsPhone instance: " + subId);
+ "Cannot find RcsFeatureController instance for sub: " + subId);
}
- RcsFeatureManager rcsFeatureManager = imsPhone.getRcsManager();
- if (rcsFeatureManager == null) {
- throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
- "Cannot find RcsFeatureManager instance: " + subId);
- }
- return rcsFeatureManager;
+ return c;
}
void setRcsService(TelephonyRcsService rcsService) {
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index b0e0105..d361bb1 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -375,7 +375,9 @@
imsRcsController = ImsRcsController.init(this);
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS)) {
- mTelephonyRcsService = new TelephonyRcsService(this);
+ mTelephonyRcsService = new TelephonyRcsService(this,
+ PhoneFactory.getPhones().length);
+ mTelephonyRcsService.initialize();
imsRcsController.setRcsService(mTelephonyRcsService);
}
@@ -912,6 +914,12 @@
e.printStackTrace();
}
pw.decreaseIndent();
+ pw.println("RcsService:");
+ try {
+ if (mTelephonyRcsService != null) mTelephonyRcsService.dump(fd, pw, args);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
pw.decreaseIndent();
pw.println("------- End PhoneGlobals -------");
}
diff --git a/src/com/android/services/telephony/rcs/PresenceHelper.java b/src/com/android/services/telephony/rcs/PresenceHelper.java
deleted file mode 100644
index 5f7e35f..0000000
--- a/src/com/android/services/telephony/rcs/PresenceHelper.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.services.telephony.rcs;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.telephony.SubscriptionManager;
-import android.util.Log;
-import android.util.SparseArray;
-
-import com.android.ims.RcsFeatureConnection;
-import com.android.ims.RcsFeatureManager;
-import com.android.internal.telephony.Phone;
-import com.android.internal.telephony.PhoneFactory;
-import com.android.internal.telephony.imsphone.ImsPhone;
-import com.android.internal.telephony.imsphone.ImsRcsStatusListener;
-import com.android.phone.R;
-import com.android.service.ims.presence.PresencePublication;
-import com.android.service.ims.presence.PresenceSubscriber;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-
-class PresenceHelper {
-
- private static final String LOG_TAG = "PresenceHelper";
-
- private final Context mContext;
- private final List<Phone> mPhones;
-
- private final SparseArray<PresencePublication> mPresencePublications = new SparseArray<>();
- private final SparseArray<PresenceSubscriber> mPresenceSubscribers = new SparseArray<>();
-
- PresenceHelper(Context context) {
- mContext = context;
-
- // Get phones
- Phone[] phoneAry = PhoneFactory.getPhones();
- mPhones = (phoneAry != null) ? Arrays.asList(phoneAry) : new ArrayList<>();
-
- initRcsPresencesInstance();
- registerRcsConnectionStatus();
-
- Log.i(LOG_TAG, "initialized: phone size=" + mPhones.size());
- }
-
- private void initRcsPresencesInstance() {
- String[] volteError = mContext.getResources().getStringArray(
- R.array.config_volte_provision_error_on_publish_response);
- String[] rcsError = mContext.getResources().getStringArray(
- R.array.config_rcs_provision_error_on_publish_response);
-
- mPhones.forEach((phone) -> {
- RcsFeatureConnection rcsConnection = getRcsFeatureConnection(phone);
- // Initialize PresencePublication
- mPresencePublications.put(
- phone.getPhoneId(),
- new PresencePublication(rcsConnection, mContext, volteError, rcsError));
- // Initialize PresenceSubscriber
- mPresenceSubscribers.put(
- phone.getPhoneId(),
- new PresenceSubscriber(rcsConnection, mContext, volteError, rcsError));
- });
- }
-
- private @Nullable RcsFeatureConnection getRcsFeatureConnection(Phone phone) {
- ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
- if (imsPhone != null) {
- RcsFeatureManager rcsFeatureManager = imsPhone.getRcsManager();
- if (rcsFeatureManager != null) {
- return rcsFeatureManager.getRcsFeatureConnection();
- }
- }
- return null;
- }
-
- /*
- * RcsFeatureManager in ImsPhone is not null only when RCS is connected. Register a callback to
- * receive the RCS connection status.
- */
- private void registerRcsConnectionStatus() {
- mPhones.forEach((phone) -> {
- ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
- if (imsPhone != null) {
- imsPhone.setRcsStatusListener(mStatusListener);
- }
- });
- }
-
- /**
- * The IMS RCS status listener to listen the status changed
- */
- private ImsRcsStatusListener mStatusListener = new ImsRcsStatusListener() {
- @Override
- public void onRcsConnected(int phoneId, RcsFeatureManager rcsFeatureManager) {
- int subId = getSubscriptionId(phoneId);
- if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
- Log.e(LOG_TAG, "onRcsConnected: invalid subId, phoneId=" + phoneId);
- return;
- }
-
- Log.i(LOG_TAG, "onRcsConnected: phoneId=" + phoneId + ", subId=" + subId);
- RcsFeatureConnection connection = rcsFeatureManager.getRcsFeatureConnection();
- PresencePublication presencePublication = getPresencePublication(phoneId);
- if (presencePublication != null) {
- Log.i(LOG_TAG, "Update PresencePublisher because RCS is connected");
- presencePublication.updatePresencePublisher(subId, connection);
- }
- PresenceSubscriber presenceSubscriber = getPresenceSubscriber(phoneId);
- if (presenceSubscriber != null) {
- Log.i(LOG_TAG, "Update PresenceSubscriber because RCS is connected");
- presenceSubscriber.updatePresenceSubscriber(subId, connection);
- }
- }
-
- @Override
- public void onRcsDisconnected(int phoneId) {
- int subId = getSubscriptionId(phoneId);
- Log.i(LOG_TAG, "onRcsDisconnected: phoneId=" + phoneId + ", subId=" + subId);
- PresencePublication publication = getPresencePublication(phoneId);
- if (publication != null) {
- Log.i(LOG_TAG, "Remove PresencePublisher because RCS is disconnected");
- publication.removePresencePublisher(subId);
- }
-
- PresenceSubscriber subscriber = getPresenceSubscriber(phoneId);
- if (subscriber != null) {
- Log.i(LOG_TAG, "Remove PresencePublisher because RCS is disconnected");
- subscriber.removePresenceSubscriber(subId);
- }
- }
- };
-
- private int getSubscriptionId(int phoneId) {
- Optional<Phone> phone = mPhones.stream()
- .filter(p -> p.getPhoneId() == phoneId).findFirst();
- if (phone.isPresent()) {
- return phone.get().getSubId();
- }
- return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
- }
-
- public @Nullable PresencePublication getPresencePublication(int phoneId) {
- return mPresencePublications.get(phoneId);
- }
-
- public @Nullable PresenceSubscriber getPresenceSubscriber(int phoneId) {
- return mPresenceSubscribers.get(phoneId);
- }
-}
diff --git a/src/com/android/services/telephony/rcs/RcsFeatureController.java b/src/com/android/services/telephony/rcs/RcsFeatureController.java
new file mode 100644
index 0000000..f451e9b
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/RcsFeatureController.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.rcs;
+
+import android.annotation.AnyThread;
+import android.content.Context;
+import android.net.Uri;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IImsRegistrationCallback;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.ims.FeatureConnector;
+import com.android.ims.IFeatureConnector;
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.imsphone.ImsRegistrationCallbackHelper;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Contains the RCS feature implementations that are associated with this slot's RcsFeature.
+ */
+@AnyThread
+public class RcsFeatureController {
+ private static final String LOG_TAG = "RcsFeatureController";
+
+ /**
+ * Interface used by RCS features that need to listen for when the associated service has been
+ * connected.
+ */
+ public interface Feature {
+ /**
+ * The RcsFeature has been connected to the framework and is ready.
+ */
+ void onRcsConnected(RcsFeatureManager manager);
+
+ /**
+ * The framework has lost the binding to the RcsFeature or it is in the process of changing.
+ */
+ void onRcsDisconnected();
+
+ /**
+ * The subscription associated with the slot this controller is bound to has changed or its
+ * carrier configuration has changed.
+ */
+ void onAssociatedSubscriptionUpdated(int subId);
+
+ /**
+ * Called when the feature should be destroyed.
+ */
+ void onDestroy();
+ }
+
+ /**
+ * Used to inject FeatureConnector instances for testing.
+ */
+ @VisibleForTesting
+ public interface FeatureConnectorFactory<T extends IFeatureConnector> {
+ /**
+ * @return a {@link FeatureConnector} associated for the given {@link IFeatureConnector}
+ * and slot id.
+ */
+ FeatureConnector<T> create(Context context, int slotId,
+ FeatureConnector.Listener<T> listener, Executor executor, String tag);
+ }
+
+ /**
+ * Used to inject ImsRegistrationCallbackHelper instances for testing.
+ */
+ @VisibleForTesting
+ public interface RegistrationHelperFactory {
+ /**
+ * @return an {@link ImsRegistrationCallbackHelper}, which helps manage IMS registration
+ * state.
+ */
+ ImsRegistrationCallbackHelper create(
+ ImsRegistrationCallbackHelper.ImsRegistrationUpdate cb, Executor executor);
+ }
+
+ private FeatureConnectorFactory<RcsFeatureManager> mFeatureFactory = FeatureConnector::new;
+ private RegistrationHelperFactory mRegistrationHelperFactory =
+ ImsRegistrationCallbackHelper::new;
+
+ private final Map<Class<?>, Feature> mFeatures = new ArrayMap<>();
+ private final Context mContext;
+ private final ImsRegistrationCallbackHelper mImsRcsRegistrationHelper;
+ private final int mSlotId;
+ private final Object mLock = new Object();
+ private FeatureConnector<RcsFeatureManager> mFeatureConnector;
+ private RcsFeatureManager mFeatureManager;
+
+ private FeatureConnector.Listener<RcsFeatureManager> mFeatureConnectorListener =
+ new FeatureConnector.Listener<RcsFeatureManager>() {
+ @Override
+ public RcsFeatureManager getFeatureManager() {
+ return new RcsFeatureManager(mContext, mSlotId);
+ }
+
+ @Override
+ public void connectionReady(RcsFeatureManager manager)
+ throws com.android.ims.ImsException {
+ if (manager == null) {
+ Log.w(LOG_TAG, "connectionReady returned null RcsFeatureManager");
+ return;
+ }
+ try {
+ // May throw ImsException if for some reason the connection to the
+ // ImsService is gone.
+ setupConnectionToService(manager);
+ } catch (ImsException e) {
+ // Use deprecated Exception for compatibility.
+ throw new com.android.ims.ImsException(e.getMessage(),
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ updateConnectionStatus(manager);
+ }
+
+ @Override
+ public void connectionUnavailable() {
+ // Call before disabling connection to manager.
+ removeConnectionToService();
+ updateConnectionStatus(null /*manager*/);
+ }
+ };
+
+ private ImsRegistrationCallbackHelper.ImsRegistrationUpdate mRcsRegistrationUpdate = new
+ ImsRegistrationCallbackHelper.ImsRegistrationUpdate() {
+ @Override
+ public void handleImsRegistered(int imsRadioTech) {
+ }
+
+ @Override
+ public void handleImsRegistering(int imsRadioTech) {
+ }
+
+ @Override
+ public void handleImsUnregistered(ImsReasonInfo imsReasonInfo) {
+ }
+
+ @Override
+ public void handleImsSubscriberAssociatedUriChanged(Uri[] uris) {
+ }
+ };
+
+ public RcsFeatureController(Context context, int slotId) {
+ mContext = context;
+ mSlotId = slotId;
+ mImsRcsRegistrationHelper = mRegistrationHelperFactory.create(mRcsRegistrationUpdate,
+ mContext.getMainExecutor());
+ }
+
+ /**
+ * Should only be used to inject registration helpers for testing.
+ */
+ @VisibleForTesting
+ public RcsFeatureController(Context context, int slotId, RegistrationHelperFactory f) {
+ mContext = context;
+ mSlotId = slotId;
+ mRegistrationHelperFactory = f;
+ mImsRcsRegistrationHelper = mRegistrationHelperFactory.create(mRcsRegistrationUpdate,
+ mContext.getMainExecutor());
+ }
+
+ /**
+ * This method should be called after constructing an instance of this class to start the
+ * connection process to the associated RcsFeature.
+ */
+ public void connect() {
+ synchronized (mLock) {
+ mFeatureConnector = mFeatureFactory.create(mContext, mSlotId, mFeatureConnectorListener,
+ mContext.getMainExecutor(), LOG_TAG);
+ mFeatureConnector.connect();
+ }
+ }
+
+ /**
+ * Adds a {@link Feature} to be tracked by this FeatureController.
+ */
+ public <T extends Feature> void addFeature(T connector, Class<T> clazz) {
+ synchronized (mLock) {
+ mFeatures.put(clazz, connector);
+ }
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager != null) {
+ connector.onRcsConnected(manager);
+ } else {
+ connector.onRcsDisconnected();
+ }
+ }
+
+ /**
+ * @return The RCS feature implementation tracked by this controller.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T getFeature(Class<T> clazz) {
+ synchronized (mLock) {
+ return (T) mFeatures.get(clazz);
+ }
+ }
+
+ /**
+ * Update the subscription associated with this controller.
+ */
+ public void updateAssociatedSubscription(int newSubId) {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager != null) {
+ try {
+ manager.updateCapabilities();
+ } catch (ImsException e) {
+ Log.w(LOG_TAG, "associatedSubscriptionChanged failed:" + e);
+ }
+ }
+ synchronized (mLock) {
+ for (Feature c : mFeatures.values()) {
+ c.onAssociatedSubscriptionUpdated(newSubId);
+ }
+ }
+ }
+
+ /**
+ * Call before this controller is destroyed to tear down associated features.
+ */
+ public void destroy() {
+ synchronized (mLock) {
+ mFeatureConnector.disconnect();
+ for (Feature c : mFeatures.values()) {
+ c.onRcsDisconnected();
+ c.onDestroy();
+ }
+ mFeatures.clear();
+ }
+ }
+
+ @VisibleForTesting
+ public void setFeatureConnectorFactory(FeatureConnectorFactory factory) {
+ mFeatureFactory = factory;
+ }
+
+ /**
+ * Add a {@link RegistrationManager.RegistrationCallback} callback that gets called when IMS
+ * registration has changed for a specific subscription.
+ */
+ public void registerImsRegistrationCallback(int subId, IImsRegistrationCallback callback)
+ throws ImsException {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager == null) {
+ throw new ImsException("Service is not available",
+ ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+ }
+ manager.registerImsRegistrationCallback(subId, callback);
+ }
+
+ /**
+ * Removes a previously registered {@link RegistrationManager.RegistrationCallback} callback
+ * that is associated with a specific subscription.
+ */
+ public void unregisterImsRegistrationCallback(int subId, IImsRegistrationCallback callback) {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager != null) {
+ manager.unregisterImsRegistrationCallback(subId, callback);
+ }
+ }
+
+ /**
+ * Register an {@link ImsRcsManager.AvailabilityCallback} with the associated RcsFeature,
+ * which will provide availability updates.
+ */
+ public void registerRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback)
+ throws ImsException {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager == null) {
+ throw new ImsException("Service is not available",
+ ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+ }
+ manager.registerRcsAvailabilityCallback(subId, callback);
+ }
+
+ /**
+ * Remove a registered {@link ImsRcsManager.AvailabilityCallback} from the RcsFeature.
+ */
+ public void unregisterRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback) {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager != null) {
+ manager.unregisterRcsAvailabilityCallback(subId, callback);
+ }
+ }
+
+ /**
+ * Query for the specific capability.
+ */
+ public boolean isCapable(int capability, int radioTech)
+ throws android.telephony.ims.ImsException {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager == null) {
+ throw new ImsException("Service is not available",
+ ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+ }
+ return manager.isCapable(capability, radioTech);
+ }
+
+ /**
+ * Query the availability of an IMS RCS capability.
+ */
+ public boolean isAvailable(int capability) throws android.telephony.ims.ImsException {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager == null) {
+ throw new ImsException("Service is not available",
+ ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+ }
+ return manager.isAvailable(capability);
+ }
+
+ /**
+ * Get the IMS RCS registration technology for this Phone.
+ */
+ public void getRegistrationTech(Consumer<Integer> callback) {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager != null) {
+ manager.getImsRegistrationTech(callback);
+ }
+ callback.accept(ImsRegistrationImplBase.REGISTRATION_TECH_NONE);
+ }
+
+ /**
+ * Retrieve the current RCS registration state.
+ */
+ public void getRegistrationState(Consumer<Integer> callback) {
+ callback.accept(mImsRcsRegistrationHelper.getImsRegistrationState());
+ }
+
+ private void setupConnectionToService(RcsFeatureManager manager) throws ImsException {
+ // Open persistent listener connection, sends RcsFeature#onFeatureReady.
+ manager.openConnection();
+ manager.updateCapabilities();
+ manager.registerImsRegistrationCallback(mImsRcsRegistrationHelper.getCallbackBinder());
+ }
+
+ private void removeConnectionToService() {
+ RcsFeatureManager manager = getFeatureManager();
+ if (manager != null) {
+ manager.unregisterImsRegistrationCallback(
+ mImsRcsRegistrationHelper.getCallbackBinder());
+ // Remove persistent listener connection.
+ manager.releaseConnection();
+ }
+ mImsRcsRegistrationHelper.reset();
+ }
+
+ private void updateConnectionStatus(RcsFeatureManager manager) {
+ synchronized (mLock) {
+ mFeatureManager = manager;
+ if (mFeatureManager != null) {
+ for (Feature c : mFeatures.values()) {
+ c.onRcsConnected(manager);
+ }
+ } else {
+ for (Feature c : mFeatures.values()) {
+ c.onRcsDisconnected();
+ }
+ }
+ }
+ }
+
+ private RcsFeatureManager getFeatureManager() {
+ synchronized (mLock) {
+ return mFeatureManager;
+ }
+ }
+
+ /**
+ * Dump this controller's instance information for usage in dumpsys.
+ */
+ public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.print("slotId=");
+ pw.println(mSlotId);
+ pw.print("RegistrationState=");
+ pw.println(mImsRcsRegistrationHelper.getImsRegistrationState());
+ pw.print("connected=");
+ synchronized (mLock) {
+ pw.println(mFeatureManager != null);
+ }
+ }
+}
diff --git a/src/com/android/services/telephony/rcs/TelephonyRcsService.java b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
index 0e8f8de..b4223d3 100644
--- a/src/com/android/services/telephony/rcs/TelephonyRcsService.java
+++ b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
@@ -16,195 +16,225 @@
package com.android.services.telephony.rcs;
+import android.annotation.AnyThread;
+import android.content.BroadcastReceiver;
import android.content.Context;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.telephony.ims.ImsException;
-import android.telephony.ims.RcsContactUceCapability;
-import android.telephony.ims.RcsUceAdapter;
-import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
import android.util.Log;
-import com.android.ims.ResultCode;
-import com.android.service.ims.presence.ContactCapabilityResponse;
-import com.android.service.ims.presence.PresenceBase;
-import com.android.service.ims.presence.PresencePublication;
-import com.android.service.ims.presence.PresenceSubscriber;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.PhoneConfigurationManager;
+import com.android.internal.util.IndentingPrintWriter;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
/**
- * Telephony RCS Service integrates PresencePublication and PresenceSubscriber into the service.
+ * Singleton service setup to manage RCS related services that the platform provides such as User
+ * Capability Exchange.
*/
+@AnyThread
public class TelephonyRcsService {
private static final String LOG_TAG = "TelephonyRcsService";
+ /**
+ * Used to inject RcsFeatureController and UserCapabilityExchangeImpl instances for testing.
+ */
+ @VisibleForTesting
+ public interface FeatureFactory {
+ /**
+ * @return an {@link RcsFeatureController} assoicated with the slot specified.
+ */
+ RcsFeatureController createController(Context context, int slotId);
+
+ /**
+ * @return an instance of {@link UserCapabilityExchangeImpl} associated with the slot
+ * specified.
+ */
+ UserCapabilityExchangeImpl createUserCapabilityExchange(Context context, int slotId,
+ int subId);
+ }
+
+ private FeatureFactory mFeatureFactory = new FeatureFactory() {
+ @Override
+ public RcsFeatureController createController(Context context, int slotId) {
+ return new RcsFeatureController(context, slotId);
+ }
+
+ @Override
+ public UserCapabilityExchangeImpl createUserCapabilityExchange(Context context, int slotId,
+ int subId) {
+ return new UserCapabilityExchangeImpl(context, slotId, subId);
+ }
+ };
+
+ // Notifies this service that there has been a change in available slots.
+ private static final int HANDLER_MSIM_CONFIGURATION_CHANGE = 1;
+
private final Context mContext;
+ private final Object mLock = new Object();
+ private int mNumSlots;
- // A helper class to manage the RCS Presences instances.
- private final PresenceHelper mPresenceHelper;
+ // Index corresponds to the slot ID.
+ private List<RcsFeatureController> mFeatureControllers;
- private ConcurrentHashMap<Integer, IRcsUceControllerCallback> mPendingRequests;
+ private BroadcastReceiver mCarrierConfigChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+ Bundle bundle = intent.getExtras();
+ if (bundle == null) {
+ return;
+ }
+ int slotId = bundle.getInt(CarrierConfigManager.EXTRA_SLOT_INDEX);
+ int subId = bundle.getInt(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX);
+ updateFeatureControllerSubscription(slotId, subId);
+ }
+ }
+ };
- public TelephonyRcsService(Context context) {
+ private Handler mHandler = new Handler(Looper.getMainLooper(), (msg) -> {
+ switch (msg.what) {
+ case HANDLER_MSIM_CONFIGURATION_CHANGE: {
+ AsyncResult result = (AsyncResult) msg.obj;
+ Integer numSlots = (Integer) result.result;
+ if (numSlots == null) {
+ Log.w(LOG_TAG, "msim config change with null num slots.");
+ break;
+ }
+ updateFeatureControllerSize(numSlots);
+ break;
+ }
+ default:
+ return false;
+ }
+ return true;
+ });
+
+ public TelephonyRcsService(Context context, int numSlots) {
Log.i(LOG_TAG, "initialize");
mContext = context;
- mPresenceHelper = new PresenceHelper(mContext);
- mPendingRequests = new ConcurrentHashMap<>();
+ mNumSlots = numSlots;
+ mFeatureControllers = new ArrayList<>(numSlots);
}
/**
- * @return the UCE Publish state for the phone ID specified.
+ * @return the {@link RcsFeatureController} associated with the given slot.
*/
- public int getUcePublishState(int phoneId) {
- PresencePublication publisher = getPresencePublication(phoneId);
- if (publisher == null) {
- throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
- "UCE service is not currently running.");
+ public RcsFeatureController getFeatureController(int slotId) {
+ synchronized (mLock) {
+ return mFeatureControllers.get(slotId);
}
- int publishState = publisher.getPublishState();
- return toUcePublishState(publishState);
}
/**
- * Perform a capabilities request and call {@link IRcsUceControllerCallback} with the result.
+ * Called after instance creation to initialize internal structures as well as register for
+ * system callbacks.
*/
- public void requestCapabilities(int phoneId, List<Uri> contactNumbers,
- IRcsUceControllerCallback c) {
- PresenceSubscriber subscriber = getPresenceSubscriber(phoneId);
- if (subscriber == null) {
- throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
- "UCE service is not currently running.");
+ public void initialize() {
+ synchronized (mLock) {
+ for (int i = 0; i < mNumSlots; i++) {
+ mFeatureControllers.add(constructFeatureController(i));
+ }
}
- List<String> numbers = contactNumbers.stream().map(TelephonyRcsService::getNumberFromUri)
- .collect(Collectors.toList());
- int taskId = subscriber.requestCapability(numbers, new ContactCapabilityResponse() {
- @Override
- public void onSuccess(int reqId) {
- Log.i(LOG_TAG, "onSuccess called for reqId:" + reqId);
- }
- @Override
- public void onError(int reqId, int resultCode) {
- IRcsUceControllerCallback c = mPendingRequests.remove(reqId);
- try {
- if (c != null) {
- c.onError(toUceError(resultCode));
- } else {
- Log.w(LOG_TAG, "onError called for unknown reqId:" + reqId);
- }
- } catch (RemoteException e) {
- Log.i(LOG_TAG, "Calling back to dead service");
- }
- }
+ PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler,
+ HANDLER_MSIM_CONFIGURATION_CHANGE, null);
+ mContext.registerReceiver(mCarrierConfigChangedReceiver, new IntentFilter(
+ CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+ }
- @Override
- public void onFinish(int reqId) {
- Log.i(LOG_TAG, "onFinish called for reqId:" + reqId);
- }
+ @VisibleForTesting
+ public void setFeatureFactory(FeatureFactory f) {
+ mFeatureFactory = f;
+ }
- @Override
- public void onTimeout(int reqId) {
- IRcsUceControllerCallback c = mPendingRequests.remove(reqId);
- try {
- if (c != null) {
- c.onError(RcsUceAdapter.ERROR_REQUEST_TIMEOUT);
- } else {
- Log.w(LOG_TAG, "onTimeout called for unknown reqId:" + reqId);
- }
- } catch (RemoteException e) {
- Log.i(LOG_TAG, "Calling back to dead service");
- }
- }
-
- @Override
- public void onCapabilitiesUpdated(int reqId,
- List<RcsContactUceCapability> contactCapabilities,
- boolean updateLastTimestamp) {
- IRcsUceControllerCallback c = mPendingRequests.remove(reqId);
- try {
- if (c != null) {
- c.onCapabilitiesReceived(contactCapabilities);
- } else {
- Log.w(LOG_TAG, "onCapabilitiesUpdated, unknown reqId:" + reqId);
- }
- } catch (RemoteException e) {
- Log.w(LOG_TAG, "onCapabilitiesUpdated on dead service");
- }
- }
- });
- if (taskId < 0) {
- try {
- c.onError(toUceError(taskId));
+ /**
+ * Update the number of {@link RcsFeatureController}s that are created based on the number of
+ * active slots on the device.
+ */
+ @VisibleForTesting
+ public void updateFeatureControllerSize(int newNumSlots) {
+ synchronized (mLock) {
+ int oldNumSlots = mFeatureControllers.size();
+ if (oldNumSlots == newNumSlots) {
return;
- } catch (RemoteException e) {
- Log.i(LOG_TAG, "Calling back to dead service");
+ }
+ mNumSlots = newNumSlots;
+ if (oldNumSlots < newNumSlots) {
+ for (int i = oldNumSlots; i < newNumSlots; i++) {
+ mFeatureControllers.add(constructFeatureController(i));
+ }
+ } else {
+ for (int i = (oldNumSlots - 1); i > (newNumSlots - 1); i--) {
+ RcsFeatureController controller = mFeatureControllers.remove(i);
+ controller.destroy();
+ }
}
}
- mPendingRequests.put(taskId, c);
}
- private PresencePublication getPresencePublication(int phoneId) {
- return mPresenceHelper.getPresencePublication(phoneId);
- }
-
- private PresenceSubscriber getPresenceSubscriber(int phoneId) {
- return mPresenceHelper.getPresenceSubscriber(phoneId);
- }
-
- private static String getNumberFromUri(Uri uri) {
- String number = uri.getSchemeSpecificPart();
- String[] numberParts = number.split("[@;:]");
-
- if (numberParts.length == 0) {
- return null;
- }
- return numberParts[0];
- }
-
- private static int toUcePublishState(int publishState) {
- switch (publishState) {
- case PresenceBase.PUBLISH_STATE_200_OK:
- return RcsUceAdapter.PUBLISH_STATE_200_OK;
- case PresenceBase.PUBLISH_STATE_NOT_PUBLISHED:
- return RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
- case PresenceBase.PUBLISH_STATE_VOLTE_PROVISION_ERROR:
- return RcsUceAdapter.PUBLISH_STATE_VOLTE_PROVISION_ERROR;
- case PresenceBase.PUBLISH_STATE_RCS_PROVISION_ERROR:
- return RcsUceAdapter.PUBLISH_STATE_RCS_PROVISION_ERROR;
- case PresenceBase.PUBLISH_STATE_REQUEST_TIMEOUT:
- return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
- case PresenceBase.PUBLISH_STATE_OTHER_ERROR:
- return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
- default:
- return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ private void updateFeatureControllerSubscription(int slotId, int newSubId) {
+ synchronized (mLock) {
+ RcsFeatureController f = mFeatureControllers.get(slotId);
+ if (f == null) {
+ Log.w(LOG_TAG, "unexpected null FeatureContainer for slot " + slotId);
+ return;
+ }
+ f.updateAssociatedSubscription(newSubId);
}
}
- private static int toUceError(int resultCode) {
- switch(resultCode) {
- case ResultCode.SUBSCRIBE_NOT_REGISTERED:
- return RcsUceAdapter.ERROR_NOT_REGISTERED;
- case ResultCode.SUBSCRIBE_REQUEST_TIMEOUT:
- return RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
- case ResultCode.SUBSCRIBE_FORBIDDEN:
- return RcsUceAdapter.ERROR_FORBIDDEN;
- case ResultCode.SUBSCRIBE_NOT_FOUND:
- return RcsUceAdapter.ERROR_NOT_FOUND;
- case ResultCode.SUBSCRIBE_TOO_LARGE:
- return RcsUceAdapter.ERROR_REQUEST_TOO_LARGE;
- case ResultCode.SUBSCRIBE_INSUFFICIENT_MEMORY:
- return RcsUceAdapter.ERROR_INSUFFICIENT_MEMORY;
- case ResultCode.SUBSCRIBE_LOST_NETWORK:
- return RcsUceAdapter.ERROR_LOST_NETWORK;
- case ResultCode.SUBSCRIBE_ALREADY_IN_QUEUE:
- return RcsUceAdapter.ERROR_ALREADY_IN_QUEUE;
- default:
- return RcsUceAdapter.ERROR_GENERIC_FAILURE;
+ private RcsFeatureController constructFeatureController(int slotId) {
+ RcsFeatureController c = mFeatureFactory.createController(mContext, slotId);
+ // TODO: integrate user setting into whether or not this feature is added as well as logic
+ // to listen for changes in user setting.
+ c.addFeature(mFeatureFactory.createUserCapabilityExchange(mContext, slotId,
+ getSubscriptionFromSlot(slotId)), UserCapabilityExchangeImpl.class);
+ c.connect();
+ return c;
+ }
+
+ private int getSubscriptionFromSlot(int slotId) {
+ SubscriptionManager manager = mContext.getSystemService(SubscriptionManager.class);
+ if (manager == null) {
+ Log.w(LOG_TAG, "Couldn't find SubscriptionManager for slotId=" + slotId);
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
+ int[] subIds = manager.getSubscriptionIds(slotId);
+ if (subIds != null && subIds.length > 0) {
+ return subIds[0];
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
+ /**
+ * Dump this instance into a readable format for dumpsys usage.
+ */
+ public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("RcsFeatureControllers:");
+ pw.increaseIndent();
+ synchronized (mLock) {
+ for (RcsFeatureController f : mFeatureControllers) {
+ pw.increaseIndent();
+ f.dump(fd, printWriter, args);
+ pw.decreaseIndent();
+ }
+ }
+ pw.decreaseIndent();
}
}
diff --git a/src/com/android/services/telephony/rcs/UserCapabilityExchangeImpl.java b/src/com/android/services/telephony/rcs/UserCapabilityExchangeImpl.java
new file mode 100644
index 0000000..7521205
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/UserCapabilityExchangeImpl.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.rcs;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.ResultCode;
+import com.android.phone.R;
+import com.android.service.ims.presence.ContactCapabilityResponse;
+import com.android.service.ims.presence.PresenceBase;
+import com.android.service.ims.presence.PresencePublication;
+import com.android.service.ims.presence.PresencePublisher;
+import com.android.service.ims.presence.PresenceSubscriber;
+import com.android.service.ims.presence.SubscribePublisher;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * Implements User Capability Exchange using Presence.
+ */
+public class UserCapabilityExchangeImpl implements RcsFeatureController.Feature, SubscribePublisher,
+ PresencePublisher {
+
+ private static final String LOG_TAG = "UserCapabilityExchangeImpl";
+
+ private int mSlotId;
+ private int mSubId;
+
+ private final PresencePublication mPresencePublication;
+ private final PresenceSubscriber mPresenceSubscriber;
+
+ private final ConcurrentHashMap<Integer, IRcsUceControllerCallback> mPendingCapabilityRequests =
+ new ConcurrentHashMap<>();
+
+ UserCapabilityExchangeImpl(Context context, int slotId, int subId) {
+ mSlotId = slotId;
+ mSubId = subId;
+
+ String[] volteError = context.getResources().getStringArray(
+ R.array.config_volte_provision_error_on_publish_response);
+ String[] rcsError = context.getResources().getStringArray(
+ R.array.config_rcs_provision_error_on_publish_response);
+
+ // Initialize PresencePublication
+ mPresencePublication = new PresencePublication(null /*PresencePublisher*/, context,
+ volteError, rcsError);
+ // Initialize PresenceSubscriber
+ mPresenceSubscriber = new PresenceSubscriber(null /*SubscribePublisher*/, context,
+ volteError, rcsError);
+
+ onAssociatedSubscriptionUpdated(mSubId);
+ }
+
+
+ // Runs on main thread.
+ @Override
+ public void onRcsConnected(RcsFeatureManager rcsFeatureManager) {
+ Log.i(LOG_TAG, "onRcsConnected: slotId=" + mSlotId + ", subId=" + mSubId);
+ mPresencePublication.updatePresencePublisher(this);
+ mPresenceSubscriber.updatePresenceSubscriber(this);
+ }
+
+ // Runs on main thread.
+ @Override
+ public void onRcsDisconnected() {
+ Log.i(LOG_TAG, "onRcsDisconnected: phoneId=" + mSlotId + ", subId=" + mSubId);
+ mPresencePublication.removePresencePublisher();
+ mPresenceSubscriber.removePresenceSubscriber();
+ }
+
+ // Runs on main thread.
+ @Override
+ public void onAssociatedSubscriptionUpdated(int subId) {
+ mPresencePublication.handleAssociatedSubscriptionChanged(subId);
+ mPresenceSubscriber.handleAssociatedSubscriptionChanged(subId);
+ }
+
+ /**
+ * Should be called before destroying this instance.
+ * This instance is not usable after this method is called.
+ */
+ // Called on main thread.
+ public void onDestroy() {
+ onRcsDisconnected();
+ }
+
+ /**
+ * @return the UCE Publish state.
+ */
+ // May happen on a Binder thread, PresencePublication locks to get result.
+ public int getUcePublishState() {
+ int publishState = mPresencePublication.getPublishState();
+ return toUcePublishState(publishState);
+ }
+
+ /**
+ * Perform a capabilities request and call {@link IRcsUceControllerCallback} with the result.
+ */
+ // May happen on a Binder thread, PresenceSubscriber locks when requesting Capabilities.
+ public void requestCapabilities(List<Uri> contactNumbers, IRcsUceControllerCallback c) {
+ List<String> numbers = contactNumbers.stream()
+ .map(UserCapabilityExchangeImpl::getNumberFromUri).collect(Collectors.toList());
+ int taskId = mPresenceSubscriber.requestCapability(numbers,
+ new ContactCapabilityResponse() {
+ @Override
+ public void onSuccess(int reqId) {
+ Log.i(LOG_TAG, "onSuccess called for reqId:" + reqId);
+ }
+
+ @Override
+ public void onError(int reqId, int resultCode) {
+ IRcsUceControllerCallback c = mPendingCapabilityRequests.remove(reqId);
+ try {
+ if (c != null) {
+ c.onError(toUceError(resultCode));
+ } else {
+ Log.w(LOG_TAG, "onError called for unknown reqId:" + reqId);
+ }
+ } catch (RemoteException e) {
+ Log.i(LOG_TAG, "Calling back to dead service");
+ }
+ }
+
+ @Override
+ public void onFinish(int reqId) {
+ Log.i(LOG_TAG, "onFinish called for reqId:" + reqId);
+ }
+
+ @Override
+ public void onTimeout(int reqId) {
+ IRcsUceControllerCallback c = mPendingCapabilityRequests.remove(reqId);
+ try {
+ if (c != null) {
+ c.onError(RcsUceAdapter.ERROR_REQUEST_TIMEOUT);
+ } else {
+ Log.w(LOG_TAG, "onTimeout called for unknown reqId:" + reqId);
+ }
+ } catch (RemoteException e) {
+ Log.i(LOG_TAG, "Calling back to dead service");
+ }
+ }
+
+ @Override
+ public void onCapabilitiesUpdated(int reqId,
+ List<RcsContactUceCapability> contactCapabilities,
+ boolean updateLastTimestamp) {
+ IRcsUceControllerCallback c = mPendingCapabilityRequests.remove(reqId);
+ try {
+ if (c != null) {
+ c.onCapabilitiesReceived(contactCapabilities);
+ } else {
+ Log.w(LOG_TAG, "onCapabilitiesUpdated, unknown reqId:" + reqId);
+ }
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, "onCapabilitiesUpdated on dead service");
+ }
+ }
+ });
+ if (taskId < 0) {
+ try {
+ c.onError(toUceError(taskId));
+ return;
+ } catch (RemoteException e) {
+ Log.i(LOG_TAG, "Calling back to dead service");
+ }
+ }
+ mPendingCapabilityRequests.put(taskId, c);
+ }
+
+ @Override
+ public int getPublisherState() {
+ return 0;
+ }
+
+ @Override
+ public int requestPublication(RcsContactUceCapability capabilities, String contactUri,
+ int taskId) {
+ return 0;
+ }
+
+ @Override
+ public int requestCapability(String[] formatedContacts, int taskId) {
+ return 0;
+ }
+
+ @Override
+ public int requestAvailability(String formattedContact, int taskId) {
+ return 0;
+ }
+
+ @Override
+ public int getStackStatusForCapabilityRequest() {
+ return 0;
+ }
+
+ @Override
+ public void updatePublisherState(int publishState) {
+
+ }
+
+ private static String getNumberFromUri(Uri uri) {
+ String number = uri.getSchemeSpecificPart();
+ String[] numberParts = number.split("[@;:]");
+
+ if (numberParts.length == 0) {
+ return null;
+ }
+ return numberParts[0];
+ }
+
+ private static int toUcePublishState(int publishState) {
+ switch (publishState) {
+ case PresenceBase.PUBLISH_STATE_200_OK:
+ return RcsUceAdapter.PUBLISH_STATE_200_OK;
+ case PresenceBase.PUBLISH_STATE_NOT_PUBLISHED:
+ return RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
+ case PresenceBase.PUBLISH_STATE_VOLTE_PROVISION_ERROR:
+ return RcsUceAdapter.PUBLISH_STATE_VOLTE_PROVISION_ERROR;
+ case PresenceBase.PUBLISH_STATE_RCS_PROVISION_ERROR:
+ return RcsUceAdapter.PUBLISH_STATE_RCS_PROVISION_ERROR;
+ case PresenceBase.PUBLISH_STATE_REQUEST_TIMEOUT:
+ return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
+ case PresenceBase.PUBLISH_STATE_OTHER_ERROR:
+ return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ default:
+ return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ }
+ }
+
+ private static int toUceError(int resultCode) {
+ switch (resultCode) {
+ case ResultCode.SUBSCRIBE_NOT_REGISTERED:
+ return RcsUceAdapter.ERROR_NOT_REGISTERED;
+ case ResultCode.SUBSCRIBE_REQUEST_TIMEOUT:
+ return RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
+ case ResultCode.SUBSCRIBE_FORBIDDEN:
+ return RcsUceAdapter.ERROR_FORBIDDEN;
+ case ResultCode.SUBSCRIBE_NOT_FOUND:
+ return RcsUceAdapter.ERROR_NOT_FOUND;
+ case ResultCode.SUBSCRIBE_TOO_LARGE:
+ return RcsUceAdapter.ERROR_REQUEST_TOO_LARGE;
+ case ResultCode.SUBSCRIBE_INSUFFICIENT_MEMORY:
+ return RcsUceAdapter.ERROR_INSUFFICIENT_MEMORY;
+ case ResultCode.SUBSCRIBE_LOST_NETWORK:
+ return RcsUceAdapter.ERROR_LOST_NETWORK;
+ case ResultCode.SUBSCRIBE_ALREADY_IN_QUEUE:
+ return RcsUceAdapter.ERROR_ALREADY_IN_QUEUE;
+ default:
+ return RcsUceAdapter.ERROR_GENERIC_FAILURE;
+ }
+ }
+}
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
index 01267d8..86c5402 100644
--- a/tests/src/com/android/TelephonyTestBase.java
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -16,6 +16,8 @@
package com.android;
+import static org.mockito.Mockito.spy;
+
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
@@ -34,7 +36,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mContext = new TestContext();
+ mContext = spy(new TestContext());
// Set up the looper if it does not exist on the test thread.
if (Looper.myLooper() == null) {
Looper.prepare();
diff --git a/tests/src/com/android/TestContext.java b/tests/src/com/android/TestContext.java
index 776ec6a..c190be9 100644
--- a/tests/src/com/android/TestContext.java
+++ b/tests/src/com/android/TestContext.java
@@ -34,6 +34,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.Executor;
+
public class TestContext extends MockContext {
@Mock CarrierConfigManager mMockCarrierConfigManager;
@@ -49,6 +51,12 @@
}
@Override
+ public Executor getMainExecutor() {
+ // Just run on current thread
+ return Runnable::run;
+ }
+
+ @Override
public Context getApplicationContext() {
return this;
}
diff --git a/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java b/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java
new file mode 100644
index 0000000..cfede94
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/RcsFeatureControllerTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.rcs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.telephony.AccessNetworkConstants;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IImsRegistrationCallback;
+import android.telephony.ims.feature.RcsFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.ims.FeatureConnector;
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.telephony.imsphone.ImsRegistrationCallbackHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.concurrent.Executor;
+
+@RunWith(AndroidJUnit4.class)
+public class RcsFeatureControllerTest extends TelephonyTestBase {
+
+ private static final ImsReasonInfo REASON_DISCONNECTED = new ImsReasonInfo(
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN, 0, "test");
+
+ @Mock RcsFeatureManager mFeatureManager;
+ @Mock RcsFeatureController.FeatureConnectorFactory<RcsFeatureManager> mFeatureFactory;
+ @Mock ImsRegistrationCallbackHelper.ImsRegistrationUpdate mRegistrationCallback;
+ @Mock FeatureConnector<RcsFeatureManager> mFeatureConnector;
+ @Mock RcsFeatureController.Feature mMockFeature;
+ @Captor ArgumentCaptor<FeatureConnector.Listener<RcsFeatureManager>> mConnectorListener;
+
+ private RcsFeatureController.RegistrationHelperFactory mRegistrationFactory =
+ new RcsFeatureController.RegistrationHelperFactory() {
+ @Override
+ public ImsRegistrationCallbackHelper create(
+ ImsRegistrationCallbackHelper.ImsRegistrationUpdate cb, Executor executor) {
+ // Run on current thread for testing.
+ return new ImsRegistrationCallbackHelper(mRegistrationCallback, Runnable::run);
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testRcsFeatureManagerConnectDisconnect() throws Exception {
+ RcsFeatureController controller = createFeatureController();
+ controller.addFeature(mMockFeature, RcsFeatureController.Feature.class);
+ verify(mMockFeature).onRcsDisconnected();
+ // Connect the RcsFeatureManager
+ mConnectorListener.getValue().connectionReady(mFeatureManager);
+
+ verify(mFeatureManager).updateCapabilities();
+ verify(mFeatureManager).registerImsRegistrationCallback(any());
+ verify(mMockFeature).onRcsConnected(mFeatureManager);
+
+ // Disconnect
+ mConnectorListener.getValue().connectionUnavailable();
+
+ verify(mFeatureManager).unregisterImsRegistrationCallback(any());
+ verify(mMockFeature, times(2)).onRcsDisconnected();
+ }
+
+ @Test
+ public void testFeatureManagerConnectedAddFeature() throws Exception {
+ RcsFeatureController controller = createFeatureController();
+ // Connect the RcsFeatureManager
+ mConnectorListener.getValue().connectionReady(mFeatureManager);
+ controller.addFeature(mMockFeature, RcsFeatureController.Feature.class);
+
+ verify(mMockFeature).onRcsConnected(mFeatureManager);
+ assertEquals(mMockFeature, controller.getFeature(RcsFeatureController.Feature.class));
+ }
+
+ @Test
+ public void testFeatureManagerConnectedRegister() throws Exception {
+ RcsFeatureController controller = createFeatureController();
+ IImsRegistrationCallback regCb = mock(IImsRegistrationCallback.class);
+ IImsCapabilityCallback capCb = mock(IImsCapabilityCallback.class);
+ // Connect the RcsFeatureManager
+ mConnectorListener.getValue().connectionReady(mFeatureManager);
+
+ try {
+ controller.registerImsRegistrationCallback(0 /*subId*/, regCb);
+ controller.registerRcsAvailabilityCallback(0 /*subId*/, capCb);
+ controller.isCapable(RcsFeature.RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ controller.isAvailable(RcsFeature.RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE);
+ controller.getRegistrationTech(integer -> {
+ });
+ verify(mFeatureManager).registerImsRegistrationCallback(0, regCb);
+ verify(mFeatureManager).registerRcsAvailabilityCallback(0, capCb);
+ verify(mFeatureManager).isCapable(
+ RcsFeature.RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ verify(mFeatureManager).isAvailable(
+ RcsFeature.RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE);
+ verify(mFeatureManager).getImsRegistrationTech(any());
+ } catch (ImsException e) {
+ fail("ImsException not expected.");
+ }
+
+ controller.unregisterImsRegistrationCallback(0, regCb);
+ controller.unregisterRcsAvailabilityCallback(0, capCb);
+ verify(mFeatureManager).unregisterImsRegistrationCallback(0, regCb);
+ verify(mFeatureManager).unregisterRcsAvailabilityCallback(0, capCb);
+ }
+
+ @Test
+ public void testFeatureManagerConnectedHelper() throws Exception {
+ RcsFeatureController controller = createFeatureController();
+ // Connect the RcsFeatureManager
+ mConnectorListener.getValue().connectionReady(mFeatureManager);
+ ArgumentCaptor<IImsRegistrationCallback> captor =
+ ArgumentCaptor.forClass(IImsRegistrationCallback.class);
+ verify(mFeatureManager).registerImsRegistrationCallback(captor.capture());
+ assertNotNull(captor.getValue());
+
+ captor.getValue().onDeregistered(REASON_DISCONNECTED);
+ controller.getRegistrationState(result -> {
+ assertNotNull(result);
+ assertEquals(RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED, result.intValue());
+ });
+ verify(mRegistrationCallback).handleImsUnregistered(REASON_DISCONNECTED);
+
+ captor.getValue().onRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ controller.getRegistrationState(result -> {
+ assertNotNull(result);
+ assertEquals(RegistrationManager.REGISTRATION_STATE_REGISTERING, result.intValue());
+ });
+ verify(mRegistrationCallback).handleImsRegistering(
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+
+ captor.getValue().onRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ controller.getRegistrationState(result -> {
+ assertNotNull(result);
+ assertEquals(RegistrationManager.REGISTRATION_STATE_REGISTERED, result.intValue());
+ });
+ verify(mRegistrationCallback).handleImsRegistered(
+ AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+ }
+
+ @Test
+ public void testFeatureManagerDisconnectedAddFeature() {
+ RcsFeatureController controller = createFeatureController();
+ // Disconnect the RcsFeatureManager
+ mConnectorListener.getValue().connectionUnavailable();
+ controller.addFeature(mMockFeature, RcsFeatureController.Feature.class);
+
+ verify(mMockFeature).onRcsDisconnected();
+ }
+
+ @Test
+ public void testFeatureManagerDisconnectedException() {
+ RcsFeatureController controller = createFeatureController();
+ IImsRegistrationCallback regCb = mock(IImsRegistrationCallback.class);
+ IImsCapabilityCallback capCb = mock(IImsCapabilityCallback.class);
+ // Disconnect the RcsFeatureManager
+ mConnectorListener.getValue().connectionUnavailable();
+
+ try {
+ controller.registerImsRegistrationCallback(0 /*subId*/, null /*callback*/);
+ fail("ImsException expected for IMS registration.");
+ } catch (ImsException e) {
+ //expected
+ }
+ try {
+ controller.registerRcsAvailabilityCallback(0 /*subId*/, null /*callback*/);
+ fail("ImsException expected for availability");
+ } catch (ImsException e) {
+ //expected
+ }
+ try {
+ controller.isCapable(RcsFeature.RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ fail("ImsException expected for capability check");
+ } catch (ImsException e) {
+ //expected
+ }
+ try {
+ controller.isAvailable(RcsFeature.RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE);
+ fail("ImsException expected for availability check");
+ } catch (ImsException e) {
+ //expected
+ }
+ controller.getRegistrationTech(integer -> {
+ assertNotNull(integer);
+ assertEquals(ImsRegistrationImplBase.REGISTRATION_TECH_NONE, integer.intValue());
+ });
+ controller.unregisterImsRegistrationCallback(0, regCb);
+ controller.unregisterRcsAvailabilityCallback(0, capCb);
+ verify(mFeatureManager, never()).unregisterImsRegistrationCallback(0, regCb);
+ verify(mFeatureManager, never()).unregisterRcsAvailabilityCallback(0, capCb);
+ }
+
+ @Test
+ public void testChangeSubId() throws Exception {
+ RcsFeatureController controller = createFeatureController();
+ // Connect the RcsFeatureManager
+ mConnectorListener.getValue().connectionReady(mFeatureManager);
+ verify(mFeatureManager).updateCapabilities();
+ controller.addFeature(mMockFeature, RcsFeatureController.Feature.class);
+
+ controller.updateAssociatedSubscription(1 /*new sub id*/);
+
+ verify(mFeatureManager, times(2)).updateCapabilities();
+ verify(mMockFeature).onAssociatedSubscriptionUpdated(1 /*new sub id*/);
+ }
+
+ @Test
+ public void testDestroy() throws Exception {
+ RcsFeatureController controller = createFeatureController();
+ // Connect the RcsFeatureManager
+ mConnectorListener.getValue().connectionReady(mFeatureManager);
+ controller.addFeature(mMockFeature, RcsFeatureController.Feature.class);
+ controller.destroy();
+
+ verify(mFeatureConnector).disconnect();
+ verify(mMockFeature).onRcsDisconnected();
+ verify(mMockFeature).onDestroy();
+ assertNull(controller.getFeature(RcsFeatureController.Feature.class));
+ }
+
+ private RcsFeatureController createFeatureController() {
+ RcsFeatureController controller = new RcsFeatureController(mContext, 0 /*slotId*/,
+ mRegistrationFactory);
+ controller.setFeatureConnectorFactory(mFeatureFactory);
+ doReturn(mFeatureConnector).when(mFeatureFactory).create(any(), anyInt(),
+ mConnectorListener.capture(), any(), any());
+ controller.connect();
+ assertNotNull(mConnectorListener.getValue());
+ return controller;
+ }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java b/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
new file mode 100644
index 0000000..68b08a7
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/TelephonyRcsServiceTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.rcs;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.telephony.CarrierConfigManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class TelephonyRcsServiceTest extends TelephonyTestBase {
+
+ @Captor ArgumentCaptor<BroadcastReceiver> mReceiverCaptor;
+ @Mock TelephonyRcsService.FeatureFactory mFeatureFactory;
+ @Mock RcsFeatureController mFeatureControllerSlot0;
+ @Mock RcsFeatureController mFeatureControllerSlot1;
+ @Mock UserCapabilityExchangeImpl mMockUceSlot0;
+ @Mock UserCapabilityExchangeImpl mMockUceSlot1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mFeatureControllerSlot0).when(mFeatureFactory).createController(any(), eq(0));
+ doReturn(mFeatureControllerSlot1).when(mFeatureFactory).createController(any(), eq(1));
+ doReturn(mMockUceSlot0).when(mFeatureFactory).createUserCapabilityExchange(any(), eq(0),
+ anyInt());
+ doReturn(mMockUceSlot1).when(mFeatureFactory).createUserCapabilityExchange(any(), eq(1),
+ anyInt());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testUserCapabilityExchangeConnected() {
+ createRcsService(1 /*numSlots*/);
+ verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot0).connect();
+ }
+
+ @Test
+ public void testSlotUpdates() {
+ TelephonyRcsService service = createRcsService(1 /*numSlots*/);
+ verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot0).connect();
+
+ // there should be no changes if the new num slots = old num
+ service.updateFeatureControllerSize(1 /*newNumSlots*/);
+ verify(mFeatureControllerSlot0, times(1)).addFeature(mMockUceSlot0,
+ UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot0, times(1)).connect();
+
+ // Add a new slot.
+ verify(mFeatureControllerSlot1, never()).addFeature(mMockUceSlot1,
+ UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot1, never()).connect();
+ service.updateFeatureControllerSize(2 /*newNumSlots*/);
+ // This shouldn't have changed for slot 0.
+ verify(mFeatureControllerSlot0, times(1)).addFeature(mMockUceSlot0,
+ UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot0, times(1)).connect();
+ verify(mFeatureControllerSlot1, times(1)).addFeature(mMockUceSlot1,
+ UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot1, times(1)).connect();
+
+ // Remove a slot.
+ verify(mFeatureControllerSlot0, never()).destroy();
+ verify(mFeatureControllerSlot1, never()).destroy();
+ service.updateFeatureControllerSize(1 /*newNumSlots*/);
+ // addFeature/connect shouldn't have been called again
+ verify(mFeatureControllerSlot0, times(1)).addFeature(mMockUceSlot0,
+ UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot0, times(1)).connect();
+ verify(mFeatureControllerSlot1, times(1)).addFeature(mMockUceSlot1,
+ UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot1, times(1)).connect();
+ // Verify destroy is only called for slot 1.
+ verify(mFeatureControllerSlot0, never()).destroy();
+ verify(mFeatureControllerSlot1, times(1)).destroy();
+ }
+
+ @Test
+ public void testCarrierConfigUpdate() {
+ createRcsService(2 /*numSlots*/);
+ verify(mFeatureControllerSlot0).addFeature(mMockUceSlot0, UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot1).addFeature(mMockUceSlot1, UserCapabilityExchangeImpl.class);
+ verify(mFeatureControllerSlot0).connect();
+ verify(mFeatureControllerSlot1).connect();
+
+
+ // Send carrier config update for each slot.
+ sendCarrierConfigChanged(0 /*slotId*/, 1 /*subId*/);
+ verify(mFeatureControllerSlot0).updateAssociatedSubscription(1);
+ verify(mFeatureControllerSlot1, never()).updateAssociatedSubscription(1);
+ sendCarrierConfigChanged(1 /*slotId*/, 2 /*subId*/);
+ verify(mFeatureControllerSlot0, never()).updateAssociatedSubscription(2);
+ verify(mFeatureControllerSlot1, times(1)).updateAssociatedSubscription(2);
+ }
+
+ private void sendCarrierConfigChanged(int slotId, int subId) {
+ Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+ intent.putExtra(CarrierConfigManager.EXTRA_SLOT_INDEX, slotId);
+ intent.putExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, subId);
+ mReceiverCaptor.getValue().onReceive(mContext, intent);
+ }
+
+ private TelephonyRcsService createRcsService(int numSlots) {
+ TelephonyRcsService service = new TelephonyRcsService(mContext, numSlots);
+ service.setFeatureFactory(mFeatureFactory);
+ service.initialize();
+ verify(mContext).registerReceiver(mReceiverCaptor.capture(), any());
+ return service;
+ }
+}