Merge "[Settings] Implement APIs in PhoneInterfaceManager"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 98d1919..a1949dd 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -129,7 +129,7 @@
     <uses-permission android:name="android.permission.SEND_SMS" />
     <uses-permission android:name="android.permission.SEND_RESPOND_VIA_MESSAGE" />
     <uses-permission android:name="android.permission.SET_TIME_ZONE" />
-    <uses-permission android:name="android.permission.SUGGEST_PHONE_TIME_AND_ZONE" />
+    <uses-permission android:name="android.permission.SUGGEST_TELEPHONY_TIME_AND_ZONE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 8d84baf..fca8acf 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -15,21 +15,6 @@
 -->
 
 <resources>
-    <!-- Base attributes available to CheckBoxPreference. Copied from frameworks/base/core/res. -->
-    <declare-styleable name="CheckBoxPreference">
-        <!-- The summary for the Preference in a PreferenceActivity screen when the
-             CheckBoxPreference is checked. If separate on/off summaries are not
-             needed, the summary attribute can be used instead. -->
-        <attr name="android:summaryOn" />
-        <!-- The summary for the Preference in a PreferenceActivity screen when the
-             CheckBoxPreference is unchecked. If separate on/off summaries are not
-             needed, the summary attribute can be used instead. -->
-        <attr name="android:summaryOff" />
-        <!-- The state (true for on, or false for off) that causes dependents to be disabled. By default,
-             dependents will be disabled when this is unchecked, so the value of this preference is false. -->
-        <attr name="android:disableDependentsState" />
-    </declare-styleable>
-
     <declare-styleable name="EditPhoneNumberPreference">
         <!-- The enable button text. -->
         <attr name="enableButtonText" format="string" />
diff --git a/sip/src/com/android/services/telephony/sip/SipSettings.java b/sip/src/com/android/services/telephony/sip/SipSettings.java
index 700fe81..813ba51 100644
--- a/sip/src/com/android/services/telephony/sip/SipSettings.java
+++ b/sip/src/com/android/services/telephony/sip/SipSettings.java
@@ -241,7 +241,7 @@
     private void processActiveProfilesFromSipService() {
         List<SipProfile> activeList = new ArrayList<>();
         try {
-            activeList = mSipManager.getListOfProfiles();
+            activeList = mSipManager.getProfiles();
         } catch (SipException e) {
             log("SipManager could not retrieve SIP profiles: " + e);
         }
diff --git a/src/com/android/phone/EditPhoneNumberPreference.java b/src/com/android/phone/EditPhoneNumberPreference.java
index 35af20d..74b8a45 100644
--- a/src/com/android/phone/EditPhoneNumberPreference.java
+++ b/src/com/android/phone/EditPhoneNumberPreference.java
@@ -136,9 +136,9 @@
         a.recycle();
 
         //get the summary settings, use CheckBoxPreference as the standard.
-        a = context.obtainStyledAttributes(attrs, R.styleable.CheckBoxPreference, 0, 0);
-        mSummaryOn = a.getString(R.styleable.CheckBoxPreference_summaryOn);
-        mSummaryOff = a.getString(R.styleable.CheckBoxPreference_summaryOff);
+        a = context.obtainStyledAttributes(attrs, android.R.styleable.CheckBoxPreference, 0, 0);
+        mSummaryOn = a.getString(android.R.styleable.CheckBoxPreference_summaryOn);
+        mSummaryOff = a.getString(android.R.styleable.CheckBoxPreference_summaryOff);
         a.recycle();
     }
 
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/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 2191bfc..4eb1788 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -5645,24 +5645,15 @@
     }
 
     /**
-     * Get whether mobile data is enabled.
+     * Checks if the device is capable of mobile data by considering whether whether the
+     * user has enabled mobile data, whether the carrier has enabled mobile data, and
+     * whether the network policy allows data connections.
      *
-     * Comparable to {@link #isUserDataEnabled(int)}, this considers all factors deciding
-     * whether mobile data is actually enabled.
-     *
-     * Accepts either ACCESS_NETWORK_STATE, MODIFY_PHONE_STATE or carrier privileges.
-     *
-     * @return {@code true} if data is enabled else {@code false}
+     * @return {@code true} if the overall data connection is capable; {@code false} if not.
      */
     @Override
     public boolean isDataEnabled(int subId) {
-        try {
-            mApp.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_NETWORK_STATE,
-                    null);
-        } catch (Exception e) {
-            TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
-                    mApp, subId, "isDataEnabled");
-        }
+        enforceReadPrivilegedPermission("isDataEnabled");
 
         final long identity = Binder.clearCallingIdentity();
         try {
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/testapps/Android.mk b/testapps/Android.mk
deleted file mode 100644
index 5053e7d..0000000
--- a/testapps/Android.mk
+++ /dev/null
@@ -1 +0,0 @@
-include $(call all-subdir-makefiles)
diff --git a/testapps/EmbmsServiceTestApp/Android.bp b/testapps/EmbmsServiceTestApp/Android.bp
new file mode 100644
index 0000000..e4a54cb
--- /dev/null
+++ b/testapps/EmbmsServiceTestApp/Android.bp
@@ -0,0 +1,11 @@
+// Build the Sample Embms Services
+android_app {
+    name: "EmbmsTestService",
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+    privileged: true,
+    // Uncomment the following line to build the EmbmsTestService
+    // into the userdebug build:
+    // LOCAL_MODULE_TAGS := debug
+}
diff --git a/testapps/EmbmsServiceTestApp/Android.mk b/testapps/EmbmsServiceTestApp/Android.mk
deleted file mode 100644
index 29b8112..0000000
--- a/testapps/EmbmsServiceTestApp/Android.mk
+++ /dev/null
@@ -1,20 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-# Build the Sample Embms Services
-include $(CLEAR_VARS)
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_PACKAGE_NAME := EmbmsTestService
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-LOCAL_PRIVILEGED_MODULE := true
-# Uncomment the following line to build the EmbmsTestService into the userdebug build.
-# LOCAL_MODULE_TAGS := debug
-
-include $(BUILD_PACKAGE)
diff --git a/testapps/EmbmsTestDownloadApp/Android.bp b/testapps/EmbmsTestDownloadApp/Android.bp
new file mode 100644
index 0000000..63f4e83
--- /dev/null
+++ b/testapps/EmbmsTestDownloadApp/Android.bp
@@ -0,0 +1,12 @@
+src_dirs = ["src"]
+res_dirs = ["res"]
+android_test {
+    name: "EmbmsTestDownloadApp",
+    static_libs: [
+        "androidx.recyclerview_recyclerview",
+        "androidx.legacy_legacy-support-v4",
+    ],
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+}
diff --git a/testapps/EmbmsTestDownloadApp/Android.mk b/testapps/EmbmsTestDownloadApp/Android.mk
deleted file mode 100644
index bd53d79..0000000
--- a/testapps/EmbmsTestDownloadApp/Android.mk
+++ /dev/null
@@ -1,22 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-# Build the Sample Embms Download frontend
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-        androidx.recyclerview_recyclerview \
-        androidx.legacy_legacy-support-v4
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_PACKAGE_NAME := EmbmsTestDownloadApp
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-LOCAL_MODULE_TAGS := tests
-
-include $(BUILD_PACKAGE)
diff --git a/testapps/EmbmsTestStreamingApp/Android.bp b/testapps/EmbmsTestStreamingApp/Android.bp
new file mode 100644
index 0000000..814c5ca
--- /dev/null
+++ b/testapps/EmbmsTestStreamingApp/Android.bp
@@ -0,0 +1,7 @@
+android_test {
+    name: "EmbmsTestStreamingApp",
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+    //LOCAL_MODULE_TAGS := debug
+}
diff --git a/testapps/EmbmsTestStreamingApp/Android.mk b/testapps/EmbmsTestStreamingApp/Android.mk
deleted file mode 100644
index f574990..0000000
--- a/testapps/EmbmsTestStreamingApp/Android.mk
+++ /dev/null
@@ -1,19 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-# Build the Sample Embms Streaming frontend
-include $(CLEAR_VARS)
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_PACKAGE_NAME := EmbmsTestStreamingApp
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-LOCAL_MODULE_TAGS := tests
-#LOCAL_MODULE_TAGS := debug
-
-include $(BUILD_PACKAGE)
diff --git a/testapps/ImsTestService/Android.bp b/testapps/ImsTestService/Android.bp
new file mode 100644
index 0000000..a0b4edb
--- /dev/null
+++ b/testapps/ImsTestService/Android.bp
@@ -0,0 +1,13 @@
+android_app {
+    name: "ImsTestApp",
+    static_libs: [
+        "androidx.legacy_legacy-support-v4",
+        "androidx.appcompat_appcompat",
+        "androidx.recyclerview_recyclerview",
+        "androidx.cardview_cardview",
+    ],
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+    privileged: true,
+}
diff --git a/testapps/ImsTestService/Android.mk b/testapps/ImsTestService/Android.mk
deleted file mode 100644
index 2869c86..0000000
--- a/testapps/ImsTestService/Android.mk
+++ /dev/null
@@ -1,27 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    androidx.legacy_legacy-support-v4 \
-    androidx.appcompat_appcompat \
-    androidx.recyclerview_recyclerview \
-    androidx.cardview_cardview
-
-LOCAL_USE_AAPT2 := true
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_PACKAGE_NAME := ImsTestApp
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-LOCAL_PRIVILEGED_MODULE := true
-
-include $(BUILD_PACKAGE)
diff --git a/testapps/SmsManagerTestApp/Android.bp b/testapps/SmsManagerTestApp/Android.bp
new file mode 100644
index 0000000..5333eab
--- /dev/null
+++ b/testapps/SmsManagerTestApp/Android.bp
@@ -0,0 +1,5 @@
+android_app {
+    name: "SmsManagerTestApp",
+    srcs: ["src/**/*.java"],
+    sdk_version: "current",
+}
diff --git a/testapps/SmsManagerTestApp/Android.mk b/testapps/SmsManagerTestApp/Android.mk
deleted file mode 100644
index 307366b..0000000
--- a/testapps/SmsManagerTestApp/Android.mk
+++ /dev/null
@@ -1,17 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := optional
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_PACKAGE_NAME := SmsManagerTestApp
-
-LOCAL_SDK_VERSION := current
-
-include $(BUILD_PACKAGE)
diff --git a/testapps/TelephonyManagerTestApp/Android.bp b/testapps/TelephonyManagerTestApp/Android.bp
new file mode 100644
index 0000000..8a37c99
--- /dev/null
+++ b/testapps/TelephonyManagerTestApp/Android.bp
@@ -0,0 +1,7 @@
+android_test {
+    name: "TelephonyManagerTestApp",
+    srcs: ["src/**/*.java"],
+    javacflags: ["-parameters"],
+    platform_apis: true,
+    certificate: "platform",
+}
diff --git a/testapps/TelephonyManagerTestApp/Android.mk b/testapps/TelephonyManagerTestApp/Android.mk
deleted file mode 100644
index 290b261..0000000
--- a/testapps/TelephonyManagerTestApp/Android.mk
+++ /dev/null
@@ -1,19 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_JAVACFLAGS := -parameters
-
-LOCAL_PACKAGE_NAME := TelephonyManagerTestApp
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-LOCAL_MODULE_TAGS := tests
-
-include $(BUILD_PACKAGE)
diff --git a/testapps/TelephonyRegistryTestApp/Android.bp b/testapps/TelephonyRegistryTestApp/Android.bp
new file mode 100644
index 0000000..fec5286
--- /dev/null
+++ b/testapps/TelephonyRegistryTestApp/Android.bp
@@ -0,0 +1,7 @@
+android_test {
+    name: "TelephonyRegistryTestApp",
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+    //LOCAL_MODULE_TAGS := debug
+}
diff --git a/testapps/TelephonyRegistryTestApp/Android.mk b/testapps/TelephonyRegistryTestApp/Android.mk
deleted file mode 100644
index 8c0d286..0000000
--- a/testapps/TelephonyRegistryTestApp/Android.mk
+++ /dev/null
@@ -1,18 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-src_dirs := src
-res_dirs := res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
-
-LOCAL_PACKAGE_NAME := TelephonyRegistryTestApp
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-LOCAL_MODULE_TAGS := tests
-#LOCAL_MODULE_TAGS := debug
-
-include $(BUILD_PACKAGE)
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;
+    }
+}