DO NOT MERGE - Mark RQ3A.210410.001 as merged

Bug: 190855093
Merged-In: Ifdfd0919580beaea21ae7c6bfb787663134b2d56
Change-Id: I7d94fd644bdde70418bce13ad36eb638c204a029
diff --git a/Android.bp b/Android.bp
index 860c740..6b78ebb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,6 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["frameworks_opt_net_ims_license"],
+}
+
+// Added automatically by a large-scale-change that took the approach of
+// 'apply every license found to every target'. While this makes sure we respect
+// every license restriction, it may not be entirely correct.
+//
+// e.g. GPL in an MIT project might only apply to the contrib/ directory.
+//
+// Please consider splitting the single license below into multiple licenses,
+// taking care not to lose any license_kind information, and overriding the
+// default license using the 'licenses: [...]' property on targets as needed.
+//
+// For unused files, consider creating a 'fileGroup' with "//visibility:private"
+// to attach the license to, and including a comment whether the files may be
+// used in the current project.
+// See: http://go/android-license-faq
+license {
+    name: "frameworks_opt_net_ims_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-Apache-2.0",
+        "SPDX-license-identifier-BSD",
+    ],
+    // large-scale-change unable to identify any license_text files
+}
+
 java_library {
     name: "ims-common",
     installable: true,
diff --git a/OWNERS b/OWNERS
index 94409ef..8a0d4ed 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,4 @@
 breadley@google.com
 hallliu@google.com
 tgunn@google.com
-paulye@google.com
+dbright@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index e75dcb0..4b2fe34 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,7 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "TeleServiceTests",
+      "name": "ImsCommonTests",
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/src/java/com/android/ims/FeatureConnection.java b/src/java/com/android/ims/FeatureConnection.java
index 97736b9..748ae57 100644
--- a/src/java/com/android/ims/FeatureConnection.java
+++ b/src/java/com/android/ims/FeatureConnection.java
@@ -18,21 +18,19 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.IImsConfig;
 import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
 import android.telephony.ims.feature.ImsFeature;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
 import android.util.Log;
 
-import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.util.HandlerExecutor;
-
-import java.util.concurrent.Executor;
 
 import java.util.NoSuchElementException;
 
@@ -42,50 +40,29 @@
 public abstract class FeatureConnection {
     protected static final String TAG = "FeatureConnection";
 
-    public interface IFeatureUpdate {
-        /**
-         * Called when the ImsFeature has changed its state. Use
-         * {@link ImsFeature#getFeatureState()} to get the new state.
-         */
-        void notifyStateChanged();
-
-        /**
-         * Called when the ImsFeature has become unavailable due to the binder switching or app
-         * crashing. A new ImsServiceProxy should be requested for that feature.
-         */
-        void notifyUnavailable();
-    }
-
     protected static boolean sImsSupportedOnDevice = true;
 
     protected final int mSlotId;
     protected Context mContext;
     protected IBinder mBinder;
-    @VisibleForTesting
-    public Executor mExecutor;
 
     // We are assuming the feature is available when started.
     protected volatile boolean mIsAvailable = true;
     // ImsFeature Status from the ImsService. Cached.
     protected Integer mFeatureStateCached = null;
-    protected IFeatureUpdate mStatusCallback;
-    protected IImsRegistration mRegistrationBinder;
+    protected long mFeatureCapabilities;
+    private final IImsRegistration mRegistrationBinder;
+    private final IImsConfig mConfigBinder;
+    private final ISipTransport mSipTransportBinder;
     protected final Object mLock = new Object();
 
-    public FeatureConnection(Context context, int slotId) {
+    public FeatureConnection(Context context, int slotId, IImsConfig c, IImsRegistration r,
+            ISipTransport s) {
         mSlotId = slotId;
         mContext = context;
-
-        // Callbacks should be scheduled on the main thread.
-        if (context.getMainLooper() != null) {
-            mExecutor = context.getMainExecutor();
-        } else {
-            // Fallback to the current thread.
-            if (Looper.myLooper() == null) {
-                Looper.prepare();
-            }
-            mExecutor = new HandlerExecutor(new Handler(Looper.myLooper()));
-        }
+        mRegistrationBinder = r;
+        mConfigBinder = c;
+        mSipTransportBinder = s;
     }
 
     protected TelephonyManager getTelephonyManager() {
@@ -129,7 +106,6 @@
         synchronized (mLock) {
             if (mIsAvailable) {
                 mIsAvailable = false;
-                mRegistrationBinder = null;
                 try {
                     if (mBinder != null) {
                         mBinder.unlinkToDeath(mDeathRecipient, 0);
@@ -137,54 +113,10 @@
                 } catch (NoSuchElementException e) {
                     Log.w(TAG, "onRemovedOrDied: unlinkToDeath called on unlinked Binder.");
                 }
-                if (mStatusCallback != null) {
-                    Log.d(TAG, "onRemovedOrDied: notifyUnavailable");
-                    mStatusCallback.notifyUnavailable();
-                    // Unlink because this FeatureConnection should no longer send callbacks.
-                    mStatusCallback = null;
-                }
             }
         }
     }
 
-    /**
-     * The listener for ImsManger and RcsFeatureManager to receive IMS feature status changed.
-     * @param callback Callback that will fire when the feature status has changed.
-     */
-    public void setStatusCallback(IFeatureUpdate callback) {
-        mStatusCallback = callback;
-    }
-
-    @VisibleForTesting
-    public IImsServiceFeatureCallback getListener() {
-        return mListenerBinder;
-    }
-
-    /**
-     * The callback to receive ImsFeature status changed.
-     */
-    private final IImsServiceFeatureCallback mListenerBinder =
-        new IImsServiceFeatureCallback.Stub() {
-            @Override
-            public void imsFeatureCreated(int slotId, int feature) {
-                mExecutor.execute(() -> {
-                    handleImsFeatureCreatedCallback(slotId, feature);
-                });
-            }
-            @Override
-            public void imsFeatureRemoved(int slotId, int feature) {
-                mExecutor.execute(() -> {
-                    handleImsFeatureRemovedCallback(slotId, feature);
-                });
-            }
-            @Override
-            public void imsStatusChanged(int slotId, int feature, int status) {
-                mExecutor.execute(() -> {
-                    handleImsStatusChangedCallback(slotId, feature, status);
-                });
-            }
-        };
-
     public @ImsRegistrationImplBase.ImsRegistrationTech int getRegistrationTech()
             throws RemoteException {
         IImsRegistration registration = getRegistration();
@@ -197,24 +129,17 @@
     }
 
     public @Nullable IImsRegistration getRegistration() {
-        synchronized (mLock) {
-            // null if cache is invalid;
-            if (mRegistrationBinder != null) {
-                return mRegistrationBinder;
-            }
-        }
-        // We don't want to synchronize on a binder call to another process.
-        IImsRegistration regBinder = getRegistrationBinder();
-        synchronized (mLock) {
-            // mRegistrationBinder may have changed while we tried to get the registration
-            // interface.
-            if (mRegistrationBinder == null) {
-                mRegistrationBinder = regBinder;
-            }
-        }
         return mRegistrationBinder;
     }
 
+    public @Nullable IImsConfig getConfig() {
+        return mConfigBinder;
+    }
+
+    public @Nullable ISipTransport getSipTransport() {
+        return mSipTransportBinder;
+    }
+
     @VisibleForTesting
     public void checkServiceIsReady() throws RemoteException {
         if (!sImsSupportedOnDevice) {
@@ -245,6 +170,35 @@
         return mIsAvailable && mBinder != null && mBinder.isBinderAlive();
     }
 
+    public void updateFeatureState(int state) {
+        synchronized (mLock) {
+            mFeatureStateCached = state;
+        }
+    }
+
+    public long getFeatureCapabilties() {
+        synchronized (mLock) {
+            return mFeatureCapabilities;
+        }
+    }
+
+    public void updateFeatureCapabilities(long caps) {
+        synchronized (mLock) {
+            if (mFeatureCapabilities != caps) {
+                mFeatureCapabilities = caps;
+                onFeatureCapabilitiesUpdated(caps);
+            }
+        }
+    }
+
+    public boolean isCapable(@ImsService.ImsServiceCapability long capabilities)
+            throws RemoteException {
+        if (!isBinderAlive()) {
+            throw new RemoteException("isCapable: ImsService is not alive");
+        }
+        return (getFeatureCapabilties() & capabilities) > 0;
+    }
+
     /**
      * @return an integer describing the current Feature Status, defined in
      * {@link ImsFeature.ImsState}.
@@ -270,36 +224,9 @@
     }
 
     /**
-     * An ImsFeature has been created for this FeatureConnection for the associated
-     * {@link ImsFeature.FeatureType}.
-     * @param slotId The slot ID associated with the event.
-     * @param feature The {@link ImsFeature.FeatureType} associated with the event.
-     */
-    protected abstract void handleImsFeatureCreatedCallback(int slotId, int feature);
-
-    /**
-     * An ImsFeature has been removed for this FeatureConnection for the associated
-     * {@link ImsFeature.FeatureType}.
-     * @param slotId The slot ID associated with the event.
-     * @param feature The {@link ImsFeature.FeatureType} associated with the event.
-     */
-    protected abstract void handleImsFeatureRemovedCallback(int slotId, int feature);
-
-    /**
-     * The status of an ImsFeature has changed for the associated {@link ImsFeature.FeatureType}.
-     * @param slotId The slot ID associated with the event.
-     * @param feature The {@link ImsFeature.FeatureType} associated with the event.
-     * @param status The new {@link ImsFeature.ImsState} associated with the ImsFeature
-     */
-    protected abstract void handleImsStatusChangedCallback(int slotId, int feature, int status);
-
-    /**
      * Internal method used to retrieve the feature status from the corresponding ImsService.
      */
     protected abstract Integer retrieveFeatureState();
 
-    /**
-     * @return The ImsRegistration instance associated with the FeatureConnection.
-     */
-    protected abstract IImsRegistration getRegistrationBinder();
+    protected abstract void onFeatureCapabilitiesUpdated(long capabilities);
 }
diff --git a/src/java/com/android/ims/FeatureConnector.java b/src/java/com/android/ims/FeatureConnector.java
index e7c1c74..19e2151 100644
--- a/src/java/com/android/ims/FeatureConnector.java
+++ b/src/java/com/android/ims/FeatureConnector.java
@@ -16,114 +16,227 @@
 
 package com.android.ims;
 
+import android.annotation.IntDef;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.Looper;
+import android.os.RemoteException;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsService;
 import android.telephony.ims.feature.ImsFeature;
 
+import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.util.HandlerExecutor;
 import com.android.telephony.Rlog;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
  * Helper class for managing a connection to the ImsFeature manager.
  */
-public class FeatureConnector<T extends IFeatureConnector> extends Handler {
+public class FeatureConnector<U extends FeatureUpdates> {
     private static final String TAG = "FeatureConnector";
     private static final boolean DBG = false;
 
-    // Initial condition for ims connection retry.
-    private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms
+    /**
+     * This Connection has become unavailable due to the ImsService being disconnected due to
+     * an event such as SIM Swap, carrier configuration change, etc...
+     *
+     * {@link Listener#connectionReady}  will be called when a new Manager is available.
+     */
+    public static final int UNAVAILABLE_REASON_DISCONNECTED = 0;
 
-    // Ceiling bitshift amount for service query timeout, calculated as:
-    // 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
-    // mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
-    private static final int CEILING_SERVICE_RETRY_COUNT = 6;
+    /**
+     * This Connection has become unavailable due to the ImsService moving to the NOT_READY state.
+     *
+     * {@link Listener#connectionReady}  will be called when the manager moves back to ready.
+     */
+    public static final int UNAVAILABLE_REASON_NOT_READY = 1;
 
-    public interface Listener<T> {
+    /**
+     * IMS is not supported on this device. This should be considered a permanent error and
+     * a Manager will never become available.
+     */
+    public static final int UNAVAILABLE_REASON_IMS_UNSUPPORTED = 2;
+
+    /**
+     * The server of this information has crashed or otherwise generated an error that will require
+     * a retry to connect. This is rare, however in this case, {@link #disconnect()} and
+     * {@link #connect()} will need to be called again to recreate the connection with the server.
+     * <p>
+     * Only applicable if this is used outside of the server's own process.
+     */
+    public static final int UNAVAILABLE_REASON_SERVER_UNAVAILABLE = 3;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "UNAVAILABLE_REASON_", value = {
+            UNAVAILABLE_REASON_DISCONNECTED,
+            UNAVAILABLE_REASON_NOT_READY,
+            UNAVAILABLE_REASON_IMS_UNSUPPORTED,
+            UNAVAILABLE_REASON_SERVER_UNAVAILABLE
+    })
+    public @interface UnavailableReason {}
+
+    /**
+     * Factory used to create a new instance of the manager that this FeatureConnector is waiting
+     * to connect the FeatureConnection to.
+     * @param <U> The Manager that this FeatureConnector has been created for.
+     */
+    public interface ManagerFactory<U extends FeatureUpdates> {
         /**
-         * Get ImsFeature manager instance
+         * Create a manager instance, which will connect to the FeatureConnection.
          */
-        T getFeatureManager();
+        U createManager(Context context, int phoneId);
+    }
 
+    /**
+     * Listener interface used by Listeners of FeatureConnector that are waiting for a Manager
+     * interface for a specific ImsFeature.
+     * @param <U> The Manager that the listener is listening for.
+     */
+    public interface Listener<U extends FeatureUpdates> {
         /**
          * ImsFeature manager is connected to the underlying IMS implementation.
          */
-        void connectionReady(T manager) throws ImsException;
+        void connectionReady(U manager) throws ImsException;
 
         /**
          * The underlying IMS implementation is unavailable and can not be used to communicate.
          */
-        void connectionUnavailable();
+        void connectionUnavailable(@UnavailableReason int reason);
     }
 
-    public interface RetryTimeout {
-        int get();
-    }
+    private final IImsServiceFeatureCallback mCallback = new IImsServiceFeatureCallback.Stub() {
 
-    protected final int mPhoneId;
-    protected final Context mContext;
-    protected final Executor mExecutor;
-    protected final Object mLock = new Object();
-    protected final String mLogPrefix;
-
-    @VisibleForTesting
-    public Listener<T> mListener;
-
-    // The IMS feature manager which interacts with ImsService
-    @VisibleForTesting
-    public T mManager;
-
-    protected int mRetryCount = 0;
-
-    @VisibleForTesting
-    public RetryTimeout mRetryTimeout = () -> {
-        synchronized (mLock) {
-            int timeout = (1 << mRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
-            if (mRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
-                mRetryCount++;
+        @Override
+        public void imsFeatureCreated(ImsFeatureContainer c) {
+            log("imsFeatureCreated: " + c);
+            synchronized (mLock) {
+                mManager.associate(c);
+                mManager.updateFeatureCapabilities(c.getCapabilities());
+                mDisconnectedReason = null;
             }
-            return timeout;
+            // Notifies executor, so notify outside of lock
+            imsStatusChanged(c.getState());
+        }
+
+        @Override
+        public void imsFeatureRemoved(@UnavailableReason int reason) {
+            log("imsFeatureRemoved: reason=" + reason);
+            synchronized (mLock) {
+                // only generate new events if the disconnect event isn't the same as before, except
+                // for UNAVAILABLE_REASON_SERVER_UNAVAILABLE, which indicates a local issue and
+                // each event is actionable.
+                if (mDisconnectedReason != null
+                        && (mDisconnectedReason == reason
+                        && mDisconnectedReason != UNAVAILABLE_REASON_SERVER_UNAVAILABLE)) {
+                    log("imsFeatureRemoved: ignore");
+                    return;
+                }
+                mDisconnectedReason = reason;
+                // Ensure that we set ready state back to false so that we do not miss setting ready
+                // later if the initial state when recreated is READY.
+                mLastReadyState = false;
+            }
+            // Allow the listener to do cleanup while the connection still potentially valid (unless
+            // the process crashed).
+            mExecutor.execute(() -> mListener.connectionUnavailable(reason));
+            mManager.invalidate();
+        }
+
+        @Override
+        public void imsStatusChanged(int status) {
+            log("imsStatusChanged: status=" + ImsFeature.STATE_LOG_MAP.get(status));
+            final U manager;
+            final boolean isReady;
+            synchronized (mLock) {
+                if (mDisconnectedReason != null) {
+                    log("imsStatusChanged: ignore");
+                    return;
+                }
+                mManager.updateFeatureState(status);
+                manager = mManager;
+                isReady = mReadyFilter.contains(status);
+                boolean didReadyChange = isReady ^ mLastReadyState;
+                mLastReadyState = isReady;
+                if (!didReadyChange) {
+                    log("imsStatusChanged: ready didn't change, ignore");
+                    return;
+                }
+            }
+            mExecutor.execute(() -> {
+                try {
+                    if (isReady) {
+                        notifyReady(manager);
+                    } else {
+                        notifyNotReady();
+                    }
+                } catch (ImsException e) {
+                    if (e.getCode()
+                            == ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE) {
+                        mListener.connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+                    } else {
+                        notifyNotReady();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void updateCapabilities(long caps) {
+            log("updateCapabilities: capabilities=" + ImsService.getCapabilitiesString(caps));
+            synchronized (mLock) {
+                if (mDisconnectedReason != null) {
+                    log("updateCapabilities: ignore");
+                    return;
+                }
+                mManager.updateFeatureCapabilities(caps);
+            }
         }
     };
 
-    public FeatureConnector(Context context, int phoneId, Listener<T> listener,
-            String logPrefix) {
+    private final int mPhoneId;
+    private final Context mContext;
+    private final ManagerFactory<U> mFactory;
+    private final Listener<U> mListener;
+    private final Executor mExecutor;
+    private final Object mLock = new Object();
+    private final String mLogPrefix;
+    // A List of integers, each corresponding to an ImsFeature.ImsState, that the FeatureConnector
+    // will use to call Listener#connectionReady when the ImsFeature that this connector is waiting
+    // for changes into one of the states in this list.
+    private final List<Integer> mReadyFilter = new ArrayList<>();
+
+    private U mManager;
+    // Start in disconnected state;
+    private Integer mDisconnectedReason = UNAVAILABLE_REASON_DISCONNECTED;
+    // Stop redundant connectionAvailable if the ready filter contains multiple states.
+    // Also, do not send the first unavailable until after we have moved to available once.
+    private boolean mLastReadyState = false;
+
+
+
+    @VisibleForTesting
+    public FeatureConnector(Context context, int phoneId, ManagerFactory<U> factory,
+            String logPrefix, List<Integer> readyFilter, Listener<U> listener, Executor executor) {
         mContext = context;
         mPhoneId = phoneId;
+        mFactory = factory;
+        mLogPrefix = logPrefix;
+        mReadyFilter.addAll(readyFilter);
         mListener = listener;
-        mExecutor = new HandlerExecutor(this);
-        mLogPrefix = logPrefix;
-    }
-
-    @VisibleForTesting
-    public FeatureConnector(Context context, int phoneId, Listener<T> listener,
-            Executor executor, String logPrefix) {
-        mContext = context;
-        mPhoneId = phoneId;
-        mListener= listener;
         mExecutor = executor;
-        mLogPrefix = logPrefix;
-    }
-
-    @VisibleForTesting
-    public FeatureConnector(Context context, int phoneId, Listener<T> listener,
-            Executor executor, Looper looper) {
-        super(looper);
-        mContext = context;
-        mPhoneId = phoneId;
-        mListener= listener;
-        mExecutor = executor;
-        mLogPrefix = "?";
     }
 
     /**
      * Start the creation of a connection to the underlying ImsService implementation. When the
-     * service is connected, {@link FeatureConnector.Listener#connectionReady(Object)} will be
+     * service is connected, {@link FeatureConnector.Listener#connectionReady} will be
      * called with an active instance.
      *
      * If this device does not support an ImsStack (i.e. doesn't support
@@ -132,133 +245,44 @@
     public void connect() {
         if (DBG) log("connect");
         if (!isSupported()) {
+            mExecutor.execute(() -> mListener.connectionUnavailable(
+                    UNAVAILABLE_REASON_IMS_UNSUPPORTED));
             logw("connect: not supported.");
             return;
         }
-        mRetryCount = 0;
-
-        // Send a message to connect to the Ims Service and open a connection through
-        // getImsService().
-        post(mGetServiceRunnable);
+        synchronized (mLock) {
+            if (mManager == null) {
+                mManager = mFactory.createManager(mContext, mPhoneId);
+            }
+        }
+        mManager.registerFeatureCallback(mPhoneId, mCallback);
     }
 
     // Check if this ImsFeature is supported or not.
     private boolean isSupported() {
-        return ImsManager.isImsSupportedOnDevice(mContext);
+        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
     }
 
     /**
      * Disconnect from the ImsService Implementation and clean up. When this is complete,
-     * {@link FeatureConnector.Listener#connectionUnavailable()} will be called one last time.
+     * {@link FeatureConnector.Listener#connectionUnavailable(int)} will be called one last time.
      */
     public void disconnect() {
         if (DBG) log("disconnect");
-        removeCallbacks(mGetServiceRunnable);
-        synchronized (mLock) {
-            if (mManager != null) {
-                mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
-            }
-        }
-        notifyNotReady();
-    }
-
-    private final Runnable mGetServiceRunnable = () -> {
-        try {
-            createImsService();
-        } catch (android.telephony.ims.ImsException e) {
-            int errorCode = e.getCode();
-            if (DBG) logw("Create IMS service error: " + errorCode);
-            if (android.telephony.ims.ImsException.CODE_ERROR_UNSUPPORTED_OPERATION != errorCode) {
-                // Retry when error is not CODE_ERROR_UNSUPPORTED_OPERATION
-                retryGetImsService();
-            }
-        }
-    };
-
-    @VisibleForTesting
-    public void createImsService() throws android.telephony.ims.ImsException {
-        synchronized (mLock) {
-            if (DBG) log("createImsService");
-            mManager = mListener.getFeatureManager();
-            // Adding to set, will be safe adding multiple times. If the ImsService is not
-            // active yet, this method will throw an ImsException.
-            mManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
-        }
-        // Wait for ImsService.STATE_READY to start listening for calls.
-        // Call the callback right away for compatibility with older devices that do not use
-        // states.
-        mNotifyStatusChangedCallback.notifyStateChanged();
-    }
-
-    /**
-     * Remove callback and re-running mGetServiceRunnable
-     */
-    public void retryGetImsService() {
-        if (mManager != null) {
-            // remove callback so we do not receive updates from old ImsServiceProxy when
-            // switching between ImsServices.
-            mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
-            //Leave mImsManager as null, then CallStateException will be thrown when dialing
-            mManager = null;
-        }
-
-        // Exponential backoff during retry, limited to 32 seconds.
-        removeCallbacks(mGetServiceRunnable);
-        int timeout = mRetryTimeout.get();
-        postDelayed(mGetServiceRunnable, timeout);
-        if (DBG) log("retryGetImsService: unavailable, retrying in " + timeout + " ms");
-    }
-
-    // Callback fires when IMS Feature changes state
-    public FeatureConnection.IFeatureUpdate mNotifyStatusChangedCallback =
-            new FeatureConnection.IFeatureUpdate() {
-                @Override
-                public void notifyStateChanged() {
-                    mExecutor.execute(() -> {
-                        try {
-                            int status = ImsFeature.STATE_UNAVAILABLE;
-                            synchronized (mLock) {
-                                if (mManager != null) {
-                                    status = mManager.getImsServiceState();
-                                }
-                            }
-                            switch (status) {
-                                case ImsFeature.STATE_READY: {
-                                    notifyReady();
-                                    break;
-                                }
-                                case ImsFeature.STATE_INITIALIZING:
-                                    // fall through
-                                case ImsFeature.STATE_UNAVAILABLE: {
-                                    notifyNotReady();
-                                    break;
-                                }
-                                default: {
-                                    logw("Unexpected State! " + status);
-                                }
-                            }
-                        } catch (ImsException e) {
-                            // Could not get the ImsService, retry!
-                            notifyNotReady();
-                            retryGetImsService();
-                        }
-                    });
-                }
-
-                @Override
-                public void notifyUnavailable() {
-                    mExecutor.execute(() -> {
-                        notifyNotReady();
-                        retryGetImsService();
-                    });
-                }
-            };
-
-    private void notifyReady() throws ImsException {
-        T manager;
+        final U manager;
         synchronized (mLock) {
             manager = mManager;
         }
+        if (manager == null) return;
+
+        manager.unregisterFeatureCallback(mCallback);
+        try {
+            mCallback.imsFeatureRemoved(UNAVAILABLE_REASON_DISCONNECTED);
+        } catch (RemoteException ignore) {} // local call
+    }
+
+    // Should be called on executor
+    private void notifyReady(U manager) throws ImsException {
         try {
             if (DBG) log("notifyReady");
             mListener.connectionReady(manager);
@@ -267,22 +291,19 @@
             if(DBG) log("notifyReady exception: " + e.getMessage());
             throw e;
         }
-        // Only reset retry count if connectionReady does not generate an ImsException/
-        synchronized (mLock) {
-            mRetryCount = 0;
-        }
     }
 
-    protected void notifyNotReady() {
+    // Should be called on executor.
+    private void notifyNotReady() {
         if (DBG) log("notifyNotReady");
-        mListener.connectionUnavailable();
+        mListener.connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
     }
 
-    private final void log(String message) {
+    private void log(String message) {
         Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
     }
 
-    private final void logw(String message) {
+    private void logw(String message) {
         Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
     }
 }
diff --git a/src/java/com/android/ims/FeatureUpdates.java b/src/java/com/android/ims/FeatureUpdates.java
new file mode 100644
index 0000000..446a78b
--- /dev/null
+++ b/src/java/com/android/ims/FeatureUpdates.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2019 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.ims;
+
+import android.telephony.ims.ImsService;
+import android.telephony.ims.feature.ImsFeature;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+
+/**
+ * Interface used by Manager interfaces that will use a {@link FeatureConnector} to connect to
+ * remote ImsFeature Binder interfaces.
+ */
+public interface FeatureUpdates {
+    /**
+     * Register a callback for the slot specified so that the FeatureConnector can notify its
+     * listener of changes.
+     * @param slotId The slot the callback is registered for.
+     * @param cb The callback that the FeatureConnector will use to update its state and notify
+     *           its callback of changes.
+     */
+    void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb);
+
+    /**
+     * Unregister a previously registered callback due to the FeatureConnector disconnecting.
+     * <p>
+     * This does not need to be called if the callback was previously registered for a one
+     * shot result.
+     * @param cb The callback to unregister.
+     */
+    void unregisterFeatureCallback(IImsServiceFeatureCallback cb);
+
+    /**
+     * Associate this Manager instance with the IMS Binder interfaces specified. This is usually
+     * done by creating a FeatureConnection instance with these interfaces.
+     * @param container Contains all of the related interfaces attached to a specific ImsFeature.
+     */
+    void associate(ImsFeatureContainer container);
+
+    /**
+     * Invalidate the previously associated Binder interfaces set in {@link #associate}.
+     */
+    void invalidate();
+
+    /**
+     * Update the state of the remote ImsFeature associated with this Manager instance.
+     */
+    void updateFeatureState(@ImsFeature.ImsState int state);
+
+    /**
+     * Update the capabilities of the remove ImsFeature associated with this Manager instance.
+     */
+    void updateFeatureCapabilities(@ImsService.ImsServiceCapability long capabilities);
+}
\ No newline at end of file
diff --git a/src/java/com/android/ims/IFeatureConnector.java b/src/java/com/android/ims/IFeatureConnector.java
deleted file mode 100644
index 66428ce..0000000
--- a/src/java/com/android/ims/IFeatureConnector.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (c) 2019 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.ims;
-
-public interface IFeatureConnector<T> {
-    int getImsServiceState() throws ImsException;
-    void addNotifyStatusChangedCallbackIfAvailable(FeatureConnection.IFeatureUpdate callback)
-            throws android.telephony.ims.ImsException;
-    void removeNotifyStatusChangedCallback(FeatureConnection.IFeatureUpdate callback);
-}
\ No newline at end of file
diff --git a/src/java/com/android/ims/ImsCall.java b/src/java/com/android/ims/ImsCall.java
index c0cb286..0d5b396 100755
--- a/src/java/com/android/ims/ImsCall.java
+++ b/src/java/com/android/ims/ImsCall.java
@@ -16,9 +16,11 @@
 
 package com.android.ims;
 
+import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Message;
 import android.os.Parcel;
@@ -30,10 +32,12 @@
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsCallProfile;
 import android.telephony.ims.ImsCallSession;
+import android.telephony.ims.ImsCallSessionListener;
 import android.telephony.ims.ImsConferenceState;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsStreamMediaProfile;
 import android.telephony.ims.ImsSuppServiceNotification;
+import android.telephony.ims.RtpHeaderExtension;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -46,9 +50,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -93,11 +95,21 @@
      */
     public static class Listener {
         /**
-         * Called when a request is sent out to initiate a new call
-         * and 1xx response is received from the network.
+         * Called after the network first begins to establish the call session and is now connecting
+         * to the remote party.
          * The default implementation calls {@link #onCallStateChanged}.
-         *
-         * @param call the call object that carries out the IMS call
+         * <p/>
+         * see: {@link ImsCallSessionListener#callSessionInitiating}
+         */
+        public void onCallInitiating(ImsCall call) {
+            onCallStateChanged(call);
+        }
+
+        /**
+         * Called after the network has contacted the remote party.
+         * The default implementation calls {@link #onCallStateChanged}.
+         * <p/>
+         * see: {@link ImsCallSessionListener#callSessionProgressing}
          */
         public void onCallProgressing(ImsCall call) {
             onCallStateChanged(call);
@@ -501,6 +513,14 @@
         }
 
         /**
+         * Reports a DTMF tone received from the network.
+         * @param imsCall The IMS call the tone was received from.
+         * @param digit The digit received.
+         */
+        public void onCallSessionDtmfReceived(ImsCall imsCall, char digit) {
+        }
+
+        /**
          * Called when the call quality has changed.
          *
          * @param imsCall ImsCall object
@@ -508,6 +528,15 @@
          */
         public void onCallQualityChanged(ImsCall imsCall, CallQuality callQuality) {
         }
+
+        /**
+         * Called when RTP header extension data is received from the network.
+         * @param imsCall The ImsCall the data was received on.
+         * @param rtpHeaderExtensionData The RTP extension data received.
+         */
+        public void onCallSessionRtpHeaderExtensionsReceived(ImsCall imsCall,
+                @NonNull Set<RtpHeaderExtension> rtpHeaderExtensionData) {
+        }
     }
 
     // List of update operation for IMS call control
@@ -952,7 +981,7 @@
      *
      * @return {@code True} if the call is a multiparty call.
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public boolean isMultiparty() {
         synchronized(mLockObj) {
             if (mSession == null) {
@@ -1217,7 +1246,7 @@
      * @param number number to be deflected to.
      * @throws ImsException if the IMS service fails to deflect the call
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void deflect(String number) throws ImsException {
         logi("deflect :: session=" + mSession + ", number=" + Rlog.pii(TAG, number));
 
@@ -1243,7 +1272,7 @@
      * @see Listener#onCallStartFailed
      * @throws ImsException if the IMS service fails to reject the call
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void reject(int reason) throws ImsException {
         logi("reject :: reason=" + reason);
 
@@ -1328,7 +1357,7 @@
      *
      * @param reason reason code to terminate a call
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void terminate(int reason) {
         logi("terminate :: reason=" + reason);
 
@@ -1795,6 +1824,30 @@
         }
     }
 
+    /**
+     * Requests that RTP header extensions are added to the next RTP packet sent by the IMS stack.
+     * <p>
+     * The {@link RtpHeaderExtension#getLocalIdentifier()} local identifiers specified here must match
+     * agreed upon identifiers as indicated in
+     * {@link ImsCallProfile#getAcceptedRtpHeaderExtensionTypes()} for the current
+     * {@link #getCallProfile()}.
+     * <p>
+     * By specification, the RTP header extension is an unacknowledged transmission and there is no
+     * guarantee that the header extension will be delivered by the network to the other end of the
+     * call.
+     * @param rtpHeaderExtensions The RTP header extension(s) to be included in the next RTP
+     *                            packet.
+     */
+    public void sendRtpHeaderExtensions(@NonNull Set<RtpHeaderExtension> rtpHeaderExtensions) {
+        logi("sendRtpHeaderExtensions; extensionsSent=" + rtpHeaderExtensions.size());
+        synchronized(mLockObj) {
+            if (mSession == null) {
+                loge("sendRtpHeaderExtensions::no session");
+            }
+            mSession.sendRtpHeaderExtensions(rtpHeaderExtensions);
+        }
+    }
+
     public void setAnswerWithRtt() {
         mAnswerWithRtt = true;
     }
@@ -2394,6 +2447,32 @@
     @VisibleForTesting
     public class ImsCallSessionListenerProxy extends ImsCallSession.Listener {
         @Override
+        public void callSessionInitiating(ImsCallSession session, ImsCallProfile profile) {
+            logi("callSessionInitiating :: session=" + session + " profile=" + profile);
+            if (isTransientConferenceSession(session)) {
+                // If it is a transient (conference) session, there is no action for this signal.
+                logi("callSessionInitiating :: not supported for transient conference session=" +
+                        session);
+                return;
+            }
+
+            ImsCall.Listener listener;
+
+            synchronized(ImsCall.this) {
+                listener = mListener;
+                setCallProfile(profile);
+            }
+
+            if (listener != null) {
+                try {
+                    listener.onCallInitiating(ImsCall.this);
+                } catch (Throwable t) {
+                    loge("callSessionInitiating :: ", t);
+                }
+            }
+        }
+
+        @Override
         public void callSessionProgressing(ImsCallSession session, ImsStreamMediaProfile profile) {
             logi("callSessionProgressing :: session=" + session + " profile=" + profile);
 
@@ -2406,8 +2485,14 @@
 
             ImsCall.Listener listener;
 
+            ImsCallProfile updatedProfile = session.getCallProfile();
             synchronized(ImsCall.this) {
                 listener = mListener;
+                // The ImsCallProfile may have updated here (for example call state change). Query
+                // the potentially updated call profile to pick up these changes.
+                setCallProfile(updatedProfile);
+                // Apply the new mediaProfile on top of the Call Profile so it is not ignored in
+                // case the ImsService has not had a chance to update it yet.
                 mCallProfile.mMediaProfile.copyFrom(profile);
             }
 
@@ -3341,6 +3426,23 @@
         }
 
         @Override
+        public void callSessionDtmfReceived(char digit) {
+            ImsCall.Listener listener;
+
+            synchronized(ImsCall.this) {
+                listener = mListener;
+            }
+
+            if (listener != null) {
+                try {
+                    listener.onCallSessionDtmfReceived(ImsCall.this, digit);
+                } catch (Throwable t) {
+                    loge("callSessionDtmfReceived:: ", t);
+                }
+            }
+        }
+
+        @Override
         public void callQualityChanged(CallQuality callQuality) {
             ImsCall.Listener listener;
 
@@ -3356,6 +3458,24 @@
                 }
             }
         }
+
+        @Override
+        public void callSessionRtpHeaderExtensionsReceived(
+                @NonNull Set<RtpHeaderExtension> extensions) {
+            ImsCall.Listener listener;
+
+            synchronized (ImsCall.this) {
+                listener = mListener;
+            }
+
+            if (listener != null) {
+                try {
+                    listener.onCallSessionRtpHeaderExtensionsReceived(ImsCall.this, extensions);
+                } catch (Throwable t) {
+                    loge("callSessionRtpHeaderExtensionsReceived:: ", t);
+                }
+            }
+        }
     }
 
     /**
@@ -3672,8 +3792,7 @@
      * @param profile The current {@link ImsCallProfile} for the call.
      */
     private void trackVideoStateHistory(ImsCallProfile profile) {
-        mWasVideoCall = mWasVideoCall
-                || profile != null ? profile.isVideoCall() : false;
+        mWasVideoCall = mWasVideoCall || ( profile != null && profile.isVideoCall());
     }
 
     /**
diff --git a/src/java/com/android/ims/ImsEcbm.java b/src/java/com/android/ims/ImsEcbm.java
index 13a5925..e0624f2 100644
--- a/src/java/com/android/ims/ImsEcbm.java
+++ b/src/java/com/android/ims/ImsEcbm.java
@@ -30,6 +30,7 @@
 package com.android.ims;
 
 import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
 import android.os.RemoteException;
 import android.telephony.ims.ImsReasonInfo;
 
@@ -38,9 +39,7 @@
 import com.android.telephony.Rlog;
 
 /**
- * Provides APIs for the supplementary service settings using IMS (Ut interface).
- * It is created from 3GPP TS 24.623 (XCAP(XML Configuration Access Protocol)
- * over the Ut interface for manipulating supplementary services).
+ * Provides APIs for the modem to communicate the CDMA Emergency Callback Mode status for IMS.
  *
  * @hide
  */
@@ -55,16 +54,12 @@
         miEcbm = iEcbm;
     }
 
-    public void setEcbmStateListener(ImsEcbmStateListener ecbmListener) throws ImsException {
-        try {
-            miEcbm.setListener(new ImsEcbmListenerProxy(ecbmListener));
-        } catch (RemoteException e) {
-            throw new ImsException("setEcbmStateListener()", e,
-                    ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
-        }
+    public void setEcbmStateListener(ImsEcbmStateListener ecbmListener) throws RemoteException {
+            miEcbm.setListener(ecbmListener != null ?
+                    new ImsEcbmListenerProxy(ecbmListener) : null);
     }
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void exitEmergencyCallbackMode() throws ImsException {
         try {
             miEcbm.exitEmergencyCallbackMode();
@@ -81,8 +76,8 @@
     /**
      * Adapter class for {@link IImsEcbmListener}.
      */
-    private class ImsEcbmListenerProxy extends IImsEcbmListener.Stub {
-        private ImsEcbmStateListener mListener;
+    private static class ImsEcbmListenerProxy extends IImsEcbmListener.Stub {
+        private final ImsEcbmStateListener mListener;
 
         public ImsEcbmListenerProxy(ImsEcbmStateListener listener) {
             mListener = listener;
diff --git a/src/java/com/android/ims/ImsFeatureBinderRepository.java b/src/java/com/android/ims/ImsFeatureBinderRepository.java
new file mode 100644
index 0000000..538e5cf
--- /dev/null
+++ b/src/java/com/android/ims/ImsFeatureBinderRepository.java
@@ -0,0 +1,438 @@
+/*
+ * 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.ims;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.feature.ImsFeature;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+/**
+ * A repository of ImsFeature connections made available by an ImsService once it has been
+ * successfully bound.
+ *
+ * Provides the ability for listeners to register callbacks and the repository notify registered
+ * listeners when a connection has been created/removed for a specific connection type.
+ */
+public class ImsFeatureBinderRepository {
+
+    private static final String TAG = "ImsFeatureBinderRepo";
+
+    /**
+     * Internal class representing a listener that is listening for changes to specific
+     * ImsFeature instances.
+     */
+    private static class ListenerContainer {
+        private final IImsServiceFeatureCallback mCallback;
+        private final Executor mExecutor;
+
+        public ListenerContainer(@NonNull IImsServiceFeatureCallback c, @NonNull Executor e) {
+            mCallback = c;
+            mExecutor = e;
+        }
+
+        public void notifyFeatureCreatedOrRemoved(ImsFeatureContainer connector) {
+            if (connector == null) {
+                mExecutor.execute(() -> {
+                    try {
+                        mCallback.imsFeatureRemoved(
+                                FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+                    } catch (RemoteException e) {
+                        // This listener will eventually be caught and removed during stale checks.
+                    }
+                });
+            }
+            else {
+                mExecutor.execute(() -> {
+                    try {
+                        mCallback.imsFeatureCreated(connector);
+                    } catch (RemoteException e) {
+                        // This listener will eventually be caught and removed during stale checks.
+                    }
+                });
+            }
+        }
+
+        public void notifyStateChanged(int state) {
+            mExecutor.execute(() -> {
+                try {
+                    mCallback.imsStatusChanged(state);
+                } catch (RemoteException e) {
+                    // This listener will eventually be caught and removed during stale checks.
+                }
+            });
+        }
+
+        public void notifyUpdateCapabilties(long caps) {
+            mExecutor.execute(() -> {
+                try {
+                    mCallback.updateCapabilities(caps);
+                } catch (RemoteException e) {
+                    // This listener will eventually be caught and removed during stale checks.
+                }
+            });
+        }
+
+        public boolean isStale() {
+            return !mCallback.asBinder().isBinderAlive();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            ListenerContainer that = (ListenerContainer) o;
+            // Do not count executor for equality.
+            return mCallback.equals(that.mCallback);
+        }
+
+        @Override
+        public int hashCode() {
+            // Do not use executor for hash.
+            return Objects.hash(mCallback);
+        }
+
+        @Override
+        public String toString() {
+            return "ListenerContainer{" + "cb=" + mCallback + '}';
+        }
+    }
+
+    /**
+     * Contains the mapping from ImsFeature type (MMTEL/RCS) to List of listeners listening for
+     * updates to the ImsFeature instance contained in the ImsFeatureContainer.
+     */
+    private static final class UpdateMapper {
+        public final int phoneId;
+        public final @ImsFeature.FeatureType int imsFeatureType;
+        private final List<ListenerContainer> mListeners = new ArrayList<>();
+        private ImsFeatureContainer mFeatureContainer;
+        private final Object mLock = new Object();
+
+
+        public UpdateMapper(int pId, @ImsFeature.FeatureType int t) {
+            phoneId = pId;
+            imsFeatureType = t;
+        }
+
+        public void addFeatureContainer(ImsFeatureContainer c) {
+            List<ListenerContainer> listeners;
+            synchronized (mLock) {
+                if (Objects.equals(c, mFeatureContainer)) return;
+                mFeatureContainer = c;
+                listeners = copyListenerList(mListeners);
+            }
+            listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer));
+        }
+
+        public ImsFeatureContainer removeFeatureContainer() {
+            ImsFeatureContainer oldContainer;
+            List<ListenerContainer> listeners;
+            synchronized (mLock) {
+                if (mFeatureContainer == null) return null;
+                oldContainer = mFeatureContainer;
+                mFeatureContainer = null;
+                listeners = copyListenerList(mListeners);
+            }
+            listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer));
+            return oldContainer;
+        }
+
+        public ImsFeatureContainer getFeatureContainer() {
+            synchronized(mLock) {
+                return mFeatureContainer;
+            }
+        }
+
+        public void addListener(ListenerContainer c) {
+            ImsFeatureContainer featureContainer;
+            synchronized (mLock) {
+                removeStaleListeners();
+                if (mListeners.contains(c)) {
+                    return;
+                }
+                featureContainer = mFeatureContainer;
+                mListeners.add(c);
+            }
+            // Do not call back until the feature container has been set.
+            if (featureContainer != null) {
+                c.notifyFeatureCreatedOrRemoved(featureContainer);
+            }
+        }
+
+        public void removeListener(IImsServiceFeatureCallback callback) {
+            synchronized (mLock) {
+                removeStaleListeners();
+                List<ListenerContainer> oldListeners = mListeners.stream()
+                        .filter((c) -> Objects.equals(c.mCallback, callback))
+                        .collect(Collectors.toList());
+                mListeners.removeAll(oldListeners);
+            }
+        }
+
+        public void notifyStateUpdated(int newState) {
+            ImsFeatureContainer featureContainer;
+            List<ListenerContainer> listeners;
+            synchronized (mLock) {
+                removeStaleListeners();
+                featureContainer = mFeatureContainer;
+                listeners = copyListenerList(mListeners);
+                if (mFeatureContainer != null) {
+                    if (mFeatureContainer.getState() != newState) {
+                        mFeatureContainer.setState(newState);
+                    }
+                }
+            }
+            // Only update if the feature container is set.
+            if (featureContainer != null) {
+                listeners.forEach(l -> l.notifyStateChanged(newState));
+            }
+        }
+
+        public void notifyUpdateCapabilities(long caps) {
+            ImsFeatureContainer featureContainer;
+            List<ListenerContainer> listeners;
+            synchronized (mLock) {
+                removeStaleListeners();
+                featureContainer = mFeatureContainer;
+                listeners = copyListenerList(mListeners);
+                if (mFeatureContainer != null) {
+                    if (mFeatureContainer.getCapabilities() != caps) {
+                        mFeatureContainer.setCapabilities(caps);
+                    }
+                }
+            }
+            // Only update if the feature container is set.
+            if (featureContainer != null) {
+                listeners.forEach(l -> l.notifyUpdateCapabilties(caps));
+            }
+        }
+
+        @GuardedBy("mLock")
+        private void removeStaleListeners() {
+            List<ListenerContainer> staleListeners = mListeners.stream().filter(
+                    ListenerContainer::isStale)
+                    .collect(Collectors.toList());
+            mListeners.removeAll(staleListeners);
+        }
+
+        @Override
+        public String toString() {
+            synchronized (mLock) {
+                return "UpdateMapper{" + "phoneId=" + phoneId + ", type="
+                        + ImsFeature.FEATURE_LOG_MAP.get(imsFeatureType) + ", container="
+                        + mFeatureContainer + '}';
+            }
+        }
+
+
+        private List<ListenerContainer> copyListenerList(List<ListenerContainer> listeners) {
+            return new ArrayList<>(listeners);
+        }
+    }
+
+    private final List<UpdateMapper> mFeatures = new ArrayList<>();
+    private final LocalLog mLocalLog = new LocalLog(50 /*lines*/);
+
+    public ImsFeatureBinderRepository() {
+        logInfoLineLocked(-1, "FeatureConnectionRepository - created");
+    }
+
+    /**
+     * Get the Container for a specific ImsFeature now if it exists.
+     *
+     * @param phoneId The phone ID that the connection is related to.
+     * @param type The ImsFeature type to get the cotnainr for (MMTEL/RCS).
+     * @return The Container containing the requested ImsFeature if it exists.
+     */
+    public Optional<ImsFeatureContainer> getIfExists(
+            int phoneId, @ImsFeature.FeatureType int type) {
+        if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
+            throw new IllegalArgumentException("Incorrect feature type");
+        }
+        UpdateMapper m;
+        m = getUpdateMapper(phoneId, type);
+        ImsFeatureContainer c = m.getFeatureContainer();
+        logVerboseLineLocked(phoneId, "getIfExists, type= " + ImsFeature.FEATURE_LOG_MAP.get(type)
+                + ", result= " + c);
+        return Optional.ofNullable(c);
+    }
+
+    /**
+     * Register a callback that will receive updates when the requested ImsFeature type becomes
+     * available or unavailable for the specified phone ID.
+     * <p>
+     * This callback will not be called the first time until there is a valid ImsFeature.
+     * @param phoneId The phone ID that the connection will be related to.
+     * @param type The ImsFeature type to get (MMTEL/RCS).
+     * @param callback The callback that will be used to notify when the callback is
+     *                 available/unavailable.
+     * @param executor The executor that the callback will be run on.
+     */
+    public void registerForConnectionUpdates(int phoneId,
+            @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback,
+            @NonNull Executor executor) {
+        if (type < 0 || type >= ImsFeature.FEATURE_MAX || callback == null || executor == null) {
+            throw new IllegalArgumentException("One or more invalid arguments have been passed in");
+        }
+        ListenerContainer container = new ListenerContainer(callback, executor);
+        logInfoLineLocked(phoneId, "registerForConnectionUpdates, type= "
+                + ImsFeature.FEATURE_LOG_MAP.get(type) +", conn= " + container);
+        UpdateMapper m = getUpdateMapper(phoneId, type);
+        m.addListener(container);
+    }
+
+    /**
+     * Unregister for updates on a previously registered callback.
+     *
+     * @param callback The callback to unregister.
+     */
+    public void unregisterForConnectionUpdates(@NonNull IImsServiceFeatureCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("this method does not accept null arguments");
+        }
+        logInfoLineLocked(-1, "unregisterForConnectionUpdates, callback= " + callback);
+        synchronized (mFeatures) {
+            for (UpdateMapper m : mFeatures) {
+                // warning: no callbacks should be called while holding locks
+                m.removeListener(callback);
+            }
+        }
+    }
+
+    /**
+     * Add a Container containing the IBinder interfaces associated with a specific ImsFeature type
+     * (MMTEL/RCS). If one already exists, it will be replaced. This will notify listeners of the
+     * change.
+     * @param phoneId The phone ID associated with this Container.
+     * @param type The ImsFeature type to get (MMTEL/RCS).
+     * @param newConnection A Container containing the IBinder interface connections associated with
+     *                      the ImsFeature type.
+     */
+    public void addConnection(int phoneId, @ImsFeature.FeatureType int type,
+            @Nullable ImsFeatureContainer newConnection) {
+        if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
+            throw new IllegalArgumentException("The type must valid");
+        }
+        logInfoLineLocked(phoneId, "addConnection, type=" + ImsFeature.FEATURE_LOG_MAP.get(type)
+                + ", conn=" + newConnection);
+        UpdateMapper m = getUpdateMapper(phoneId, type);
+        m.addFeatureContainer(newConnection);
+    }
+
+    /**
+     * Remove the IBinder Container associated with a specific ImsService type. Listeners will be
+     * notified of this change.
+     * @param phoneId The phone ID associated with this connection.
+     * @param type The ImsFeature type to get (MMTEL/RCS).
+     */
+    public ImsFeatureContainer removeConnection(int phoneId, @ImsFeature.FeatureType int type) {
+        if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
+            throw new IllegalArgumentException("The type must valid");
+        }
+        logInfoLineLocked(phoneId, "removeConnection, type="
+                + ImsFeature.FEATURE_LOG_MAP.get(type));
+        UpdateMapper m = getUpdateMapper(phoneId, type);
+        return m.removeFeatureContainer();
+    }
+
+    /**
+     * Notify listeners that the state of a specific ImsFeature that this repository is
+     * tracking has changed. Listeners will be notified of the change in the ImsFeature's state.
+     * @param phoneId The phoneId of the feature that has changed state.
+     * @param type The ImsFeature type to get (MMTEL/RCS).
+     * @param state The new state of the ImsFeature
+     */
+    public void notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type,
+            @ImsFeature.ImsState int state) {
+        logInfoLineLocked(phoneId, "notifyFeatureStateChanged, type="
+                + ImsFeature.FEATURE_LOG_MAP.get(type) + ", state="
+                + ImsFeature.STATE_LOG_MAP.get(state));
+        UpdateMapper m = getUpdateMapper(phoneId, type);
+        m.notifyStateUpdated(state);
+    }
+
+    /**
+     *  Notify listeners that the capabilities of a specific ImsFeature that this repository is
+     * tracking has changed. Listeners will be notified of the change in the ImsFeature's
+     * capabilities.
+     * @param phoneId The phoneId of the feature that has changed capabilities.
+     * @param type The ImsFeature type to get (MMTEL/RCS).
+     * @param capabilities The new capabilities of the ImsFeature
+     */
+    public void notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type,
+            @ImsService.ImsServiceCapability long capabilities) {
+        logInfoLineLocked(phoneId, "notifyFeatureCapabilitiesChanged, type="
+                + ImsFeature.FEATURE_LOG_MAP.get(type) + ", caps="
+                + ImsService.getCapabilitiesString(capabilities));
+        UpdateMapper m = getUpdateMapper(phoneId, type);
+        m.notifyUpdateCapabilities(capabilities);
+    }
+
+    /**
+     * Prints the dump of log events that have occurred on this repository.
+     */
+    public void dump(PrintWriter printWriter) {
+        synchronized (mLocalLog) {
+            mLocalLog.dump(printWriter);
+        }
+    }
+
+    private UpdateMapper getUpdateMapper(int phoneId, int type) {
+        synchronized (mFeatures) {
+            UpdateMapper mapper = mFeatures.stream()
+                    .filter((c) -> ((c.phoneId == phoneId) && (c.imsFeatureType == type)))
+                    .findFirst().orElse(null);
+            if (mapper == null) {
+                mapper = new UpdateMapper(phoneId, type);
+                mFeatures.add(mapper);
+            }
+            return mapper;
+        }
+    }
+
+    private void logVerboseLineLocked(int phoneId, String log) {
+        if (!Log.isLoggable(TAG, Log.VERBOSE)) return;
+        final String phoneIdPrefix = "[" + phoneId + "] ";
+        Log.v(TAG, phoneIdPrefix + log);
+        synchronized (mLocalLog) {
+            mLocalLog.log(phoneIdPrefix + log);
+        }
+    }
+
+    private void logInfoLineLocked(int phoneId, String log) {
+        final String phoneIdPrefix = "[" + phoneId + "] ";
+        Log.i(TAG, phoneIdPrefix + log);
+        synchronized (mLocalLog) {
+            mLocalLog.log(phoneIdPrefix + log);
+        }
+    }
+}
diff --git a/src/java/com/android/ims/ImsManager.java b/src/java/com/android/ims/ImsManager.java
index 9a436c3..0e1892d 100644
--- a/src/java/com/android/ims/ImsManager.java
+++ b/src/java/com/android/ims/ImsManager.java
@@ -16,22 +16,24 @@
 
 package com.android.ims;
 
-import android.annotation.Nullable;
+import static android.telephony.ims.ProvisioningManager.KEY_VOIMS_OPT_IN_STATUS;
+
+import android.annotation.NonNull;
 import android.app.PendingIntent;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
+import android.os.Build;
 import android.os.Message;
-import android.os.Parcel;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.provider.Settings;
 import android.telecom.TelecomManager;
 import android.telephony.AccessNetworkConstants;
+import android.telephony.BinderCacheManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyFrameworkInitializer;
@@ -43,48 +45,52 @@
 import android.telephony.ims.ImsService;
 import android.telephony.ims.ProvisioningManager;
 import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.RtpHeaderExtensionType;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
 import android.telephony.ims.aidl.IImsConfig;
 import android.telephony.ims.aidl.IImsConfigCallback;
+import android.telephony.ims.aidl.IImsMmTelFeature;
+import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.telephony.ims.aidl.IImsSmsListener;
+import android.telephony.ims.aidl.ISipTransport;
 import android.telephony.ims.feature.CapabilityChangeRequest;
 import android.telephony.ims.feature.ImsFeature;
 import android.telephony.ims.feature.MmTelFeature;
 import android.telephony.ims.stub.ImsCallSessionImplBase;
 import android.telephony.ims.stub.ImsConfigImplBase;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.SparseArray;
 
 import com.android.ims.internal.IImsCallSession;
-import com.android.ims.internal.IImsEcbm;
-import com.android.ims.internal.IImsMultiEndpoint;
-import com.android.ims.internal.IImsUt;
+import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.ITelephony;
-import com.android.internal.telephony.util.HandlerExecutor;
 import com.android.telephony.Rlog;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Arrays;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentLinkedDeque;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 
 /**
- * Provides APIs for IMS services, such as initiating IMS calls, and provides access to
- * the operator's IMS network. This class is the starting point for any IMS actions.
- * You can acquire an instance of it with {@link #getInstance getInstance()}.</p>
+ * Provides APIs for MMTEL IMS services, such as initiating IMS calls, and provides access to
+ * the operator's IMS network. This class is the starting point for any IMS MMTEL actions.
+ * You can acquire an instance of it with {@link #getInstance getInstance()}.
+ * {Use {@link RcsFeatureManager} for RCS services}.
  * For internal use ONLY! Use {@link ImsMmTelManager} instead.
  * @hide
  */
-public class ImsManager implements IFeatureConnector {
+public class ImsManager implements FeatureUpdates {
 
     /*
      * Debug flag to override configuration flag
@@ -174,7 +180,7 @@
      * The value "true" indicates that the incoming call is for USSD.
      * Internal use only.
      * @deprecated Keeping around to not break old vendor components. Use
-     * {@link MmTelFeature#EXTRA_USSD} instead.
+     * {@link MmTelFeature#EXTRA_IS_USSD} instead.
      * @hide
      */
     public static final String EXTRA_USSD = "android:ussd";
@@ -205,68 +211,248 @@
 
     private static final int RESPONSE_WAIT_TIME_MS = 3000;
 
-    @VisibleForTesting
-    public interface ExecutorFactory {
-        void executeRunnable(Runnable runnable);
+    private static final int[] LOCAL_IMS_CONFIG_KEYS = {
+            KEY_VOIMS_OPT_IN_STATUS
+    };
+
+    /**
+     * Create a Lazy Executor that is not instantiated for this instance unless it is used. This
+     * is to stop threads from being started on ImsManagers that are created to do simple tasks.
+     */
+    private static class LazyExecutor implements Executor {
+        private Executor mExecutor;
+
+        @Override
+        public void execute(Runnable runnable) {
+            startExecutorIfNeeded();
+            mExecutor.execute(runnable);
+        }
+
+        private synchronized void startExecutorIfNeeded() {
+            if (mExecutor != null) return;
+            mExecutor = Executors.newSingleThreadExecutor();
+        }
     }
 
     @VisibleForTesting
-    public static class ImsExecutorFactory implements ExecutorFactory {
+    public interface MmTelFeatureConnectionFactory {
+        MmTelFeatureConnection create(Context context, int phoneId, IImsMmTelFeature feature,
+                IImsConfig c, IImsRegistration r, ISipTransport s);
+    }
 
-        private final HandlerThread mThreadHandler;
-        private final Handler mHandler;
+    @VisibleForTesting
+    public interface SettingsProxy {
+        /** @see Settings.Secure#getInt(ContentResolver, String, int) */
+        int getSecureIntSetting(ContentResolver cr, String name, int def);
+        /** @see Settings.Secure#putInt(ContentResolver, String, int) */
+        boolean putSecureIntSetting(ContentResolver cr, String name, int value);
+    }
 
-        public ImsExecutorFactory() {
-            mThreadHandler = new HandlerThread("ImsHandlerThread");
-            mThreadHandler.start();
-            mHandler = new Handler(mThreadHandler.getLooper());
+    @VisibleForTesting
+    public interface SubscriptionManagerProxy {
+        boolean isValidSubscriptionId(int subId);
+        int[] getSubscriptionIds(int slotIndex);
+        int getDefaultVoicePhoneId();
+        int getIntegerSubscriptionProperty(int subId, String propKey, int defValue);
+        void setSubscriptionProperty(int subId, String propKey, String propValue);
+        int[] getActiveSubscriptionIdList();
+    }
+
+    // Default implementations, which is mocked for testing
+    private static class DefaultSettingsProxy implements SettingsProxy {
+        @Override
+        public int getSecureIntSetting(ContentResolver cr, String name, int def) {
+            return Settings.Secure.getInt(cr, name, def);
         }
 
         @Override
-        public void executeRunnable(Runnable runnable) {
-            mHandler.post(runnable);
+        public boolean putSecureIntSetting(ContentResolver cr, String name, int value) {
+            return Settings.Secure.putInt(cr, name, value);
+        }
+    }
+
+    // Default implementation which is mocked to make static dependency validation easier.
+    private static class DefaultSubscriptionManagerProxy implements SubscriptionManagerProxy {
+
+        private Context mContext;
+
+        public DefaultSubscriptionManagerProxy(Context context) {
+            mContext = context;
         }
 
-        public void destroy() {
-            mThreadHandler.quit();
+        @Override
+        public boolean isValidSubscriptionId(int subId) {
+            return SubscriptionManager.isValidSubscriptionId(subId);
+        }
+
+        @Override
+        public int[] getSubscriptionIds(int slotIndex) {
+            return getSubscriptionManager().getSubscriptionIds(slotIndex);
+        }
+
+        @Override
+        public int getDefaultVoicePhoneId() {
+            return SubscriptionManager.getDefaultVoicePhoneId();
+        }
+
+        @Override
+        public int getIntegerSubscriptionProperty(int subId, String propKey, int defValue) {
+            return SubscriptionManager.getIntegerSubscriptionProperty(subId, propKey, defValue,
+                    mContext);
+        }
+
+        @Override
+        public void setSubscriptionProperty(int subId, String propKey, String propValue) {
+            SubscriptionManager.setSubscriptionProperty(subId, propKey, propValue);
+        }
+
+        @Override
+        public int[] getActiveSubscriptionIdList() {
+            return getSubscriptionManager().getActiveSubscriptionIdList();
+        }
+
+        private SubscriptionManager getSubscriptionManager() {
+            return mContext.getSystemService(SubscriptionManager.class);
+        }
+    }
+
+    /**
+     * Events that will be triggered as part of metrics collection.
+     */
+    public interface ImsStatsCallback {
+        /**
+         * The MmTel capabilities that are enabled have changed.
+         * @param capability The MmTel capability
+         * @param regTech The IMS registration technology associated with the capability.
+         * @param isEnabled {@code true} if the capability is enabled, {@code false} if it is
+         *                  disabled.
+         */
+        void onEnabledMmTelCapabilitiesChanged(
+                @MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
+                @ImsRegistrationImplBase.ImsRegistrationTech int regTech,
+                boolean isEnabled);
+    }
+
+    /**
+     * Internally we will create a FeatureConnector when {@link #getInstance(Context, int)} is
+     * called to keep the MmTelFeatureConnection instance fresh as new SIM cards are
+     * inserted/removed and MmTelFeature potentially changes.
+     * <p>
+     * For efficiency purposes, there is only one ImsManager created per-slot when using
+     * {@link #getInstance(Context, int)} and the same instance is returned for multiple callers.
+     * This is due to the ImsManager being a potentially heavyweight object depending on what it is
+     * being used for.
+     */
+    private static class InstanceManager implements FeatureConnector.Listener<ImsManager> {
+        // If this is the first time connecting, wait a small amount of time in case IMS has already
+        // connected. Otherwise, ImsManager will become ready when the ImsService is connected.
+        private static final int CONNECT_TIMEOUT_MS = 50;
+
+        private final FeatureConnector<ImsManager> mConnector;
+        private final ImsManager mImsManager;
+
+        private final Object mLock = new Object();
+        private boolean isConnectorActive = false;
+        private CountDownLatch mConnectedLatch;
+
+        public InstanceManager(ImsManager manager) {
+            mImsManager = manager;
+            // Set a special prefix so that logs generated by getInstance are distinguishable.
+            mImsManager.mLogTagPostfix = "IM";
+
+            ArrayList<Integer> readyFilter = new ArrayList<>();
+            readyFilter.add(ImsFeature.STATE_READY);
+            readyFilter.add(ImsFeature.STATE_INITIALIZING);
+            readyFilter.add(ImsFeature.STATE_UNAVAILABLE);
+            // Pass a reference of the ImsManager being managed into the connector, allowing it to
+            // update the internal MmTelFeatureConnection as it is being updated.
+            mConnector = new FeatureConnector<>(manager.mContext, manager.mPhoneId,
+                    (c,p) -> mImsManager, "InstanceManager", readyFilter, this,
+                    manager.getImsThreadExecutor());
+        }
+
+        public ImsManager getInstance() {
+            return mImsManager;
+        }
+
+        public void reconnect() {
+            boolean requiresReconnect = false;
+            synchronized (mLock) {
+                if (!isConnectorActive) {
+                    requiresReconnect = true;
+                    isConnectorActive = true;
+                    mConnectedLatch = new CountDownLatch(1);
+                }
+            }
+            if (requiresReconnect) {
+                mConnector.connect();
+            }
+            try {
+                // If this is during initial reconnect, let all threads wait for connect
+                // (or timeout)
+                mConnectedLatch.await(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // Do nothing and allow ImsService to attach behind the scenes
+            }
+        }
+
+        @Override
+        public void connectionReady(ImsManager manager) {
+            synchronized (mLock) {
+                mConnectedLatch.countDown();
+            }
+        }
+
+        @Override
+        public void connectionUnavailable(int reason) {
+            synchronized (mLock) {
+                // only need to track the connection becoming unavailable due to telephony going
+                // down.
+                if (reason == FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE) {
+                    isConnectorActive = false;
+                }
+                mConnectedLatch.countDown();
+            }
+
         }
     }
 
     // Replaced with single-threaded executor for testing.
-    @VisibleForTesting
-    public ExecutorFactory mExecutorFactory = new ImsExecutorFactory();
-
-    private static HashMap<Integer, ImsManager> sImsManagerInstances =
-            new HashMap<Integer, ImsManager>();
+    private final Executor mExecutor;
+    // Replaced With mock for testing
+    private MmTelFeatureConnectionFactory mMmTelFeatureConnectionFactory =
+            MmTelFeatureConnection::new;
+    private final SubscriptionManagerProxy mSubscriptionManagerProxy;
+    private final SettingsProxy mSettingsProxy;
 
     private Context mContext;
     private CarrierConfigManager mConfigManager;
     private int mPhoneId;
-    private @Nullable MmTelFeatureConnection mMmTelFeatureConnection = null;
+    private AtomicReference<MmTelFeatureConnection> mMmTelConnectionRef = new AtomicReference<>();
+    // Used for debug purposes only currently
     private boolean mConfigUpdated = false;
-
+    private BinderCacheManager<ITelephony> mBinderCache;
     private ImsConfigListener mImsConfigListener;
 
-    //TODO: Move these caches into the MmTelFeature Connection and restrict their lifetimes to the
-    // lifetime of the MmTelFeature.
-    // Ut interface for the supplementary service configuration
-    private ImsUt mUt = null;
-    // ECBM interface
-    private ImsEcbm mEcbm = null;
-    private ImsMultiEndpoint mMultiEndpoint = null;
-
-    private Set<FeatureConnection.IFeatureUpdate> mStatusCallbacks = new CopyOnWriteArraySet<>();
-
     public static final String TRUE = "true";
     public static final String FALSE = "false";
+    // Map of phoneId -> InstanceManager
+    private static final SparseArray<InstanceManager> IMS_MANAGER_INSTANCES = new SparseArray<>(2);
+    // Map of phoneId -> ImsStatsCallback
+    private static final SparseArray<ImsStatsCallback> IMS_STATS_CALLBACKS = new SparseArray<>(2);
 
-    // mRecentDisconnectReasons stores the last 16 disconnect reasons
-    private static final int MAX_RECENT_DISCONNECT_REASONS = 16;
-    private ConcurrentLinkedDeque<ImsReasonInfo> mRecentDisconnectReasons =
-            new ConcurrentLinkedDeque<>();
+    // A log prefix added to some instances of ImsManager to make it distinguishable from others.
+    // - "IM" added to ImsManager for ImsManagers created using getInstance.
+    private String mLogTagPostfix = "";
 
     /**
-     * Gets a manager instance.
+     * Gets a manager instance and blocks for a limited period of time, connecting to the
+     * corresponding ImsService MmTelFeature if it exists.
+     * <p>
+     * If the ImsService is unavailable or becomes unavailable, the associated methods will fail and
+     * a new ImsManager will need to be requested. Instead, a {@link FeatureConnector} can be
+     * requested using {@link #getConnector}, which will notify the caller when a new ImsManager is
+     * available.
      *
      * @param context application context for creating the manager object
      * @param phoneId the phone ID for the IMS Service
@@ -274,21 +460,41 @@
      */
     @UnsupportedAppUsage
     public static ImsManager getInstance(Context context, int phoneId) {
-        synchronized (sImsManagerInstances) {
-            if (sImsManagerInstances.containsKey(phoneId)) {
-                ImsManager m = sImsManagerInstances.get(phoneId);
-                // May be null for some tests
-                if (m != null) {
-                    m.connectIfServiceIsAvailable();
-                }
-                return m;
+        InstanceManager instanceManager;
+        synchronized (IMS_MANAGER_INSTANCES) {
+            instanceManager = IMS_MANAGER_INSTANCES.get(phoneId);
+            if (instanceManager == null) {
+                ImsManager m = new ImsManager(context, phoneId);
+                instanceManager = new InstanceManager(m);
+                IMS_MANAGER_INSTANCES.put(phoneId, instanceManager);
             }
-
-            ImsManager mgr = new ImsManager(context, phoneId);
-            sImsManagerInstances.put(phoneId, mgr);
-
-            return mgr;
         }
+        // If the ImsManager became disconnected for some reason, try to reconnect it now.
+        instanceManager.reconnect();
+        return instanceManager.getInstance();
+    }
+
+    /**
+     * Retrieve an FeatureConnector for ImsManager, which allows a Listener to listen for when
+     * the ImsManager becomes available or unavailable due to the ImsService MmTelFeature moving to
+     * the READY state or destroyed on a specific phone modem index.
+     *
+     * @param context The Context that will be used to connect the ImsManager.
+     * @param phoneId The modem phone ID that the ImsManager will be created for.
+     * @param logPrefix The log prefix used for debugging purposes.
+     * @param listener The Listener that will deliver ImsManager updates as it becomes available.
+     * @param executor The Executor that the Listener callbacks will be called on.
+     * @return A FeatureConnector instance for generating ImsManagers as the associated
+     * MmTelFeatures become available.
+     */
+    public static FeatureConnector<ImsManager> getConnector(Context context,
+            int phoneId, String logPrefix, FeatureConnector.Listener<ImsManager> listener,
+            Executor executor) {
+        // Only listen for the READY state from the MmTelFeature here.
+        ArrayList<Integer> readyFilter = new ArrayList<>();
+        readyFilter.add(ImsFeature.STATE_READY);
+        return new FeatureConnector<>(context, phoneId, ImsManager::new, logPrefix, readyFilter,
+                listener, executor);
     }
 
     public static boolean isImsSupportedOnDevice(Context context) {
@@ -296,15 +502,40 @@
     }
 
     /**
+     * Sets the callback that will be called when events related to IMS metric collection occur.
+     * <p>
+     * Note: Subsequent calls to this method will replace the previous stats callback.
+     */
+    public static void setImsStatsCallback(int phoneId, ImsStatsCallback cb) {
+        synchronized (IMS_STATS_CALLBACKS) {
+            if (cb == null) {
+                IMS_STATS_CALLBACKS.remove(phoneId);
+            } else {
+                IMS_STATS_CALLBACKS.put(phoneId, cb);
+            }
+        }
+    }
+
+    /**
+     * @return the {@link ImsStatsCallback} instance associated with the provided phoneId or
+     * {@link null} if none currently exists.
+     */
+    private static ImsStatsCallback getStatsCallback(int phoneId) {
+        synchronized (IMS_STATS_CALLBACKS) {
+            return IMS_STATS_CALLBACKS.get(phoneId);
+        }
+    }
+
+    /**
      * Returns the user configuration of Enhanced 4G LTE Mode setting.
      *
      * @deprecated Doesn't support MSIM devices. Use
      * {@link #isEnhanced4gLteModeSettingEnabledByUser()} instead.
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static boolean isEnhanced4gLteModeSettingEnabledByUser(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isEnhanced4gLteModeSettingEnabledByUser();
         }
@@ -316,25 +547,28 @@
     /**
      * Returns the user configuration of Enhanced 4G LTE Mode setting for slot. If the option is
      * not editable ({@link CarrierConfigManager#KEY_EDITABLE_ENHANCED_4G_LTE_BOOL} is false),
-     * hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true), or
-     * the setting is not initialized, this method will return default value specified by
-     * {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}.
+     * hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true), the setting is
+     * not initialized, and VoIMS opt-in status disabled, this method will return default value
+     * specified by {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}.
      *
      * Note that even if the setting was set, it may no longer be editable. If this is the case we
      * return the default value.
      */
     public boolean isEnhanced4gLteModeSettingEnabledByUser() {
-        int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+        int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
                 getSubId(), SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
-                SUB_PROPERTY_NOT_INITIALIZED, mContext);
+                SUB_PROPERTY_NOT_INITIALIZED);
         boolean onByDefault = getBooleanCarrierConfig(
                 CarrierConfigManager.KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL);
+        boolean isUiUnEditable =
+                !getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL)
+                || getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL);
+        boolean isSettingNotInitialized = setting == SUB_PROPERTY_NOT_INITIALIZED;
 
-        // If Enhanced 4G LTE Mode is uneditable, hidden or not initialized, we use the default
-        // value
-        if (!getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL)
-                || getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL)
-                || setting == SUB_PROPERTY_NOT_INITIALIZED) {
+        // If Enhanced 4G LTE Mode is uneditable, hidden, not initialized and VoIMS opt-in disabled
+        // we use the default value. If VoIMS opt-in is enabled, we will always allow the user to
+        // change the IMS enabled setting.
+        if ((isUiUnEditable || isSettingNotInitialized) && !isVoImsOptInEnabled()) {
             return onByDefault;
         } else {
             return (setting == ProvisioningManager.PROVISIONING_VALUE_ENABLED);
@@ -348,8 +582,8 @@
      * instead.
      */
     public static void setEnhanced4gLteModeSetting(Context context, boolean enabled) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.setEnhanced4gLteModeSetting(enabled);
         }
@@ -358,9 +592,9 @@
 
     /**
      * Change persistent Enhanced 4G LTE Mode setting. If the option is not editable
-     * ({@link CarrierConfigManager#KEY_EDITABLE_ENHANCED_4G_LTE_BOOL} is false)
-     * or hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true),
-     * this method will set the setting to the default value specified by
+     * ({@link CarrierConfigManager#KEY_EDITABLE_ENHANCED_4G_LTE_BOOL} is false),
+     * hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true), and VoIMS opt-in
+     * status disabled, this method will set the setting to the default value specified by
      * {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}.
      */
     public void setEnhanced4gLteModeSetting(boolean enabled) {
@@ -369,35 +603,50 @@
             return;
         }
         int subId = getSubId();
+        if (!isSubIdValid(subId)) {
+            loge("setEnhanced4gLteModeSetting: invalid sub id, can not set property in " +
+                    " siminfo db; subId=" + subId);
+            return;
+        }
         // If editable=false or hidden=true, we must keep default advanced 4G mode.
-        if (!getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL) ||
-                getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL)) {
+        boolean isUiUnEditable =
+                !getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL) ||
+                        getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL);
+
+        // If VoIMS opt-in is enabled, we will always allow the user to change the IMS enabled
+        // setting.
+        if (isUiUnEditable && !isVoImsOptInEnabled()) {
             enabled = getBooleanCarrierConfig(
                     CarrierConfigManager.KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL);
         }
 
-        int prevSetting = SubscriptionManager.getIntegerSubscriptionProperty(subId,
-                SubscriptionManager.ENHANCED_4G_MODE_ENABLED, SUB_PROPERTY_NOT_INITIALIZED,
-                mContext);
+        int prevSetting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(subId,
+                SubscriptionManager.ENHANCED_4G_MODE_ENABLED, SUB_PROPERTY_NOT_INITIALIZED);
 
-        if (prevSetting != (enabled ?
-                ProvisioningManager.PROVISIONING_VALUE_ENABLED :
+        if (prevSetting == (enabled ? ProvisioningManager.PROVISIONING_VALUE_ENABLED :
                 ProvisioningManager.PROVISIONING_VALUE_DISABLED)) {
-            if (isSubIdValid(subId)) {
-                SubscriptionManager.setSubscriptionProperty(subId,
-                        SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
-                        booleanToPropertyString(enabled));
+            // No change in setting.
+            return;
+        }
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
+                booleanToPropertyString(enabled));
+        try {
+            if (enabled) {
+                CapabilityChangeRequest request = new CapabilityChangeRequest();
+                boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
+                // This affects voice and video enablement
+                updateVoiceCellFeatureValue(request, isNonTty);
+                updateVideoCallFeatureValue(request, isNonTty);
+                changeMmTelCapability(request);
+                // Ensure IMS is on if this setting is enabled.
+                turnOnIms();
             } else {
-                loge("setEnhanced4gLteModeSetting: invalid sub id, can not set property in " +
-                        " siminfo db; subId=" + subId);
+                // This may trigger entire IMS interface to be disabled, so recalculate full state.
+                reevaluateCapabilities();
             }
-            if (isNonTtyOrTtyOnVolteEnabled()) {
-                try {
-                    setAdvanced4GMode(enabled);
-                } catch (ImsException ie) {
-                    // do nothing
-                }
-            }
+        } catch (ImsException e) {
+            loge("setEnhanced4gLteModeSetting couldn't set config: " + e);
         }
     }
 
@@ -407,10 +656,10 @@
      * @deprecated Does not support MSIM devices. Please use
      * {@link #isNonTtyOrTtyOnVolteEnabled()} instead.
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static boolean isNonTtyOrTtyOnVolteEnabled(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isNonTtyOrTtyOnVolteEnabled();
         }
@@ -444,10 +693,10 @@
      * @deprecated Does not support MSIM devices. Please use
      * {@link #isVolteEnabledByPlatform()} instead.
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static boolean isVolteEnabledByPlatform(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isVolteEnabledByPlatform();
         }
@@ -460,8 +709,9 @@
      * supported.
      */
     public void isSupported(int capability, int transportType, Consumer<Boolean> result) {
-        mExecutorFactory.executeRunnable(() -> {
+        getImsThreadExecutor().execute(() -> {
             switch(transportType) {
+                // Does not take into account NR, as NR is a superset of LTE support currently.
                 case (AccessNetworkConstants.TRANSPORT_TYPE_WWAN): {
                     switch (capability) {
                         case (MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE): {
@@ -523,6 +773,11 @@
             return true;
         }
 
+        if (getLocalImsConfigKeyInt(KEY_VOIMS_OPT_IN_STATUS)
+                == ProvisioningManager.PROVISIONING_VALUE_ENABLED) {
+            return true;
+        }
+
         return mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_device_volte_available)
                 && getBooleanCarrierConfig(CarrierConfigManager.KEY_CARRIER_VOLTE_AVAILABLE_BOOL)
@@ -530,14 +785,27 @@
     }
 
     /**
+     * @return {@code true} if IMS over NR is enabled by the platform, {@code false} otherwise.
+     */
+    public boolean isImsOverNrEnabledByPlatform() {
+        int[] nrCarrierCaps = getIntArrayCarrierConfig(
+                CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY);
+        if (nrCarrierCaps == null)  return false;
+        boolean voNrCarrierSupported = Arrays.stream(nrCarrierCaps)
+                .anyMatch(cap -> cap == CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA);
+        if (!voNrCarrierSupported) return false;
+        return isGbaValid();
+    }
+
+    /**
      * Indicates whether VoLTE is provisioned on device.
      *
      * @deprecated Does not support MSIM devices. Please use
      * {@link #isVolteProvisionedOnDevice()} instead.
      */
     public static boolean isVolteProvisionedOnDevice(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isVolteProvisionedOnDevice();
         }
@@ -579,8 +847,8 @@
      * {@link #isWfcProvisionedOnDevice()} instead.
      */
     public static boolean isWfcProvisionedOnDevice(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isWfcProvisionedOnDevice();
         }
@@ -617,8 +885,8 @@
      * {@link #isVtProvisionedOnDevice()} instead.
      */
     public static boolean isVtProvisionedOnDevice(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isVtProvisionedOnDevice();
         }
@@ -648,8 +916,8 @@
      * {@link #isVtEnabledByPlatform()} instead.
      */
     public static boolean isVtEnabledByPlatform(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isVtEnabledByPlatform();
         }
@@ -685,8 +953,8 @@
      * {@link #isVtEnabledByUser()} instead.
      */
     public static boolean isVtEnabledByUser(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isVtEnabledByUser();
         }
@@ -699,9 +967,9 @@
      * returns true as default value.
      */
     public boolean isVtEnabledByUser() {
-        int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+        int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
                 getSubId(), SubscriptionManager.VT_IMS_ENABLED,
-                SUB_PROPERTY_NOT_INITIALIZED, mContext);
+                SUB_PROPERTY_NOT_INITIALIZED);
 
         // If it's never set, by default we return true.
         return (setting == SUB_PROPERTY_NOT_INITIALIZED
@@ -709,13 +977,25 @@
     }
 
     /**
+     * Returns whether the user sets call composer setting per sub.
+     */
+    public boolean isCallComposerEnabledByUser() {
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        if (tm == null) {
+            loge("isCallComposerEnabledByUser: TelephonyManager is null, returning false");
+            return false;
+        }
+        return tm.getCallComposerStatus() == TelephonyManager.CALL_COMPOSER_STATUS_ON;
+    }
+
+    /**
      * Change persistent VT enabled setting
      *
      * @deprecated Does not support MSIM devices. Please use {@link #setVtSetting(boolean)} instead.
      */
     public static void setVtSetting(Context context, boolean enabled) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.setVtSetting(enabled);
         }
@@ -732,26 +1012,23 @@
         }
 
         int subId = getSubId();
-        if (isSubIdValid(subId)) {
-            SubscriptionManager.setSubscriptionProperty(subId, SubscriptionManager.VT_IMS_ENABLED,
-                    booleanToPropertyString(enabled));
-        } else {
+        if (!isSubIdValid(subId)) {
             loge("setVtSetting: sub id invalid, skip modifying vt state in subinfo db; subId="
                     + subId);
+            return;
         }
-
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId, SubscriptionManager.VT_IMS_ENABLED,
+                booleanToPropertyString(enabled));
         try {
-            changeMmTelCapability(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
-                    ImsRegistrationImplBase.REGISTRATION_TECH_LTE, enabled);
-
             if (enabled) {
-                log("setVtSetting(b) : turnOnIms");
+                CapabilityChangeRequest request = new CapabilityChangeRequest();
+                updateVideoCallFeatureValue(request, isNonTtyOrTtyOnVolteEnabled());
+                changeMmTelCapability(request);
+                // ensure IMS is enabled.
                 turnOnIms();
-            } else if (isTurnOffImsAllowedByPlatform()
-                    && (!isVolteEnabledByPlatform()
-                    || !isEnhanced4gLteModeSettingEnabledByUser())) {
-                log("setVtSetting(b) : imsServiceAllowTurnOff -> turnOffIms");
-                turnOffIms();
+            } else {
+                // This may cause IMS to be disabled, re-evaluate all.
+                reevaluateCapabilities();
             }
         } catch (ImsException e) {
             // The ImsService is down. Since the SubscriptionManager already recorded the user's
@@ -764,23 +1041,6 @@
     /**
      * Returns whether turning off ims is allowed by platform.
      * The platform property may override the carrier config.
-     *
-     * @deprecated Does not support MSIM devices. Please use
-     * {@link #isTurnOffImsAllowedByPlatform()} instead.
-     */
-    private static boolean isTurnOffImsAllowedByPlatform(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
-        if (mgr != null) {
-            return mgr.isTurnOffImsAllowedByPlatform();
-        }
-        Rlog.e(TAG, "isTurnOffImsAllowedByPlatform: ImsManager null, returning default value.");
-        return true;
-    }
-
-    /**
-     * Returns whether turning off ims is allowed by platform.
-     * The platform property may override the carrier config.
      */
     private boolean isTurnOffImsAllowedByPlatform() {
         // We first read the per slot value. If doesn't exist, we read the general value. If still
@@ -803,8 +1063,8 @@
      * {@link #isWfcEnabledByUser()} instead.
      */
     public static boolean isWfcEnabledByUser(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isWfcEnabledByUser();
         }
@@ -817,9 +1077,9 @@
      * queries CarrierConfig value as default.
      */
     public boolean isWfcEnabledByUser() {
-        int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+        int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
                 getSubId(), SubscriptionManager.WFC_IMS_ENABLED,
-                SUB_PROPERTY_NOT_INITIALIZED, mContext);
+                SUB_PROPERTY_NOT_INITIALIZED);
 
         // SUB_PROPERTY_NOT_INITIALIZED indicates it's never set in sub db.
         if (setting == SUB_PROPERTY_NOT_INITIALIZED) {
@@ -836,8 +1096,8 @@
      * {@link #setWfcSetting} instead.
      */
     public static void setWfcSetting(Context context, boolean enabled) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.setWfcSetting(enabled);
         }
@@ -852,20 +1112,29 @@
             log("setWfcSetting: Not possible to enable WFC due to provisioning.");
             return;
         }
-
         int subId = getSubId();
-        if (isSubIdValid(subId)) {
-            SubscriptionManager.setSubscriptionProperty(subId, SubscriptionManager.WFC_IMS_ENABLED,
-                    booleanToPropertyString(enabled));
-        } else {
+        if (!isSubIdValid(subId)) {
             loge("setWfcSetting: invalid sub id, can not set WFC setting in siminfo db; subId="
                     + subId);
+            return;
         }
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.WFC_IMS_ENABLED, booleanToPropertyString(enabled));
 
-        TelephonyManager tm = (TelephonyManager)
-                mContext.getSystemService(Context.TELEPHONY_SERVICE);
-        boolean isRoaming = tm.isNetworkRoaming(subId);
-        setWfcNonPersistent(enabled, getWfcMode(isRoaming));
+        try {
+            if (enabled) {
+                CapabilityChangeRequest request = new CapabilityChangeRequest();
+                updateVoiceWifiFeatureAndProvisionedValues(request);
+                changeMmTelCapability(request);
+                // Ensure IMS is on if this setting is updated.
+                turnOnIms();
+            } else {
+                // This may cause IMS to be disabled, re-evaluate all caps
+                reevaluateCapabilities();
+            }
+        } catch (ImsException e) {
+            loge("setWfcSetting: " + e);
+        }
     }
 
     /**
@@ -880,28 +1149,23 @@
         // Force IMS to register over LTE when turning off WFC
         int imsWfcModeFeatureValue =
                 enabled ? wfcMode : ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED;
-
         try {
-            changeMmTelCapability(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
-                    ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, enabled);
-
+            changeMmTelCapability(enabled, MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
             // Set the mode and roaming enabled settings before turning on IMS
             setWfcModeInternal(imsWfcModeFeatureValue);
             // If enabled is false, shortcut to false because of the ImsService
             // implementation for WFC roaming, otherwise use the correct user's setting.
             setWfcRoamingSettingInternal(enabled && isWfcRoamingEnabledByUser());
-
+            // Do not re-evaluate all capabilities because this is a temporary override of WFC
+            // settings.
             if (enabled) {
-                log("setWfcSetting() : turnOnIms");
+                log("setWfcNonPersistent() : turnOnIms");
+                // Ensure IMS is turned on if this is enabled.
                 turnOnIms();
-            } else if (isTurnOffImsAllowedByPlatform()
-                    && (!isVolteEnabledByPlatform()
-                    || !isEnhanced4gLteModeSettingEnabledByUser())) {
-                log("setWfcSetting() : imsServiceAllowTurnOff -> turnOffIms");
-                turnOffIms();
             }
         } catch (ImsException e) {
-            loge("setWfcSetting(): ", e);
+            loge("setWfcNonPersistent(): ", e);
         }
     }
 
@@ -911,8 +1175,8 @@
      * @deprecated Doesn't support MSIM devices. Use {@link #getWfcMode(boolean roaming)} instead.
      */
     public static int getWfcMode(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.getWfcMode();
         }
@@ -934,8 +1198,8 @@
      * @deprecated Doesn't support MSIM devices. Use {@link #setWfcMode(int)} instead.
      */
     public static void setWfcMode(Context context, int wfcMode) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.setWfcMode(wfcMode);
         }
@@ -958,8 +1222,8 @@
      * @deprecated Doesn't support MSIM devices. Use {@link #getWfcMode(boolean)} instead.
      */
     public static int getWfcMode(Context context, boolean roaming) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.getWfcMode(roaming);
         }
@@ -1011,8 +1275,8 @@
      */
     private int getSettingFromSubscriptionManager(String subSetting, String defaultConfigKey) {
         int result;
-        result = SubscriptionManager.getIntegerSubscriptionProperty(getSubId(), subSetting,
-                SUB_PROPERTY_NOT_INITIALIZED, mContext);
+        result = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(getSubId(), subSetting,
+                SUB_PROPERTY_NOT_INITIALIZED);
 
         // SUB_PROPERTY_NOT_INITIALIZED indicates it's never set in sub db.
         if (result == SUB_PROPERTY_NOT_INITIALIZED) {
@@ -1030,8 +1294,8 @@
      * instead.
      */
     public static void setWfcMode(Context context, int wfcMode, boolean roaming) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.setWfcMode(wfcMode, roaming);
         }
@@ -1048,11 +1312,11 @@
         if (isSubIdValid(subId)) {
             if (!roaming) {
                 if (DBG) log("setWfcMode(i,b) - setting=" + wfcMode);
-                SubscriptionManager.setSubscriptionProperty(subId, SubscriptionManager.WFC_IMS_MODE,
+                mSubscriptionManagerProxy.setSubscriptionProperty(subId, SubscriptionManager.WFC_IMS_MODE,
                         Integer.toString(wfcMode));
             } else {
                 if (DBG) log("setWfcMode(i,b) (roaming) - setting=" + wfcMode);
-                SubscriptionManager.setSubscriptionProperty(subId,
+                mSubscriptionManagerProxy.setSubscriptionProperty(subId,
                         SubscriptionManager.WFC_IMS_ROAMING_MODE, Integer.toString(wfcMode));
             }
         } else {
@@ -1062,16 +1326,21 @@
 
         TelephonyManager tm = (TelephonyManager)
                 mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        if (tm == null) {
+            loge("setWfcMode: TelephonyManager is null, can not set WFC.");
+            return;
+        }
+        tm = tm.createForSubscriptionId(getSubId());
         // Unfortunately, the WFC mode is the same for home/roaming (we do not have separate
         // config keys), so we have to change the WFC mode when moving home<->roaming. So, only
         // call setWfcModeInternal when roaming == telephony roaming status. Otherwise, ignore.
-        if (roaming == tm.isNetworkRoaming(getSubId())) {
+        if (roaming == tm.isNetworkRoaming()) {
             setWfcModeInternal(wfcMode);
         }
     }
 
     private int getSubId() {
-        int[] subIds = SubscriptionManager.getSubId(mPhoneId);
+        int[] subIds = mSubscriptionManagerProxy.getSubscriptionIds(mPhoneId);
         int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
         if (subIds != null && subIds.length >= 1) {
             subId = subIds[0];
@@ -1081,7 +1350,7 @@
 
     private void setWfcModeInternal(int wfcMode) {
         final int value = wfcMode;
-        mExecutorFactory.executeRunnable(() -> {
+        getImsThreadExecutor().execute(() -> {
             try {
                 getConfigInterface().setConfig(
                         ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE, value);
@@ -1098,8 +1367,8 @@
      * {@link #isWfcRoamingEnabledByUser()} instead.
      */
     public static boolean isWfcRoamingEnabledByUser(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isWfcRoamingEnabledByUser();
         }
@@ -1112,9 +1381,9 @@
      * queries CarrierConfig value as default.
      */
     public boolean isWfcRoamingEnabledByUser() {
-        int setting =  SubscriptionManager.getIntegerSubscriptionProperty(
+        int setting =  mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
                 getSubId(), SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
-                SUB_PROPERTY_NOT_INITIALIZED, mContext);
+                SUB_PROPERTY_NOT_INITIALIZED);
         if (setting == SUB_PROPERTY_NOT_INITIALIZED) {
             return getBooleanCarrierConfig(
                             CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL);
@@ -1127,8 +1396,8 @@
      * Change persistent WFC roaming enabled setting
      */
     public static void setWfcRoamingSetting(Context context, boolean enabled) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.setWfcRoamingSetting(enabled);
         }
@@ -1139,7 +1408,7 @@
      * Change persistent WFC roaming enabled setting
      */
     public void setWfcRoamingSetting(boolean enabled) {
-        SubscriptionManager.setSubscriptionProperty(getSubId(),
+        mSubscriptionManagerProxy.setSubscriptionProperty(getSubId(),
                 SubscriptionManager.WFC_IMS_ROAMING_ENABLED, booleanToPropertyString(enabled)
         );
 
@@ -1150,7 +1419,7 @@
         final int value = enabled
                 ? ProvisioningManager.PROVISIONING_VALUE_ENABLED
                 : ProvisioningManager.PROVISIONING_VALUE_DISABLED;
-        mExecutorFactory.executeRunnable(() -> {
+        getImsThreadExecutor().execute(() -> {
             try {
                 getConfigInterface().setConfig(
                         ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE, value);
@@ -1169,8 +1438,8 @@
      * instead.
      */
     public static boolean isWfcEnabledByPlatform(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             return mgr.isWfcEnabledByPlatform();
         }
@@ -1221,8 +1490,13 @@
     private boolean isGbaValid() {
         if (getBooleanCarrierConfig(
                 CarrierConfigManager.KEY_CARRIER_IMS_GBA_REQUIRED_BOOL)) {
-            final TelephonyManager telephonyManager = new TelephonyManager(mContext, getSubId());
-            String efIst = telephonyManager.getIsimIst();
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+            if (tm == null) {
+                loge("isGbaValid: TelephonyManager is null, returning false.");
+                return false;
+            }
+            tm = tm.createForSubscriptionId(getSubId());
+            String efIst = tm.getIsimIst();
             if (efIst == null) {
                 loge("isGbaValid - ISF is NULL");
                 return true;
@@ -1292,109 +1566,101 @@
     }
 
     /**
-     * Sync carrier config and user settings with ImsConfigImplBase implementation.
-     *
-     * @param context for the manager object
-     * @param phoneId phone id
-     * @param force update
-     *
-     * @deprecated Doesn't support MSIM devices. Use {@link #updateImsServiceConfig(boolean)}
-     * instead.
+     * Push configuration updates to the ImsService implementation.
      */
-    public static void updateImsServiceConfig(Context context, int phoneId, boolean force) {
-        ImsManager mgr = ImsManager.getInstance(context, phoneId);
-        if (mgr != null) {
-            mgr.updateImsServiceConfig(force);
+    public void updateImsServiceConfig() {
+        try {
+            int subId = getSubId();
+            if (!isSubIdValid(subId)) {
+                loge("updateImsServiceConfig: invalid sub id, skipping!");
+                return;
+            }
+            PersistableBundle imsCarrierConfigs =
+                    mConfigManager.getConfigByComponentForSubId(
+                            CarrierConfigManager.Ims.KEY_PREFIX, subId);
+            updateImsCarrierConfigs(imsCarrierConfigs);
+            reevaluateCapabilities();
+            mConfigUpdated = true;
+        } catch (ImsException e) {
+            loge("updateImsServiceConfig: ", e);
+            mConfigUpdated = false;
         }
-        Rlog.e(TAG, "updateImsServiceConfig: ImsManager null, returning without update.");
     }
 
     /**
-     * Sync carrier config and user settings with ImsConfigImplBase implementation.
-     *
-     * @param force update
+     * Evaluate the state of the IMS capabilities and push the updated state to the ImsService.
      */
-    public void updateImsServiceConfig(boolean force) {
-        if (!force) {
-            TelephonyManager tm = new TelephonyManager(mContext, getSubId());
-            if (tm.getSimState() != TelephonyManager.SIM_STATE_READY) {
-                log("updateImsServiceConfig: SIM not ready");
-                // Don't disable IMS if SIM is not ready
-                return;
-            }
-        }
+    private void reevaluateCapabilities() throws ImsException {
+        logi("reevaluateCapabilities");
+        CapabilityChangeRequest request = new CapabilityChangeRequest();
+        boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
+        updateVoiceCellFeatureValue(request, isNonTty);
+        updateVoiceWifiFeatureAndProvisionedValues(request);
+        updateVideoCallFeatureValue(request, isNonTty);
+        updateCallComposerFeatureValue(request);
+        // Only turn on IMS for RTT if there's an active subscription present. If not, the
+        // modem will be in emergency-call-only mode and will use separate signaling to
+        // establish an RTT emergency call.
+        boolean isImsNeededForRtt = updateRttConfigValue() && isActiveSubscriptionPresent();
+        // Supplementary services over UT do not require IMS registration. Do not alter IMS
+        // registration based on UT.
+        updateUtFeatureValue(request);
 
-        if (!mConfigUpdated || force) {
-            try {
-                PersistableBundle imsCarrierConfigs =
-                        mConfigManager.getConfigByComponentForSubId(
-                                CarrierConfigManager.Ims.KEY_PREFIX, getSubId());
+        // Send the batched request to the modem.
+        changeMmTelCapability(request);
 
-                updateImsCarrierConfigs(imsCarrierConfigs);
-
-                // Note: currently the order of updates is set to produce different order of
-                // changeEnabledCapabilities() function calls from setAdvanced4GMode(). This is done
-                // to differentiate this code path from vendor code perspective.
-                CapabilityChangeRequest request = new CapabilityChangeRequest();
-                updateVolteFeatureValue(request);
-                updateWfcFeatureAndProvisionedValues(request);
-                updateVideoCallFeatureValue(request);
-                // Only turn on IMS for RTT if there's an active subscription present. If not, the
-                // modem will be in emergency-call-only mode and will use separate signaling to
-                // establish an RTT emergency call.
-                boolean isImsNeededForRtt = updateRttConfigValue() && isActiveSubscriptionPresent();
-                // Supplementary services over UT do not require IMS registration. Do not alter IMS
-                // registration based on UT.
-                updateUtFeatureValue(request);
-
-                // Send the batched request to the modem.
-                changeMmTelCapability(request);
-
-                if (isImsNeededForRtt || !isTurnOffImsAllowedByPlatform() || isImsNeeded(request)) {
-                    // Turn on IMS if it is used.
-                    // Also, if turning off is not allowed for current carrier,
-                    // we need to turn IMS on because it might be turned off before
-                    // phone switched to current carrier.
-                    log("updateImsServiceConfig: turnOnIms");
-                    turnOnIms();
-                } else {
-                    // Turn off IMS if it is not used AND turning off is allowed for carrier.
-                    log("updateImsServiceConfig: turnOffIms");
-                    turnOffIms();
-                }
-
-                mConfigUpdated = true;
-            } catch (ImsException e) {
-                loge("updateImsServiceConfig: ", e);
-                mConfigUpdated = false;
-            }
+        if (isImsNeededForRtt || !isTurnOffImsAllowedByPlatform() || isImsNeeded(request)) {
+            // Turn on IMS if it is used.
+            // Also, if turning off is not allowed for current carrier,
+            // we need to turn IMS on because it might be turned off before
+            // phone switched to current carrier.
+            log("reevaluateCapabilities: turnOnIms");
+            turnOnIms();
+        } else {
+            // Turn off IMS if it is not used AND turning off is allowed for carrier.
+            log("reevaluateCapabilities: turnOffIms");
+            turnOffIms();
         }
     }
 
+    /**
+     * @return {@code true} if IMS needs to be turned on for the request, {@code false} if it can
+     * be disabled.
+     */
     private boolean isImsNeeded(CapabilityChangeRequest r) {
-        // IMS is not needed for UT, so only enabled IMS if any other capability is enabled.
         return r.getCapabilitiesToEnable().stream()
-                .anyMatch((c) ->
-                        (c.getCapability() != MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT));
+                .anyMatch(c -> isImsNeededForCapability(c.getCapability()));
+    }
+
+    /**
+     * @return {@code true} if IMS needs to be turned on for the capability.
+     */
+    private boolean isImsNeededForCapability(int capability) {
+        // UT does not require IMS to be enabled.
+        return capability != MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT &&
+                // call composer is used as part of calling, so it should not trigger the enablement
+                // of IMS.
+                capability != MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER;
     }
 
     /**
      * Update VoLTE config
      */
-    private void updateVolteFeatureValue(CapabilityChangeRequest request) {
+    private void updateVoiceCellFeatureValue(CapabilityChangeRequest request, boolean isNonTty) {
         boolean available = isVolteEnabledByPlatform();
         boolean enabled = isEnhanced4gLteModeSettingEnabledByUser();
-        boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
         boolean isProvisioned = isVolteProvisionedOnDevice();
-        boolean isFeatureOn = available && enabled && isNonTty && isProvisioned;
+        boolean voLteFeatureOn = available && enabled && isNonTty && isProvisioned;
+        boolean voNrAvailable = isImsOverNrEnabledByPlatform();
 
-        log("updateVolteFeatureValue: available = " + available
+        log("updateVoiceCellFeatureValue: available = " + available
                 + ", enabled = " + enabled
                 + ", nonTTY = " + isNonTty
                 + ", provisioned = " + isProvisioned
-                + ", isFeatureOn = " + isFeatureOn);
+                + ", voLteFeatureOn = " + voLteFeatureOn
+                + ", voNrAvailable = " + voNrAvailable);
 
-        if (isFeatureOn) {
+        if (voLteFeatureOn) {
             request.addCapabilitiesToEnableForTech(
                     MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
                     ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
@@ -1403,46 +1669,80 @@
                     MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
                     ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
         }
+        if (voLteFeatureOn && voNrAvailable) {
+            request.addCapabilitiesToEnableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+        } else {
+            request.addCapabilitiesToDisableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+        }
     }
 
     /**
-     * Update video call over LTE config
+     * Update video call configuration
      */
-    private void updateVideoCallFeatureValue(CapabilityChangeRequest request) {
+    private void updateVideoCallFeatureValue(CapabilityChangeRequest request, boolean isNonTty) {
         boolean available = isVtEnabledByPlatform();
-        boolean enabled = isVtEnabledByUser();
-        boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
+        boolean vtEnabled = isVtEnabledByUser();
+        boolean advancedEnabled = isEnhanced4gLteModeSettingEnabledByUser();
         boolean isDataEnabled = isDataEnabled();
         boolean ignoreDataEnabledChanged = getBooleanCarrierConfig(
                 CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
         boolean isProvisioned = isVtProvisionedOnDevice();
-        boolean isFeatureOn = available && enabled && isNonTty && isProvisioned
-                && (ignoreDataEnabledChanged || isDataEnabled);
+        // TODO: Support carrier config setting about if VT settings should be associated with
+        //  advanced calling settings.
+        boolean isLteFeatureOn = available && vtEnabled && isNonTty && isProvisioned
+                && advancedEnabled && (ignoreDataEnabledChanged || isDataEnabled);
+        boolean nrAvailable = isImsOverNrEnabledByPlatform();
 
         log("updateVideoCallFeatureValue: available = " + available
-                + ", enabled = " + enabled
+                + ", vtenabled = " + vtEnabled
+                + ", advancedCallEnabled = " + advancedEnabled
                 + ", nonTTY = " + isNonTty
                 + ", data enabled = " + isDataEnabled
                 + ", provisioned = " + isProvisioned
-                + ", isFeatureOn = " + isFeatureOn);
+                + ", isLteFeatureOn = " + isLteFeatureOn
+                + ", nrAvailable = " + nrAvailable);
 
-        if (isFeatureOn) {
+        if (isLteFeatureOn) {
             request.addCapabilitiesToEnableForTech(
                     MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
                     ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+            // VT does not differentiate transport today, do not set IWLAN.
         } else {
             request.addCapabilitiesToDisableForTech(
                     MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
                     ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+            // VT does not differentiate transport today, do not set IWLAN.
+        }
+
+        if (isLteFeatureOn && nrAvailable) {
+            request.addCapabilitiesToEnableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+        } else {
+            request.addCapabilitiesToDisableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_NR);
         }
     }
 
     /**
      * Update WFC config
      */
-    private void updateWfcFeatureAndProvisionedValues(CapabilityChangeRequest request) {
-        TelephonyManager tm = new TelephonyManager(mContext, getSubId());
-        boolean isNetworkRoaming = tm.isNetworkRoaming();
+    private void updateVoiceWifiFeatureAndProvisionedValues(CapabilityChangeRequest request) {
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        boolean isNetworkRoaming =  false;
+        if (tm == null) {
+            loge("updateVoiceWifiFeatureAndProvisionedValues: TelephonyManager is null, assuming"
+                    + " not roaming.");
+        } else {
+            tm = tm.createForSubscriptionId(getSubId());
+            isNetworkRoaming = tm.isNetworkRoaming();
+        }
+
         boolean available = isWfcEnabledByPlatform();
         boolean enabled = isWfcEnabledByUser();
         boolean isProvisioned = isWfcProvisionedOnDevice();
@@ -1483,11 +1783,7 @@
         // Count as "provisioned" if we do not require provisioning.
         boolean isProvisioned = true;
         if (requiresProvisioning) {
-            ITelephony telephony = ITelephony.Stub.asInterface(
-                    TelephonyFrameworkInitializer
-                            .getTelephonyServiceManager()
-                            .getTelephonyServiceRegisterer()
-                            .get());
+            ITelephony telephony = getITelephony();
             // Only track UT over LTE, since we do not differentiate between UT over LTE and IWLAN
             // currently.
             try {
@@ -1518,96 +1814,118 @@
     }
 
     /**
+     * Update call composer capability
+     */
+    private void updateCallComposerFeatureValue(CapabilityChangeRequest request) {
+        boolean isUserSetEnabled = isCallComposerEnabledByUser();
+        boolean isCarrierConfigEnabled = getBooleanCarrierConfig(
+                CarrierConfigManager.KEY_SUPPORTS_CALL_COMPOSER_BOOL);
+
+        boolean isFeatureOn = isUserSetEnabled && isCarrierConfigEnabled;
+        boolean nrAvailable = isImsOverNrEnabledByPlatform();
+
+        log("updateCallComposerFeatureValue: isUserSetEnabled = " + isUserSetEnabled
+                + ", isCarrierConfigEnabled = " + isCarrierConfigEnabled
+                + ", isFeatureOn = " + isFeatureOn
+                + ", nrAvailable = " + nrAvailable);
+
+        if (isFeatureOn) {
+            request.addCapabilitiesToEnableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+                            ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+        } else {
+            request.addCapabilitiesToDisableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+                            ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+        }
+        if (isFeatureOn && nrAvailable) {
+            request.addCapabilitiesToEnableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+        } else {
+            request.addCapabilitiesToDisableForTech(
+                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+                    ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+        }
+    }
+
+    /**
      * Do NOT use this directly, instead use {@link #getInstance(Context, int)}.
      */
-    @VisibleForTesting
-    public ImsManager(Context context, int phoneId) {
+    private ImsManager(Context context, int phoneId) {
         mContext = context;
         mPhoneId = phoneId;
+        mSubscriptionManagerProxy = new DefaultSubscriptionManagerProxy(context);
+        mSettingsProxy = new DefaultSettingsProxy();
         mConfigManager = (CarrierConfigManager) context.getSystemService(
                 Context.CARRIER_CONFIG_SERVICE);
-        createImsService();
+        mExecutor = new LazyExecutor();
+        mBinderCache = new BinderCacheManager<>(ImsManager::getITelephonyInterface);
+        // Start off with an empty MmTelFeatureConnection, which will be replaced one an
+        // ImsService is available (ImsManager expects a non-null FeatureConnection)
+        associate(null /*container*/);
+    }
+
+    /**
+     * Used for testing only to inject dependencies.
+     */
+    @VisibleForTesting
+    public ImsManager(Context context, int phoneId, MmTelFeatureConnectionFactory factory,
+            SubscriptionManagerProxy subManagerProxy, SettingsProxy settingsProxy) {
+        mContext = context;
+        mPhoneId = phoneId;
+        mMmTelFeatureConnectionFactory = factory;
+        mSubscriptionManagerProxy = subManagerProxy;
+        mSettingsProxy = settingsProxy;
+        mConfigManager = (CarrierConfigManager) context.getSystemService(
+                Context.CARRIER_CONFIG_SERVICE);
+        // Do not multithread tests
+        mExecutor = Runnable::run;
+        mBinderCache = new BinderCacheManager<>(ImsManager::getITelephonyInterface);
+        // MmTelFeatureConnection should be replaced for tests with mMmTelFeatureConnectionFactory.
+        associate(null /*container*/);
     }
 
     /*
-     * Returns a flag indicating whether the IMS service is available. If it is not available or
-     * busy, it will try to connect before reporting failure.
+     * Returns a flag indicating whether the IMS service is available.
      */
     public boolean isServiceAvailable() {
-        connectIfServiceIsAvailable();
-        // mImsServiceProxy will always create an ImsServiceProxy.
-        return mMmTelFeatureConnection.isBinderAlive();
+        return mMmTelConnectionRef.get().isBinderAlive();
     }
 
     /*
      * Returns a flag indicating whether the IMS service is ready to send requests to lower layers.
      */
     public boolean isServiceReady() {
-        connectIfServiceIsAvailable();
-        return mMmTelFeatureConnection.isBinderReady();
+        return mMmTelConnectionRef.get().isBinderReady();
     }
 
     /**
-     * If the service is available, try to reconnect.
-     */
-    public void connectIfServiceIsAvailable() {
-        if (mMmTelFeatureConnection == null || !mMmTelFeatureConnection.isBinderAlive()) {
-            createImsService();
-        }
-    }
-
-    public void setConfigListener(ImsConfigListener listener) {
-        mImsConfigListener = listener;
-    }
-
-
-    /**
-     * Adds a callback for status changed events if the binder is already available. If it is not,
-     * this method will throw an ImsException.
-     */
-    @Override
-    @VisibleForTesting
-    public void addNotifyStatusChangedCallbackIfAvailable(FeatureConnection.IFeatureUpdate c)
-            throws android.telephony.ims.ImsException {
-        if (!mMmTelFeatureConnection.isBinderAlive()) {
-            throw new android.telephony.ims.ImsException("Can not connect to ImsService",
-                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
-        }
-        if (c != null) {
-            mStatusCallbacks.add(c);
-        }
-    }
-
-    @Override
-    public void removeNotifyStatusChangedCallback(FeatureConnection.IFeatureUpdate c) {
-        if (c != null) {
-            mStatusCallbacks.remove(c);
-        } else {
-            logw("removeNotifyStatusChangedCallback: callback is null!");
-        }
-    }
-
-    /**
-     * Opens the IMS service for making calls and/or receiving generic IMS calls.
+     * Opens the IMS service for making calls and/or receiving generic IMS calls as well as
+     * register listeners for ECBM, Multiendpoint, and UT if the ImsService supports it.
+     * <p>
      * The caller may make subsequent calls through {@link #makeCall}.
      * The IMS service will register the device to the operator's network with the credentials
      * (from ISIM) periodically in order to receive calls from the operator's network.
      * When the IMS service receives a new call, it will call
      * {@link MmTelFeature.Listener#onIncomingCall}
      * @param listener A {@link MmTelFeature.Listener}, which is the interface the
-     * {@link MmTelFeature} uses to notify the framework of updates
+     * {@link MmTelFeature} uses to notify the framework of updates.
+     * @param ecbmListener Listener used for ECBM indications.
+     * @param multiEndpointListener Listener used for multiEndpoint indications.
      * @throws NullPointerException if {@code listener} is null
      * @throws ImsException if calling the IMS service results in an error
      */
-    public void open(MmTelFeature.Listener listener) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+    public void open(MmTelFeature.Listener listener, ImsEcbmStateListener ecbmListener,
+            ImsExternalCallStateListener multiEndpointListener) throws ImsException {
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
 
         if (listener == null) {
             throw new NullPointerException("listener can't be null");
         }
 
         try {
-            mMmTelFeatureConnection.openConnection(listener);
+            c.openConnection(listener, ecbmListener, multiEndpointListener);
         } catch (RemoteException e) {
             throw new ImsException("open()", e, ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
         }
@@ -1635,14 +1953,14 @@
      * @param listener To listen to IMS registration events; It cannot be null
      * @throws NullPointerException if {@code listener} is null
      * @throws ImsException if calling the IMS service results in an error
-     * @deprecated use {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback)}
-     * instead.
+     * @deprecated use {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback,
+     * Executor)} instead.
      */
     public void addRegistrationListener(ImsConnectionStateListener listener) throws ImsException {
         if (listener == null) {
             throw new NullPointerException("listener can't be null");
         }
-        addRegistrationCallback(listener);
+        addRegistrationCallback(listener, getImsThreadExecutor());
         // connect the ImsConnectionStateListener to the new CapabilityCallback.
         addCapabilitiesCallback(new ImsMmTelManager.CapabilityCallback() {
             @Override
@@ -1650,7 +1968,7 @@
                     MmTelFeature.MmTelCapabilities capabilities) {
                 listener.onFeatureCapabilityChangedAdapter(getRegistrationTech(), capabilities);
             }
-        });
+        }, getImsThreadExecutor());
         log("Registration Callback registered.");
     }
 
@@ -1659,17 +1977,19 @@
      * associated with this ImsManager.
      * @param callback A {@link RegistrationManager.RegistrationCallback} that will notify the
      *                 caller when IMS registration status has changed.
+     * @param executor The Executor that the callback should be called on.
      * @throws ImsException when the ImsService connection is not available.
      */
-    public void addRegistrationCallback(RegistrationManager.RegistrationCallback callback)
+    public void addRegistrationCallback(RegistrationManager.RegistrationCallback callback,
+            Executor executor)
             throws ImsException {
         if (callback == null) {
             throw new NullPointerException("registration callback can't be null");
         }
 
         try {
-            callback.setExecutor(getThreadExecutor());
-            mMmTelFeatureConnection.addRegistrationCallback(callback.getBinder());
+            callback.setExecutor(executor);
+            mMmTelConnectionRef.get().addRegistrationCallback(callback.getBinder());
             log("Registration Callback registered.");
             // Only record if there isn't a RemoteException.
         } catch (IllegalStateException e) {
@@ -1680,15 +2000,14 @@
 
     /**
      * Removes a previously added registration callback that was added via
-     * {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback)} .
+     * {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback, Executor)} .
      * @param callback A {@link RegistrationManager.RegistrationCallback} that was previously added.
      */
     public void removeRegistrationListener(RegistrationManager.RegistrationCallback callback) {
         if (callback == null) {
             throw new NullPointerException("registration callback can't be null");
         }
-
-        mMmTelFeatureConnection.removeRegistrationCallback(callback.getBinder());
+        mMmTelConnectionRef.get().removeRegistrationCallback(callback.getBinder());
         log("Registration callback removed.");
     }
 
@@ -1706,7 +2025,7 @@
         if (callback == null) {
             throw new IllegalArgumentException("registration callback can't be null");
         }
-        mMmTelFeatureConnection.addRegistrationCallbackForSubscription(callback, subId);
+        mMmTelConnectionRef.get().addRegistrationCallbackForSubscription(callback, subId);
         log("Registration Callback registered.");
         // Only record if there isn't a RemoteException.
     }
@@ -1720,8 +2039,7 @@
         if (callback == null) {
             throw new IllegalArgumentException("registration callback can't be null");
         }
-
-        mMmTelFeatureConnection.removeRegistrationCallbackForSubscription(callback, subId);
+        mMmTelConnectionRef.get().removeRegistrationCallbackForSubscription(callback, subId);
     }
 
     /**
@@ -1729,18 +2047,19 @@
      * Voice over IMS or VT over IMS is not available currently.
      * @param callback A {@link ImsMmTelManager.CapabilityCallback} that will notify the caller when
      *                 MMTel capability status has changed.
+     * @param executor The Executor that the callback should be called on.
      * @throws ImsException when the ImsService connection is not available.
      */
-    public void addCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback)
-            throws ImsException {
+    public void addCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback,
+            Executor executor) throws ImsException {
         if (callback == null) {
             throw new NullPointerException("capabilities callback can't be null");
         }
 
-        checkAndThrowExceptionIfServiceUnavailable();
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
         try {
-            callback.setExecutor(getThreadExecutor());
-            mMmTelFeatureConnection.addCapabilityCallback(callback.getBinder());
+            callback.setExecutor(executor);
+            c.addCapabilityCallback(callback.getBinder());
             log("Capability Callback registered.");
             // Only record if there isn't a RemoteException.
         } catch (IllegalStateException e) {
@@ -1751,16 +2070,18 @@
 
     /**
      * Removes a previously registered {@link ImsMmTelManager.CapabilityCallback} callback.
-     * @throws ImsException when the ImsService connection is not available.
      */
-    public void removeCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback)
-            throws ImsException {
+    public void removeCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback) {
         if (callback == null) {
             throw new NullPointerException("capabilities callback can't be null");
         }
 
-        checkAndThrowExceptionIfServiceUnavailable();
-        mMmTelFeatureConnection.removeCapabilityCallback(callback.getBinder());
+        try {
+            MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+            c.removeCapabilityCallback(callback.getBinder());
+        } catch (ImsException e) {
+            log("Exception removing Capability , exception=" + e);
+        }
     }
 
     /**
@@ -1776,8 +2097,7 @@
         if (callback == null) {
             throw new IllegalArgumentException("registration callback can't be null");
         }
-
-        mMmTelFeatureConnection.addCapabilityCallbackForSubscription(callback, subId);
+        mMmTelConnectionRef.get().addCapabilityCallbackForSubscription(callback, subId);
         log("Capability Callback registered for subscription.");
     }
 
@@ -1790,8 +2110,7 @@
         if (callback == null) {
             throw new IllegalArgumentException("capabilities callback can't be null");
         }
-
-        mMmTelFeatureConnection.removeCapabilityCallbackForSubscription(callback, subId);
+        mMmTelConnectionRef.get().removeCapabilityCallbackForSubscription(callback, subId);
     }
 
     /**
@@ -1808,8 +2127,8 @@
             throw new NullPointerException("listener can't be null");
         }
 
-        checkAndThrowExceptionIfServiceUnavailable();
-        mMmTelFeatureConnection.removeRegistrationCallback(listener.getBinder());
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+        c.removeRegistrationCallback(listener.getBinder());
         log("Registration Callback/Listener registered.");
         // Only record if there isn't a RemoteException.
     }
@@ -1827,7 +2146,7 @@
             throw new IllegalArgumentException("provisioning callback can't be null");
         }
 
-        mMmTelFeatureConnection.addProvisioningCallbackForSubscription(callback, subId);
+        mMmTelConnectionRef.get().addProvisioningCallbackForSubscription(callback, subId);
         log("Capability Callback registered for subscription.");
     }
 
@@ -1842,12 +2161,12 @@
             throw new IllegalArgumentException("provisioning callback can't be null");
         }
 
-        mMmTelFeatureConnection.removeProvisioningCallbackForSubscription(callback, subId);
+        mMmTelConnectionRef.get().removeProvisioningCallbackForSubscription(callback, subId);
     }
 
     public @ImsRegistrationImplBase.ImsRegistrationTech int getRegistrationTech() {
         try {
-            return mMmTelFeatureConnection.getRegistrationTech();
+            return mMmTelConnectionRef.get().getRegistrationTech();
         } catch (RemoteException e) {
             logw("getRegistrationTech: no connection to ImsService.");
             return ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
@@ -1855,9 +2174,9 @@
     }
 
     public void getRegistrationTech(Consumer<Integer> callback) {
-        mExecutorFactory.executeRunnable(() -> {
+        getImsThreadExecutor().execute(() -> {
             try {
-                int tech = mMmTelFeatureConnection.getRegistrationTech();
+                int tech = mMmTelConnectionRef.get().getRegistrationTech();
                 callback.accept(tech);
             } catch (RemoteException e) {
                 logw("getRegistrationTech(C): no connection to ImsService.");
@@ -1867,45 +2186,36 @@
     }
 
     /**
-     * Closes the connection and removes all active callbacks.
-     * All the resources that were allocated to the service are also released.
+     * Closes the connection opened in {@link #open} and removes the associated listeners.
      */
     public void close() {
-        if (mMmTelFeatureConnection != null) {
-            mMmTelFeatureConnection.closeConnection();
-        }
-        mUt = null;
-        mEcbm = null;
-        mMultiEndpoint = null;
+        mMmTelConnectionRef.get().closeConnection();
     }
 
     /**
-     * Gets the configuration interface to provision / withdraw the supplementary service settings.
+     * Create or get the existing configuration interface to provision / withdraw the supplementary
+     * service settings.
+     * <p>
+     * There can only be one connection to the UT interface, so this may only be called by one
+     * ImsManager instance. Otherwise, an IllegalStateException will be thrown.
      *
      * @return the Ut interface instance
      * @throws ImsException if getting the Ut interface results in an error
      */
-    public ImsUtInterface getSupplementaryServiceConfiguration() throws ImsException {
-        // FIXME: manage the multiple Ut interfaces based on the session id
-        if (mUt != null && mUt.isBinderAlive()) {
-            return mUt;
-        }
-
-        checkAndThrowExceptionIfServiceUnavailable();
+    public ImsUtInterface createOrGetSupplementaryServiceConfiguration() throws ImsException {
+        ImsUt iUt;
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
         try {
-            IImsUt iUt = mMmTelFeatureConnection.getUtInterface();
-
+            iUt = c.createOrGetUtInterface();
             if (iUt == null) {
                 throw new ImsException("getSupplementaryServiceConfiguration()",
                         ImsReasonInfo.CODE_UT_NOT_SUPPORTED);
             }
-
-            mUt = new ImsUt(iUt);
         } catch (RemoteException e) {
             throw new ImsException("getSupplementaryServiceConfiguration()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
         }
-        return mUt;
+        return iUt;
     }
 
     /**
@@ -1928,10 +2238,10 @@
      * @throws ImsException if calling the IMS service results in an error
      */
     public ImsCallProfile createCallProfile(int serviceType, int callType) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
 
         try {
-            return mMmTelFeatureConnection.createCallProfile(serviceType, callType);
+            return c.createCallProfile(serviceType, callType);
         } catch (RemoteException e) {
             throw new ImsException("createCallProfile()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -1939,6 +2249,27 @@
     }
 
     /**
+     * Informs the {@link ImsService} of the {@link RtpHeaderExtensionType}s which the framework
+     * intends to use for incoming and outgoing calls.
+     * <p>
+     * See {@link RtpHeaderExtensionType} for more information.
+     * @param types The RTP header extension types to use for incoming and outgoing calls, or
+     *              empty list if none defined.
+     * @throws ImsException
+     */
+    public void setOfferedRtpHeaderExtensionTypes(@NonNull Set<RtpHeaderExtensionType> types)
+            throws ImsException {
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+
+        try {
+            c.changeOfferedRtpHeaderExtensionTypes(types);
+        } catch (RemoteException e) {
+            throw new ImsException("setOfferedRtpHeaderExtensionTypes()", e,
+                    ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+        }
+    }
+
+    /**
      * Creates a {@link ImsCall} to make a call.
      *
      * @param profile a call profile to make the call
@@ -1954,7 +2285,8 @@
             log("makeCall :: profile=" + profile);
         }
 
-        checkAndThrowExceptionIfServiceUnavailable();
+        // Check we are still alive
+        getOrThrowExceptionIfServiceUnavailable();
 
         ImsCall call = new ImsCall(mContext, profile);
 
@@ -1979,7 +2311,8 @@
      */
     public ImsCall takeCall(IImsCallSession session, ImsCall.Listener listener)
             throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+        // Check we are still alive
+        getOrThrowExceptionIfServiceUnavailable();
         try {
             if (session == null) {
                 throw new ImsException("No pending session for the call",
@@ -2006,9 +2339,9 @@
      */
     @UnsupportedAppUsage
     public ImsConfig getConfigInterface() throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
 
-        IImsConfig config = mMmTelFeatureConnection.getConfigInterface();
+        IImsConfig config = c.getConfig();
         if (config == null) {
             throw new ImsException("getConfigInterface()",
                     ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE);
@@ -2016,39 +2349,42 @@
         return new ImsConfig(config);
     }
 
-    public void changeMmTelCapability(
-            @MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
-            @ImsRegistrationImplBase.ImsRegistrationTech int radioTech,
-            boolean isEnabled) throws ImsException {
-
+    /**
+     * Enable or disable a capability for multiple radio technologies.
+     */
+    public void changeMmTelCapability(boolean isEnabled, int capability,
+            int... radioTechs) throws ImsException {
         CapabilityChangeRequest request = new CapabilityChangeRequest();
         if (isEnabled) {
-            request.addCapabilitiesToEnableForTech(capability, radioTech);
+            for (int tech : radioTechs) {
+                request.addCapabilitiesToEnableForTech(capability, tech);
+            }
         } else {
-            request.addCapabilitiesToDisableForTech(capability, radioTech);
+            for (int tech : radioTechs) {
+                request.addCapabilitiesToDisableForTech(capability, tech);
+            }
         }
         changeMmTelCapability(request);
     }
 
-    public void changeMmTelCapability(CapabilityChangeRequest r) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+    private void changeMmTelCapability(CapabilityChangeRequest r) throws ImsException {
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
         try {
             logi("changeMmTelCapability: changing capabilities for sub: " + getSubId()
                     + ", request: " + r);
-            mMmTelFeatureConnection.changeEnabledCapabilities(r, null);
-            if (mImsConfigListener == null) {
+            c.changeEnabledCapabilities(r, null);
+            ImsStatsCallback cb = getStatsCallback(mPhoneId);
+            if (cb == null) {
                 return;
             }
             for (CapabilityChangeRequest.CapabilityPair enabledCaps : r.getCapabilitiesToEnable()) {
-                mImsConfigListener.onSetFeatureResponse(enabledCaps.getCapability(),
-                        enabledCaps.getRadioTech(),
-                        ProvisioningManager.PROVISIONING_VALUE_ENABLED, -1);
+                cb.onEnabledMmTelCapabilitiesChanged(enabledCaps.getCapability(),
+                        enabledCaps.getRadioTech(), true);
             }
             for (CapabilityChangeRequest.CapabilityPair disabledCaps :
                     r.getCapabilitiesToDisable()) {
-                mImsConfigListener.onSetFeatureResponse(disabledCaps.getCapability(),
-                        disabledCaps.getRadioTech(),
-                        ProvisioningManager.PROVISIONING_VALUE_DISABLED, -1);
+                cb.onEnabledMmTelCapabilitiesChanged(disabledCaps.getCapability(),
+                        disabledCaps.getRadioTech(), false);
             }
         } catch (RemoteException e) {
             throw new ImsException("changeMmTelCapability(CCR)", e,
@@ -2056,7 +2392,7 @@
         }
     }
 
-    public boolean updateRttConfigValue() {
+    private boolean updateRttConfigValue() {
         // If there's no active sub anywhere on the device, enable RTT on the modem so that
         // the device can make an emergency call.
 
@@ -2065,10 +2401,20 @@
                 getBooleanCarrierConfig(CarrierConfigManager.KEY_RTT_SUPPORTED_BOOL)
                 || !isActiveSubscriptionPresent;
 
-        boolean isRttUiSettingEnabled = Settings.Secure.getInt(mContext.getContentResolver(),
-                Settings.Secure.RTT_CALLING_MODE, 0) != 0;
+        int defaultRttMode =
+                getIntCarrierConfig(CarrierConfigManager.KEY_DEFAULT_RTT_MODE_INT);
+        int rttMode = mSettingsProxy.getSecureIntSetting(mContext.getContentResolver(),
+                Settings.Secure.RTT_CALLING_MODE, defaultRttMode);
+        logi("defaultRttMode = " + defaultRttMode + " rttMode = " + rttMode);
         boolean isRttAlwaysOnCarrierConfig = getBooleanCarrierConfig(
                 CarrierConfigManager.KEY_IGNORE_RTT_MODE_SETTING_BOOL);
+        if (isRttAlwaysOnCarrierConfig && rttMode == defaultRttMode) {
+            mSettingsProxy.putSecureIntSetting(mContext.getContentResolver(),
+                    Settings.Secure.RTT_CALLING_MODE, defaultRttMode);
+        }
+
+        boolean isRttUiSettingEnabled = mSettingsProxy.getSecureIntSetting(
+                mContext.getContentResolver(), Settings.Secure.RTT_CALLING_MODE, 0) != 0;
 
         boolean shouldImsRttBeOn = isRttUiSettingEnabled || isRttAlwaysOnCarrierConfig;
         logi("update RTT: settings value: " + isRttUiSettingEnabled + " always-on carrierconfig: "
@@ -2086,7 +2432,7 @@
     private void setRttConfig(boolean enabled) {
         final int value = enabled ? ProvisioningManager.PROVISIONING_VALUE_ENABLED :
                 ProvisioningManager.PROVISIONING_VALUE_DISABLED;
-        mExecutorFactory.executeRunnable(() -> {
+        getImsThreadExecutor().execute(() -> {
             try {
                 logi("Setting RTT enabled to " + enabled);
                 getConfigInterface().setProvisionedValue(
@@ -2100,13 +2446,12 @@
     public boolean queryMmTelCapability(
             @MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
             @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
 
         BlockingQueue<Boolean> result = new LinkedBlockingDeque<>(1);
 
         try {
-            mMmTelFeatureConnection.queryEnabledCapabilities(capability, radioTech,
-                    new IImsCapabilityCallback.Stub() {
+            c.queryEnabledCapabilities(capability, radioTech, new IImsCapabilityCallback.Stub() {
                         @Override
                         public void onQueryCapabilityConfiguration(int resCap, int resTech,
                                 boolean enabled) {
@@ -2142,7 +2487,7 @@
     public boolean queryMmTelCapabilityStatus(
             @MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
             @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
 
         if (getRegistrationTech() != radioTech)
             return false;
@@ -2150,7 +2495,7 @@
         try {
 
             MmTelFeature.MmTelCapabilities capabilities =
-                    mMmTelFeatureConnection.queryCapabilityStatus();
+                    c.queryCapabilityStatus();
 
             return capabilities.isCapable(capability);
         } catch (RemoteException e) {
@@ -2159,27 +2504,30 @@
         }
     }
 
+    /**
+     * Enable the RTT configuration on this device.
+     */
     public void setRttEnabled(boolean enabled) {
-        try {
-            if (enabled) {
-                setEnhanced4gLteModeSetting(enabled);
-            } else {
-                setAdvanced4GMode(enabled || isEnhanced4gLteModeSettingEnabledByUser());
-            }
-            setRttConfig(enabled);
-        } catch (ImsException e) {
-            loge("Unable to set RTT enabled to " + enabled + ": " + e);
+        if (enabled) {
+            // Override this setting if RTT is enabled.
+            setEnhanced4gLteModeSetting(true /*enabled*/);
         }
+        setRttConfig(enabled);
     }
 
     /**
      * Set the TTY mode. This is the actual tty mode (varies depending on peripheral status)
      */
     public void setTtyMode(int ttyMode) throws ImsException {
-        if (!getBooleanCarrierConfig(
-                CarrierConfigManager.KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL)) {
-            setAdvanced4GMode((ttyMode == TelecomManager.TTY_MODE_OFF) &&
-                    isEnhanced4gLteModeSettingEnabledByUser());
+        boolean isNonTtyOrTtyOnVolteEnabled = isTtyOnVoLteCapable() ||
+                (ttyMode == TelecomManager.TTY_MODE_OFF);
+        logi("setTtyMode: isNonTtyOrTtyOnVolteEnabled=" + isNonTtyOrTtyOnVolteEnabled);
+        CapabilityChangeRequest request = new CapabilityChangeRequest();
+        updateVoiceCellFeatureValue(request, isNonTtyOrTtyOnVolteEnabled);
+        updateVideoCallFeatureValue(request, isNonTtyOrTtyOnVolteEnabled);
+        if (isImsNeeded(request)) {
+            changeMmTelCapability(request);
+            turnOnIms();
         }
     }
 
@@ -2199,48 +2547,32 @@
     public void setUiTTYMode(Context context, int uiTtyMode, Message onComplete)
             throws ImsException {
 
-        checkAndThrowExceptionIfServiceUnavailable();
-
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
         try {
-            mMmTelFeatureConnection.setUiTTYMode(uiTtyMode, onComplete);
+            c.setUiTTYMode(uiTtyMode, onComplete);
         } catch (RemoteException e) {
             throw new ImsException("setTTYMode()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
         }
     }
 
-    private ImsReasonInfo makeACopy(ImsReasonInfo imsReasonInfo) {
-        Parcel p = Parcel.obtain();
-        imsReasonInfo.writeToParcel(p, 0);
-        p.setDataPosition(0);
-        ImsReasonInfo clonedReasonInfo = ImsReasonInfo.CREATOR.createFromParcel(p);
-        p.recycle();
-        return clonedReasonInfo;
-    }
-
-    /**
-     * Get Recent IMS Disconnect Reasons.
-     *
-     * @return ArrayList of ImsReasonInfo objects. MAX size of the arraylist
-     * is MAX_RECENT_DISCONNECT_REASONS. The objects are in the
-     * chronological order.
-     */
-    public ArrayList<ImsReasonInfo> getRecentImsDisconnectReasons() {
-        ArrayList<ImsReasonInfo> disconnectReasons = new ArrayList<>();
-
-        for (ImsReasonInfo reason : mRecentDisconnectReasons) {
-            disconnectReasons.add(makeACopy(reason));
-        }
-        return disconnectReasons;
+    public int getImsServiceState() throws ImsException {
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+        return c.getFeatureState();
     }
 
     @Override
-    public int getImsServiceState() throws ImsException {
-        return mMmTelFeatureConnection.getFeatureState();
+    public void updateFeatureState(int state) {
+        mMmTelConnectionRef.get().updateFeatureState(state);
+    }
+
+    @Override
+    public void updateFeatureCapabilities(long capabilities) {
+        mMmTelConnectionRef.get().updateFeatureCapabilities(capabilities);
     }
 
     public void getImsServiceState(Consumer<Integer> result) {
-        mExecutorFactory.executeRunnable(() -> {
+        getImsThreadExecutor().execute(() -> {
             try {
                 result.accept(getImsServiceState());
             } catch (ImsException e) {
@@ -2250,11 +2582,11 @@
         });
     }
 
-    private Executor getThreadExecutor() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-        return new HandlerExecutor(new Handler(Looper.myLooper()));
+    /**
+     * @return An Executor that should be used to execute potentially long-running operations.
+     */
+    private Executor getImsThreadExecutor() {
+        return mExecutor;
     }
 
     /**
@@ -2298,43 +2630,117 @@
     }
 
     /**
+     * Get the int[] config from carrier config manager.
+     *
+     * @param key config key defined in CarrierConfigManager
+     * @return int[] values of the corresponding key.
+     */
+    private int[] getIntArrayCarrierConfig(String key) {
+        PersistableBundle b = null;
+        if (mConfigManager != null) {
+            // If an invalid subId is used, this bundle will contain default values.
+            b = mConfigManager.getConfigForSubId(getSubId());
+        }
+        if (b != null) {
+            return b.getIntArray(key);
+        } else {
+            // Return static default defined in CarrierConfigManager.
+            return CarrierConfigManager.getDefaultConfig().getIntArray(key);
+        }
+    }
+
+    /**
      * Checks to see if the ImsService Binder is connected. If it is not, we try to create the
      * connection again.
      */
-    private void checkAndThrowExceptionIfServiceUnavailable()
+    private MmTelFeatureConnection getOrThrowExceptionIfServiceUnavailable()
             throws ImsException {
         if (!isImsSupportedOnDevice(mContext)) {
             throw new ImsException("IMS not supported on device.",
                     ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE);
         }
-        if (mMmTelFeatureConnection == null || !mMmTelFeatureConnection.isBinderAlive()) {
-            createImsService();
+        MmTelFeatureConnection c = mMmTelConnectionRef.get();
+        if (c == null || !c.isBinderAlive()) {
+            throw new ImsException("Service is unavailable",
+                    ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+        }
+        return c;
+    }
 
-            if (mMmTelFeatureConnection == null) {
-                throw new ImsException("Service is unavailable",
-                        ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+    @Override
+    public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
+        try {
+            ITelephony telephony = mBinderCache.listenOnBinder(cb, () -> {
+                try {
+                    cb.imsFeatureRemoved(
+                            FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+                } catch (RemoteException ignore) {} // This is local.
+            });
+
+            if (telephony != null) {
+                telephony.registerMmTelFeatureCallback(slotId, cb);
+            } else {
+                cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
             }
+        } catch (ServiceSpecificException e) {
+            try {
+                switch (e.errorCode) {
+                    case android.telephony.ims.ImsException.CODE_ERROR_UNSUPPORTED_OPERATION:
+                        cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+                        break;
+                    default: {
+                        cb.imsFeatureRemoved(
+                                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+                    }
+                }
+            } catch (RemoteException ignore) {} // Already dead anyway if this happens.
+        } catch (RemoteException e) {
+            try {
+                cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+            } catch (RemoteException ignore) {} // Already dead if this happens.
         }
     }
 
-    /**
-     * Creates a connection to the ImsService associated with this slot.
-     */
-    private void createImsService() {
-        mMmTelFeatureConnection = MmTelFeatureConnection.create(mContext, mPhoneId);
-
-        // Forwarding interface to tell mStatusCallbacks that the Proxy is unavailable.
-        mMmTelFeatureConnection.setStatusCallback(new FeatureConnection.IFeatureUpdate() {
-            @Override
-            public void notifyStateChanged() {
-                mStatusCallbacks.forEach(FeatureConnection.IFeatureUpdate::notifyStateChanged);
+    @Override
+    public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
+        try {
+            ITelephony telephony = mBinderCache.removeRunnable(cb);
+            if (telephony != null) {
+                telephony.unregisterImsFeatureCallback(cb);
             }
+        } catch (RemoteException e) {
+            // This means that telephony died, so do not worry about it.
+            loge("unregisterImsFeatureCallback (MMTEL), RemoteException: " + e.getMessage());
+        }
+    }
 
-            @Override
-            public void notifyUnavailable() {
-                mStatusCallbacks.forEach(FeatureConnection.IFeatureUpdate::notifyUnavailable);
-            }
-        });
+    @Override
+    public void associate(ImsFeatureContainer c) {
+        if (c == null) {
+            mMmTelConnectionRef.set(mMmTelFeatureConnectionFactory.create(
+                    mContext, mPhoneId, null, null, null, null));
+        } else {
+            mMmTelConnectionRef.set(mMmTelFeatureConnectionFactory.create(
+                    mContext, mPhoneId, IImsMmTelFeature.Stub.asInterface(c.imsFeature),
+                    c.imsConfig, c.imsRegistration, c.sipTransport));
+        }
+    }
+
+    @Override
+    public void invalidate() {
+        mMmTelConnectionRef.get().onRemovedOrDied();
+    }
+
+    private ITelephony getITelephony() {
+        return mBinderCache.getBinder();
+    }
+
+    private static ITelephony getITelephonyInterface() {
+        return ITelephony.Stub.asInterface(
+                TelephonyFrameworkInitializer
+                        .getTelephonyServiceManager()
+                        .getTelephonyServiceRegisterer()
+                        .get());
     }
 
     /**
@@ -2346,8 +2752,9 @@
      */
     private ImsCallSession createCallSession(ImsCallProfile profile) throws ImsException {
         try {
+            MmTelFeatureConnection c = mMmTelConnectionRef.get();
             // Throws an exception if the ImsService Feature is not ready to accept commands.
-            return new ImsCallSession(mMmTelFeatureConnection.createCallSession(profile));
+            return new ImsCallSession(c.createCallSession(profile));
         } catch (RemoteException e) {
             logw("CreateCallSession: Error, remote exception: " + e.getMessage());
             throw new ImsException("createCallSession()", e,
@@ -2357,23 +2764,23 @@
     }
 
     private void log(String s) {
-        Rlog.d(TAG + " [" + mPhoneId + "]", s);
+        Rlog.d(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
     }
 
     private void logi(String s) {
-        Rlog.i(TAG + " [" + mPhoneId + "]", s);
+        Rlog.i(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
     }
     
     private void logw(String s) {
-        Rlog.w(TAG + " [" + mPhoneId + "]", s);
+        Rlog.w(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
     }
 
     private void loge(String s) {
-        Rlog.e(TAG + " [" + mPhoneId + "]", s);
+        Rlog.e(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
     }
 
     private void loge(String s, Throwable t) {
-        Rlog.e(TAG + " [" + mPhoneId + "]", s, t);
+        Rlog.e(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s, t);
     }
 
     /**
@@ -2391,60 +2798,6 @@
                 || !isWfcEnabledByUser());
     }
 
-    private void setLteFeatureValues(boolean turnOn) {
-        log("setLteFeatureValues: " + turnOn);
-        CapabilityChangeRequest request = new CapabilityChangeRequest();
-        if (turnOn) {
-            request.addCapabilitiesToEnableForTech(
-                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
-                    ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
-        } else {
-            request.addCapabilitiesToDisableForTech(
-                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
-                    ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
-        }
-
-        if (isVtEnabledByPlatform()) {
-            boolean ignoreDataEnabledChanged = getBooleanCarrierConfig(
-                    CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
-            boolean enableViLte = turnOn && isVtEnabledByUser() &&
-                    (ignoreDataEnabledChanged || isDataEnabled());
-            if (enableViLte) {
-                request.addCapabilitiesToEnableForTech(
-                        MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
-                        ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
-            } else {
-                request.addCapabilitiesToDisableForTech(
-                        MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
-                        ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
-            }
-        }
-        try {
-            mMmTelFeatureConnection.changeEnabledCapabilities(request, null);
-        } catch (RemoteException e) {
-            loge("setLteFeatureValues: Exception: " + e.getMessage());
-        }
-    }
-
-    private void setAdvanced4GMode(boolean turnOn) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
-
-        // if turnOn: first set feature values then call turnOnIms()
-        // if turnOff: only set feature values if IMS turn off is not allowed. If turn off is
-        // allowed, first call turnOffIms() then set feature values
-        if (turnOn) {
-            setLteFeatureValues(turnOn);
-            log("setAdvanced4GMode: turnOnIms");
-            turnOnIms();
-        } else {
-            if (isImsTurnOffAllowed()) {
-                log("setAdvanced4GMode: turnOffIms");
-                turnOffIms();
-            }
-            setLteFeatureValues(turnOn);
-        }
-    }
-
     /**
      * Used for turning off IMS completely in order to make the device CSFB'ed.
      * Once turned off, all calls will be over CS.
@@ -2455,45 +2808,29 @@
         tm.disableIms(mPhoneId);
     }
 
-    private void addToRecentDisconnectReasons(ImsReasonInfo reason) {
-        if (reason == null) return;
-        while (mRecentDisconnectReasons.size() >= MAX_RECENT_DISCONNECT_REASONS) {
-            mRecentDisconnectReasons.removeFirst();
-        }
-        mRecentDisconnectReasons.addLast(reason);
-    }
-
     /**
      * Gets the ECBM interface to request ECBM exit.
+     * <p>
+     * This should only be called after {@link #open} has been called.
      *
      * @return the ECBM interface instance
      * @throws ImsException if getting the ECBM interface results in an error
      */
     public ImsEcbm getEcbmInterface() throws ImsException {
-        if (mEcbm != null && mEcbm.isBinderAlive()) {
-            return mEcbm;
-        }
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+        ImsEcbm iEcbm = c.getEcbmInterface();
 
-        checkAndThrowExceptionIfServiceUnavailable();
-        try {
-            IImsEcbm iEcbm = mMmTelFeatureConnection.getEcbmInterface();
-
-            if (iEcbm == null) {
-                throw new ImsException("getEcbmInterface()",
-                        ImsReasonInfo.CODE_ECBM_NOT_SUPPORTED);
-            }
-            mEcbm = new ImsEcbm(iEcbm);
-        } catch (RemoteException e) {
-            throw new ImsException("getEcbmInterface()", e,
-                    ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+        if (iEcbm == null) {
+            throw new ImsException("getEcbmInterface()",
+                    ImsReasonInfo.CODE_ECBM_NOT_SUPPORTED);
         }
-        return mEcbm;
+        return iEcbm;
     }
 
     public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
             byte[] pdu) throws ImsException {
         try {
-            mMmTelFeatureConnection.sendSms(token, messageRef, format, smsc, isRetry, pdu);
+            mMmTelConnectionRef.get().sendSms(token, messageRef, format, smsc, isRetry, pdu);
         } catch (RemoteException e) {
             throw new ImsException("sendSms()", e, ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
         }
@@ -2501,7 +2838,7 @@
 
     public void acknowledgeSms(int token, int messageRef, int result) throws ImsException {
         try {
-            mMmTelFeatureConnection.acknowledgeSms(token, messageRef, result);
+            mMmTelConnectionRef.get().acknowledgeSms(token, messageRef, result);
         } catch (RemoteException e) {
             throw new ImsException("acknowledgeSms()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2510,7 +2847,7 @@
 
     public void acknowledgeSmsReport(int token, int messageRef, int result) throws  ImsException{
         try {
-            mMmTelFeatureConnection.acknowledgeSmsReport(token, messageRef, result);
+            mMmTelConnectionRef.get().acknowledgeSmsReport(token, messageRef, result);
         } catch (RemoteException e) {
             throw new ImsException("acknowledgeSmsReport()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2519,7 +2856,7 @@
 
     public String getSmsFormat() throws ImsException{
         try {
-            return mMmTelFeatureConnection.getSmsFormat();
+            return mMmTelConnectionRef.get().getSmsFormat();
         } catch (RemoteException e) {
             throw new ImsException("getSmsFormat()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2528,7 +2865,7 @@
 
     public void setSmsListener(IImsSmsListener listener) throws ImsException {
         try {
-            mMmTelFeatureConnection.setSmsListener(listener);
+            mMmTelConnectionRef.get().setSmsListener(listener);
         } catch (RemoteException e) {
             throw new ImsException("setSmsListener()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2537,7 +2874,7 @@
 
     public void onSmsReady() throws ImsException {
         try {
-            mMmTelFeatureConnection.onSmsReady();
+            mMmTelConnectionRef.get().onSmsReady();
         } catch (RemoteException e) {
             throw new ImsException("onSmsReady()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2559,7 +2896,7 @@
     public @MmTelFeature.ProcessCallResult int shouldProcessCall(boolean isEmergency,
             String[] numbers) throws ImsException {
         try {
-            return mMmTelFeatureConnection.shouldProcessCall(isEmergency, numbers);
+            return mMmTelConnectionRef.get().shouldProcessCall(isEmergency, numbers);
         } catch (RemoteException e) {
             throw new ImsException("shouldProcessCall()", e,
                     ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2567,34 +2904,6 @@
     }
 
     /**
-     * Gets the Multi-Endpoint interface to subscribe to multi-enpoint notifications..
-     *
-     * @return the multi-endpoint interface instance
-     * @throws ImsException if getting the multi-endpoint interface results in an error
-     */
-    public ImsMultiEndpoint getMultiEndpointInterface() throws ImsException {
-        if (mMultiEndpoint != null && mMultiEndpoint.isBinderAlive()) {
-            return mMultiEndpoint;
-        }
-
-        checkAndThrowExceptionIfServiceUnavailable();
-        try {
-            IImsMultiEndpoint iImsMultiEndpoint = mMmTelFeatureConnection.getMultiEndpointInterface();
-
-            if (iImsMultiEndpoint == null) {
-                throw new ImsException("getMultiEndpointInterface()",
-                        ImsReasonInfo.CODE_MULTIENDPOINT_NOT_SUPPORTED);
-            }
-            mMultiEndpoint = new ImsMultiEndpoint(iImsMultiEndpoint);
-        } catch (RemoteException e) {
-            throw new ImsException("getMultiEndpointInterface()", e,
-                    ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
-        }
-
-        return mMultiEndpoint;
-    }
-
-    /**
      * Resets ImsManager settings back to factory defaults.
      *
      * @deprecated Doesn't support MSIM devices. Use {@link #factoryReset()} instead.
@@ -2602,8 +2911,8 @@
      * @hide
      */
     public static void factoryReset(Context context) {
-        ImsManager mgr = ImsManager.getInstance(context,
-                SubscriptionManager.getDefaultVoicePhoneId());
+        DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+        ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
         if (mgr != null) {
             mgr.factoryReset();
         }
@@ -2617,48 +2926,52 @@
      */
     public void factoryReset() {
         int subId = getSubId();
-        if (isSubIdValid(subId)) {
-            // Set VoLTE to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
-                    Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-            // Set VoWiFi to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.WFC_IMS_ENABLED,
-                    Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-            // Set VoWiFi mode to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.WFC_IMS_MODE,
-                    Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-            // Set VoWiFi roaming to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
-                    Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-            // Set VoWiFi roaming mode to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.WFC_IMS_ROAMING_MODE,
-                    Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-
-            // Set VT to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.VT_IMS_ENABLED,
-                    Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-            // Set RCS UCE to default
-            SubscriptionManager.setSubscriptionProperty(subId,
-                    SubscriptionManager.IMS_RCS_UCE_ENABLED, Integer.toString(
-                            SUBINFO_PROPERTY_FALSE));
-        } else {
-            loge("factoryReset: invalid sub id, can not reset siminfo db settings; subId=" + subId);
+        if (!isSubIdValid(subId)) {
+            loge("factoryReset: invalid sub id, can not reset siminfo db settings; subId="
+                    + subId);
+            return;
         }
+        // Set VoLTE to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
+                Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
 
-        // Push settings to ImsConfig
-        updateImsServiceConfig(true);
+        // Set VoWiFi to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.WFC_IMS_ENABLED,
+                Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+        // Set VoWiFi mode to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.WFC_IMS_MODE,
+                Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+        // Set VoWiFi roaming to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
+                Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+        // Set VoWiFi roaming mode to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.WFC_IMS_ROAMING_MODE,
+                Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+
+        // Set VT to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.VT_IMS_ENABLED,
+                Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+        // Set RCS UCE to default
+        mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+                SubscriptionManager.IMS_RCS_UCE_ENABLED, Integer.toString(
+                        SUBINFO_PROPERTY_FALSE));
+        // Push settings
+        try {
+            reevaluateCapabilities();
+        } catch (ImsException e) {
+            loge("factoryReset, exception: " + e);
+        }
     }
 
     public void setVolteProvisioned(boolean isProvisioned) {
@@ -2690,7 +3003,13 @@
     }
 
     private boolean isDataEnabled() {
-        return new TelephonyManager(mContext, getSubId()).isDataConnectionAllowed();
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        if (tm == null) {
+            loge("isDataEnabled: TelephonyManager not available, returning false...");
+            return false;
+        }
+        tm = tm.createForSubscriptionId(getSubId());
+        return tm.isDataConnectionAllowed();
     }
 
     private boolean isVolteProvisioned() {
@@ -2717,13 +3036,165 @@
         return bool ? "1" : "0";
     }
 
+    public int getConfigInt(int key) throws ImsException {
+        if (isLocalImsConfigKey(key)) {
+            return getLocalImsConfigKeyInt(key);
+        } else {
+            return getConfigInterface().getConfigInt(key);
+        }
+    }
+
+    public String getConfigString(int key) throws ImsException {
+        if (isLocalImsConfigKey(key)) {
+            return getLocalImsConfigKeyString(key);
+        } else {
+            return getConfigInterface().getConfigString(key);
+        }
+    }
+
+    public int setConfig(int key, int value) throws ImsException, RemoteException {
+        if (isLocalImsConfigKey(key)) {
+            return setLocalImsConfigKeyInt(key, value);
+        } else {
+            return getConfigInterface().setConfig(key, value);
+        }
+    }
+
+    public int setConfig(int key, String value) throws ImsException, RemoteException {
+        if (isLocalImsConfigKey(key)) {
+            return setLocalImsConfigKeyString(key, value);
+        } else {
+            return getConfigInterface().setConfig(key, value);
+        }
+    }
+
+    /**
+     * Gets the configuration value that supported in frameworks.
+     *
+     * @param key, as defined in com.android.ims.ProvisioningManager.
+     * @return the value in Integer format
+     */
+    private int getLocalImsConfigKeyInt(int key) {
+        int result = ProvisioningManager.PROVISIONING_RESULT_UNKNOWN;
+
+        switch (key) {
+            case KEY_VOIMS_OPT_IN_STATUS:
+                result = isVoImsOptInEnabled() ? 1 : 0;
+                break;
+        }
+        log("getLocalImsConfigKeInt() for key:" + key + ", result: " + result);
+        return result;
+    }
+
+    /**
+     * Gets the configuration value that supported in frameworks.
+     *
+     * @param key, as defined in com.android.ims.ProvisioningManager.
+     * @return the value in String format
+     */
+    private String getLocalImsConfigKeyString(int key) {
+        String result = "";
+
+        switch (key) {
+            case KEY_VOIMS_OPT_IN_STATUS:
+                result = booleanToPropertyString(isVoImsOptInEnabled());
+
+                break;
+        }
+        log("getLocalImsConfigKeyString() for key:" + key + ", result: " + result);
+        return result;
+    }
+
+    /**
+     * Sets the configuration value that supported in frameworks.
+     *
+     * @param key, as defined in com.android.ims.ProvisioningManager.
+     * @param value in Integer format.
+     * @return as defined in com.android.ims.ProvisioningManager#OperationStatusConstants
+     */
+    private int setLocalImsConfigKeyInt(int key, int value) throws ImsException, RemoteException {
+        int result = ImsConfig.OperationStatusConstants.UNKNOWN;
+
+        switch (key) {
+            case KEY_VOIMS_OPT_IN_STATUS:
+                result = setVoImsOptInSetting(value);
+                reevaluateCapabilities();
+                break;
+        }
+        log("setLocalImsConfigKeyInt() for" +
+                " key: " + key +
+                ", value: " + value +
+                ", result: " + result);
+
+        // Notify ims config changed
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+        IImsConfig config = c.getConfig();
+        config.notifyIntImsConfigChanged(key, value);
+
+        return result;
+    }
+
+    /**
+     * Sets the configuration value that supported in frameworks.
+     *
+     * @param key, as defined in com.android.ims.ProvisioningManager.
+     * @param value in String format.
+     * @return as defined in com.android.ims.ProvisioningManager#OperationStatusConstants
+     */
+    private int setLocalImsConfigKeyString(int key, String value)
+            throws ImsException, RemoteException {
+        int result = ImsConfig.OperationStatusConstants.UNKNOWN;
+
+        switch (key) {
+            case KEY_VOIMS_OPT_IN_STATUS:
+                result = setVoImsOptInSetting(Integer.parseInt(value));
+                reevaluateCapabilities();
+                break;
+        }
+        log("setLocalImsConfigKeyString() for" +
+                " key: " + key +
+                ", value: " + value +
+                ", result: " + result);
+
+        // Notify ims config changed
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+        IImsConfig config = c.getConfig();
+        config.notifyStringImsConfigChanged(key, value);
+
+        return result;
+    }
+
+    /**
+     * Check the config whether supported by framework.
+     *
+     * @param key, as defined in com.android.ims.ProvisioningManager.
+     * @return true if the config is supported by framework.
+     */
+    private boolean isLocalImsConfigKey(int key) {
+        return Arrays.stream(LOCAL_IMS_CONFIG_KEYS).anyMatch(value -> value == key);
+    }
+
+    private boolean isVoImsOptInEnabled() {
+        int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
+                getSubId(), SubscriptionManager.VOIMS_OPT_IN_STATUS,
+                SUB_PROPERTY_NOT_INITIALIZED);
+        return (setting == ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+    }
+
+    private int setVoImsOptInSetting(int value) {
+        mSubscriptionManagerProxy.setSubscriptionProperty(
+                getSubId(),
+                SubscriptionManager.VOIMS_OPT_IN_STATUS,
+                String.valueOf(value));
+        return ImsConfig.OperationStatusConstants.SUCCESS;
+    }
 
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("ImsManager:");
         pw.println("  device supports IMS = " + isImsSupportedOnDevice(mContext));
         pw.println("  mPhoneId = " + mPhoneId);
         pw.println("  mConfigUpdated = " + mConfigUpdated);
-        pw.println("  mImsServiceProxy = " + mMmTelFeatureConnection);
+        pw.println("  mImsServiceProxy = " + mMmTelConnectionRef.get());
         pw.println("  mDataEnabled = " + isDataEnabled());
         pw.println("  ignoreDataEnabledChanged = " + getBooleanCarrierConfig(
                 CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS));
@@ -2733,6 +3204,7 @@
         pw.println("  isNonTtyOrTtyOnVolteEnabled = " + isNonTtyOrTtyOnVolteEnabled());
 
         pw.println("  isVolteEnabledByPlatform = " + isVolteEnabledByPlatform());
+        pw.println("  isVoImsOptInEnabled = " + isVoImsOptInEnabled());
         pw.println("  isVolteProvisionedOnDevice = " + isVolteProvisionedOnDevice());
         pw.println("  isEnhanced4gLteModeSettingEnabledByUser = " +
                 isEnhanced4gLteModeSettingEnabledByUser());
@@ -2747,6 +3219,7 @@
 
         pw.println("  isVtProvisionedOnDevice = " + isVtProvisionedOnDevice());
         pw.println("  isWfcProvisionedOnDevice = " + isWfcProvisionedOnDevice());
+        pw.println("  isImsOverNrEnabledByPlatform = " + isImsOverNrEnabledByPlatform());
         pw.flush();
     }
 
@@ -2757,20 +3230,18 @@
      * @return {@code true} if valid, {@code false} otherwise.
      */
     private boolean isSubIdValid(int subId) {
-        return SubscriptionManager.isValidSubscriptionId(subId) &&
+        return mSubscriptionManagerProxy.isValidSubscriptionId(subId) &&
                 subId != SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
     }
 
     private boolean isActiveSubscriptionPresent() {
-        SubscriptionManager sm = (SubscriptionManager) mContext.getSystemService(
-                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
-        return sm.getActiveSubscriptionIdList().length > 0;
+        return mSubscriptionManagerProxy.getActiveSubscriptionIdList().length > 0;
     }
 
     private void updateImsCarrierConfigs(PersistableBundle configs) throws ImsException {
-        checkAndThrowExceptionIfServiceUnavailable();
+        MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
 
-        IImsConfig config = mMmTelFeatureConnection.getConfigInterface();
+        IImsConfig config = c.getConfig();
         if (config == null) {
             throw new ImsException("getConfigInterface()",
                     ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE);
diff --git a/src/java/com/android/ims/ImsMultiEndpoint.java b/src/java/com/android/ims/ImsMultiEndpoint.java
index dc297b6..7c22537 100644
--- a/src/java/com/android/ims/ImsMultiEndpoint.java
+++ b/src/java/com/android/ims/ImsMultiEndpoint.java
@@ -70,15 +70,10 @@
     }
 
     public void setExternalCallStateListener(ImsExternalCallStateListener externalCallStateListener)
-            throws ImsException {
-        try {
-            if (DBG) Rlog.d(TAG, "setExternalCallStateListener");
-            mImsMultiendpoint.setListener(new ImsExternalCallStateListenerProxy(
-                    externalCallStateListener));
-        } catch (RemoteException e) {
-            throw new ImsException("setExternalCallStateListener could not be set.", e,
-                    ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
-        }
+            throws RemoteException {
+        if (DBG) Rlog.d(TAG, "setExternalCallStateListener");
+        mImsMultiendpoint.setListener(externalCallStateListener != null ?
+                new ImsExternalCallStateListenerProxy(externalCallStateListener) : null);
     }
 
     public boolean isBinderAlive() {
diff --git a/src/java/com/android/ims/MmTelFeatureConnection.java b/src/java/com/android/ims/MmTelFeatureConnection.java
index 4d5a179..7201313 100644
--- a/src/java/com/android/ims/MmTelFeatureConnection.java
+++ b/src/java/com/android/ims/MmTelFeatureConnection.java
@@ -16,13 +16,15 @@
 
 package com.android.ims;
 
-import android.annotation.NonNull;
 import android.content.Context;
+import android.os.Binder;
 import android.os.IBinder;
+import android.os.IInterface;
 import android.os.Message;
 import android.os.RemoteException;
-import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsCallProfile;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.RtpHeaderExtensionType;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
 import android.telephony.ims.aidl.IImsConfig;
 import android.telephony.ims.aidl.IImsConfigCallback;
@@ -30,9 +32,10 @@
 import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.telephony.ims.aidl.IImsSmsListener;
+import android.telephony.ims.aidl.ISipTransport;
 import android.telephony.ims.feature.CapabilityChangeRequest;
-import android.telephony.ims.feature.ImsFeature;
 import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsEcbmImplBase;
 import android.telephony.ims.stub.ImsSmsImplBase;
 import android.util.Log;
 
@@ -40,7 +43,10 @@
 import com.android.ims.internal.IImsEcbm;
 import com.android.ims.internal.IImsMultiEndpoint;
 import com.android.ims.internal.IImsUt;
-import com.android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.Set;
 
 /**
  * A container of the IImsServiceController binder, which implements all of the ImsFeatures that
@@ -48,7 +54,7 @@
  */
 
 public class MmTelFeatureConnection extends FeatureConnection {
-    protected static final String TAG = "MmTelFeatureConnection";
+    protected static final String TAG = "MmTelFeatureConn";
 
     private class ImsRegistrationCallbackAdapter extends
             ImsCallbackAdapterManager<IImsRegistrationCallback> {
@@ -160,7 +166,7 @@
 
         @Override
         public void registerCallback(IImsConfigCallback localCallback) {
-            IImsConfig binder = getConfigInterface();
+            IImsConfig binder = getConfig();
             if (binder == null) {
                 // Config interface is not currently available.
                 Log.w(TAG + " [" + mSlotId + "]", "ProvisioningCallbackManager - couldn't register,"
@@ -176,7 +182,7 @@
 
         @Override
         public void unregisterCallback(IImsConfigCallback localCallback) {
-            IImsConfig binder = getConfigInterface();
+            IImsConfig binder = getConfig();
             if (binder == null) {
                 Log.w(TAG + " [" + mSlotId + "]", "ProvisioningCallbackManager - couldn't"
                         + " unregister, binder is null.");
@@ -191,47 +197,64 @@
         }
     }
 
+    private static final class BinderAccessState<T> {
+        /**
+         * We have not tried to get the interface yet.
+         */
+        static final int STATE_NOT_SET = 0;
+        /**
+         * We have tried to get the interface, but it is not supported.
+         */
+        static final int STATE_NOT_SUPPORTED = 1;
+        /**
+         * The interface is available from the service.
+         */
+        static final int STATE_AVAILABLE = 2;
+
+        public static <T> BinderAccessState<T> of(T value) {
+            return new BinderAccessState<>(value);
+        }
+
+        private final int mState;
+        private final T mInterface;
+
+        public BinderAccessState(int state) {
+            mState = state;
+            mInterface = null;
+        }
+
+        public BinderAccessState(T binderInterface) {
+            mState = STATE_AVAILABLE;
+            mInterface = binderInterface;
+        }
+
+        public int getState() {
+            return mState;
+        }
+
+        public T getInterface() {
+            return mInterface;
+        }
+    }
+
     // Updated by IImsServiceFeatureCallback when FEATURE_EMERGENCY_MMTEL is sent.
     private boolean mSupportsEmergencyCalling = false;
+    private BinderAccessState<ImsEcbm> mEcbm =
+            new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+    private BinderAccessState<ImsMultiEndpoint> mMultiEndpoint =
+            new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+    private MmTelFeature.Listener mMmTelFeatureListener;
+    private ImsUt mUt;
 
-    // Cache the Registration and Config interfaces as long as the MmTel feature is connected. If
-    // it becomes disconnected, invalidate.
-    private IImsConfig mConfigBinder;
     private final ImsRegistrationCallbackAdapter mRegistrationCallbackManager;
     private final CapabilityCallbackManager mCapabilityCallbackManager;
     private final ProvisioningCallbackManager mProvisioningCallbackManager;
 
-    public static @NonNull MmTelFeatureConnection create(Context context , int slotId) {
-        MmTelFeatureConnection serviceProxy = new MmTelFeatureConnection(context, slotId);
-        if (!ImsManager.isImsSupportedOnDevice(context)) {
-            // Return empty service proxy in the case that IMS is not supported.
-            sImsSupportedOnDevice = false;
-            return serviceProxy;
-        }
+    public MmTelFeatureConnection(Context context, int slotId, IImsMmTelFeature f,
+            IImsConfig c, IImsRegistration r, ISipTransport s) {
+        super(context, slotId, c, r, s);
 
-        TelephonyManager tm = serviceProxy.getTelephonyManager();
-        if (tm == null) {
-            Rlog.w(TAG + " [" + slotId + "]", "create: TelephonyManager is null!");
-            // Binder can be unset in this case because it will be torn down/recreated as part of
-            // a retry mechanism until the serviceProxy binder is set successfully.
-            return serviceProxy;
-        }
-
-        IImsMmTelFeature binder = tm.getImsMmTelFeatureAndListen(slotId,
-                serviceProxy.getListener());
-        if (binder != null) {
-            serviceProxy.setBinder(binder.asBinder());
-            // Trigger the cache to be updated for feature status.
-            serviceProxy.getFeatureState();
-        } else {
-            Rlog.w(TAG + " [" + slotId + "]", "create: binder is null!");
-        }
-        return serviceProxy;
-    }
-
-    public MmTelFeatureConnection(Context context, int slotId) {
-        super(context, slotId);
-
+        setBinder((f != null) ? f.asBinder() : null);
         mRegistrationCallbackManager = new ImsRegistrationCallbackAdapter(context, mLock);
         mCapabilityCallbackManager = new CapabilityCallbackManager(context, mLock);
         mProvisioningCallbackManager = new ProvisioningCallbackManager(context, mLock);
@@ -239,100 +262,21 @@
 
     @Override
     protected void onRemovedOrDied() {
-        removeImsFeatureCallback();
+        // Release all callbacks being tracked and unregister them from the connected MmTelFeature.
+        mRegistrationCallbackManager.close();
+        mCapabilityCallbackManager.close();
+        mProvisioningCallbackManager.close();
+        // Close mUt interface separately from other listeners, as it is not tied directly to
+        // calling. There is still a limitation currently that only one UT listener can be set
+        // (through ImsPhoneCallTracker), but this could be relaxed in the future via the ability
+        // to register multiple callbacks.
         synchronized (mLock) {
+            if (mUt != null) {
+                mUt.close();
+                mUt = null;
+            }
+            closeConnection();
             super.onRemovedOrDied();
-            mRegistrationCallbackManager.close();
-            mCapabilityCallbackManager.close();
-            mProvisioningCallbackManager.close();
-            mConfigBinder = null;
-        }
-    }
-
-    private void removeImsFeatureCallback() {
-        TelephonyManager tm = getTelephonyManager();
-        if (tm != null) {
-            tm.unregisterImsFeatureCallback(mSlotId, ImsFeature.FEATURE_MMTEL, getListener());
-        }
-    }
-
-    private IImsConfig getConfig() {
-        synchronized (mLock) {
-            // null if cache is invalid;
-            if (mConfigBinder != null) {
-                return mConfigBinder;
-            }
-        }
-        TelephonyManager tm = getTelephonyManager();
-        IImsConfig configBinder = tm != null
-                ? tm.getImsConfig(mSlotId, ImsFeature.FEATURE_MMTEL) : null;
-        synchronized (mLock) {
-            // mConfigBinder may have changed while we tried to get the config interface.
-            if (mConfigBinder == null) {
-                mConfigBinder = configBinder;
-            }
-        }
-        return mConfigBinder;
-    }
-
-    @Override
-    protected void handleImsFeatureCreatedCallback(int slotId, int feature) {
-        // The feature has been enabled. This happens when the feature is first created and
-        // may happen when the feature is re-enabled.
-        synchronized (mLock) {
-            if(mSlotId != slotId) {
-                return;
-            }
-            switch (feature) {
-                case ImsFeature.FEATURE_MMTEL: {
-                    if (!mIsAvailable) {
-                        Log.i(TAG + " [" + mSlotId + "]", "MmTel enabled");
-                        mIsAvailable = true;
-                    }
-                    break;
-                }
-                case ImsFeature.FEATURE_EMERGENCY_MMTEL: {
-                    mSupportsEmergencyCalling = true;
-                    Log.i(TAG + " [" + mSlotId + "]", "Emergency calling enabled");
-                    break;
-                }
-            }
-        }
-    }
-
-    @Override
-    protected void handleImsFeatureRemovedCallback(int slotId, int feature) {
-        synchronized (mLock) {
-            if (mSlotId != slotId) {
-                return;
-            }
-            switch (feature) {
-                case ImsFeature.FEATURE_MMTEL: {
-                    Log.i(TAG + " [" + mSlotId + "]", "MmTel removed");
-                    onRemovedOrDied();
-                    break;
-                }
-                case ImsFeature.FEATURE_EMERGENCY_MMTEL: {
-                    mSupportsEmergencyCalling = false;
-                    Log.i(TAG + " [" + mSlotId + "]", "Emergency calling disabled");
-                    break;
-                }
-            }
-        }
-    }
-
-    @Override
-    protected void handleImsStatusChangedCallback(int slotId, int feature, int status) {
-        synchronized (mLock) {
-            Log.i(TAG + " [" + mSlotId + "]", "imsStatusChanged: slot: " + slotId + " feature: "
-                + ImsFeature.FEATURE_LOG_MAP.get(feature) +
-                " status: " + ImsFeature.STATE_LOG_MAP.get(status));
-            if (mSlotId == slotId && feature == ImsFeature.FEATURE_MMTEL) {
-                mFeatureStateCached = status;
-                if (mStatusCallback != null) {
-                    mStatusCallback.notifyStateChanged();
-                }
-            }
         }
     }
 
@@ -344,28 +288,46 @@
      * Opens the connection to the {@link MmTelFeature} and establishes a listener back to the
      * framework. Calling this method multiple times will reset the listener attached to the
      * {@link MmTelFeature}.
-     * @param listener A {@link MmTelFeature.Listener} that will be used by the {@link MmTelFeature}
-     * to notify the framework of updates.
+     * @param mmTelListener A {@link MmTelFeature.Listener} that will be used by the
+     *         {@link MmTelFeature} to notify the framework of mmtel calling updates.
+     * @param ecbmListener Listener used to listen for ECBM updates from {@link ImsEcbmImplBase}
+     *         implementation.
      */
-    public void openConnection(MmTelFeature.Listener listener) throws RemoteException {
+    public void openConnection(MmTelFeature.Listener mmTelListener,
+            ImsEcbmStateListener ecbmListener,
+            ImsExternalCallStateListener multiEndpointListener) throws RemoteException {
         synchronized (mLock) {
             checkServiceIsReady();
-            getServiceInterface(mBinder).setListener(listener);
+            mMmTelFeatureListener = mmTelListener;
+            getServiceInterface(mBinder).setListener(mmTelListener);
+            setEcbmInterface(ecbmListener);
+            setMultiEndpointInterface(multiEndpointListener);
         }
     }
 
+    /**
+     * Closes the connection to the {@link MmTelFeature} if it was previously opened via
+     * {@link #openConnection} by removing all listeners.
+     */
     public void closeConnection() {
-        mRegistrationCallbackManager.close();
-        mCapabilityCallbackManager.close();
-        mProvisioningCallbackManager.close();
-        try {
-            synchronized (mLock) {
-                if (isBinderAlive()) {
+        synchronized (mLock) {
+            if (!isBinderAlive()) return;
+            try {
+                if (mMmTelFeatureListener != null) {
+                    mMmTelFeatureListener = null;
                     getServiceInterface(mBinder).setListener(null);
                 }
+                if (mEcbm.getState() == BinderAccessState.STATE_AVAILABLE) {
+                    mEcbm.getInterface().setEcbmStateListener(null);
+                    mEcbm = new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+                }
+                if (mMultiEndpoint.getState() == BinderAccessState.STATE_AVAILABLE) {
+                    mMultiEndpoint.getInterface().setExternalCallStateListener(null);
+                    mMultiEndpoint = new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG + " [" + mSlotId + "]", "closeConnection: couldn't remove listeners!");
             }
-        } catch (RemoteException e) {
-            Log.w(TAG + " [" + mSlotId + "]", "closeConnection: couldn't remove listener!");
         }
     }
 
@@ -448,6 +410,15 @@
         }
     }
 
+    public void changeOfferedRtpHeaderExtensionTypes(Set<RtpHeaderExtensionType> types)
+            throws RemoteException {
+        synchronized (mLock) {
+            checkServiceIsReady();
+            getServiceInterface(mBinder).changeOfferedRtpHeaderExtensionTypes(
+                    new ArrayList<>(types));
+        }
+    }
+
     public IImsCallSession createCallSession(ImsCallProfile profile)
             throws RemoteException {
         synchronized (mLock) {
@@ -456,21 +427,45 @@
         }
     }
 
-    public IImsUt getUtInterface() throws RemoteException {
+    public ImsUt createOrGetUtInterface() throws RemoteException {
         synchronized (mLock) {
+            if (mUt != null) return mUt;
+
             checkServiceIsReady();
-            return getServiceInterface(mBinder).getUtInterface();
+            IImsUt imsUt = getServiceInterface(mBinder).getUtInterface();
+            // This will internally set up a listener on the ImsUtImplBase interface, and there is
+            // a limitation that there can only be one. If multiple connections try to create this
+            // UT interface, it will throw an IllegalStateException.
+            mUt = (imsUt != null) ? new ImsUt(imsUt) : null;
+            return mUt;
         }
     }
 
-    public IImsConfig getConfigInterface() {
-        return getConfig();
+    private void setEcbmInterface(ImsEcbmStateListener ecbmListener) throws RemoteException {
+        synchronized (mLock) {
+            if (mEcbm.getState() != BinderAccessState.STATE_NOT_SET) {
+                throw new IllegalStateException("ECBM interface already open");
+            }
+
+            checkServiceIsReady();
+            IImsEcbm imsEcbm = getServiceInterface(mBinder).getEcbmInterface();
+            mEcbm = (imsEcbm != null) ? BinderAccessState.of(new ImsEcbm(imsEcbm)) :
+                    new BinderAccessState<>(BinderAccessState.STATE_NOT_SUPPORTED);
+            if (mEcbm.getState() == BinderAccessState.STATE_AVAILABLE) {
+                // May throw an IllegalStateException if a listener already exists.
+                mEcbm.getInterface().setEcbmStateListener(ecbmListener);
+            }
+        }
     }
 
-    public IImsEcbm getEcbmInterface() throws RemoteException {
+    public ImsEcbm getEcbmInterface() {
         synchronized (mLock) {
-            checkServiceIsReady();
-            return getServiceInterface(mBinder).getEcbmInterface();
+            if (mEcbm.getState() == BinderAccessState.STATE_NOT_SET) {
+                throw new IllegalStateException("ECBM interface has not been opened");
+            }
+
+            return mEcbm.getState() == BinderAccessState.STATE_AVAILABLE ?
+                    mEcbm.getInterface() : null;
         }
     }
 
@@ -482,10 +477,22 @@
         }
     }
 
-    public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException {
+    private void setMultiEndpointInterface(ImsExternalCallStateListener listener)
+            throws RemoteException {
         synchronized (mLock) {
+            if (mMultiEndpoint.getState() != BinderAccessState.STATE_NOT_SET) {
+                throw new IllegalStateException("multiendpoint interface is already open");
+            }
+
             checkServiceIsReady();
-            return getServiceInterface(mBinder).getMultiEndpointInterface();
+            IImsMultiEndpoint imEndpoint = getServiceInterface(mBinder).getMultiEndpointInterface();
+            mMultiEndpoint = (imEndpoint != null)
+                    ? BinderAccessState.of(new ImsMultiEndpoint(imEndpoint)) :
+                    new BinderAccessState<>(BinderAccessState.STATE_NOT_SUPPORTED);
+            if (mMultiEndpoint.getState() == BinderAccessState.STATE_AVAILABLE) {
+                // May throw an IllegalStateException if a listener already exists.
+                mMultiEndpoint.getInterface().setExternalCallStateListener(listener);
+            }
         }
     }
 
@@ -562,9 +569,12 @@
     }
 
     @Override
-    protected IImsRegistration getRegistrationBinder() {
-        TelephonyManager tm = getTelephonyManager();
-        return  tm != null ? tm.getImsRegistration(mSlotId, ImsFeature.FEATURE_MMTEL) : null;
+    public void onFeatureCapabilitiesUpdated(long capabilities)
+    {
+        synchronized (mLock) {
+            mSupportsEmergencyCalling =
+                    ((capabilities | ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL) > 0);
+        }
     }
 
     private IImsMmTelFeature getServiceInterface(IBinder b) {
diff --git a/src/java/com/android/ims/RcsFeatureConnection.java b/src/java/com/android/ims/RcsFeatureConnection.java
index 98e5576..b090810 100644
--- a/src/java/com/android/ims/RcsFeatureConnection.java
+++ b/src/java/com/android/ims/RcsFeatureConnection.java
@@ -18,20 +18,26 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.net.Uri;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.telephony.TelephonyManager;
+import android.telephony.ims.aidl.ICapabilityExchangeEventListener;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IImsConfig;
 import android.telephony.ims.aidl.IImsRcsFeature;
 import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
-import android.telephony.ims.aidl.IRcsFeatureListener;
+import android.telephony.ims.aidl.IPublishResponseCallback;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
 import android.telephony.ims.feature.CapabilityChangeRequest;
-import android.telephony.ims.feature.ImsFeature;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.telephony.Rlog;
 
+import java.util.List;
+
 /**
  * A container of the IImsServiceController binder, which implements all of the RcsFeatures that
  * the platform currently supports: RCS
@@ -107,134 +113,57 @@
         }
     }
 
-    public static @NonNull RcsFeatureConnection create(Context context , int slotId,
-            IFeatureUpdate callback) {
-
-        RcsFeatureConnection serviceProxy = new RcsFeatureConnection(context, slotId, callback);
-
-        if (!ImsManager.isImsSupportedOnDevice(context)) {
-            // Return empty service proxy in the case that IMS is not supported.
-            sImsSupportedOnDevice = false;
-            Rlog.w(TAG, "create: IMS is not supported");
-            return serviceProxy;
-        }
-
-        TelephonyManager tm =
-                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        if (tm == null) {
-            Rlog.w(TAG, "create: TelephonyManager is null");
-            return serviceProxy;
-        }
-
-        IImsRcsFeature binder = tm.getImsRcsFeatureAndListen(slotId, serviceProxy.getListener());
-        if (binder != null) {
-            Rlog.d(TAG, "create: set binder");
-            serviceProxy.setBinder(binder.asBinder());
-            // Trigger the cache to be updated for feature status.
-            serviceProxy.getFeatureState();
-        } else {
-            Rlog.i(TAG, "create: binder is null! Slot Id: " + slotId);
-        }
-        return serviceProxy;
-    }
-
     @VisibleForTesting
     public AvailabilityCallbackManager mAvailabilityCallbackManager;
     @VisibleForTesting
     public RegistrationCallbackManager mRegistrationCallbackManager;
 
-    private RcsFeatureConnection(Context context, int slotId, IFeatureUpdate callback) {
-        super(context, slotId);
-        setStatusCallback(callback);
+    public RcsFeatureConnection(Context context, int slotId, IImsRcsFeature feature, IImsConfig c,
+            IImsRegistration r, ISipTransport s) {
+        super(context, slotId, c, r, s);
+        setBinder(feature != null ? feature.asBinder() : null);
         mAvailabilityCallbackManager = new AvailabilityCallbackManager(mContext);
         mRegistrationCallbackManager = new RegistrationCallbackManager(mContext);
     }
 
     public void close() {
-        removeRcsFeatureListener();
+        removeCapabilityExchangeEventListener();
         mAvailabilityCallbackManager.close();
         mRegistrationCallbackManager.close();
     }
 
     @Override
     protected void onRemovedOrDied() {
-        removeImsFeatureCallback();
+        close();
         super.onRemovedOrDied();
+    }
+
+    public void setCapabilityExchangeEventListener(ICapabilityExchangeEventListener listener)
+            throws RemoteException {
         synchronized (mLock) {
-            close();
+            // Only check if service is alive. The feature status may not be READY.
+            checkServiceIsAlive();
+            getServiceInterface(mBinder).setCapabilityExchangeEventListener(listener);
         }
     }
 
-    private void removeImsFeatureCallback() {
-        TelephonyManager tm = getTelephonyManager();
-        if (tm != null) {
-            tm.unregisterImsFeatureCallback(mSlotId, ImsFeature.FEATURE_RCS, getListener());
-        }
-    }
-
-    @Override
-    @VisibleForTesting
-    public void handleImsFeatureCreatedCallback(int slotId, int feature) {
-        logi("IMS feature created: slotId= " + slotId + ", feature=" + feature);
-        if (!isUpdateForThisFeatureAndSlot(slotId, feature)) {
-            return;
-        }
-        synchronized(mLock) {
-            if (!mIsAvailable) {
-                logi("RCS enabled on slotId: " + slotId);
-                mIsAvailable = true;
-            }
-        }
-    }
-
-    @Override
-    @VisibleForTesting
-    public void handleImsFeatureRemovedCallback(int slotId, int feature) {
-        logi("IMS feature removed: slotId= " + slotId + ", feature=" + feature);
-        if (!isUpdateForThisFeatureAndSlot(slotId, feature)) {
-            return;
-        }
-        synchronized(mLock) {
-            logi("Rcs UCE removed on slotId: " + slotId);
-            onRemovedOrDied();
-        }
-    }
-
-    @Override
-    @VisibleForTesting
-    public void handleImsStatusChangedCallback(int slotId, int feature, int status) {
-        logi("IMS status changed: slotId=" + slotId + ", feature=" + feature + ", status="
-                + status);
-        if (!isUpdateForThisFeatureAndSlot(slotId, feature)) {
-            return;
-        }
-        synchronized(mLock) {
-            mFeatureStateCached = status;
-        }
-    }
-
-    private boolean isUpdateForThisFeatureAndSlot(int slotId, int feature) {
-        if (mSlotId == slotId && feature == ImsFeature.FEATURE_RCS) {
-            return true;
-        }
-        return false;
-    }
-
-    public void setRcsFeatureListener(IRcsFeatureListener listener) throws RemoteException {
-        synchronized (mLock) {
-            checkServiceIsReady();
-            getServiceInterface(mBinder).setListener(listener);
-        }
-    }
-
-    public void removeRcsFeatureListener() {
+    public void removeCapabilityExchangeEventListener() {
         try {
-            setRcsFeatureListener(null);
+            setCapabilityExchangeEventListener(null);
         } catch (RemoteException e) {
             // If we are not still connected, there is no need to fail removing.
         }
     }
 
+    private void checkServiceIsAlive() throws RemoteException {
+        if (!sImsSupportedOnDevice) {
+            throw new RemoteException("IMS is not supported on this device.");
+        }
+        if (!isBinderAlive()) {
+            throw new RemoteException("ImsServiceProxy is not alive.");
+        }
+    }
+
     public int queryCapabilityStatus() throws RemoteException {
         synchronized (mLock) {
             checkServiceIsReady();
@@ -298,6 +227,31 @@
         }
     }
 
+    public void requestPublication(String pidfXml, IPublishResponseCallback responseCallback)
+            throws RemoteException {
+        synchronized (mLock) {
+            checkServiceIsReady();
+            getServiceInterface(mBinder).publishCapabilities(pidfXml, responseCallback);
+        }
+    }
+
+    public void requestCapabilities(List<Uri> uris, ISubscribeResponseCallback c)
+            throws RemoteException {
+        synchronized (mLock) {
+            checkServiceIsReady();
+            getServiceInterface(mBinder).subscribeForCapabilities(uris, c);
+        }
+    }
+
+    public void sendOptionsCapabilityRequest(Uri contactUri, List<String> myCapabilities,
+            IOptionsResponseCallback callback) throws RemoteException {
+        synchronized (mLock) {
+            checkServiceIsReady();
+            getServiceInterface(mBinder).sendOptionsCapabilityRequest(contactUri, myCapabilities,
+                    callback);
+        }
+    }
+
     @Override
     @VisibleForTesting
     public Integer retrieveFeatureState() {
@@ -312,9 +266,9 @@
     }
 
     @Override
-    protected IImsRegistration getRegistrationBinder() {
-        TelephonyManager tm = getTelephonyManager();
-        return  tm != null ? tm.getImsRegistration(mSlotId, ImsFeature.FEATURE_RCS) : null;
+    public void onFeatureCapabilitiesUpdated(long capabilities)
+    {
+        // doesn't do anything for RCS yet.
     }
 
     @VisibleForTesting
diff --git a/src/java/com/android/ims/RcsFeatureManager.java b/src/java/com/android/ims/RcsFeatureManager.java
index 7f1f819..af2298a 100644
--- a/src/java/com/android/ims/RcsFeatureManager.java
+++ b/src/java/com/android/ims/RcsFeatureManager.java
@@ -18,32 +18,47 @@
 
 import android.content.Context;
 import android.net.Uri;
+import android.os.IBinder;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.telephony.BinderCacheManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
-import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.TelephonyFrameworkInitializer;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType;
 import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.aidl.ICapabilityExchangeEventListener;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRcsController;
+import android.telephony.ims.aidl.IImsRcsFeature;
+import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
-import android.telephony.ims.aidl.IRcsFeatureListener;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.aidl.IPublishResponseCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
 import android.telephony.ims.feature.CapabilityChangeRequest;
+import android.telephony.ims.feature.ImsFeature;
 import android.telephony.ims.feature.RcsFeature;
 import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
-import android.telephony.ims.stub.RcsCapabilityExchange;
-import android.telephony.ims.stub.RcsPresenceExchangeImplBase;
-import android.telephony.ims.stub.RcsSipOptionsImplBase;
 import android.util.Log;
 
-import com.android.ims.FeatureConnection.IFeatureUpdate;
+import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.telephony.Rlog;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 
@@ -53,7 +68,7 @@
  * - Registering/Unregistering availability/registration callbacks.
  * - Querying Registration and Capability information.
  */
-public class RcsFeatureManager implements IFeatureConnector {
+public class RcsFeatureManager implements FeatureUpdates {
     private static final String TAG = "RcsFeatureManager";
     private static boolean DBG = true;
 
@@ -61,129 +76,98 @@
     private static final int CAPABILITY_PRESENCE = RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE;
 
     /**
-     * Callbacks from the RcsFeature, which have an empty default implementation and can be
-     * overridden for each Feature.
+     * The capability exchange event callbacks from the RcsFeature.
      */
-    public static class RcsFeatureCallbacks {
-        /** See {@link RcsCapabilityExchange#onCommandUpdate(int, int)} */
-        void onCommandUpdate(int commandCode, int operationToken) {}
-
-        /** See {@link RcsPresenceExchangeImplBase#onNetworkResponse(int, String, int)} */
-        public void onNetworkResponse(int code, String reason, int operationToken) {}
-
-        /** See {@link RcsPresenceExchangeImplBase#onCapabilityRequestResponse(List, int)} */
-        public void onCapabilityRequestResponsePresence(List<RcsContactUceCapability> infos,
-                int operationToken) {}
-
-        /** See {@link RcsPresenceExchangeImplBase#onNotifyUpdateCapabilites(int)} */
-        public void onNotifyUpdateCapabilities(int publishTriggerType) {}
-
-        /** See {@link RcsPresenceExchangeImplBase#onUnpublish()} */
-        public void onUnpublish() {}
+    public interface CapabilityExchangeEventCallback {
+        /**
+         * Triggered by RcsFeature to publish the device's capabilities to the network.
+         */
+        void onRequestPublishCapabilities(@StackPublishTriggerType int publishTriggerType);
 
         /**
-         * See {@link RcsSipOptionsImplBase#onCapabilityRequestResponse(int,String,
-         * RcsContactUceCapability, int)}
+         * Notify that the devices is unpublished.
          */
-        public void onCapabilityRequestResponseOptions(int code, String reason,
-                RcsContactUceCapability info, int operationToken) {}
+        void onUnpublish();
 
         /**
-         * See {@link RcsSipOptionsImplBase#onRemoteCapabilityRequest(Uri, RcsContactUceCapability,
-         * int)}
+         * Receive a capabilities request from the remote client.
          */
-        public void onRemoteCapabilityRequest(Uri contactUri, RcsContactUceCapability remoteInfo,
-                int operationToken) {}
+        void onRemoteCapabilityRequest(Uri contactUri,
+                List<String> remoteCapabilities, IOptionsRequestCallback cb);
     }
 
-    private final IRcsFeatureListener mRcsFeatureCallbackAdapter = new IRcsFeatureListener.Stub() {
-        @Override
-        public void onCommandUpdate(int commandCode, int operationToken) {
-            mRcsFeatureCallbacks.forEach(listener-> listener.onCommandUpdate(commandCode,
-                    operationToken));
-        }
+    /*
+     * Setup the listener to listen to the requests and updates from ImsService.
+     */
+    private ICapabilityExchangeEventListener mCapabilityEventListener =
+            new ICapabilityExchangeEventListener.Stub() {
+                @Override
+                public void onRequestPublishCapabilities(@StackPublishTriggerType int type) {
+                    mCapabilityEventCallback.forEach(
+                            callback -> callback.onRequestPublishCapabilities(type));
+                }
 
-        @Override
-        public void onNetworkResponse(int code, String reason, int operationToken) {
-            mRcsFeatureCallbacks.forEach(listener-> listener.onNetworkResponse(code, reason,
-                    operationToken));
-        }
+                @Override
+                public void onUnpublish() {
+                    mCapabilityEventCallback.forEach(callback -> callback.onUnpublish());
+                }
 
-        @Override
-        public void onCapabilityRequestResponsePresence(List<RcsContactUceCapability> infos,
-                int operationToken) {
-            mRcsFeatureCallbacks.forEach(listener-> listener.onCapabilityRequestResponsePresence(
-                    infos, operationToken));
-        }
-
-        @Override
-        public void onNotifyUpdateCapabilities(int publishTriggerType) {
-            mRcsFeatureCallbacks.forEach(listener-> listener.onNotifyUpdateCapabilities(
-                    publishTriggerType));
-        }
-
-        @Override
-        public void onUnpublish() {
-            mRcsFeatureCallbacks.forEach(listener-> listener.onUnpublish());
-        }
-
-        @Override
-        public void onCapabilityRequestResponseOptions(int code, String reason,
-                RcsContactUceCapability info, int operationToken) {
-            mRcsFeatureCallbacks.forEach(listener -> listener.onCapabilityRequestResponseOptions(
-                    code, reason, info, operationToken));
-        }
-
-        @Override
-        public void onRemoteCapabilityRequest(Uri contactUri, RcsContactUceCapability remoteInfo,
-                int operationToken) {
-            mRcsFeatureCallbacks.forEach(listener -> listener.onRemoteCapabilityRequest(
-                    contactUri, remoteInfo, operationToken));
-        }
-    };
+                @Override
+                public void onRemoteCapabilityRequest(Uri contactUri,
+                        List<String> remoteCapabilities, IOptionsRequestCallback cb) {
+                    mCapabilityEventCallback.forEach(
+                            callback -> callback.onRemoteCapabilityRequest(
+                                    contactUri, remoteCapabilities, cb));
+                }
+            };
 
     private final int mSlotId;
     private final Context mContext;
-    @VisibleForTesting
-    public final Set<IFeatureUpdate> mStatusCallbacks = new CopyOnWriteArraySet<>();
-    private final Set<RcsFeatureCallbacks> mRcsFeatureCallbacks = new CopyOnWriteArraySet<>();
+    private final Set<CapabilityExchangeEventCallback> mCapabilityEventCallback
+            = new CopyOnWriteArraySet<>();
+    private final BinderCacheManager<IImsRcsController> mBinderCache
+            = new BinderCacheManager<>(RcsFeatureManager::getIImsRcsControllerInterface);
 
     @VisibleForTesting
     public RcsFeatureConnection mRcsFeatureConnection;
 
-    public RcsFeatureManager(Context context, int slotId) {
-        mContext = context;
-        mSlotId = slotId;
-
-        createImsService();
+    /**
+     * Use to obtain a FeatureConnector, which will maintain a consistent listener to the
+     * RcsFeature attached to the specified slotId. If the RcsFeature changes (due to things like
+     * SIM swap), a new RcsFeatureManager will be delivered to this Listener.
+     * @param context The Context this connector should use.
+     * @param slotId The slotId associated with the Listener and requested RcsFeature
+     * @param listener The listener, which will be used to generate RcsFeatureManager instances.
+     * @param executor The executor that the Listener callbacks will be called on.
+     * @param logPrefix The prefix used in logging of the FeatureConnector for notable events.
+     * @return A FeatureConnector, which will start delivering RcsFeatureManagers as the underlying
+     * RcsFeature instances become available to the platform.
+     * @see {@link FeatureConnector#connect()}.
+     */
+    public static FeatureConnector<RcsFeatureManager> getConnector(Context context, int slotId,
+            FeatureConnector.Listener<RcsFeatureManager> listener, Executor executor,
+            String logPrefix) {
+        ArrayList<Integer> filter = new ArrayList<>();
+        filter.add(ImsFeature.STATE_READY);
+        return new FeatureConnector<>(context, slotId, RcsFeatureManager::new, logPrefix, filter,
+                listener, executor);
     }
 
-    // Binds the IMS service to the RcsFeature instance.
-    private void createImsService() {
-        mRcsFeatureConnection = RcsFeatureConnection.create(mContext, mSlotId,
-                new IFeatureUpdate() {
-                    @Override
-                    public void notifyStateChanged() {
-                        mStatusCallbacks.forEach(
-                            FeatureConnection.IFeatureUpdate::notifyStateChanged);
-                    }
-                    @Override
-                    public void notifyUnavailable() {
-                        logi("RcsFeature is unavailable");
-                        mStatusCallbacks.forEach(
-                            FeatureConnection.IFeatureUpdate::notifyUnavailable);
-                    }
-                });
+    /**
+     * Use {@link #getConnector} to get an instance of this class.
+     */
+    private RcsFeatureManager(Context context, int slotId) {
+        mContext = context;
+        mSlotId = slotId;
     }
 
     /**
      * Opens a persistent connection to the RcsFeature. This must be called before the RcsFeature
-     * can be used to communicate. Triggers a {@link RcsFeature#onFeatureReady()} call on the
-     * service side.
+     * can be used to communicate.
      */
     public void openConnection() throws android.telephony.ims.ImsException {
         try {
-            mRcsFeatureConnection.setRcsFeatureListener(mRcsFeatureCallbackAdapter);
+            mRcsFeatureConnection.setCapabilityExchangeEventListener(mCapabilityEventListener);
         } catch (RemoteException e){
             throw new android.telephony.ims.ImsException("Service is not available.",
                     android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
@@ -196,39 +180,38 @@
      */
     public void releaseConnection() {
         try {
-            mRcsFeatureConnection.setRcsFeatureListener(null);
+            mRcsFeatureConnection.setCapabilityExchangeEventListener(null);
         } catch (RemoteException e){
             // Connection may not be available at this point.
         }
-        mStatusCallbacks.clear();
         mRcsFeatureConnection.close();
-        mRcsFeatureCallbacks.clear();
+        mCapabilityEventCallback.clear();
     }
 
     /**
-     * Adds a callback for {@link RcsFeatureCallbacks}.
+     * Adds a callback for {@link CapabilityExchangeEventCallback}.
      * Note: These callbacks will be sent on the binder thread used to notify the callback.
      */
-    public void addFeatureListenerCallback(RcsFeatureCallbacks listener) {
-        mRcsFeatureCallbacks.add(listener);
+    public void addCapabilityEventCallback(CapabilityExchangeEventCallback listener) {
+        mCapabilityEventCallback.add(listener);
     }
 
     /**
-     * Removes an existing {@link RcsFeatureCallbacks}.
+     * Removes an existing {@link CapabilityExchangeEventCallback}.
      */
-    public void removeFeatureListenerCallback(RcsFeatureCallbacks listener) {
-        mRcsFeatureCallbacks.remove(listener);
+    public void removeCapabilityEventCallback(CapabilityExchangeEventCallback listener) {
+        mCapabilityEventCallback.remove(listener);
     }
 
     /**
      * Update the capabilities for this RcsFeature.
      */
-    public void updateCapabilities() throws android.telephony.ims.ImsException {
-        boolean optionsSupport = isOptionsSupported();
-        boolean presenceSupported = isPresenceSupported();
+    public void updateCapabilities(int newSubId) throws android.telephony.ims.ImsException {
+        boolean optionsSupport = isOptionsSupported(newSubId);
+        boolean presenceSupported = isPresenceSupported(newSubId);
 
-        logi("Update capabilities for slot " + mSlotId + ": options=" + optionsSupport
-                + ", presence=" + presenceSupported);
+        logi("Update capabilities for slot " + mSlotId + " and sub " + newSubId + ": options="
+                + optionsSupport+ ", presence=" + presenceSupported);
 
         if (optionsSupport || presenceSupported) {
             CapabilityChangeRequest request = new CapabilityChangeRequest();
@@ -326,6 +309,30 @@
             mRcsFeatureConnection.removeCallbackForSubscription(subId, callback);
     }
 
+    public boolean isImsServiceCapable(@ImsService.ImsServiceCapability long capabilities)
+            throws ImsException {
+        try {
+            return mRcsFeatureConnection.isCapable(capabilities);
+        } catch (RemoteException e) {
+            throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    /**
+     * @return The SipTransport interface if it exists or {@code null} if it does not exist due to
+     * the ImsService not supporting it.
+     */
+    public ISipTransport getSipTransport() throws ImsException {
+        if (!isImsServiceCapable(ImsService.CAPABILITY_SIP_DELEGATE_CREATION)) {
+            return null;
+        }
+        return mRcsFeatureConnection.getSipTransport();
+    }
+
+    public IImsRegistration getImsRegistration() {
+        return mRcsFeatureConnection.getRegistration();
+    }
+
     /**
      * Query for the specific capability.
      */
@@ -383,9 +390,13 @@
     /**
      * Query the availability of an IMS RCS capability.
      */
-    public boolean isAvailable(@RcsImsCapabilities.RcsImsCapabilityFlag int capability)
+    public boolean isAvailable(@RcsImsCapabilities.RcsImsCapabilityFlag int capability,
+            @ImsRegistrationImplBase.ImsRegistrationTech int radioTech)
             throws android.telephony.ims.ImsException {
         try {
+            if (mRcsFeatureConnection.getRegistrationTech() != radioTech) {
+                return false;
+            }
             int currentStatus = mRcsFeatureConnection.queryCapabilityStatus();
             return new RcsImsCapabilities(currentStatus).isCapable(capability);
         } catch (RemoteException e) {
@@ -396,49 +407,46 @@
     }
 
     /**
-     * Adds a callback for status changed events if the binder is already available. If it is not,
-     * this method will throw an ImsException.
-     */
-    @Override
-    public void addNotifyStatusChangedCallbackIfAvailable(FeatureConnection.IFeatureUpdate c)
-            throws android.telephony.ims.ImsException {
-        if (!mRcsFeatureConnection.isBinderAlive()) {
-            throw new android.telephony.ims.ImsException("Can not connect to service.",
-                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
-        }
-        if (c != null) {
-            mStatusCallbacks.add(c);
-        }
-    }
-
-    @Override
-    public void removeNotifyStatusChangedCallback(FeatureConnection.IFeatureUpdate c) {
-        if (c != null) {
-            mStatusCallbacks.remove(c);
-        }
-    }
-
-    /**
      * Add UCE capabilities with given type.
      * @param capability the specific RCS UCE capability wants to enable
      */
     public void addRcsUceCapability(CapabilityChangeRequest request,
             @RcsImsCapabilities.RcsImsCapabilityFlag int capability) {
         request.addCapabilitiesToEnableForTech(capability,
+                ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+        request.addCapabilitiesToEnableForTech(capability,
                 ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
         request.addCapabilitiesToEnableForTech(capability,
                 ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
     }
 
+    public void requestPublication(String pidfXml, IPublishResponseCallback responseCallback)
+            throws RemoteException {
+        mRcsFeatureConnection.requestPublication(pidfXml, responseCallback);
+    }
+
+    public void requestCapabilities(List<Uri> uris, ISubscribeResponseCallback c)
+            throws RemoteException {
+        mRcsFeatureConnection.requestCapabilities(uris, c);
+    }
+
+    public void sendOptionsCapabilityRequest(Uri contactUri, List<String> myCapabilities,
+            IOptionsResponseCallback callback) throws RemoteException {
+        mRcsFeatureConnection.sendOptionsCapabilityRequest(contactUri, myCapabilities, callback);
+    }
+
     /**
      * Disable all of the UCE capabilities.
      */
     private void disableAllRcsUceCapabilities() throws android.telephony.ims.ImsException {
+        final int techNr = ImsRegistrationImplBase.REGISTRATION_TECH_NR;
         final int techLte = ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
         final int techIWlan = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
         CapabilityChangeRequest request = new CapabilityChangeRequest();
+        request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techNr);
         request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techLte);
         request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techIWlan);
+        request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techNr);
         request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techLte);
         request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techIWlan);
         sendCapabilityChangeRequest(request);
@@ -455,50 +463,128 @@
         }
     }
 
-    private boolean isOptionsSupported() {
-        return isCapabilityTypeSupported(mContext, mSlotId, CAPABILITY_OPTIONS);
+    private boolean isOptionsSupported(int subId) {
+        return isCapabilityTypeSupported(mContext, subId, CAPABILITY_OPTIONS);
     }
 
-    private boolean isPresenceSupported() {
-        return isCapabilityTypeSupported(mContext, mSlotId, CAPABILITY_PRESENCE);
+    private boolean isPresenceSupported(int subId) {
+        return isCapabilityTypeSupported(mContext, subId, CAPABILITY_PRESENCE);
     }
 
     /*
      * Check if the given type of capability is supported.
      */
     private static boolean isCapabilityTypeSupported(
-        Context context, int slotId, int capabilityType) {
+        Context context, int subId, int capabilityType) {
 
-        int subId = sSubscriptionManagerProxy.getSubId(slotId);
         if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            Log.e(TAG, "isCapabilityTypeSupported: Getting subIds is failure! slotId=" + slotId);
+            Log.e(TAG, "isCapabilityTypeSupported: Invalid subId=" + subId);
             return false;
         }
 
         CarrierConfigManager configManager =
             (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
         if (configManager == null) {
-            Log.e(TAG, "isCapabilityTypeSupported: CarrierConfigManager is null, " + slotId);
+            Log.e(TAG, "isCapabilityTypeSupported: CarrierConfigManager is null, " + subId);
             return false;
         }
 
         PersistableBundle b = configManager.getConfigForSubId(subId);
         if (b == null) {
-            Log.e(TAG, "isCapabilityTypeSupported: PersistableBundle is null, " + slotId);
+            Log.e(TAG, "isCapabilityTypeSupported: PersistableBundle is null, " + subId);
             return false;
         }
 
         if (capabilityType == CAPABILITY_OPTIONS) {
             return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL, false);
         } else if (capabilityType == CAPABILITY_PRESENCE) {
-            return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, false);
+            return b.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_PUBLISH_BOOL, false);
         }
         return false;
     }
 
     @Override
-    public int getImsServiceState() throws ImsException {
-        return mRcsFeatureConnection.getFeatureState();
+    public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
+        IImsRcsController controller = mBinderCache.listenOnBinder(cb, () -> {
+            try {
+                cb.imsFeatureRemoved(
+                        FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+            } catch (RemoteException ignore) {} // This is local.
+        });
+
+        try {
+            if (controller == null) {
+                Log.e(TAG, "registerRcsFeatureListener: IImsRcsController is null");
+                cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+                return;
+            }
+            controller.registerRcsFeatureCallback(slotId, cb);
+        } catch (ServiceSpecificException e) {
+            try {
+                switch (e.errorCode) {
+                    case ImsException.CODE_ERROR_UNSUPPORTED_OPERATION:
+                        cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+                        break;
+                    default: {
+                        cb.imsFeatureRemoved(
+                                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+                    }
+                }
+            } catch (RemoteException ignore) {} // Already dead anyway if this happens.
+        } catch (RemoteException e) {
+            try {
+                cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+            } catch (RemoteException ignore) {} // Already dead if this happens.
+        }
+    }
+
+    @Override
+    public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
+        try {
+            IImsRcsController imsRcsController = mBinderCache.removeRunnable(cb);
+            if (imsRcsController != null) {
+                imsRcsController.unregisterImsFeatureCallback(cb);
+            }
+        } catch (RemoteException e) {
+            // This means that telephony died, so do not worry about it.
+            Rlog.e(TAG, "unregisterImsFeatureCallback (RCS), RemoteException: "
+                    + e.getMessage());
+        }
+    }
+
+    private IImsRcsController getIImsRcsController() {
+        return mBinderCache.getBinder();
+    }
+
+    private static IImsRcsController getIImsRcsControllerInterface() {
+        IBinder binder = TelephonyFrameworkInitializer
+                .getTelephonyServiceManager()
+                .getTelephonyImsServiceRegisterer()
+                .get();
+        IImsRcsController c = IImsRcsController.Stub.asInterface(binder);
+        return c;
+    }
+
+    @Override
+    public void associate(ImsFeatureContainer c) {
+        IImsRcsFeature f = IImsRcsFeature.Stub.asInterface(c.imsFeature);
+        mRcsFeatureConnection = new RcsFeatureConnection(mContext, mSlotId, f, c.imsConfig,
+                c.imsRegistration, c.sipTransport);
+    }
+
+    @Override
+    public void invalidate() {
+        mRcsFeatureConnection.onRemovedOrDied();
+    }
+
+    @Override
+    public void updateFeatureState(int state) {
+        mRcsFeatureConnection.updateFeatureState(state);
+    }
+
+    @Override
+    public void updateFeatureCapabilities(long capabilities) {
+        mRcsFeatureConnection.updateFeatureCapabilities(capabilities);
     }
 
     /**
@@ -513,6 +599,10 @@
         int getSubId(int slotId);
     }
 
+    public IImsConfig getConfig() {
+        return mRcsFeatureConnection.getConfig();
+    }
+
     private static SubscriptionManagerProxy sSubscriptionManagerProxy
             = slotId -> {
                 int[] subIds = SubscriptionManager.getSubId(slotId);
diff --git a/src/java/com/android/ims/internal/ConferenceParticipant.java b/src/java/com/android/ims/internal/ConferenceParticipant.java
index 12edd16..d48ecf6 100644
--- a/src/java/com/android/ims/internal/ConferenceParticipant.java
+++ b/src/java/com/android/ims/internal/ConferenceParticipant.java
@@ -119,7 +119,6 @@
                                     callDirection);
                     participant.setConnectTime(connectTime);
                     participant.setConnectElapsedTime(elapsedRealTime);
-                    participant.setCallDirection(callDirection);
                     return participant;
                 }
 
diff --git a/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java b/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
index 57bdad0..d0592e1 100644
--- a/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
+++ b/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
@@ -19,6 +19,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -235,7 +236,7 @@
      *
      * @param videoProvider
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public ImsVideoCallProviderWrapper(IImsVideoCallProvider videoProvider)
             throws RemoteException {
 
diff --git a/src/java/com/android/ims/rcs/uce/ControllerBase.java b/src/java/com/android/ims/rcs/uce/ControllerBase.java
new file mode 100644
index 0000000..57f0fc7
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/ControllerBase.java
@@ -0,0 +1,44 @@
+/*
+ * 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.ims.rcs.uce;
+
+import com.android.ims.RcsFeatureManager;
+
+/**
+ * The base interface of each controllers.
+ */
+public interface ControllerBase {
+    /**
+     * The RcsFeature has been connected to the framework.
+     */
+    void onRcsConnected(RcsFeatureManager manager);
+
+    /**
+     * The framework has lost the binding to the RcsFeature.
+     */
+    void onRcsDisconnected();
+
+    /**
+     * Notify to destroy this instance. The UceController instance is unusable after destroyed.
+     */
+    void onDestroy();
+
+    /**
+     * Notify the controller that the Carrier Config has changed.
+     */
+    void onCarrierConfigChanged();
+}
diff --git a/src/java/com/android/ims/rcs/uce/OWNERS b/src/java/com/android/ims/rcs/uce/OWNERS
new file mode 100644
index 0000000..dff71c4
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/OWNERS
@@ -0,0 +1,3 @@
+jamescflin@google.com
+calvinpan@google.com
+allenwtsu@google.com
\ No newline at end of file
diff --git a/src/java/com/android/ims/rcs/uce/UceController.java b/src/java/com/android/ims/rcs/uce/UceController.java
new file mode 100644
index 0000000..bdc7ebd
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/UceController.java
@@ -0,0 +1,807 @@
+/*
+ * 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.ims.rcs.uce;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.eab.EabController;
+import com.android.ims.rcs.uce.eab.EabControllerImpl;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.options.OptionsControllerImpl;
+import com.android.ims.rcs.uce.presence.publish.PublishController;
+import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeControllerImpl;
+import com.android.ims.rcs.uce.request.UceRequestManager;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.os.SomeArgs;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The UceController will manage the RCS UCE requests on a per subscription basis. When it receives
+ * the UCE requests from the RCS applications and from the ImsService, it will coordinate the
+ * cooperation between the publish/subscribe/options components to complete the requests.
+ */
+public class UceController {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceController";
+
+    /**
+     * The callback interface is called by the internal controllers to receive information from
+     * others controllers.
+     */
+    public interface UceControllerCallback {
+        /**
+         * Retrieve the capabilities associated with the given uris from the cache.
+         */
+        List<EabCapabilityResult> getCapabilitiesFromCache(@NonNull List<Uri> uris);
+
+        /**
+         * Retrieve the contact's capabilities from the availability cache.
+         */
+        EabCapabilityResult getAvailabilityFromCache(@NonNull Uri uri);
+
+        /**
+         * Store the given capabilities to the cache.
+         */
+        void saveCapabilities(List<RcsContactUceCapability> contactCapabilities);
+
+        /**
+         * Retrieve the device's capabilities.
+         */
+        RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism);
+
+        /**
+         * Refresh the device state. It is called when receive the UCE request response.
+         * @param sipCode The SIP code of the request response.
+         * @param reason The reason from the network response.
+         */
+        void refreshDeviceState(int sipCode, String reason);
+
+        /**
+         * Reset the device state when then device disallowed state is expired.
+         */
+        void resetDeviceState();
+
+        /**
+         * Get the current device state to check if the device is allowed to send UCE requests.
+         */
+        DeviceStateResult getDeviceState();
+
+        /**
+         * Setup timer to exit device disallowed state.
+         */
+        void setupResetDeviceStateTimer(long resetAfterSec);
+
+        /**
+         * The device state is already reset, clear the timer.
+         */
+        void clearResetDeviceStateTimer();
+
+        /**
+         * The method is called when the given contacts' capabilities are expired and need to be
+         * refreshed.
+         */
+        void refreshCapabilities(@NonNull List<Uri> contactNumbers,
+                @NonNull IRcsUceControllerCallback callback) throws RemoteException;
+    }
+
+    /**
+     * Used to inject RequestManger instances for testing.
+     */
+    @VisibleForTesting
+    public interface RequestManagerFactory {
+        UceRequestManager createRequestManager(Context context, int subId, Looper looper,
+                UceControllerCallback callback);
+    }
+
+    private RequestManagerFactory mRequestManagerFactory = (context, subId, looper, callback) ->
+            new UceRequestManager(context, subId, looper, callback);
+
+    /**
+     * Used to inject Controller instances for testing.
+     */
+    @VisibleForTesting
+    public interface ControllerFactory {
+        /**
+         * @return an {@link EabController} associated with the subscription id specified.
+         */
+        EabController createEabController(Context context, int subId, UceControllerCallback c,
+                Looper looper);
+
+        /**
+         * @return an {@link PublishController} associated with the subscription id specified.
+         */
+        PublishController createPublishController(Context context, int subId,
+                UceControllerCallback c, Looper looper);
+
+        /**
+         * @return an {@link SubscribeController} associated with the subscription id specified.
+         */
+        SubscribeController createSubscribeController(Context context, int subId);
+
+        /**
+         * @return an {@link OptionsController} associated with the subscription id specified.
+         */
+        OptionsController createOptionsController(Context context, int subId);
+    }
+
+    private ControllerFactory mControllerFactory = new ControllerFactory() {
+        @Override
+        public EabController createEabController(Context context, int subId,
+                UceControllerCallback c, Looper looper) {
+            return new EabControllerImpl(context, subId, c, looper);
+        }
+
+        @Override
+        public PublishController createPublishController(Context context, int subId,
+                UceControllerCallback c, Looper looper) {
+            return new PublishControllerImpl(context, subId, c, looper);
+        }
+
+        @Override
+        public SubscribeController createSubscribeController(Context context, int subId) {
+            return new SubscribeControllerImpl(context, subId);
+        }
+
+        @Override
+        public OptionsController createOptionsController(Context context, int subId) {
+            return new OptionsControllerImpl(context, subId);
+        }
+    };
+
+    /**
+     * Cache the capabilities events triggered by the ImsService during the RCS connected procedure.
+     */
+    private static class CachedCapabilityEvent {
+        private Optional<Integer> mRequestPublishCapabilitiesEvent;
+        private Optional<Boolean> mUnpublishEvent;
+        private Optional<SomeArgs> mRemoteCapabilityRequestEvent;
+
+        public CachedCapabilityEvent() {
+            mRequestPublishCapabilitiesEvent = Optional.empty();
+            mUnpublishEvent = Optional.empty();
+            mRemoteCapabilityRequestEvent = Optional.empty();
+        }
+
+        /**
+         * Cache the publish capabilities request event triggered by the ImsService.
+         */
+        public synchronized void setRequestPublishCapabilitiesEvent(int triggerType) {
+            mRequestPublishCapabilitiesEvent = Optional.of(triggerType);
+        }
+
+        /**
+         * Cache the unpublish event triggered by the ImsService.
+         */
+        public synchronized void setOnUnpublishEvent() {
+            mUnpublishEvent = Optional.of(Boolean.TRUE);
+        }
+
+        /**
+         * Cache the remote capability request event triggered by the ImsService.
+         */
+        public synchronized void setRemoteCapabilityRequestEvent(Uri contactUri,
+                List<String> remoteCapabilities, IOptionsRequestCallback callback) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = contactUri;
+            args.arg2 = remoteCapabilities;
+            args.arg3 = callback;
+            mRemoteCapabilityRequestEvent = Optional.of(args);
+        }
+
+        /** @Return the cached publish request event */
+        public synchronized Optional<Integer> getRequestPublishEvent() {
+            return mRequestPublishCapabilitiesEvent;
+        }
+
+        /** @Return the cached unpublish event */
+        public synchronized Optional<Boolean> getUnpublishEvent() {
+            return mUnpublishEvent;
+        }
+
+        /** @Return the cached remote capability request event */
+        public synchronized Optional<SomeArgs> getRemoteCapabilityRequestEvent() {
+            return mRemoteCapabilityRequestEvent;
+        }
+
+        /** Clear the cached */
+        public synchronized void clear() {
+            mRequestPublishCapabilitiesEvent = Optional.empty();
+            mUnpublishEvent = Optional.empty();
+            mRemoteCapabilityRequestEvent.ifPresent(args -> args.recycle());
+            mRemoteCapabilityRequestEvent = Optional.empty();
+        }
+    }
+
+    /** The RCS state is disconnected */
+    private static final int RCS_STATE_DISCONNECTED = 0;
+
+    /** The RCS state is connecting */
+    private static final int RCS_STATE_CONNECTING = 1;
+
+    /** The RCS state is connected */
+    private static final int RCS_STATE_CONNECTED = 2;
+
+    @IntDef(value = {
+        RCS_STATE_DISCONNECTED,
+        RCS_STATE_CONNECTING,
+        RCS_STATE_CONNECTED,
+    }, prefix="RCS_STATE_")
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RcsConnectedState {}
+
+    private final int mSubId;
+    private final Context mContext;
+    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+
+    private volatile Looper mLooper;
+    private volatile boolean mIsDestroyedFlag;
+    private volatile @RcsConnectedState int mRcsConnectedState;
+
+    private RcsFeatureManager mRcsFeatureManager;
+    private EabController mEabController;
+    private PublishController mPublishController;
+    private SubscribeController mSubscribeController;
+    private OptionsController mOptionsController;
+    private UceRequestManager mRequestManager;
+    // The device state to execute UCE requests.
+    private UceDeviceState mDeviceState;
+    // The cache of the capability request event triggered by ImsService
+    private final CachedCapabilityEvent mCachedCapabilityEvent;
+
+    public UceController(Context context, int subId) {
+        mSubId = subId;
+        mContext = context;
+        mCachedCapabilityEvent = new CachedCapabilityEvent();
+        mRcsConnectedState = RCS_STATE_DISCONNECTED;
+        logi("create");
+
+        initLooper();
+        initControllers();
+        initRequestManager();
+        initUceDeviceState();
+    }
+
+    @VisibleForTesting
+    public UceController(Context context, int subId, UceDeviceState deviceState,
+            ControllerFactory controllerFactory, RequestManagerFactory requestManagerFactory) {
+        mSubId = subId;
+        mContext = context;
+        mDeviceState = deviceState;
+        mControllerFactory = controllerFactory;
+        mRequestManagerFactory = requestManagerFactory;
+        mCachedCapabilityEvent = new CachedCapabilityEvent();
+        mRcsConnectedState = RCS_STATE_DISCONNECTED;
+        initLooper();
+        initControllers();
+        initRequestManager();
+    }
+
+    private void initLooper() {
+        // Init the looper, it will be passed to each controller.
+        HandlerThread handlerThread = new HandlerThread("UceControllerHandlerThread");
+        handlerThread.start();
+        mLooper = handlerThread.getLooper();
+    }
+
+    private void initControllers() {
+        mEabController = mControllerFactory.createEabController(mContext, mSubId, mCtrlCallback,
+                mLooper);
+        mPublishController = mControllerFactory.createPublishController(mContext, mSubId,
+                mCtrlCallback, mLooper);
+        mSubscribeController = mControllerFactory.createSubscribeController(mContext, mSubId);
+        mOptionsController = mControllerFactory.createOptionsController(mContext, mSubId);
+    }
+
+    private void initRequestManager() {
+        mRequestManager = mRequestManagerFactory.createRequestManager(mContext, mSubId, mLooper,
+                mCtrlCallback);
+        mRequestManager.setSubscribeController(mSubscribeController);
+        mRequestManager.setOptionsController(mOptionsController);
+    }
+
+    private void initUceDeviceState() {
+        mDeviceState = new UceDeviceState(mSubId, mContext, mCtrlCallback);
+        mDeviceState.checkSendResetDeviceStateTimer();
+    }
+
+    /**
+     * The RcsFeature has been connected to the framework. This method runs on main thread.
+     */
+    public void onRcsConnected(RcsFeatureManager manager) {
+        logi("onRcsConnected");
+        // Set the RCS is connecting flag
+        mRcsConnectedState = RCS_STATE_CONNECTING;
+
+        // Listen to the capability exchange event which is triggered by the ImsService
+        mRcsFeatureManager = manager;
+        mRcsFeatureManager.addCapabilityEventCallback(mCapabilityEventListener);
+
+        // Notify each controllers that RCS is connected.
+        mEabController.onRcsConnected(manager);
+        mPublishController.onRcsConnected(manager);
+        mSubscribeController.onRcsConnected(manager);
+        mOptionsController.onRcsConnected(manager);
+
+        // Set the RCS is connected flag and check if there is any capability event received during
+        // the connecting process.
+        mRcsConnectedState = RCS_STATE_CONNECTED;
+        handleCachedCapabilityEvent();
+    }
+
+    /**
+     * The framework has lost the binding to the RcsFeature. This method runs on main thread.
+     */
+    public void onRcsDisconnected() {
+        logi("onRcsDisconnected");
+        mRcsConnectedState = RCS_STATE_DISCONNECTED;
+        // Remove the listener because RCS is disconnected.
+        if (mRcsFeatureManager != null) {
+            mRcsFeatureManager.removeCapabilityEventCallback(mCapabilityEventListener);
+            mRcsFeatureManager = null;
+        }
+        // Notify each controllers that RCS is disconnected.
+        mEabController.onRcsDisconnected();
+        mPublishController.onRcsDisconnected();
+        mSubscribeController.onRcsDisconnected();
+        mOptionsController.onRcsDisconnected();
+    }
+
+    /**
+     * Notify to destroy this instance. This instance is unusable after destroyed.
+     */
+    public void onDestroy() {
+        logi("onDestroy");
+        mIsDestroyedFlag = true;
+        // Remove the listener because the UceController instance is destroyed.
+        if (mRcsFeatureManager != null) {
+            mRcsFeatureManager.removeCapabilityEventCallback(mCapabilityEventListener);
+            mRcsFeatureManager = null;
+        }
+        // Destroy all the controllers
+        mRequestManager.onDestroy();
+        mEabController.onDestroy();
+        mPublishController.onDestroy();
+        mSubscribeController.onDestroy();
+        mOptionsController.onDestroy();
+
+        // Execute all the existing requests before quitting the looper.
+        mLooper.quitSafely();
+    }
+
+    /**
+     * Notify all associated classes that the carrier configuration has changed for the subId.
+     */
+    public void onCarrierConfigChanged() {
+        mEabController.onCarrierConfigChanged();
+        mPublishController.onCarrierConfigChanged();
+        mSubscribeController.onCarrierConfigChanged();
+        mOptionsController.onCarrierConfigChanged();
+    }
+
+    private void handleCachedCapabilityEvent() {
+        Optional<Integer> requestPublishEvent = mCachedCapabilityEvent.getRequestPublishEvent();
+        requestPublishEvent.ifPresent(triggerType ->
+            onRequestPublishCapabilitiesFromService(triggerType));
+
+        Optional<Boolean> unpublishEvent = mCachedCapabilityEvent.getUnpublishEvent();
+        unpublishEvent.ifPresent(unpublish -> onUnpublish());
+
+        Optional<SomeArgs> remoteRequest = mCachedCapabilityEvent.getRemoteCapabilityRequestEvent();
+        remoteRequest.ifPresent(args -> {
+            Uri contactUri = (Uri) args.arg1;
+            List<String> remoteCapabilities = (List<String>) args.arg2;
+            IOptionsRequestCallback callback = (IOptionsRequestCallback) args.arg3;
+            retrieveOptionsCapabilitiesForRemote(contactUri, remoteCapabilities, callback);
+        });
+        mCachedCapabilityEvent.clear();
+    }
+
+    /*
+     * The implementation of the interface UceControllerCallback. These methods are called by other
+     * controllers.
+     */
+    private UceControllerCallback mCtrlCallback = new UceControllerCallback() {
+        @Override
+        public List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uris) {
+            return mEabController.getCapabilities(uris);
+        }
+
+        @Override
+        public EabCapabilityResult getAvailabilityFromCache(Uri contactUri) {
+            return mEabController.getAvailability(contactUri);
+        }
+
+        @Override
+        public void saveCapabilities(List<RcsContactUceCapability> contactCapabilities) {
+            mEabController.saveCapabilities(contactCapabilities);
+        }
+
+        @Override
+        public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
+            return mPublishController.getDeviceCapabilities(mechanism);
+        }
+
+        @Override
+        public void refreshDeviceState(int sipCode, String reason) {
+            mDeviceState.refreshDeviceState(sipCode, reason);
+        }
+
+        @Override
+        public void resetDeviceState() {
+            mDeviceState.resetDeviceState();
+        }
+
+        @Override
+        public DeviceStateResult getDeviceState() {
+            return mDeviceState.getCurrentState();
+        }
+
+        @Override
+        public void setupResetDeviceStateTimer(long resetAfterSec) {
+            mPublishController.setupResetDeviceStateTimer(resetAfterSec);
+        }
+
+        @Override
+        public void clearResetDeviceStateTimer() {
+            mPublishController.clearResetDeviceStateTimer();
+        }
+
+        @Override
+        public void refreshCapabilities(@NonNull List<Uri> contactNumbers,
+                @NonNull IRcsUceControllerCallback callback) throws RemoteException{
+            logd("refreshCapabilities: " + contactNumbers.size());
+            UceController.this.requestCapabilitiesInternal(contactNumbers, true, callback);
+        }
+    };
+
+    @VisibleForTesting
+    public void setUceControllerCallback(UceControllerCallback callback) {
+        mCtrlCallback = callback;
+    }
+
+    /*
+     * Setup the listener to listen to the requests and updates from ImsService.
+     */
+    private RcsFeatureManager.CapabilityExchangeEventCallback mCapabilityEventListener =
+            new RcsFeatureManager.CapabilityExchangeEventCallback() {
+                @Override
+                public void onRequestPublishCapabilities(
+                        @StackPublishTriggerType int triggerType) {
+                    if (isRcsConnecting()) {
+                        mCachedCapabilityEvent.setRequestPublishCapabilitiesEvent(triggerType);
+                        return;
+                    }
+                    onRequestPublishCapabilitiesFromService(triggerType);
+                }
+
+                @Override
+                public void onUnpublish() {
+                    if (isRcsConnecting()) {
+                        mCachedCapabilityEvent.setOnUnpublishEvent();
+                        return;
+                    }
+                    UceController.this.onUnpublish();
+                }
+
+                @Override
+                public void onRemoteCapabilityRequest(Uri contactUri,
+                        List<String> remoteCapabilities, IOptionsRequestCallback cb) {
+                    if (contactUri == null || remoteCapabilities == null || cb == null) {
+                        logw("onRemoteCapabilityRequest: parameter cannot be null");
+                        return;
+                    }
+                    if (isRcsConnecting()) {
+                        mCachedCapabilityEvent.setRemoteCapabilityRequestEvent(contactUri,
+                                remoteCapabilities, cb);
+                        return;
+                    }
+                    retrieveOptionsCapabilitiesForRemote(contactUri, remoteCapabilities, cb);
+                }
+            };
+
+    /**
+     * Request to get the contacts' capabilities. This method will retrieve the capabilities from
+     * the cache If the capabilities are out of date, it will trigger another request to get the
+     * latest contact's capabilities from the network.
+     */
+    public void requestCapabilities(@NonNull List<Uri> uriList,
+            @NonNull IRcsUceControllerCallback c) throws RemoteException {
+        requestCapabilitiesInternal(uriList, false, c);
+    }
+
+    private void requestCapabilitiesInternal(@NonNull List<Uri> uriList, boolean skipFromCache,
+            @NonNull IRcsUceControllerCallback c) throws RemoteException {
+        if (uriList == null || uriList.isEmpty() || c == null) {
+            logw("requestCapabilities: parameter is empty");
+            if (c != null) {
+                c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+            }
+            return;
+        }
+
+        if (isUnavailable()) {
+            logw("requestCapabilities: controller is unavailable");
+            c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+            return;
+        }
+
+        // Return if the device is not allowed to execute UCE requests.
+        DeviceStateResult deviceStateResult = mDeviceState.getCurrentState();
+        if (deviceStateResult.isRequestForbidden()) {
+            int deviceState = deviceStateResult.getDeviceState();
+            int errorCode = deviceStateResult.getErrorCode()
+                    .orElse(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            long retryAfterMillis = deviceStateResult.getRequestRetryAfterMillis();
+            logw("requestCapabilities: The device is disallowed, deviceState= " + deviceState +
+                    ", errorCode=" + errorCode + ", retryAfterMillis=" + retryAfterMillis);
+            c.onError(errorCode, retryAfterMillis);
+            return;
+        }
+
+        // Trigger the capabilities request task
+        logd("requestCapabilities: size=" + uriList.size());
+        mRequestManager.sendCapabilityRequest(uriList, skipFromCache, c);
+    }
+
+    /**
+     * Request to get the contact's capabilities. It will check the availability cache first. If
+     * the capability in the availability cache is expired then it will retrieve the capability
+     * from the network.
+     */
+    public void requestAvailability(@NonNull Uri uri, @NonNull IRcsUceControllerCallback c)
+            throws RemoteException {
+        if (uri == null || c == null) {
+            logw("requestAvailability: parameter is empty");
+            if (c != null) {
+                c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+            }
+            return;
+        }
+
+        if (isUnavailable()) {
+            logw("requestAvailability: controller is unavailable");
+            c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+            return;
+        }
+
+        // Return if the device is not allowed to execute UCE requests.
+        DeviceStateResult deviceStateResult = mDeviceState.getCurrentState();
+        if (deviceStateResult.isRequestForbidden()) {
+            int deviceState = deviceStateResult.getDeviceState();
+            int errorCode = deviceStateResult.getErrorCode()
+                    .orElse(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            long retryAfterMillis = deviceStateResult.getRequestRetryAfterMillis();
+            logw("requestAvailability: The device is disallowed, deviceState= " + deviceState +
+                    ", errorCode=" + errorCode + ", retryAfterMillis=" + retryAfterMillis);
+            c.onError(errorCode, retryAfterMillis);
+            return;
+        }
+
+        // Trigger the availability request task
+        logd("requestAvailability");
+        mRequestManager.sendAvailabilityRequest(uri, c);
+    }
+
+    /**
+     * Publish the device's capabilities. This request is triggered from the ImsService.
+     */
+    public void onRequestPublishCapabilitiesFromService(@StackPublishTriggerType int triggerType) {
+        logd("onRequestPublishCapabilitiesFromService: " + triggerType);
+        // Reset the device state when the service triggers to publish the device's capabilities
+        mDeviceState.resetDeviceState();
+        // Send the publish request.
+        mPublishController.requestPublishCapabilitiesFromService(triggerType);
+    }
+
+    /**
+     * This method is triggered by the ImsService to notify framework that the device's
+     * capabilities has been unpublished from the network.
+     */
+    public void onUnpublish() {
+        logi("onUnpublish");
+        mPublishController.onUnpublish();
+    }
+
+    /**
+     * Request publish the device's capabilities. This request is from the ImsService to send the
+     * capabilities to the remote side.
+     */
+    public void retrieveOptionsCapabilitiesForRemote(@NonNull Uri contactUri,
+            @NonNull List<String> remoteCapabilities, @NonNull IOptionsRequestCallback c) {
+        logi("retrieveOptionsCapabilitiesForRemote");
+        mRequestManager.retrieveCapabilitiesForRemote(contactUri, remoteCapabilities, c);
+    }
+
+    /**
+     * Register a {@link PublishStateCallback} to receive the published state changed.
+     */
+    public void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+        mPublishController.registerPublishStateCallback(c);
+    }
+
+    /**
+     * Removes an existing {@link PublishStateCallback}.
+     */
+    public void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+        mPublishController.unregisterPublishStateCallback(c);
+    }
+
+    /**
+     * Get the UCE publish state if the PUBLISH is supported by the carrier.
+     */
+    public @PublishState int getUcePublishState() {
+        return mPublishController.getUcePublishState();
+    }
+
+    /**
+     * Add new feature tags to the Set used to calculate the capabilities in PUBLISH.
+     * <p>
+     * Used for testing ONLY.
+     * @return the new capabilities that will be used for PUBLISH.
+     */
+    public RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags) {
+        return mPublishController.addRegistrationOverrideCapabilities(featureTags);
+    }
+
+    /**
+     * Remove existing feature tags to the Set used to calculate the capabilities in PUBLISH.
+     * <p>
+     * Used for testing ONLY.
+     * @return the new capabilities that will be used for PUBLISH.
+     */
+    public RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags) {
+        return mPublishController.removeRegistrationOverrideCapabilities(featureTags);
+    }
+
+    /**
+     * Clear all overrides in the Set used to calculate the capabilities in PUBLISH.
+     * <p>
+     * Used for testing ONLY.
+     * @return the new capabilities that will be used for PUBLISH.
+     */
+    public RcsContactUceCapability clearRegistrationOverrideCapabilities() {
+        return mPublishController.clearRegistrationOverrideCapabilities();
+    }
+
+    /**
+     * @return current RcsContactUceCapability instance that will be used for PUBLISH.
+     */
+    public RcsContactUceCapability getLatestRcsContactUceCapability() {
+        return mPublishController.getLatestRcsContactUceCapability();
+    }
+
+    /**
+     * Get the PIDF XML associated with the last successful publish or null if not PUBLISHed to the
+     * network.
+     */
+    public String getLastPidfXml() {
+        return mPublishController.getLastPidfXml();
+    }
+
+    /**
+     * Remove the device disallowed state.
+     * <p>
+     * Used for testing ONLY.
+     */
+    public void removeRequestDisallowedStatus() {
+        logd("removeRequestDisallowedStatus");
+        mDeviceState.resetDeviceState();
+    }
+
+    /**
+     * Set the milliseconds of capabilities request timeout.
+     * <p>
+     * Used for testing ONLY.
+     */
+    public void setCapabilitiesRequestTimeout(long timeoutAfterMs) {
+        logd("setCapabilitiesRequestTimeout: " + timeoutAfterMs);
+        UceUtils.setCapRequestTimeoutAfterMillis(timeoutAfterMs);
+    }
+
+    /**
+     * Get the subscription ID.
+     */
+    public int getSubId() {
+        return mSubId;
+    }
+
+    /**
+     * Check if the UceController is available.
+     * @return true if RCS is connected without destroyed.
+     */
+    public boolean isUnavailable() {
+        if (!isRcsConnected() || mIsDestroyedFlag) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isRcsConnecting() {
+        return mRcsConnectedState == RCS_STATE_CONNECTING;
+    }
+
+    private boolean isRcsConnected() {
+        return mRcsConnectedState == RCS_STATE_CONNECTED;
+    }
+
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("UceController" + "[subId: " + mSubId + "]:");
+        pw.increaseIndent();
+
+        pw.println("Log:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        pw.println("---");
+
+        mPublishController.dump(pw);
+
+        pw.decreaseIndent();
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[D] " + log);
+    }
+
+    private void logi(String log) {
+        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[W] " + log);
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/UceDeviceState.java b/src/java/com/android/ims/rcs/uce/UceDeviceState.java
new file mode 100644
index 0000000..5621fde
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/UceDeviceState.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.telephony.ims.RcsUceAdapter.ErrorCode;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+
+/**
+ * Manager the device state to determine whether the device is allowed to execute UCE requests or
+ * not.
+ */
+public class UceDeviceState {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceDeviceState";
+
+    /**
+     * The device is allowed to execute UCE requests.
+     */
+    private static final int DEVICE_STATE_OK = 0;
+
+    /**
+     * The device will be in the forbidden state when the network response SIP code is 403
+     */
+    private static final int DEVICE_STATE_FORBIDDEN = 1;
+
+    /**
+     * When the network response SIP code is 489 and the carrier config also indicates that needs
+     * to handle the SIP code 489, the device will be in the BAD EVENT state.
+     */
+    private static final int DEVICE_STATE_BAD_EVENT = 2;
+
+    @IntDef(value = {
+            DEVICE_STATE_OK,
+            DEVICE_STATE_FORBIDDEN,
+            DEVICE_STATE_BAD_EVENT,
+    }, prefix="DEVICE_STATE_")
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeviceStateType {}
+
+    /**
+     * The result of the current device state.
+     */
+    public static class DeviceStateResult {
+        final @DeviceStateType int mDeviceState;
+        final @ErrorCode Optional<Integer> mErrorCode;
+        final Optional<Instant> mRequestRetryTime;
+        final Optional<Instant> mExitStateTime;
+
+        public DeviceStateResult(int deviceState, Optional<Integer> errorCode,
+                Optional<Instant> requestRetryTime, Optional<Instant> exitStateTime) {
+            mDeviceState = deviceState;
+            mErrorCode = errorCode;
+            mRequestRetryTime = requestRetryTime;
+            mExitStateTime = exitStateTime;
+        }
+
+        /**
+         * Check current state to see if the UCE request is allowed to be executed.
+         */
+        public boolean isRequestForbidden() {
+            switch(mDeviceState) {
+                case DEVICE_STATE_FORBIDDEN:
+                case DEVICE_STATE_BAD_EVENT:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        public int getDeviceState() {
+            return mDeviceState;
+        }
+
+        public Optional<Integer> getErrorCode() {
+            return mErrorCode;
+        }
+
+        public Optional<Instant> getRequestRetryTime() {
+            return mRequestRetryTime;
+        }
+
+        public long getRequestRetryAfterMillis() {
+            if (!mRequestRetryTime.isPresent()) {
+                return 0L;
+            }
+            long retryAfter = ChronoUnit.MILLIS.between(Instant.now(), mRequestRetryTime.get());
+            return (retryAfter < 0L) ? 0L : retryAfter;
+        }
+
+        public Optional<Instant> getExitStateTime() {
+            return mExitStateTime;
+        }
+
+        /**
+         * Check if the given DeviceStateResult is equal to current DeviceStateResult instance.
+         */
+        public boolean isDeviceStateEqual(DeviceStateResult otherDeviceState) {
+            if ((mDeviceState == otherDeviceState.getDeviceState()) &&
+                    mErrorCode.equals(otherDeviceState.getErrorCode()) &&
+                    mRequestRetryTime.equals(otherDeviceState.getRequestRetryTime()) &&
+                    mExitStateTime.equals(otherDeviceState.getExitStateTime())) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("DeviceState=").append(getDeviceState())
+                    .append(", ErrorCode=").append(getErrorCode())
+                    .append(", RetryTime=").append(getRequestRetryTime())
+                    .append(", retryAfterMillis=").append(getRequestRetryAfterMillis())
+                    .append(", ExitStateTime=").append(getExitStateTime());
+            return builder.toString();
+        }
+    }
+
+    private final int mSubId;
+    private final Context mContext;
+    private final UceControllerCallback mUceCtrlCallback;
+
+    private @DeviceStateType int mDeviceState;
+    private @ErrorCode Optional<Integer> mErrorCode;
+    private Optional<Instant> mRequestRetryTime;
+    private Optional<Instant> mExitStateTime;
+
+    public UceDeviceState(int subId, Context context, UceControllerCallback uceCtrlCallback) {
+        mSubId = subId;
+        mContext = context;
+        mUceCtrlCallback = uceCtrlCallback;
+
+        // Try to restore the device state from the shared preference.
+        boolean restoreFromPref = false;
+        Optional<DeviceStateResult> deviceState = UceUtils.restoreDeviceState(mContext, mSubId);
+        if (deviceState.isPresent()) {
+            restoreFromPref = true;
+            mDeviceState = deviceState.get().getDeviceState();
+            mErrorCode = deviceState.get().getErrorCode();
+            mRequestRetryTime = deviceState.get().getRequestRetryTime();
+            mExitStateTime = deviceState.get().getExitStateTime();
+        } else {
+            mDeviceState = DEVICE_STATE_OK;
+            mErrorCode = Optional.empty();
+            mRequestRetryTime = Optional.empty();
+            mExitStateTime = Optional.empty();
+        }
+        logd("UceDeviceState: restore from sharedPref=" + restoreFromPref + ", " +
+                getCurrentState());
+    }
+
+    /**
+     * Check and setup the timer to exit the request disallowed state. This method is called when
+     * the DeviceState has been initialized completed and need to restore the timer.
+     */
+    public synchronized void checkSendResetDeviceStateTimer() {
+        logd("checkSendResetDeviceStateTimer: time=" + mExitStateTime);
+        if (!mExitStateTime.isPresent()) {
+            return;
+        }
+        long expirySec = ChronoUnit.SECONDS.between(Instant.now(), mExitStateTime.get());
+        if (expirySec < 0) {
+            expirySec = 0;
+        }
+        // Setup timer to exit the request disallowed state.
+        mUceCtrlCallback.setupResetDeviceStateTimer(expirySec);
+    }
+
+    /**
+     * @return The current device state.
+     */
+    public synchronized DeviceStateResult getCurrentState() {
+        return new DeviceStateResult(mDeviceState, mErrorCode, mRequestRetryTime, mExitStateTime);
+    }
+
+    /**
+     * Update the device state to determine whether the device is allowed to send requests or not.
+     *  @param sipCode The SIP CODE of the request result.
+     *  @param reason The reason from the network response.
+     */
+    public synchronized void refreshDeviceState(int sipCode, String reason) {
+        logd("refreshDeviceState: sipCode=" + sipCode + ", reason=" + reason);
+
+        // Get the current device status before updating the state.
+        DeviceStateResult previousState = getCurrentState();
+
+        // Update the device state based on the given sip code.
+        switch (sipCode) {
+            case NetworkSipCode.SIP_CODE_FORBIDDEN:   // sip 403
+                setDeviceState(DEVICE_STATE_FORBIDDEN);
+                updateErrorCode(sipCode, reason);
+                // There is no request retry time for SIP code 403
+                removeRequestRetryTime();
+                // No timer to exit the forbidden state.
+                removeExitStateTimer();
+                break;
+
+            case NetworkSipCode.SIP_CODE_BAD_EVENT:   // sip 489
+                if (UceUtils.isRequestForbiddenBySip489(mContext, mSubId)) {
+                    setDeviceState(DEVICE_STATE_BAD_EVENT);
+                    updateErrorCode(sipCode, reason);
+                    // Setup the request retry time.
+                    setupRequestRetryTime();
+                    // Setup the timer to exit the BAD EVENT state.
+                    setupExitStateTimer();
+                }
+                break;
+
+            case NetworkSipCode.SIP_CODE_OK:
+            case NetworkSipCode.SIP_CODE_ACCEPTED:
+                // Reset the device state when the network response is OK.
+                resetInternal();
+                break;
+        }
+
+        // Get the updated device state.
+        DeviceStateResult currentState = getCurrentState();
+
+        // Remove the device state from the shared preference if the device is allowed to execute
+        // UCE requests. Otherwise, save the new state into the shared preference when the device
+        // state has changed.
+        if (!currentState.isRequestForbidden()) {
+            removeDeviceStateFromPreference();
+        } else if (!currentState.isDeviceStateEqual(previousState)) {
+            saveDeviceStateToPreference(currentState);
+        }
+
+        logd("refreshDeviceState: previous=" + previousState + ", current=" + currentState);
+    }
+
+    /**
+     * Reset the device state. This method is called when the ImsService triggers to send the
+     * PUBLISH request.
+     */
+    public synchronized void resetDeviceState() {
+        DeviceStateResult previousState = getCurrentState();
+        resetInternal();
+        DeviceStateResult currentState = getCurrentState();
+
+        // Remove the device state from shared preference because the device state has been reset.
+        removeDeviceStateFromPreference();
+
+        logd("resetDeviceState: previous=" + previousState + ", current=" + currentState);
+    }
+
+    /**
+     * The internal method to reset the device state. This method doesn't
+     */
+    private void resetInternal() {
+        setDeviceState(DEVICE_STATE_OK);
+        resetErrorCode();
+        removeRequestRetryTime();
+        removeExitStateTimer();
+    }
+
+    private void setDeviceState(@DeviceStateType int latestState) {
+        if (mDeviceState != latestState) {
+            mDeviceState = latestState;
+        }
+    }
+
+    private void updateErrorCode(int sipCode, String reason) {
+        Optional<Integer> newErrorCode = Optional.of(NetworkSipCode.getCapabilityErrorFromSipCode(
+                sipCode, reason));
+        if (!mErrorCode.equals(newErrorCode)) {
+            mErrorCode = newErrorCode;
+        }
+    }
+
+    private void resetErrorCode() {
+        if (mErrorCode.isPresent()) {
+            mErrorCode = Optional.empty();
+        }
+    }
+
+    private void setupRequestRetryTime() {
+        /*
+         * Update the request retry time when A) it has not been assigned yet or B) it has past the
+         * current time and need to be re-assigned a new retry time.
+         */
+        if (!mRequestRetryTime.isPresent() || mRequestRetryTime.get().isAfter(Instant.now())) {
+            long retryInterval = UceUtils.getRequestRetryInterval(mContext, mSubId);
+            mRequestRetryTime = Optional.of(Instant.now().plusMillis(retryInterval));
+        }
+    }
+
+    private void removeRequestRetryTime() {
+        if (mRequestRetryTime.isPresent()) {
+            mRequestRetryTime = Optional.empty();
+        }
+    }
+
+    /**
+     * Set the timer to exit the device disallowed state and then trigger a PUBLISH request.
+     */
+    private void setupExitStateTimer() {
+        if (!mExitStateTime.isPresent()) {
+            long expirySec = UceUtils.getNonRcsCapabilitiesCacheExpiration(mContext, mSubId);
+            mExitStateTime = Optional.of(Instant.now().plusSeconds(expirySec));
+            logd("setupExitStateTimer: expirationSec=" + expirySec + ", time=" + mExitStateTime);
+
+            // Setup timer to exit the request disallowed state.
+            mUceCtrlCallback.setupResetDeviceStateTimer(expirySec);
+        }
+    }
+
+    /**
+     * Remove the exit state timer.
+     */
+    private void removeExitStateTimer() {
+        if (mExitStateTime.isPresent()) {
+            mExitStateTime = Optional.empty();
+            mUceCtrlCallback.clearResetDeviceStateTimer();
+        }
+    }
+
+    /**
+     * Save the given device sate to the shared preference.
+     * @param deviceState
+     */
+    private void saveDeviceStateToPreference(DeviceStateResult deviceState) {
+        boolean result = UceUtils.saveDeviceStateToPreference(mContext, mSubId, deviceState);
+        logd("saveDeviceStateToPreference: result=" + result + ", state= " + deviceState);
+    }
+
+    /**
+     * Remove the device state information from the shared preference because the device is allowed
+     * execute UCE requests.
+     */
+    private void removeDeviceStateFromPreference() {
+        boolean result = UceUtils.removeDeviceStateFromPreference(mContext, mSubId);
+        logd("removeDeviceStateFromPreference: result=" + result);
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java b/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java
new file mode 100644
index 0000000..9b608a0
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java
@@ -0,0 +1,483 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import static com.android.ims.rcs.uce.eab.EabControllerImpl.getCapabilityCacheExpiration;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.Telephony;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class EabBulkCapabilityUpdater {
+    private final String TAG = this.getClass().getSimpleName();
+
+    private static final Uri USER_EAB_SETTING = Uri.withAppendedPath(Telephony.SimInfo.CONTENT_URI,
+            Telephony.SimInfo.COLUMN_IMS_RCS_UCE_ENABLED);
+    private static final int NUM_SECS_IN_DAY = 86400;
+
+    private final int mSubId;
+    private final Context mContext;
+    private final Handler mHandler;
+
+    private final AlarmManager.OnAlarmListener mCapabilityExpiredListener;
+    private final ContactChangedListener mContactProviderListener;
+    private final EabSettingsListener mEabSettingListener;
+    private final BroadcastReceiver mCarrierConfigChangedListener;
+    private final EabControllerImpl mEabControllerImpl;
+    private final EabContactSyncController mEabContactSyncController;
+
+    private UceController.UceControllerCallback mUceControllerCallback;
+    private List<Uri> mRefreshContactList;
+
+    private boolean mIsContactProviderListenerRegistered = false;
+    private boolean mIsEabSettingListenerRegistered = false;
+    private boolean mIsCarrierConfigListenerRegistered = false;
+    private boolean mIsCarrierConfigEnabled = false;
+
+    /**
+     * Listen capability expired intent. Only registered when
+     * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+     * capability exchange.
+     */
+    private class CapabilityExpiredListener implements AlarmManager.OnAlarmListener {
+        @Override
+        public void onAlarm() {
+            Log.d(TAG, "Capability expired.");
+            try {
+                List<Uri> expiredContactList = getExpiredContactList();
+                if (expiredContactList.size() > 0) {
+                    mUceControllerCallback.refreshCapabilities(
+                            getExpiredContactList(),
+                            mRcsUceControllerCallback);
+                } else {
+                    Log.d(TAG, "expiredContactList is empty.");
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "CapabilityExpiredListener RemoteException", e);
+            }
+        }
+    }
+
+    /**
+     * Listen contact provider change. Only registered when
+     * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+     * capability exchange.
+     */
+    private class ContactChangedListener extends ContentObserver {
+        public ContactChangedListener(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.d(TAG, "Contact changed");
+            syncContactAndRefreshCapabilities();
+        }
+    }
+
+    /**
+     * Listen EAB settings change. Only registered when
+     * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+     * capability exchange.
+     */
+    private class EabSettingsListener extends ContentObserver {
+        public EabSettingsListener(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            boolean isUserEnableUce = isUserEnableUce();
+            Log.d(TAG, "EAB user setting changed: " + isUserEnableUce);
+            if (isUserEnableUce) {
+                mHandler.post(new SyncContactRunnable());
+            } else {
+                unRegisterContactProviderListener();
+                cancelTimeAlert(mContext);
+            }
+        }
+    }
+
+    /**
+     * Listen carrier config changed to prevent this instance created before carrier config loaded.
+     */
+    private class CarrierConfigChangedListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+                CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+            Log.d(TAG, "Carrier config changed. "
+                    + "isCarrierConfigEnabled: " + mIsCarrierConfigEnabled
+                    + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+            if (!mIsCarrierConfigEnabled && isSupportBulkCapabilityExchange) {
+                enableBulkCapability();
+                updateExpiredTimeAlert();
+                mIsCarrierConfigEnabled = true;
+            } else if (mIsCarrierConfigEnabled && !isSupportBulkCapabilityExchange) {
+                onDestroy();
+            }
+        }
+    }
+
+    private IRcsUceControllerCallback mRcsUceControllerCallback = new IRcsUceControllerCallback() {
+        @Override
+        public void onCapabilitiesReceived(List<RcsContactUceCapability> contactCapabilities) {
+            Log.d(TAG, "onCapabilitiesReceived");
+            mEabControllerImpl.saveCapabilities(contactCapabilities);
+        }
+
+        @Override
+        public void onComplete() {
+            Log.d(TAG, "onComplete");
+        }
+
+        @Override
+        public void onError(int errorCode, long retryAfterMilliseconds) {
+            Log.d(TAG, "Refresh capabilities failed. Error code: " + errorCode
+                    + ", retryAfterMilliseconds: " + retryAfterMilliseconds);
+            if (retryAfterMilliseconds != 0) {
+                mHandler.postDelayed(new retryRunnable(), retryAfterMilliseconds);
+            }
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    };
+
+    private class SyncContactRunnable implements Runnable {
+        @Override
+        public void run() {
+            Log.d(TAG, "Sync contact from contact provider");
+            syncContactAndRefreshCapabilities();
+            registerContactProviderListener();
+            registerEabUserSettingsListener();
+        }
+    }
+
+    /**
+     * Re-refresh capability if error happened.
+     */
+    private class retryRunnable implements Runnable {
+        @Override
+        public void run() {
+            Log.d(TAG, "Retry refreshCapabilities()");
+
+            try {
+                mUceControllerCallback.refreshCapabilities(
+                        mRefreshContactList, mRcsUceControllerCallback);
+            } catch (RemoteException e) {
+                Log.e(TAG, "refreshCapabilities RemoteException" , e);
+            }
+        }
+    }
+
+    public EabBulkCapabilityUpdater(Context context,
+            int subId,
+            EabControllerImpl eabControllerImpl,
+            EabContactSyncController eabContactSyncController,
+            UceController.UceControllerCallback uceControllerCallback,
+            Handler handler) {
+        mContext = context;
+        mSubId = subId;
+        mEabControllerImpl = eabControllerImpl;
+        mEabContactSyncController = eabContactSyncController;
+        mUceControllerCallback = uceControllerCallback;
+
+        mHandler = handler;
+        mContactProviderListener = new ContactChangedListener(mHandler);
+        mEabSettingListener = new EabSettingsListener(mHandler);
+        mCapabilityExpiredListener = new CapabilityExpiredListener();
+        mCarrierConfigChangedListener = new CarrierConfigChangedListener();
+
+        Log.d(TAG, "create EabBulkCapabilityUpdater() subId: " + mSubId);
+
+        enableBulkCapability();
+        updateExpiredTimeAlert();
+    }
+
+    private void enableBulkCapability() {
+        boolean isUserEnableUce = isUserEnableUce();
+        boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+                CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+        Log.d(TAG, "isUserEnableUce: " + isUserEnableUce
+                + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+
+        if (isUserEnableUce && isSupportBulkCapabilityExchange) {
+            mHandler.post(new SyncContactRunnable());
+            mIsCarrierConfigEnabled = true;
+        } else if (!isUserEnableUce && isSupportBulkCapabilityExchange) {
+            registerEabUserSettingsListener();
+            mIsCarrierConfigEnabled = false;
+        } else {
+            registerCarrierConfigChanged();
+            Log.d(TAG, "Not support bulk capability exchange.");
+        }
+    }
+
+    private void syncContactAndRefreshCapabilities() {
+        mRefreshContactList = mEabContactSyncController.syncContactToEabProvider(mContext);
+        Log.d(TAG, "refresh contacts number: " + mRefreshContactList.size());
+
+        if (mUceControllerCallback == null) {
+            Log.d(TAG, "mUceControllerCallback is null.");
+            return;
+        }
+
+        try {
+            if (mRefreshContactList.size() > 0) {
+                mUceControllerCallback.refreshCapabilities(
+                        mRefreshContactList, mRcsUceControllerCallback);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "mUceControllerCallback RemoteException.", e);
+        }
+    }
+
+    protected void updateExpiredTimeAlert() {
+        boolean isUserEnableUce = isUserEnableUce();
+        boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+                CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+        Log.d(TAG, " updateExpiredTimeAlert(), isUserEnableUce: " + isUserEnableUce
+                + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+
+        if (isUserEnableUce && isSupportBulkCapabilityExchange) {
+            long expiredTimestamp = getLeastExpiredTimestamp();
+            if (expiredTimestamp == Long.MAX_VALUE) {
+                Log.d(TAG, "Can't find min timestamp in eab provider");
+                return;
+            }
+            expiredTimestamp += getCapabilityCacheExpiration(mSubId);
+            Log.d(TAG, "set time alert at " + expiredTimestamp);
+            cancelTimeAlert(mContext);
+            setTimeAlert(mContext, expiredTimestamp);
+        }
+    }
+
+    private long getLeastExpiredTimestamp() {
+        String selection = "("
+                // Query presence timestamp
+                + EabProvider.EabCommonColumns.MECHANISM + "=" + CAPABILITY_MECHANISM_PRESENCE
+                + " AND "
+                + EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + " IS NOT NULL) "
+
+                // Query options timestamp
+                + " OR " + "(" + EabProvider.EabCommonColumns.MECHANISM + "="
+                + CAPABILITY_MECHANISM_OPTIONS + " AND "
+                + EabProvider.OptionsColumns.REQUEST_TIMESTAMP + " IS NOT NULL) "
+
+                // filter by sub id
+                + " AND " + EabProvider.EabCommonColumns.SUBSCRIPTION_ID + "=" + mSubId
+
+                // filter the contact that not come from contact provider
+                + " AND " + EabProvider.ContactColumns.RAW_CONTACT_ID + " IS NOT NULL "
+                + " AND " + EabProvider.ContactColumns.DATA_ID + " IS NOT NULL ";
+
+        long minTimestamp = Long.MAX_VALUE;
+        Cursor result = mContext.getContentResolver().query(EabProvider.ALL_DATA_URI, null,
+                selection,
+                null, null);
+
+        if (result != null) {
+            while (result.moveToNext()) {
+                int mechanism = result.getInt(
+                        result.getColumnIndex(EabProvider.EabCommonColumns.MECHANISM));
+                long timestamp;
+                if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+                    timestamp = result.getLong(result.getColumnIndex(
+                            EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP));
+                } else {
+                    timestamp = result.getLong(result.getColumnIndex(
+                            EabProvider.OptionsColumns.REQUEST_TIMESTAMP));
+                }
+
+                if (timestamp < minTimestamp) {
+                    minTimestamp = timestamp;
+                }
+            }
+            result.close();
+        } else {
+            Log.d(TAG, "getLeastExpiredTimestamp() cursor is null");
+        }
+        return minTimestamp;
+    }
+
+    private void setTimeAlert(Context context, long wakeupTimeMs) {
+        AlarmManager am = context.getSystemService(AlarmManager.class);
+
+        // To prevent all devices from sending requests to the server at the same time, add a jitter
+        // time (0 sec ~ 2 days) randomly.
+        int jitterTimeSec = (int) (Math.random() * (NUM_SECS_IN_DAY * 2));
+        Log.d(TAG, " setTimeAlert: " + wakeupTimeMs + ", jitterTimeSec: " + jitterTimeSec);
+        am.set(AlarmManager.RTC_WAKEUP,
+                (wakeupTimeMs * 1000) + jitterTimeSec,
+                TAG,
+                mCapabilityExpiredListener,
+                mHandler);
+    }
+
+    private void cancelTimeAlert(Context context) {
+        Log.d(TAG, "cancelTimeAlert.");
+        AlarmManager am = context.getSystemService(AlarmManager.class);
+        am.cancel(mCapabilityExpiredListener);
+    }
+
+    private boolean getBooleanCarrierConfig(String key, int subId) {
+        CarrierConfigManager mConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+        PersistableBundle b = null;
+        if (mConfigManager != null) {
+            b = mConfigManager.getConfigForSubId(subId);
+        }
+        if (b != null) {
+            return b.getBoolean(key);
+        } else {
+            Log.w(TAG, "getConfigForSubId(subId) is null. Return the default value of " + key);
+            return CarrierConfigManager.getDefaultConfig().getBoolean(key);
+        }
+    }
+
+    private boolean isUserEnableUce() {
+        ImsManager manager = mContext.getSystemService(ImsManager.class);
+        if (manager == null) {
+            Log.e(TAG, "ImsManager is null");
+            return false;
+        }
+        try {
+            ImsRcsManager rcsManager = manager.getImsRcsManager(mSubId);
+            return (rcsManager != null) && rcsManager.getUceAdapter().isUceSettingEnabled();
+        } catch (Exception e) {
+            Log.e(TAG, "hasUserEnabledUce: exception = " + e.getMessage());
+        }
+        return false;
+    }
+
+    private List<Uri> getExpiredContactList() {
+        List<Uri> refreshList = new ArrayList<>();
+        long expiredTime = (System.currentTimeMillis() / 1000)
+                + getCapabilityCacheExpiration(mSubId);
+        String selection = "("
+                + EabProvider.EabCommonColumns.MECHANISM + "=" + CAPABILITY_MECHANISM_PRESENCE
+                + " AND " + EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + "<"
+                + expiredTime + ")";
+        selection += " OR " + "(" + EabProvider.EabCommonColumns.MECHANISM + "="
+                + CAPABILITY_MECHANISM_OPTIONS + " AND "
+                + EabProvider.OptionsColumns.REQUEST_TIMESTAMP + "<" + expiredTime + ")";
+
+        Cursor result = mContext.getContentResolver().query(EabProvider.ALL_DATA_URI, null,
+                selection,
+                null, null);
+        while (result.moveToNext()) {
+            String phoneNumber = result.getString(
+                    result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER));
+            refreshList.add(Uri.parse(phoneNumber));
+        }
+        result.close();
+        return refreshList;
+    }
+
+    protected void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        cancelTimeAlert(mContext);
+        unRegisterContactProviderListener();
+        unRegisterEabUserSettings();
+        unRegisterCarrierConfigChanged();
+    }
+
+    private void registerContactProviderListener() {
+        Log.d(TAG, "registerContactProviderListener");
+        mIsContactProviderListenerRegistered = true;
+        mContext.getContentResolver().registerContentObserver(
+                ContactsContract.Contacts.CONTENT_URI,
+                true,
+                mContactProviderListener);
+    }
+
+    private void registerEabUserSettingsListener() {
+        Log.d(TAG, "registerEabUserSettingsListener");
+        mIsEabSettingListenerRegistered = true;
+        mContext.getContentResolver().registerContentObserver(
+                USER_EAB_SETTING,
+                true,
+                mEabSettingListener);
+    }
+
+    private void registerCarrierConfigChanged() {
+        Log.d(TAG, "registerCarrierConfigChanged");
+        mIsCarrierConfigListenerRegistered = true;
+        IntentFilter FILTER_CARRIER_CONFIG_CHANGED =
+                new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        mContext.registerReceiver(mCarrierConfigChangedListener, FILTER_CARRIER_CONFIG_CHANGED);
+    }
+
+    private void unRegisterContactProviderListener() {
+        Log.d(TAG, "unRegisterContactProviderListener");
+        if (mIsContactProviderListenerRegistered) {
+            mIsContactProviderListenerRegistered = false;
+            mContext.getContentResolver().unregisterContentObserver(mContactProviderListener);
+        }
+    }
+
+    private void unRegisterEabUserSettings() {
+        Log.d(TAG, "unRegisterEabUserSettings");
+        if (mIsEabSettingListenerRegistered) {
+            mIsEabSettingListenerRegistered = false;
+            mContext.getContentResolver().unregisterContentObserver(mEabSettingListener);
+        }
+    }
+
+    private void unRegisterCarrierConfigChanged() {
+        Log.d(TAG, "unregisterCarrierConfigChanged");
+        if (mIsCarrierConfigListenerRegistered) {
+            mIsCarrierConfigListenerRegistered = false;
+            mContext.unregisterReceiver(mCarrierConfigChangedListener);
+        }
+    }
+
+    public void setUceRequestCallback(UceController.UceControllerCallback uceControllerCallback) {
+        mUceControllerCallback = uceControllerCallback;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java b/src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java
new file mode 100644
index 0000000..0e5e01f
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The result class of retrieving capabilities from cache.
+ */
+public class EabCapabilityResult {
+
+    /**
+     * Query successful.
+     */
+    public static final int EAB_QUERY_SUCCESSFUL = 0;
+
+    /**
+     * The {@link EabControllerImpl} has been destroyed.
+     */
+    public static final int EAB_CONTROLLER_DESTROYED_FAILURE = 1;
+
+    /**
+     * The contact's capabilities expired.
+     */
+    public static final int EAB_CONTACT_EXPIRED_FAILURE = 2;
+
+    /**
+     * The contact cannot be found in the contact provider.
+     */
+    public static final int EAB_CONTACT_NOT_FOUND_FAILURE = 3;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "EAB_", value = {
+            EAB_QUERY_SUCCESSFUL,
+            EAB_CONTROLLER_DESTROYED_FAILURE,
+            EAB_CONTACT_EXPIRED_FAILURE,
+            EAB_CONTACT_NOT_FOUND_FAILURE
+    })
+    public @interface QueryResult {}
+
+    private final @QueryResult int mStatus;
+    private final Uri mContactUri;
+    private final RcsContactUceCapability mContactCapabilities;
+
+    public EabCapabilityResult(@QueryResult Uri contactUri, int status,
+            RcsContactUceCapability capabilities) {
+        mStatus = status;
+        mContactUri = contactUri;
+        mContactCapabilities = capabilities;
+    }
+
+    /**
+     * Return the status of query. The possible values are
+     * {@link EabCapabilityResult#EAB_QUERY_SUCCESSFUL},
+     * {@link EabCapabilityResult#EAB_CONTROLLER_DESTROYED_FAILURE},
+     * {@link EabCapabilityResult#EAB_CONTACT_EXPIRED_FAILURE},
+     * {@link EabCapabilityResult#EAB_CONTACT_NOT_FOUND_FAILURE}.
+     *
+     */
+    public @NonNull int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Return the contact uri.
+     */
+    public @NonNull Uri getContact() {
+        return mContactUri;
+    }
+
+    /**
+     * Return the contacts capabilities which are cached in the EAB database and
+     * are not expired.
+     */
+    public @Nullable RcsContactUceCapability getContactCapabilities() {
+        return mContactCapabilities;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java b/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java
new file mode 100644
index 0000000..e9bb9ec
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java
@@ -0,0 +1,332 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Sync the contacts from Contact Provider to EAB Provider
+ */
+public class EabContactSyncController {
+    private final String TAG = this.getClass().getSimpleName();
+
+    private static final int NOT_INIT_LAST_UPDATED_TIME = -1;
+    private static final String LAST_UPDATED_TIME_KEY = "eab_last_updated_time";
+
+    /**
+     * Sync contact from Contact provider to EAB provider. There are 4 kinds of cases need to be
+     * handled when received the contact db changed:
+     *
+     * 1. Contact deleted
+     * 2. Delete the phone number in the contact
+     * 3. Update the phone number
+     * 4. Add a new contact and add phone number
+     *
+     * @return The contacts that need to refresh
+     */
+    @VisibleForTesting
+    public List<Uri> syncContactToEabProvider(Context context) {
+        Log.d(TAG, "syncContactToEabProvider");
+        List<Uri> refreshContacts = null;
+        StringBuilder selection = new StringBuilder();
+        String[] selectionArgs = null;
+
+        // Get the last update timestamp from shared preference.
+        long lastUpdatedTimeStamp = getLastUpdatedTime(context);
+        if (lastUpdatedTimeStamp != -1) {
+            selection.append(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + ">?");
+            selectionArgs = new String[]{String.valueOf(lastUpdatedTimeStamp)};
+        }
+
+        // Contact deleted cases (case 1)
+        handleContactDeletedCase(context, lastUpdatedTimeStamp);
+
+        // Query the contacts that have not been synchronized to eab contact table.
+        Cursor updatedContact = context.getContentResolver().query(
+                ContactsContract.Data.CONTENT_URI,
+                null,
+                selection.toString(),
+                selectionArgs,
+                null);
+
+        if (updatedContact != null) {
+            Log.d(TAG, "Contact changed count: " + updatedContact.getCount());
+
+            // Delete the EAB phone number that not in contact provider (case 2). Updated phone
+            // number(case 3) also delete in here and re-insert in next step.
+            handlePhoneNumberDeletedCase(context, updatedContact);
+
+            // Insert the phone number that not in EAB provider (case 3 and case 4)
+            refreshContacts = handlePhoneNumberInsertedCase(context, updatedContact);
+
+            // Update the last update time in shared preference
+            if (updatedContact.getCount() > 0) {
+                long maxTimestamp = findMaxTimestamp(updatedContact);
+                if (maxTimestamp != Long.MIN_VALUE) {
+                    setLastUpdatedTime(context, maxTimestamp);
+                }
+            }
+            updatedContact.close();
+        } else {
+            Log.e(TAG, "Cursor is null.");
+        }
+        return refreshContacts;
+    }
+
+    /**
+     * Delete the phone numbers that contact has been deleted in contact provider. Query based on
+     * {@link ContactsContract.DeletedContacts#CONTENT_URI} to know which contact has been removed.
+     *
+     * @param timeStamp last updated timestamp
+     */
+    private void handleContactDeletedCase(Context context, long timeStamp) {
+        String selection = "";
+        if (timeStamp != -1) {
+            selection =
+                    ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">" + timeStamp;
+        }
+
+        Cursor cursor = context.getContentResolver().query(
+                ContactsContract.DeletedContacts.CONTENT_URI,
+                new String[]{ContactsContract.DeletedContacts.CONTACT_ID,
+                        ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP},
+                selection,
+                null,
+                null);
+
+        if (cursor == null) {
+            Log.d(TAG, "handleContactDeletedCase() cursor is null.");
+            return;
+        }
+
+        Log.d(TAG, "(Case 1) The count of contact that need to be deleted: "
+                + cursor.getCount());
+
+        StringBuilder deleteClause = new StringBuilder();
+        while (cursor.moveToNext()) {
+            if (deleteClause.length() > 0) {
+                deleteClause.append(" OR ");
+            }
+
+            String contactId = cursor.getString(cursor.getColumnIndex(
+                    ContactsContract.DeletedContacts.CONTACT_ID));
+            deleteClause.append(EabProvider.ContactColumns.CONTACT_ID + "=" + contactId);
+        }
+
+        if (deleteClause.toString().length() > 0) {
+            int number = context.getContentResolver().delete(
+                    EabProvider.CONTACT_URI,
+                    deleteClause.toString(),
+                    null);
+            Log.d(TAG, "(Case 1) Deleted contact count=" + number);
+        }
+    }
+
+    /**
+     * Delete phone numbers that have been deleted in the contact provider. There is no API to get
+     * deleted phone numbers easily, so check all updated contact's phone number and delete the
+     * phone number. It will also delete the phone number that has been changed.
+     */
+    private void handlePhoneNumberDeletedCase(Context context, Cursor cursor) {
+        // The map represent which contacts have which numbers.
+        Map<String, List<String>> phoneNumberMap = new HashMap<>();
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            String rawContactId = cursor.getString(
+                    cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID));
+            String number = cursor.getString(
+                    cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
+
+            if (phoneNumberMap.containsKey(rawContactId)) {
+                phoneNumberMap.get(rawContactId).add(number);
+            } else {
+                List<String> phoneNumberList = new ArrayList<>();
+                phoneNumberList.add(number);
+                phoneNumberMap.put(rawContactId, phoneNumberList);
+            }
+        }
+
+        // Build a SQL statement that delete the phone number not exist in contact provider.
+        // For example:
+        // raw_contact_id = 1 AND phone_number NOT IN (12345, 23456)
+        StringBuilder deleteClause = new StringBuilder();
+        List<String> deleteClauseArgs = new ArrayList<>();
+        for (Map.Entry<String, List<String>> entry : phoneNumberMap.entrySet()) {
+            String rawContactId = entry.getKey();
+            List<String> phoneNumberList = entry.getValue();
+
+            if (deleteClause.length() > 0) {
+                deleteClause.append(" OR ");
+            }
+
+            deleteClause.append("(" + EabProvider.ContactColumns.RAW_CONTACT_ID + "=? ");
+            deleteClauseArgs.add(rawContactId);
+
+            if (phoneNumberList.size() > 0) {
+                String argsList = phoneNumberList.stream()
+                        .map(s -> "?")
+                        .collect(Collectors.joining(", "));
+                deleteClause.append(" AND "
+                        + EabProvider.ContactColumns.PHONE_NUMBER
+                        + " NOT IN (" + argsList + "))");
+                deleteClauseArgs.addAll(phoneNumberList);
+            } else {
+                deleteClause.append(")");
+            }
+        }
+
+        if (deleteClause.length() > 1) {
+            int number = context.getContentResolver().delete(
+                    EabProvider.CONTACT_URI,
+                    deleteClause.toString(),
+                    deleteClauseArgs.toArray(new String[0]));
+            Log.d(TAG, "(Case 2, 3) handlePhoneNumberDeletedCase number count= " + number);
+        } else {
+            Log.d(TAG, "(Case 2, 3) handlePhoneNumberDeletedCase number count is empty.");
+        }
+    }
+
+    /**
+     * Insert new phone number.
+     *
+     * @param contactCursor the result of updated contact
+     * @return the contacts that need to refresh
+     */
+    private List<Uri> handlePhoneNumberInsertedCase(Context context,
+            Cursor contactCursor) {
+        List<Uri> refreshContacts = new ArrayList<>();
+        List<ContentValues> allContactData = new ArrayList<>();
+        contactCursor.moveToPosition(-1);
+
+        // Query all of contacts that store in eab provider
+        Cursor eabContact = context.getContentResolver().query(
+                EabProvider.CONTACT_URI,
+                null,
+                EabProvider.ContactColumns.DATA_ID + " IS NOT NULL",
+                null,
+                EabProvider.ContactColumns.DATA_ID);
+
+        while (contactCursor.moveToNext()) {
+            String contactId = contactCursor.getString(contactCursor.getColumnIndex(
+                    ContactsContract.Data.CONTACT_ID));
+            String rawContactId = contactCursor.getString(contactCursor.getColumnIndex(
+                    ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID));
+            String dataId = contactCursor.getString(
+                    contactCursor.getColumnIndex(ContactsContract.Data._ID));
+            String number = contactCursor.getString(
+                    contactCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
+            String mimeType = contactCursor.getString(
+                    contactCursor.getColumnIndex(ContactsContract.Data.MIMETYPE));
+
+
+            if (!mimeType.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+                continue;
+            }
+
+            int index = searchDataIdIndex(eabContact, Integer.parseInt(dataId));
+            if (index == -1) {
+                Log.d(TAG, "Data id does not exist. Insert phone number into EAB db.");
+                refreshContacts.add(Uri.parse(number));
+                ContentValues data = new ContentValues();
+                data.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+                data.put(EabProvider.ContactColumns.DATA_ID, dataId);
+                data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId);
+                data.put(EabProvider.ContactColumns.PHONE_NUMBER, number);
+                allContactData.add(data);
+            }
+        }
+
+        // Insert contacts at once
+        int result = context.getContentResolver().bulkInsert(
+                EabProvider.CONTACT_URI,
+                allContactData.toArray(new ContentValues[0]));
+        Log.d(TAG, "(Case 3, 4) Phone number insert count: " + result);
+        return refreshContacts;
+    }
+
+    /**
+     * Binary search the target data_id in the cursor.
+     *
+     * @param cursor       EabProvider contact which sorted by
+     *                     {@link EabProvider.ContactColumns#DATA_ID}
+     * @param targetDataId the data_id to search for
+     * @return the index of cursor
+     */
+    private int searchDataIdIndex(Cursor cursor, int targetDataId) {
+        int start = 0;
+        int end = cursor.getCount() - 1;
+
+        while (start <= end) {
+            int position = (start + end) >>> 1;
+            cursor.moveToPosition(position);
+            int dataId = cursor.getInt(cursor.getColumnIndex(EabProvider.ContactColumns.DATA_ID));
+
+            if (dataId > targetDataId) {
+                end = position - 1;
+            } else if (dataId < targetDataId) {
+                start = position + 1;
+            } else {
+                return position;
+            }
+        }
+        return -1;
+    }
+
+
+    private long findMaxTimestamp(Cursor cursor) {
+        long maxTimestamp = Long.MIN_VALUE;
+        cursor.moveToPosition(-1);
+        while(cursor.moveToNext()) {
+            long lastUpdatedTimeStamp = cursor.getLong(cursor.getColumnIndex(
+                    ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP));
+            Log.d(TAG, lastUpdatedTimeStamp + " " + maxTimestamp);
+            if (lastUpdatedTimeStamp > maxTimestamp) {
+                maxTimestamp = lastUpdatedTimeStamp;
+            }
+        }
+        return maxTimestamp;
+    }
+
+    private void setLastUpdatedTime(Context context, long timestamp) {
+        Log.d(TAG, "setLastUpdatedTime: " + timestamp);
+        SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        sharedPreferences.edit().putLong(LAST_UPDATED_TIME_KEY, timestamp).apply();
+    }
+
+    private long getLastUpdatedTime(Context context) {
+        SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        return sharedPreferences.getLong(LAST_UPDATED_TIME_KEY, NOT_INIT_LAST_UPDATED_TIME);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabController.java b/src/java/com/android/ims/rcs/uce/eab/EabController.java
new file mode 100644
index 0000000..903a19d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabController.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+
+import com.android.ims.rcs.uce.ControllerBase;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+
+import java.util.List;
+
+/**
+ * The interface related to the Enhanced Address Book.
+ */
+public interface EabController extends ControllerBase {
+    /**
+     * Retrieve the contacts' capabilities from the EAB database.
+     */
+    @NonNull List<EabCapabilityResult> getCapabilities(@NonNull List<Uri> uris);
+
+    /**
+     * Retrieve the contact's capabilities from the availability cache.
+     */
+    @NonNull EabCapabilityResult getAvailability(@NonNull Uri contactUri);
+
+    /**
+     * Save the capabilities to the EAB database.
+     */
+    void saveCapabilities(@NonNull List<RcsContactUceCapability> contactCapabilities);
+
+    /**
+     * Set the UceRequestCallback for sending the request to UceController.
+     */
+    void setUceRequestCallback(@NonNull UceControllerCallback c);
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java b/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
new file mode 100644
index 0000000..7bbe38e
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
@@ -0,0 +1,773 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.EAB_OPTIONS_TABLE_NAME;
+import static com.android.ims.rcs.uce.eab.EabProvider.EAB_PRESENCE_TUPLE_TABLE_NAME;
+
+import android.annotation.NonNull;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.util.TimeFormatException;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.function.Predicate;
+
+/**
+ * The implementation of EabController.
+ */
+public class EabControllerImpl implements EabController {
+    private static final String TAG = "EabControllerImpl";
+
+    // 90 days
+    private static final int DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC = 90 * 24 * 60 * 60;
+    private static final int DEFAULT_AVAILABILITY_CACHE_EXPIRATION_SEC = 60;
+
+    // 1 week
+    private static final int CLEAN_UP_LEGACY_CAPABILITY_SEC = 7 * 24 * 60 * 60;
+    private static final int CLEAN_UP_LEGACY_CAPABILITY_DELAY_MILLI_SEC = 30 * 1000;
+
+    private final Context mContext;
+    private final int mSubId;
+    private final EabBulkCapabilityUpdater mEabBulkCapabilityUpdater;
+    private final Handler mHandler;
+
+    private UceControllerCallback mUceControllerCallback;
+    private volatile boolean mIsSetDestroyedFlag = false;
+
+    @VisibleForTesting
+    public final Runnable mCapabilityCleanupRunnable = () -> {
+        Log.d(TAG, "Cleanup Capabilities");
+        cleanupExpiredCapabilities();
+    };
+
+    public EabControllerImpl(Context context, int subId, UceControllerCallback c, Looper looper) {
+        mContext = context;
+        mSubId = subId;
+        mUceControllerCallback = c;
+        mHandler = new Handler(looper);
+        mEabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(mContext, mSubId,
+                this,
+                new EabContactSyncController(),
+                mUceControllerCallback,
+                mHandler);
+    }
+
+    @Override
+    public void onRcsConnected(RcsFeatureManager manager) {
+    }
+
+    @Override
+    public void onRcsDisconnected() {
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        mIsSetDestroyedFlag = true;
+        mEabBulkCapabilityUpdater.onDestroy();
+    }
+
+    @Override
+    public void onCarrierConfigChanged() {
+        // Pick up changes to CarrierConfig and run any applicable cleanup tasks associated with
+        // that configuration.
+        mCapabilityCleanupRunnable.run();
+    }
+
+    /**
+     * Set the callback for sending the request to UceController.
+     */
+    @Override
+    public void setUceRequestCallback(UceControllerCallback c) {
+        Objects.requireNonNull(c);
+        if (mIsSetDestroyedFlag) {
+            Log.d(TAG, "EabController destroyed.");
+            return;
+        }
+        mUceControllerCallback = c;
+        mEabBulkCapabilityUpdater.setUceRequestCallback(c);
+    }
+
+    /**
+     * Retrieve the contacts' capabilities from the EAB database.
+     */
+    @Override
+    public @NonNull List<EabCapabilityResult> getCapabilities(@NonNull List<Uri> uris) {
+        Objects.requireNonNull(uris);
+        if (mIsSetDestroyedFlag) {
+            Log.d(TAG, "EabController destroyed.");
+            return generateDestroyedResult(uris);
+        }
+
+        Log.d(TAG, "getCapabilities uri size=" + uris.size());
+        List<EabCapabilityResult> capabilityResultList = new ArrayList();
+
+        for (Uri uri : uris) {
+            EabCapabilityResult result = generateEabResult(uri, this::isCapabilityExpired);
+            capabilityResultList.add(result);
+        }
+        return capabilityResultList;
+    }
+
+    /**
+     * Retrieve the contact's capabilities from the availability cache.
+     */
+    @Override
+    public @NonNull EabCapabilityResult getAvailability(@NonNull Uri contactUri) {
+        Objects.requireNonNull(contactUri);
+        if (mIsSetDestroyedFlag) {
+            Log.d(TAG, "EabController destroyed.");
+            return new EabCapabilityResult(
+                    contactUri,
+                    EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
+                    null);
+        }
+        return generateEabResult(contactUri, this::isAvailabilityExpired);
+    }
+
+    /**
+     * Update the availability catch and save the capabilities to the EAB database.
+     */
+    @Override
+    public void saveCapabilities(@NonNull List<RcsContactUceCapability> contactCapabilities) {
+        Objects.requireNonNull(contactCapabilities);
+        if (mIsSetDestroyedFlag) {
+            Log.d(TAG, "EabController destroyed.");
+            return;
+        }
+
+        Log.d(TAG, "Save capabilities: " + contactCapabilities.size());
+
+        // Update the capabilities
+        for (RcsContactUceCapability capability : contactCapabilities) {
+            String phoneNumber = getNumberFromUri(capability.getContactUri());
+            Cursor c = mContext.getContentResolver().query(
+                    EabProvider.CONTACT_URI, null,
+                    EabProvider.ContactColumns.PHONE_NUMBER + "=?",
+                    new String[]{phoneNumber}, null);
+
+            if (c != null && c.moveToNext()) {
+                int contactId = getIntValue(c, EabProvider.ContactColumns._ID);
+                if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+                    Log.d(TAG, "Insert presence capability");
+                    deleteOldPresenceCapability(contactId);
+                    insertNewPresenceCapability(contactId, capability);
+                } else if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_OPTIONS) {
+                    Log.d(TAG, "Insert options capability");
+                    deleteOldOptionCapability(contactId);
+                    insertNewOptionCapability(contactId, capability);
+                }
+            } else {
+                Log.e(TAG, "The phone number can't find in contact table. ");
+                int contactId = insertNewContact(phoneNumber);
+                if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+                    insertNewPresenceCapability(contactId, capability);
+                } else if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_OPTIONS) {
+                    insertNewOptionCapability(contactId, capability);
+                }
+            }
+
+            if (c != null) {
+                c.close();
+            }
+        }
+
+        mEabBulkCapabilityUpdater.updateExpiredTimeAlert();
+
+        if (mHandler.hasCallbacks(mCapabilityCleanupRunnable)) {
+            mHandler.removeCallbacks(mCapabilityCleanupRunnable);
+        }
+        mHandler.postDelayed(mCapabilityCleanupRunnable,
+                CLEAN_UP_LEGACY_CAPABILITY_DELAY_MILLI_SEC);
+    }
+
+    private List<EabCapabilityResult> generateDestroyedResult(List<Uri> contactUri) {
+        List<EabCapabilityResult> destroyedResult = new ArrayList<>();
+        for (Uri uri : contactUri) {
+            destroyedResult.add(new EabCapabilityResult(
+                    uri,
+                    EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
+                    null));
+        }
+        return destroyedResult;
+    }
+
+    private EabCapabilityResult generateEabResult(Uri contactUri,
+            Predicate<Cursor> isExpiredMethod) {
+        RcsUceCapabilityBuilderWrapper builder = null;
+        EabCapabilityResult result;
+
+        // query EAB provider
+        Uri queryUri = Uri.withAppendedPath(
+                Uri.withAppendedPath(EabProvider.ALL_DATA_URI, String.valueOf(mSubId)),
+                getNumberFromUri(contactUri));
+        Cursor cursor = mContext.getContentResolver().query(
+                queryUri, null, null, null, null);
+
+        if (cursor != null && cursor.getCount() != 0) {
+            while (cursor.moveToNext()) {
+                if (isExpiredMethod.test(cursor)) {
+                    continue;
+                }
+
+                if (builder == null) {
+                    builder = createNewBuilder(contactUri, cursor);
+                } else {
+                    updateCapability(contactUri, cursor, builder);
+                }
+            }
+            cursor.close();
+
+            if (builder == null) {
+                result = new EabCapabilityResult(contactUri,
+                        EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
+                        null);
+            } else {
+                if (builder.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+                    PresenceBuilder presenceBuilder = builder.getPresenceBuilder();
+                    result = new EabCapabilityResult(contactUri,
+                            EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+                            presenceBuilder.build());
+                } else {
+                    OptionsBuilder optionsBuilder = builder.getOptionsBuilder();
+                    result = new EabCapabilityResult(contactUri,
+                            EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+                            optionsBuilder.build());
+                }
+
+            }
+        } else {
+            result = new EabCapabilityResult(contactUri,
+                    EabCapabilityResult.EAB_CONTACT_NOT_FOUND_FAILURE, null);
+        }
+        return result;
+    }
+
+    private void updateCapability(Uri contactUri, Cursor cursor,
+                RcsUceCapabilityBuilderWrapper builderWrapper) {
+        if (builderWrapper.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+            PresenceBuilder builder = builderWrapper.getPresenceBuilder();
+            if (builder != null) {
+                builder.addCapabilityTuple(createPresenceTuple(contactUri, cursor));
+            }
+        } else {
+            OptionsBuilder builder = builderWrapper.getOptionsBuilder();
+            if (builder != null) {
+                builder.addFeatureTag(createOptionTuple(cursor));
+            }
+        }
+    }
+
+    private RcsUceCapabilityBuilderWrapper createNewBuilder(Uri contactUri, Cursor cursor) {
+        int mechanism = getIntValue(cursor, EabProvider.EabCommonColumns.MECHANISM);
+        int result = getIntValue(cursor, EabProvider.EabCommonColumns.REQUEST_RESULT);
+        RcsUceCapabilityBuilderWrapper builderWrapper =
+                new RcsUceCapabilityBuilderWrapper(mechanism);
+
+        if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+            PresenceBuilder builder = new PresenceBuilder(
+                    contactUri, SOURCE_TYPE_CACHED, result);
+            builder.addCapabilityTuple(createPresenceTuple(contactUri, cursor));
+            builderWrapper.setPresenceBuilder(builder);
+        } else {
+            OptionsBuilder builder = new OptionsBuilder(contactUri, SOURCE_TYPE_CACHED);
+            builder.setRequestResult(result);
+            builder.addFeatureTag(createOptionTuple(cursor));
+            builderWrapper.setOptionsBuilder(builder);
+        }
+        return builderWrapper;
+    }
+
+    private String createOptionTuple(Cursor cursor) {
+        return getStringValue(cursor, EabProvider.OptionsColumns.FEATURE_TAG);
+    }
+
+    private RcsContactPresenceTuple createPresenceTuple(Uri contactUri, Cursor cursor) {
+        // RcsContactPresenceTuple fields
+        String status = getStringValue(cursor, EabProvider.PresenceTupleColumns.BASIC_STATUS);
+        String serviceId = getStringValue(cursor, EabProvider.PresenceTupleColumns.SERVICE_ID);
+        String version = getStringValue(cursor, EabProvider.PresenceTupleColumns.SERVICE_VERSION);
+        String description = getStringValue(cursor, EabProvider.PresenceTupleColumns.DESCRIPTION);
+        String timeStamp = getStringValue(cursor,
+                EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP);
+
+        // ServiceCapabilities fields
+        boolean audioCapable = getIntValue(cursor,
+                EabProvider.PresenceTupleColumns.AUDIO_CAPABLE) == 1;
+        boolean videoCapable = getIntValue(cursor,
+                EabProvider.PresenceTupleColumns.VIDEO_CAPABLE) == 1;
+        String duplexModes = getStringValue(cursor,
+                EabProvider.PresenceTupleColumns.DUPLEX_MODE);
+        String unsupportedDuplexModes = getStringValue(cursor,
+                EabProvider.PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE);
+        String[] duplexModeList, unsupportedDuplexModeList;
+
+        if (!TextUtils.isEmpty(duplexModes)) {
+            duplexModeList = duplexModes.split(",");
+        } else {
+            duplexModeList = new String[0];
+        }
+        if (!TextUtils.isEmpty(unsupportedDuplexModes)) {
+            unsupportedDuplexModeList = unsupportedDuplexModes.split(",");
+        } else {
+            unsupportedDuplexModeList = new String[0];
+        }
+
+        // Create ServiceCapabilities
+        ServiceCapabilities serviceCapabilities;
+        ServiceCapabilities.Builder serviceCapabilitiesBuilder =
+                new ServiceCapabilities.Builder(audioCapable, videoCapable);
+        if (!TextUtils.isEmpty(duplexModes)
+                || !TextUtils.isEmpty(unsupportedDuplexModes)) {
+            for (String duplexMode : duplexModeList) {
+                serviceCapabilitiesBuilder.addSupportedDuplexMode(duplexMode);
+            }
+            for (String unsupportedDuplex : unsupportedDuplexModeList) {
+                serviceCapabilitiesBuilder.addUnsupportedDuplexMode(unsupportedDuplex);
+            }
+        }
+        serviceCapabilities = serviceCapabilitiesBuilder.build();
+
+        // Create RcsContactPresenceTuple
+        RcsContactPresenceTuple.Builder rcsContactPresenceTupleBuilder =
+                new RcsContactPresenceTuple.Builder(status, serviceId, version);
+        if (description != null) {
+            rcsContactPresenceTupleBuilder.setServiceDescription(description);
+        }
+        if (contactUri != null) {
+            rcsContactPresenceTupleBuilder.setContactUri(contactUri);
+        }
+        if (serviceCapabilities != null) {
+            rcsContactPresenceTupleBuilder.setServiceCapabilities(serviceCapabilities);
+        }
+        if (timeStamp != null) {
+            try {
+                Instant instant = Instant.ofEpochSecond(Long.parseLong(timeStamp));
+                rcsContactPresenceTupleBuilder.setTime(instant);
+            } catch (NumberFormatException ex) {
+                Log.w(TAG, "Create presence tuple: NumberFormatException");
+            } catch (DateTimeParseException e) {
+                Log.w(TAG, "Create presence tuple: parse timestamp failed");
+            }
+        }
+
+        return rcsContactPresenceTupleBuilder.build();
+    }
+
+    private boolean isCapabilityExpired(Cursor cursor) {
+        boolean expired = false;
+        String requestTimeStamp = getRequestTimestamp(cursor);
+        int capabilityCacheExpiration;
+
+        if (isNonRcsCapability(cursor)) {
+            capabilityCacheExpiration = getNonRcsCapabilityCacheExpiration(mSubId);
+        } else {
+            capabilityCacheExpiration = getCapabilityCacheExpiration(mSubId);
+        }
+
+        if (requestTimeStamp != null) {
+            Instant expiredTimestamp = Instant
+                    .ofEpochSecond(Long.parseLong(requestTimeStamp))
+                    .plus(capabilityCacheExpiration, ChronoUnit.SECONDS);
+            expired = expiredTimestamp.isBefore(Instant.now());
+            Log.d(TAG, "Capability expiredTimestamp: " + expiredTimestamp.getEpochSecond() +
+                    ", isNonRcsCapability: " +  isNonRcsCapability(cursor) +
+                    ", capabilityCacheExpiration: " + capabilityCacheExpiration +
+                    ", expired:" + expired);
+        } else {
+            Log.d(TAG, "Capability requestTimeStamp is null");
+        }
+        return expired;
+    }
+
+    private boolean isNonRcsCapability(Cursor cursor) {
+        int result = getIntValue(cursor, EabProvider.EabCommonColumns.REQUEST_RESULT);
+        return result == REQUEST_RESULT_NOT_FOUND;
+    }
+
+    private boolean isAvailabilityExpired(Cursor cursor) {
+        boolean expired = false;
+        String requestTimeStamp = getRequestTimestamp(cursor);
+
+        if (requestTimeStamp != null) {
+            Instant expiredTimestamp = Instant
+                    .ofEpochSecond(Long.parseLong(requestTimeStamp))
+                    .plus(getAvailabilityCacheExpiration(mSubId), ChronoUnit.SECONDS);
+            expired = expiredTimestamp.isBefore(Instant.now());
+            Log.d(TAG, "Availability insertedTimestamp: "
+                    + expiredTimestamp.getEpochSecond() + ", expired:" + expired);
+        } else {
+            Log.d(TAG, "Capability requestTimeStamp is null");
+        }
+        return expired;
+    }
+
+    private String getRequestTimestamp(Cursor cursor) {
+        String expiredTimestamp = null;
+        int mechanism = getIntValue(cursor, EabProvider.EabCommonColumns.MECHANISM);
+        if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+            expiredTimestamp = getStringValue(cursor,
+                    EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP);
+
+        } else if (mechanism == CAPABILITY_MECHANISM_OPTIONS) {
+            expiredTimestamp = getStringValue(cursor, EabProvider.OptionsColumns.REQUEST_TIMESTAMP);
+        }
+        return expiredTimestamp;
+    }
+
+    private int getNonRcsCapabilityCacheExpiration(int subId) {
+        int value;
+        PersistableBundle carrierConfig =
+                mContext.getSystemService(CarrierConfigManager.class).getConfigForSubId(subId);
+
+        if (carrierConfig != null) {
+            value = carrierConfig.getInt(
+                    CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT);
+        } else {
+            value = DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC;
+            Log.e(TAG, "getNonRcsCapabilityCacheExpiration: " +
+                    "CarrierConfig is null, returning default");
+        }
+        return value;
+    }
+
+    protected static int getCapabilityCacheExpiration(int subId) {
+        int value = -1;
+        try {
+            ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
+            value = pm.getProvisioningIntValue(
+                    ProvisioningManager.KEY_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC);
+        } catch (Exception ex) {
+            Log.e(TAG, "Exception in getCapabilityCacheExpiration(): " + ex);
+        }
+
+        if (value <= 0) {
+            value = DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC;
+            Log.e(TAG, "The capability expiration cannot be less than 0.");
+        }
+        return value;
+    }
+
+    protected static long getAvailabilityCacheExpiration(int subId) {
+        long value = -1;
+        try {
+            ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
+            value = pm.getProvisioningIntValue(
+                    ProvisioningManager.KEY_RCS_AVAILABILITY_CACHE_EXPIRATION_SEC);
+        } catch (Exception ex) {
+            Log.e(TAG, "Exception in getAvailabilityCacheExpiration(): " + ex);
+        }
+
+        if (value <= 0) {
+            value = DEFAULT_AVAILABILITY_CACHE_EXPIRATION_SEC;
+            Log.e(TAG, "The Availability expiration cannot be less than 0.");
+        }
+        return value;
+    }
+
+    private int insertNewContact(String phoneNumber) {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(EabProvider.ContactColumns.PHONE_NUMBER, phoneNumber);
+        Uri result = mContext.getContentResolver().insert(EabProvider.CONTACT_URI, contentValues);
+        return Integer.parseInt(result.getLastPathSegment());
+    }
+
+    private void deleteOldPresenceCapability(int id) {
+        Cursor c = mContext.getContentResolver().query(
+                EabProvider.COMMON_URI,
+                new String[]{EabProvider.EabCommonColumns._ID},
+                EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
+                new String[]{String.valueOf(id)}, null);
+
+        if (c != null && c.getCount() > 0) {
+            while(c.moveToNext()) {
+                int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
+                mContext.getContentResolver().delete(
+                        EabProvider.PRESENCE_URI,
+                        EabProvider.PresenceTupleColumns.EAB_COMMON_ID + "=?",
+                        new String[]{String.valueOf(commonId)});
+            }
+        }
+
+        if (c != null) {
+            c.close();
+        }
+    }
+
+    private void insertNewPresenceCapability(int contactId, RcsContactUceCapability capability) {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, contactId);
+        contentValues.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        contentValues.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, mSubId);
+        contentValues.put(EabProvider.EabCommonColumns.REQUEST_RESULT,
+                capability.getRequestResult());
+        Uri result = mContext.getContentResolver().insert(EabProvider.COMMON_URI, contentValues);
+        int commonId = Integer.parseInt(result.getLastPathSegment());
+        Log.d(TAG, "Insert into common table. Id: " + commonId);
+
+        ContentValues[] presenceContent =
+                new ContentValues[capability.getCapabilityTuples().size()];
+        for (int i = 0; i < presenceContent.length; i++) {
+            RcsContactPresenceTuple tuple = capability.getCapabilityTuples().get(i);
+
+            // Create new ServiceCapabilities
+            ServiceCapabilities serviceCapabilities = tuple.getServiceCapabilities();
+            String duplexMode = null, unsupportedDuplexMode = null;
+            if (serviceCapabilities != null) {
+                List<String> duplexModes = serviceCapabilities.getSupportedDuplexModes();
+                if (duplexModes.size() != 0) {
+                    duplexMode = TextUtils.join(",", duplexModes);
+                }
+
+                List<String> unsupportedDuplexModes =
+                        serviceCapabilities.getUnsupportedDuplexModes();
+                if (unsupportedDuplexModes.size() != 0) {
+                    unsupportedDuplexMode =
+                            TextUtils.join(",", unsupportedDuplexModes);
+                }
+            }
+
+            // Using the current timestamp if the timestamp doesn't populate
+            Long timestamp;
+            if (tuple.getTime() != null) {
+                timestamp = tuple.getTime().getEpochSecond();
+            } else {
+                timestamp = Instant.now().getEpochSecond();
+            }
+
+            contentValues = new ContentValues();
+            contentValues.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonId);
+            contentValues.put(EabProvider.PresenceTupleColumns.BASIC_STATUS, tuple.getStatus());
+            contentValues.put(EabProvider.PresenceTupleColumns.SERVICE_ID, tuple.getServiceId());
+            contentValues.put(EabProvider.PresenceTupleColumns.SERVICE_VERSION,
+                    tuple.getServiceVersion());
+            contentValues.put(EabProvider.PresenceTupleColumns.DESCRIPTION,
+                    tuple.getServiceDescription());
+            contentValues.put(EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP, timestamp);
+            contentValues.put(EabProvider.PresenceTupleColumns.CONTACT_URI,
+                    tuple.getContactUri().toString());
+            if (serviceCapabilities != null) {
+                contentValues.put(EabProvider.PresenceTupleColumns.DUPLEX_MODE, duplexMode);
+                contentValues.put(EabProvider.PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE,
+                        unsupportedDuplexMode);
+
+                contentValues.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE,
+                        serviceCapabilities.isAudioCapable());
+                contentValues.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE,
+                        serviceCapabilities.isVideoCapable());
+            }
+            presenceContent[i] = contentValues;
+        }
+        Log.d(TAG, "Insert into presence table. count: " + presenceContent.length);
+        mContext.getContentResolver().bulkInsert(EabProvider.PRESENCE_URI, presenceContent);
+    }
+
+    private void deleteOldOptionCapability(int contactId) {
+        Cursor c = mContext.getContentResolver().query(
+                EabProvider.COMMON_URI,
+                new String[]{EabProvider.EabCommonColumns._ID},
+                EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
+                new String[]{String.valueOf(contactId)}, null);
+
+        if (c != null && c.getCount() > 0) {
+            while(c.moveToNext()) {
+                int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
+                mContext.getContentResolver().delete(
+                        EabProvider.OPTIONS_URI,
+                        EabProvider.OptionsColumns.EAB_COMMON_ID + "=?",
+                        new String[]{String.valueOf(commonId)});
+            }
+        }
+
+        if (c != null) {
+            c.close();
+        }
+    }
+
+    private void insertNewOptionCapability(int contactId, RcsContactUceCapability capability) {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, contactId);
+        contentValues.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_OPTIONS);
+        contentValues.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, mSubId);
+        contentValues.put(EabProvider.EabCommonColumns.REQUEST_RESULT,
+                capability.getRequestResult());
+        Uri result = mContext.getContentResolver().insert(EabProvider.COMMON_URI, contentValues);
+
+        int commonId = Integer.valueOf(result.getLastPathSegment());
+        List<ContentValues> optionContentList = new ArrayList<>();
+        for (String feature : capability.getFeatureTags()) {
+            contentValues = new ContentValues();
+            contentValues.put(EabProvider.OptionsColumns.EAB_COMMON_ID, commonId);
+            contentValues.put(EabProvider.OptionsColumns.FEATURE_TAG, feature);
+            contentValues.put(EabProvider.OptionsColumns.REQUEST_TIMESTAMP,
+                    Instant.now().getEpochSecond());
+            optionContentList.add(contentValues);
+        }
+
+        ContentValues[] optionContent = new ContentValues[optionContentList.size()];
+        optionContent = optionContentList.toArray(optionContent);
+        mContext.getContentResolver().bulkInsert(EabProvider.OPTIONS_URI, optionContent);
+    }
+
+    private void cleanupExpiredCapabilities() {
+        // Cleanup the capabilities that expired more than 1 week
+        long rcsCapabilitiesExpiredTime = Instant.now().getEpochSecond() -
+                getCapabilityCacheExpiration(mSubId) -
+                CLEAN_UP_LEGACY_CAPABILITY_SEC;
+
+        // Cleanup the capabilities that expired more than 1 week
+        long nonRcsCapabilitiesExpiredTime = Instant.now().getEpochSecond() -
+                getNonRcsCapabilityCacheExpiration(mSubId) -
+                CLEAN_UP_LEGACY_CAPABILITY_SEC;
+
+        cleanupCapabilities(rcsCapabilitiesExpiredTime, getRcsCommonIdList());
+        cleanupCapabilities(nonRcsCapabilitiesExpiredTime, getNonRcsCommonIdList());
+        cleanupOrphanedRows();
+    }
+
+    private void cleanupCapabilities(long rcsCapabilitiesExpiredTime, List<Integer> commonIdList) {
+        if (commonIdList.size() > 0) {
+            String presenceClause =
+                    EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
+                            " IN (" + TextUtils.join(",", commonIdList) + ") " + " AND " +
+                            EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + "<?";
+
+            String optionClause =
+                    EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
+                            " IN (" + TextUtils.join(",", commonIdList) + ") " + " AND " +
+                            EabProvider.OptionsColumns.REQUEST_TIMESTAMP + "<?";
+
+            int deletePresenceCount = mContext.getContentResolver().delete(
+                    EabProvider.PRESENCE_URI,
+                    presenceClause,
+                    new String[]{String.valueOf(rcsCapabilitiesExpiredTime)});
+
+            int deleteOptionsCount = mContext.getContentResolver().delete(
+                    EabProvider.OPTIONS_URI,
+                    optionClause,
+                    new String[]{String.valueOf(rcsCapabilitiesExpiredTime)});
+
+            Log.d(TAG, "Cleanup capabilities. deletePresenceCount: " + deletePresenceCount +
+                ",deleteOptionsCount: " + deleteOptionsCount);
+        }
+    }
+
+    private List<Integer> getRcsCommonIdList() {
+        ArrayList<Integer> list = new ArrayList<>();
+        Cursor cursor = mContext.getContentResolver().query(
+                EabProvider.COMMON_URI,
+                null,
+                EabProvider.EabCommonColumns.REQUEST_RESULT + "<>?",
+                new String[]{String.valueOf(REQUEST_RESULT_NOT_FOUND)},
+                null);
+
+        if (cursor == null) return list;
+
+        while (cursor.moveToNext()) {
+            list.add(cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID)));
+        }
+        cursor.close();
+
+        return list;
+    }
+
+    private List<Integer> getNonRcsCommonIdList() {
+        ArrayList<Integer> list = new ArrayList<>();
+        Cursor cursor = mContext.getContentResolver().query(
+                EabProvider.COMMON_URI,
+                null,
+                EabProvider.EabCommonColumns.REQUEST_RESULT + "=?",
+                new String[]{String.valueOf(REQUEST_RESULT_NOT_FOUND)},
+                null);
+
+        if (cursor == null) return list;
+
+        while (cursor.moveToNext()) {
+            list.add(cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID)));
+        }
+        cursor.close();
+
+        return list;
+    }
+
+    /**
+     * Cleanup the entry of common table that can't map to presence or option table
+     */
+    private void cleanupOrphanedRows() {
+        String presenceSelection =
+                " (SELECT " + EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
+                        " FROM " + EAB_PRESENCE_TUPLE_TABLE_NAME + ") ";
+        String optionSelection =
+                " (SELECT " + EabProvider.OptionsColumns.EAB_COMMON_ID +
+                        " FROM " + EAB_OPTIONS_TABLE_NAME + ") ";
+
+        mContext.getContentResolver().delete(
+                EabProvider.COMMON_URI,
+                EabProvider.EabCommonColumns._ID + " NOT IN " + presenceSelection +
+                        " AND " + EabProvider.EabCommonColumns._ID+ " NOT IN " + optionSelection,
+                null);
+    }
+
+    private String getStringValue(Cursor cursor, String column) {
+        return cursor.getString(cursor.getColumnIndex(column));
+    }
+
+    private int getIntValue(Cursor cursor, String column) {
+        return cursor.getInt(cursor.getColumnIndex(column));
+    }
+
+    private static String getNumberFromUri(Uri uri) {
+        String number = uri.getSchemeSpecificPart();
+        String[] numberParts = number.split("[@;:]");
+        if (numberParts.length == 0) {
+            return null;
+        }
+        return numberParts[0];
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabProvider.java b/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
new file mode 100644
index 0000000..60283c2
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
@@ -0,0 +1,667 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.content.ContentResolver.NOTIFY_DELETE;
+import static android.content.ContentResolver.NOTIFY_INSERT;
+import static android.content.ContentResolver.NOTIFY_UPDATE;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class provides the ability to query the enhanced address book databases(A.K.A. EAB) based on
+ * both SIP options and UCE presence server data.
+ *
+ * <p>
+ * There are 4 tables in EAB DB:
+ * <ul>
+ *     <li><em>Contact:</em> It stores the name and phone number of the contact.
+ *
+ *     <li><em>Common:</em> It's a general table for storing the query results and the mechanisms of
+ *     querying UCE capabilities. It should be 1:1 mapped to the contact table and has a foreign
+ *     key(eab_contact_id) that refers to the id of contact table. If the value of mechanism is
+ *     1 ({@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_PRESENCE}), the
+ *     capability information should be stored in presence table, if the value of mechanism is
+ *     2({@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_OPTIONS}), it
+ *     should be stored in options table.
+ *
+ *     <li><em>Presence:</em> It stores the information
+ *     ({@link android.telephony.ims.RcsContactUceCapability}) that queried through presence server.
+ *     It should be *:1 mapped to the common table and has a foreign key(eab_common_id) that refers
+ *     to the id of common table.
+ *
+ *     <li><em>Options:</em> It stores the information
+ *     ({@link android.telephony.ims.RcsContactUceCapability}) that queried through SIP OPTIONS. It
+ *     should be *:1 mapped to the common table and it has a foreign key(eab_common_id) that refers
+ *     to the id of common table.
+ * </ul>
+ * </p>
+ */
+public class EabProvider extends ContentProvider {
+    // The public URI for operating Eab DB. They support query, insert, delete and update.
+    public static final Uri CONTACT_URI = Uri.parse("content://eab/contact");
+    public static final Uri COMMON_URI = Uri.parse("content://eab/common");
+    public static final Uri PRESENCE_URI = Uri.parse("content://eab/presence");
+    public static final Uri OPTIONS_URI = Uri.parse("content://eab/options");
+
+    // The public URI for querying EAB DB. Only support query.
+    public static final Uri ALL_DATA_URI = Uri.parse("content://eab/all");
+
+    @VisibleForTesting
+    public static final String AUTHORITY = "eab";
+
+    private static final String TAG = "EabProvider";
+    private static final int DATABASE_VERSION = 2;
+
+    public static final String EAB_CONTACT_TABLE_NAME = "eab_contact";
+    public static final String EAB_COMMON_TABLE_NAME = "eab_common";
+    public static final String EAB_PRESENCE_TUPLE_TABLE_NAME = "eab_presence";
+    public static final String EAB_OPTIONS_TABLE_NAME = "eab_options";
+
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+    private static final int URL_CONTACT = 1;
+    private static final int URL_COMMON = 2;
+    private static final int URL_PRESENCE = 3;
+    private static final int URL_OPTIONS = 4;
+    private static final int URL_ALL = 5;
+    private static final int URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER = 6;
+
+    static {
+        URI_MATCHER.addURI(AUTHORITY, "contact", URL_CONTACT);
+        URI_MATCHER.addURI(AUTHORITY, "common", URL_COMMON);
+        URI_MATCHER.addURI(AUTHORITY, "presence", URL_PRESENCE);
+        URI_MATCHER.addURI(AUTHORITY, "options", URL_OPTIONS);
+        URI_MATCHER.addURI(AUTHORITY, "all", URL_ALL);
+        URI_MATCHER.addURI(AUTHORITY, "all/#/*", URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER);
+    }
+
+    private static final String QUERY_CONTACT_TABLE =
+            " SELECT * FROM " + EAB_CONTACT_TABLE_NAME;
+
+    private static final String JOIN_ALL_TABLES =
+            // join common table
+            " INNER JOIN " + EAB_COMMON_TABLE_NAME
+            + " ON " + EAB_CONTACT_TABLE_NAME + "." + ContactColumns._ID
+            + "=" + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns.EAB_CONTACT_ID
+
+            // join options table
+            + " LEFT JOIN " + EAB_OPTIONS_TABLE_NAME
+            + " ON " + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns._ID
+            + "=" + EAB_OPTIONS_TABLE_NAME + "." + OptionsColumns.EAB_COMMON_ID
+
+            // join presence table
+            + " LEFT JOIN " + EAB_PRESENCE_TUPLE_TABLE_NAME
+            + " ON " + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns._ID
+            + "=" + EAB_PRESENCE_TUPLE_TABLE_NAME + "."
+            + PresenceTupleColumns.EAB_COMMON_ID;
+
+    /**
+     * The contact table's columns.
+     */
+    public static class ContactColumns implements BaseColumns {
+
+        /**
+         * The contact's phone number. It may come from contact provider or someone via
+         * {@link EabControllerImpl#saveCapabilities(List)} to save the capability but the phone
+         * number not in contact provider.
+         *
+         * <P>Type: TEXT</P>
+         */
+        public static final String PHONE_NUMBER = "phone_number";
+
+        /**
+         * The ID of contact that store in contact provider. It refer to the
+         * {@link android.provider.ContactsContract.Data#CONTACT_ID}. If the phone number not in
+         * contact provider, the value should be null.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String CONTACT_ID = "contact_id";
+
+        /**
+         * The ID of contact that store in contact provider. It refer to the
+         * {@link android.provider.ContactsContract.Data#RAW_CONTACT_ID}. If the phone number not in
+         * contact provider, the value should be null.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String RAW_CONTACT_ID = "raw_contact_id";
+
+        /**
+         * The ID of phone number that store in contact provider. It refer to the
+         * {@link android.provider.ContactsContract.Data#_ID}. If the phone number not in
+         * contact provider, the value should be null.
+         *
+         * <P>Type:  INTEGER</P>
+         */
+        public static final String DATA_ID = "data_id";
+    }
+
+    /**
+     * The common table's columns. The eab_contact_id should refer to the id of contact table.
+     */
+    public static class EabCommonColumns implements BaseColumns {
+
+        /**
+         * A reference to the {@link ContactColumns#_ID} that this data belongs to.
+         * <P>Type:  INTEGER</P>
+         */
+        public static final String EAB_CONTACT_ID = "eab_contact_id";
+
+        /**
+         * The mechanism of querying UCE capability. Possible values are
+         * {@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_OPTIONS }
+         * and
+         * {@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_PRESENCE }
+         * <P>Type: INTEGER</P>
+         */
+        public static final String MECHANISM = "mechanism";
+
+        /**
+         * The result of querying UCE capability. Possible values are
+         * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_NOT_ONLINE }
+         * and
+         * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_NOT_FOUND }
+         * and
+         * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_FOUND }
+         * and
+         * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_UNKNOWN }
+         * <P>Type: INTEGER</P>
+         */
+        public static final String REQUEST_RESULT = "request_result";
+
+        /**
+         * The subscription id.
+         * <P>Type:  INTEGER</P>
+         */
+        public static final String SUBSCRIPTION_ID = "subscription_id";
+    }
+
+    /**
+     * This is used to generate a instance of {@link android.telephony.ims.RcsContactPresenceTuple}.
+     * See that class for more information on each of these parameters.
+     */
+    public static class PresenceTupleColumns implements BaseColumns {
+
+        /**
+         * A reference to the {@link ContactColumns#_ID} that this data belongs to.
+         * <P>Type:  INTEGER</P>
+         */
+        public static final String EAB_COMMON_ID = "eab_common_id";
+
+        /**
+         * The basic status of service capabilities. Possible values are
+         * {@link android.telephony.ims.RcsContactPresenceTuple#TUPLE_BASIC_STATUS_OPEN}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple#TUPLE_BASIC_STATUS_CLOSED}
+         * <P>Type:  TEXT</P>
+         */
+        public static final String BASIC_STATUS = "basic_status";
+
+        /**
+         * The OMA Presence service-id associated with this capability. See the OMA Presence SIMPLE
+         * specification v1.1, section 10.5.1.
+         * <P>Type:  TEXT</P>
+         */
+        public static final String SERVICE_ID = "service_id";
+
+        /**
+         * The contact uri of service capabilities.
+         * <P>Type:  TEXT</P>
+         */
+        public static final String CONTACT_URI = "contact_uri";
+
+        /**
+         * The service version of service capabilities.
+         * <P>Type:  TEXT</P>
+         */
+        public static final String SERVICE_VERSION = "service_version";
+
+        /**
+         * The description of service capabilities.
+         * <P>Type:  TEXT</P>
+         */
+        public static final String DESCRIPTION = "description";
+
+        /**
+         * The supported duplex mode of service capabilities. Possible values are
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_FULL}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_HALF}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_RECEIVE_ONLY}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_SEND_ONLY}
+         * <P>Type:  TEXT</P>
+         */
+        public static final String DUPLEX_MODE = "duplex_mode";
+
+        /**
+         * The unsupported duplex mode of service capabilities. Possible values are
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_FULL}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_HALF}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_RECEIVE_ONLY}
+         * and
+         * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_SEND_ONLY}
+         * <P>Type:  TEXT</P>
+         */
+        public static final String UNSUPPORTED_DUPLEX_MODE = "unsupported_duplex_mode";
+
+        /**
+         * The presence request timestamp. Represents seconds of UTC time since Unix epoch
+         * 1970-01-01 00:00:00.
+         * <P>Type:  LONG</P>
+         */
+        public static final String REQUEST_TIMESTAMP = "presence_request_timestamp";
+
+        /**
+         * The audio capable.
+         * <P>Type:  BOOLEAN </P>
+         */
+        public static final String AUDIO_CAPABLE = "audio_capable";
+
+        /**
+         * The video capable.
+         * <P>Type:  BOOLEAN </P>
+         */
+        public static final String VIDEO_CAPABLE = "video_capable";
+    }
+
+    /**
+     * This is used to generate a instance of {@link android.telephony.ims.RcsContactPresenceTuple}.
+     * See that class for more information on each of these parameters.
+     */
+    public static class OptionsColumns implements BaseColumns {
+
+        /**
+         * A reference to the {@link ContactColumns#_ID} that this data belongs to.
+         * <P>Type:  INTEGER</P>
+         */
+        public static final String EAB_COMMON_ID = "eab_common_id";
+
+        /**
+         * An IMS feature tag indicating the capabilities of the contact. See RFC3840 #section-9.
+         * <P>Type:  TEXT</P>
+         */
+        public static final String FEATURE_TAG = "feature_tag";
+
+        /**
+         * The request timestamp of options capabilities.
+         * <P>Type:  LONG</P>
+         */
+        public static final String REQUEST_TIMESTAMP = "options_request_timestamp";
+    }
+
+    @VisibleForTesting
+    public static final class EabDatabaseHelper extends SQLiteOpenHelper {
+        private static final String DB_NAME = "EabDatabase";
+        private static final List<String> CONTACT_UNIQUE_FIELDS = new ArrayList<>();
+        private static final List<String> COMMON_UNIQUE_FIELDS = new ArrayList<>();
+
+        static {
+            CONTACT_UNIQUE_FIELDS.add(ContactColumns.PHONE_NUMBER);
+
+            COMMON_UNIQUE_FIELDS.add(EabCommonColumns.EAB_CONTACT_ID);
+        }
+
+        @VisibleForTesting
+        public static final String SQL_CREATE_CONTACT_TABLE = "CREATE TABLE "
+                + EAB_CONTACT_TABLE_NAME
+                + " ("
+                + ContactColumns._ID + " INTEGER PRIMARY KEY, "
+                + ContactColumns.PHONE_NUMBER + " Text DEFAULT NULL, "
+                + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1, "
+                + ContactColumns.RAW_CONTACT_ID + " INTEGER DEFAULT -1, "
+                + ContactColumns.DATA_ID + " INTEGER DEFAULT -1, "
+                + "UNIQUE (" + TextUtils.join(", ", CONTACT_UNIQUE_FIELDS) + ")"
+                + ");";
+
+        @VisibleForTesting
+        public static final String SQL_CREATE_COMMON_TABLE = "CREATE TABLE "
+                + EAB_COMMON_TABLE_NAME
+                + " ("
+                + EabCommonColumns._ID + " INTEGER PRIMARY KEY, "
+                + EabCommonColumns.EAB_CONTACT_ID + " INTEGER DEFAULT -1, "
+                + EabCommonColumns.MECHANISM + " INTEGER DEFAULT NULL, "
+                + EabCommonColumns.REQUEST_RESULT + " INTEGER DEFAULT -1, "
+                + EabCommonColumns.SUBSCRIPTION_ID + " INTEGER DEFAULT -1, "
+                + "UNIQUE (" + TextUtils.join(", ", COMMON_UNIQUE_FIELDS) + ")"
+                + ");";
+
+        @VisibleForTesting
+        public static final String SQL_CREATE_PRESENCE_TUPLE_TABLE = "CREATE TABLE "
+                + EAB_PRESENCE_TUPLE_TABLE_NAME
+                + " ("
+                + PresenceTupleColumns._ID + " INTEGER PRIMARY KEY, "
+                + PresenceTupleColumns.EAB_COMMON_ID + " INTEGER DEFAULT -1, "
+                + PresenceTupleColumns.BASIC_STATUS + " TEXT DEFAULT NULL, "
+                + PresenceTupleColumns.SERVICE_ID + " TEXT DEFAULT NULL, "
+                + PresenceTupleColumns.SERVICE_VERSION + " TEXT DEFAULT NULL, "
+                + PresenceTupleColumns.DESCRIPTION + " TEXT DEFAULT NULL, "
+                + PresenceTupleColumns.REQUEST_TIMESTAMP + " LONG DEFAULT NULL, "
+                + PresenceTupleColumns.CONTACT_URI + " TEXT DEFAULT NULL, "
+
+                // For ServiceCapabilities
+                + PresenceTupleColumns.DUPLEX_MODE + " TEXT DEFAULT NULL, "
+                + PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE + " TEXT DEFAULT NULL, "
+                + PresenceTupleColumns.AUDIO_CAPABLE + " BOOLEAN DEFAULT NULL, "
+                + PresenceTupleColumns.VIDEO_CAPABLE + " BOOLEAN DEFAULT NULL"
+                + ");";
+
+        @VisibleForTesting
+        public static final String SQL_CREATE_OPTIONS_TABLE = "CREATE TABLE "
+                + EAB_OPTIONS_TABLE_NAME
+                + " ("
+                + OptionsColumns._ID + " INTEGER PRIMARY KEY, "
+                + OptionsColumns.EAB_COMMON_ID + " INTEGER DEFAULT -1, "
+                + OptionsColumns.REQUEST_TIMESTAMP + " LONG DEFAULT NULL, "
+                + OptionsColumns.FEATURE_TAG + " TEXT DEFAULT NULL "
+                + ");";
+
+        EabDatabaseHelper(Context context) {
+            super(context, DB_NAME, null, DATABASE_VERSION);
+        }
+
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL(SQL_CREATE_CONTACT_TABLE);
+            db.execSQL(SQL_CREATE_COMMON_TABLE);
+            db.execSQL(SQL_CREATE_PRESENCE_TUPLE_TABLE);
+            db.execSQL(SQL_CREATE_OPTIONS_TABLE);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+            Log.d(TAG, "DB upgrade from " + oldVersion + " to " + newVersion);
+
+            if (oldVersion < 2) {
+                sqLiteDatabase.execSQL("ALTER TABLE " + EAB_CONTACT_TABLE_NAME + " ADD COLUMN "
+                        + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1;");
+                oldVersion = 2;
+            }
+        }
+    }
+
+    private EabDatabaseHelper mOpenHelper;
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new EabDatabaseHelper(getContext());
+        return true;
+    }
+
+    /**
+     * Support 6 URLs for querying:
+     *
+     * <ul>
+     * <li>{@link #URL_CONTACT}: query contact table.
+     *
+     * <li>{@link #URL_COMMON}: query common table.
+     *
+     * <li>{@link #URL_PRESENCE}: query presence capability table.
+     *
+     * <li>{@link #URL_OPTIONS}: query options capability table.
+     *
+     * <li>{@link #URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER}: To provide more efficient query way,
+     * filter by the {@link ContactColumns#PHONE_NUMBER} first and join with others tables. The
+     * format is like content://eab/all/[sub_id]/[phone_number]
+     *
+     * <li> {@link #URL_ALL}: Join all of tables at once
+     * </ul>
+     */
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        SQLiteDatabase db = getReadableDatabase();
+        int match = URI_MATCHER.match(uri);
+        int subId;
+        String subIdString;
+
+        Log.d(TAG, "Query URI: " + match);
+
+        switch (match) {
+            case URL_CONTACT:
+                qb.setTables(EAB_CONTACT_TABLE_NAME);
+                break;
+
+            case URL_COMMON:
+                qb.setTables(EAB_COMMON_TABLE_NAME);
+                break;
+
+            case URL_PRESENCE:
+                qb.setTables(EAB_PRESENCE_TUPLE_TABLE_NAME);
+                break;
+
+            case URL_OPTIONS:
+                qb.setTables(EAB_OPTIONS_TABLE_NAME);
+                break;
+
+            case URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER:
+                List<String> pathSegment = uri.getPathSegments();
+
+                subIdString = pathSegment.get(1);
+                try {
+                    subId = Integer.parseInt(subIdString);
+                } catch (NumberFormatException e) {
+                    Log.e(TAG, "NumberFormatException" + e);
+                    return null;
+                }
+                qb.appendWhereStandalone(EabCommonColumns.SUBSCRIPTION_ID + "=" + subId);
+
+                String phoneNumber = pathSegment.get(2);
+                String whereClause;
+                if (TextUtils.isEmpty(phoneNumber)) {
+                    Log.e(TAG, "phone number is null");
+                    return null;
+                }
+                whereClause = " where " + ContactColumns.PHONE_NUMBER + "='" + phoneNumber + "' ";
+                qb.setTables(
+                        "((" + QUERY_CONTACT_TABLE + whereClause + ") AS " + EAB_CONTACT_TABLE_NAME
+                                + JOIN_ALL_TABLES + ")");
+                break;
+
+            case URL_ALL:
+                qb.setTables("(" + QUERY_CONTACT_TABLE + JOIN_ALL_TABLES + ")");
+                break;
+
+            default:
+                Log.d(TAG, "Query failed. Not support URL.");
+                return null;
+        }
+        return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues contentValues) {
+        SQLiteDatabase db = getWritableDatabase();
+        int match = URI_MATCHER.match(uri);
+        long result = 0;
+        String tableName = "";
+        switch (match) {
+            case URL_CONTACT:
+                tableName = EAB_CONTACT_TABLE_NAME;
+                break;
+            case URL_COMMON:
+                tableName = EAB_COMMON_TABLE_NAME;
+                break;
+            case URL_PRESENCE:
+                tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+                break;
+            case URL_OPTIONS:
+                tableName = EAB_OPTIONS_TABLE_NAME;
+                break;
+        }
+        if (!TextUtils.isEmpty(tableName)) {
+            result = db.insertWithOnConflict(tableName, null, contentValues,
+                    SQLiteDatabase.CONFLICT_REPLACE);
+            Log.d(TAG, "Insert uri: " + match + " ID: " + result);
+            if (result > 0) {
+                getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
+            }
+        } else {
+            Log.d(TAG, "Insert. Not support URI.");
+        }
+
+        return Uri.withAppendedPath(uri, String.valueOf(result));
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        SQLiteDatabase db = getWritableDatabase();
+        int match = URI_MATCHER.match(uri);
+        int result = 0;
+        String tableName = "";
+        switch (match) {
+            case URL_CONTACT:
+                tableName = EAB_CONTACT_TABLE_NAME;
+                break;
+            case URL_COMMON:
+                tableName = EAB_COMMON_TABLE_NAME;
+                break;
+            case URL_PRESENCE:
+                tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+                break;
+            case URL_OPTIONS:
+                tableName = EAB_OPTIONS_TABLE_NAME;
+                break;
+        }
+
+        if (TextUtils.isEmpty(tableName)) {
+            Log.d(TAG, "bulkInsert. Not support URI.");
+            return 0;
+        }
+
+        try {
+            // Batch all insertions in a single transaction to improve efficiency.
+            db.beginTransaction();
+            for (ContentValues contentValue : values) {
+                if (contentValue != null) {
+                    db.insertWithOnConflict(tableName, null, contentValue,
+                            SQLiteDatabase.CONFLICT_REPLACE);
+                    result++;
+                }
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        if (result > 0) {
+            getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
+        }
+        Log.d(TAG, "bulkInsert uri: " + match + " count: " + result);
+        return result;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = getWritableDatabase();
+        int match = URI_MATCHER.match(uri);
+        int result = 0;
+        String tableName = "";
+        switch (match) {
+            case URL_CONTACT:
+                tableName = EAB_CONTACT_TABLE_NAME;
+                break;
+            case URL_COMMON:
+                tableName = EAB_COMMON_TABLE_NAME;
+                break;
+            case URL_PRESENCE:
+                tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+                break;
+            case URL_OPTIONS:
+                tableName = EAB_OPTIONS_TABLE_NAME;
+                break;
+        }
+        if (!TextUtils.isEmpty(tableName)) {
+            result = db.delete(tableName, selection, selectionArgs);
+            if (result > 0) {
+                getContext().getContentResolver().notifyChange(uri, null, NOTIFY_DELETE);
+            }
+            Log.d(TAG, "Delete uri: " + match + " result: " + result);
+        } else {
+            Log.d(TAG, "Delete. Not support URI.");
+        }
+        return result;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues contentValues, String selection,
+            String[] selectionArgs) {
+        SQLiteDatabase db = getWritableDatabase();
+        int match = URI_MATCHER.match(uri);
+        int result = 0;
+        String tableName = "";
+        switch (match) {
+            case URL_CONTACT:
+                tableName = EAB_CONTACT_TABLE_NAME;
+                break;
+            case URL_COMMON:
+                tableName = EAB_COMMON_TABLE_NAME;
+                break;
+            case URL_PRESENCE:
+                tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+                break;
+            case URL_OPTIONS:
+                tableName = EAB_OPTIONS_TABLE_NAME;
+                break;
+        }
+        if (!TextUtils.isEmpty(tableName)) {
+            result = db.updateWithOnConflict(tableName, contentValues,
+                    selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
+            if (result > 0) {
+                getContext().getContentResolver().notifyChange(uri, null, NOTIFY_UPDATE);
+            }
+            Log.d(TAG, "Update uri: " + match + " result: " + result);
+        } else {
+            Log.d(TAG, "Update. Not support URI.");
+        }
+        return result;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @VisibleForTesting
+    public SQLiteDatabase getWritableDatabase() {
+        return mOpenHelper.getWritableDatabase();
+    }
+
+    @VisibleForTesting
+    public SQLiteDatabase getReadableDatabase() {
+        return mOpenHelper.getReadableDatabase();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabUtil.java b/src/java/com/android/ims/rcs/uce/eab/EabUtil.java
new file mode 100644
index 0000000..da76aab
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabUtil.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.eab;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.eab.EabProvider.ContactColumns;
+import com.android.ims.rcs.uce.eab.EabProvider.EabCommonColumns;
+import com.android.ims.rcs.uce.eab.EabProvider.OptionsColumns;
+import com.android.ims.rcs.uce.eab.EabProvider.PresenceTupleColumns;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * The util to modify the EAB database.
+ */
+public class EabUtil {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "EabUtil";
+
+    /**
+     * Get the given EAB contacts from the EAB database.
+     *
+     * Output format:
+     * [PHONE_NUMBER], [RAW_CONTACT_ID], [CONTACT_ID], [DATA_ID]
+     */
+    public static String getContactFromEab(Context context, String contact) {
+        StringBuilder result = new StringBuilder();
+        try (Cursor cursor = context.getContentResolver().query(
+                EabProvider.CONTACT_URI,
+                new String[]{ContactColumns.PHONE_NUMBER,
+                        ContactColumns.RAW_CONTACT_ID,
+                        ContactColumns.CONTACT_ID,
+                        ContactColumns.DATA_ID},
+                ContactColumns.PHONE_NUMBER + "=?",
+                new String[]{contact}, null)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                result.append(cursor.getString(cursor.getColumnIndex(
+                        ContactColumns.PHONE_NUMBER)));
+                result.append(",");
+                result.append(cursor.getString(cursor.getColumnIndex(
+                        ContactColumns.RAW_CONTACT_ID)));
+                result.append(",");
+                result.append(cursor.getString(cursor.getColumnIndex(
+                        ContactColumns.CONTACT_ID)));
+                result.append(",");
+                result.append(cursor.getString(cursor.getColumnIndex(
+                        ContactColumns.DATA_ID)));
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "getEabContactId exception " + e);
+        }
+        Log.d(LOG_TAG, "getContactFromEab() result: " + result);
+        return result.toString();
+    }
+
+    /**
+     * Remove the given EAB contacts from the EAB database.
+     */
+    public static int removeContactFromEab(int subId, String contacts, Context context) {
+        if (TextUtils.isEmpty(contacts)) {
+            return -1;
+        }
+        List<String> contactList = Arrays.stream(contacts.split(",")).collect(Collectors.toList());
+        if (contactList == null || contactList.isEmpty()) {
+            return -1;
+        }
+        int count = 0;
+        for (String contact : contactList) {
+            int contactId = getEabContactId(contact, context);
+            if (contactId == -1) {
+                continue;
+            }
+            int commonId = getEabCommonId(contactId, context);
+            count += removeContactCapabilities(contactId, commonId, context);
+        }
+        return count;
+    }
+
+    private static int getEabContactId(String contactNumber, Context context) {
+        int contactId = -1;
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(
+                    EabProvider.CONTACT_URI,
+                    new String[] { EabProvider.EabCommonColumns._ID },
+                    EabProvider.ContactColumns.PHONE_NUMBER + "=?",
+                    new String[] { contactNumber }, null);
+            if (cursor != null && cursor.moveToFirst()) {
+                contactId = cursor.getInt(cursor.getColumnIndex(EabProvider.ContactColumns._ID));
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "getEabContactId exception " + e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return contactId;
+    }
+
+    private static int getEabCommonId(int contactId, Context context) {
+        int commonId = -1;
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(
+                    EabProvider.COMMON_URI,
+                    new String[] { EabProvider.EabCommonColumns._ID },
+                    EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
+                    new String[] { String.valueOf(contactId) }, null);
+            if (cursor != null && cursor.moveToFirst()) {
+                commonId = cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID));
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "getEabCommonId exception " + e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return commonId;
+    }
+
+    private static int removeContactCapabilities(int contactId, int commonId, Context context) {
+        int count = 0;
+        count = context.getContentResolver().delete(EabProvider.PRESENCE_URI,
+                PresenceTupleColumns.EAB_COMMON_ID + "=?", new String[]{String.valueOf(commonId)});
+        context.getContentResolver().delete(EabProvider.OPTIONS_URI,
+                OptionsColumns.EAB_COMMON_ID + "=?", new String[]{String.valueOf(commonId)});
+        context.getContentResolver().delete(EabProvider.COMMON_URI,
+                EabCommonColumns.EAB_CONTACT_ID + "=?", new String[]{String.valueOf(contactId)});
+        context.getContentResolver().delete(EabProvider.CONTACT_URI,
+                ContactColumns._ID + "=?", new String[]{String.valueOf(contactId)});
+        return count;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java b/src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java
new file mode 100644
index 0000000..8e42b61
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.eab;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+
+/**
+ * The wrapper class of the PresenceBuilder and the OptionsBuilder.
+ */
+public class RcsUceCapabilityBuilderWrapper {
+    private final int mMechanism;
+    private PresenceBuilder mPresenceBuilder;
+    private OptionsBuilder mOptionsBuilder;
+
+    public RcsUceCapabilityBuilderWrapper(int mechanism) {
+        mMechanism = mechanism;
+    }
+
+    public int getMechanism() {
+        return mMechanism;
+    }
+
+    public void setPresenceBuilder(@NonNull PresenceBuilder presenceBuilder) {
+        mPresenceBuilder = presenceBuilder;
+    }
+
+    public @Nullable PresenceBuilder getPresenceBuilder() {
+        return mPresenceBuilder;
+    }
+
+    public void setOptionsBuilder(@NonNull OptionsBuilder optionsBuilder) {
+        mOptionsBuilder = optionsBuilder;
+    }
+
+    public @Nullable OptionsBuilder getOptionsBuilder() {
+        return mOptionsBuilder;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/options/OptionsController.java b/src/java/com/android/ims/rcs/uce/options/OptionsController.java
new file mode 100644
index 0000000..b4b3260
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/options/OptionsController.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ims.rcs.uce.options;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+
+import com.android.ims.rcs.uce.ControllerBase;
+
+import java.util.Set;
+
+/**
+ * The interface to define the operations of the SIP OPTIONS
+ */
+public interface OptionsController extends ControllerBase {
+    /**
+     * Request the contact's capabilities of the given contact.
+     * @param contactUri The contact of the capabilities is being requested for.
+     * @param deviceFeatureTags The feature tags of the device's capabilities.
+     * @param c The response callback of the OPTIONS capabilities request.
+     */
+    void sendCapabilitiesRequest(@NonNull Uri contactUri, @NonNull Set<String> deviceFeatureTags,
+            @NonNull IOptionsResponseCallback c) throws RemoteException;
+}
diff --git a/src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java b/src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java
new file mode 100644
index 0000000..e3b708f
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ims.rcs.uce.options;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * The implementation of OptionsController.
+ */
+public class OptionsControllerImpl implements OptionsController {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "OptionsController";
+
+    private final int mSubId;
+    private final Context mContext;
+    private volatile boolean mIsDestroyedFlag;
+    private volatile RcsFeatureManager mRcsFeatureManager;
+
+    public OptionsControllerImpl(Context context, int subId) {
+        mSubId = subId;
+        mContext = context;
+    }
+
+    @Override
+    public void onRcsConnected(RcsFeatureManager manager) {
+        mRcsFeatureManager = manager;
+    }
+
+    @Override
+    public void onRcsDisconnected() {
+        mRcsFeatureManager = null;
+    }
+
+    @Override
+    public void onDestroy() {
+        mIsDestroyedFlag = true;
+        mRcsFeatureManager = null;
+    }
+
+    @Override
+    public void onCarrierConfigChanged() {
+        // Nothing required here.
+    }
+
+    public void sendCapabilitiesRequest(Uri contactUri, @NonNull Set<String> deviceFeatureTags,
+            IOptionsResponseCallback c) throws RemoteException {
+
+        if (mIsDestroyedFlag) {
+            throw new RemoteException("OPTIONS controller is destroyed");
+        }
+
+        RcsFeatureManager featureManager = mRcsFeatureManager;
+        if (featureManager == null) {
+            Log.w(LOG_TAG, "sendCapabilitiesRequest: Service is unavailable");
+            c.onCommandError(RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE);
+            return;
+        }
+
+        featureManager.sendOptionsCapabilityRequest(contactUri, new ArrayList<>(deviceFeatureTags),
+            c);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java
new file mode 100644
index 0000000..6a03f69
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java
@@ -0,0 +1,84 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import android.util.Log;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The base class of the pidf element.
+ */
+public abstract class ElementBase {
+    private String mNamespace;
+    private String mElementName;
+
+    public ElementBase() {
+        mNamespace = initNamespace();
+        mElementName = initElementName();
+    }
+
+    protected abstract String initNamespace();
+    protected abstract String initElementName();
+
+    /**
+     * @return The namespace of this element
+     */
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /**
+     * @return The name of this element.
+     */
+    public String getElementName() {
+        return mElementName;
+    }
+
+    public abstract void serialize(XmlSerializer serializer) throws IOException;
+
+    public abstract void parse(XmlPullParser parser) throws IOException, XmlPullParserException;
+
+    protected boolean verifyParsingElement(String namespace, String elementName) {
+        if (!getNamespace().equals(namespace) || !getElementName().equals(elementName)) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    // Move to the end tag of this element
+    protected void moveToElementEndTag(XmlPullParser parser, int type)
+            throws IOException, XmlPullParserException {
+        int eventType = type;
+
+        // Move to the end tag of this element.
+        while(!(eventType == XmlPullParser.END_TAG
+                && getNamespace().equals(parser.getNamespace())
+                && getElementName().equals(parser.getName()))) {
+            eventType = parser.next();
+
+            // Leave directly if the event type is the end of the document.
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                return;
+            }
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java
new file mode 100644
index 0000000..2660f1d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java
@@ -0,0 +1,274 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Audio;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.CapsConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Duplex;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Video;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.OmaPresConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Basic;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Presence;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Tuple;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * Convert between the class RcsContactUceCapability and the pidf format.
+ */
+public class PidfParser {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PidfParser";
+
+    private static final Pattern PIDF_PATTERN = Pattern.compile("\t|\r|\n");
+
+    /**
+     * Convert the RcsContactUceCapability to the string of pidf.
+     */
+    public static String convertToPidf(RcsContactUceCapability capabilities) {
+        StringWriter pidfWriter = new StringWriter();
+        try {
+            // Init the instance of the XmlSerializer.
+            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+            XmlSerializer serializer = factory.newSerializer();
+
+            // setup output and namespace
+            serializer.setOutput(pidfWriter);
+            serializer.setPrefix("", PidfConstant.NAMESPACE);
+            serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+            serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+
+            // Get the Presence element
+            Presence presence = PidfParserUtils.getPresence(capabilities);
+
+            // Start serializing.
+            serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+            presence.serialize(serializer);
+            serializer.endDocument();
+            serializer.flush();
+
+        } catch (XmlPullParserException parserEx) {
+            parserEx.printStackTrace();
+            return null;
+        } catch (IOException ioException) {
+            ioException.printStackTrace();
+            return null;
+        }
+        return pidfWriter.toString();
+    }
+
+    /**
+     * Get the RcsContactUceCapability from the given PIDF xml format.
+     */
+    public static @Nullable RcsContactUceCapability getRcsContactUceCapability(String pidf) {
+        if (TextUtils.isEmpty(pidf)) {
+            Log.w(LOG_TAG, "getRcsContactUceCapability: The given pidf is empty");
+            return null;
+        }
+
+        // Filter the newline characters
+        Matcher matcher = PIDF_PATTERN.matcher(pidf);
+        String formattedPidf = matcher.replaceAll("");
+        if (TextUtils.isEmpty(formattedPidf)) {
+            Log.w(LOG_TAG, "getRcsContactUceCapability: The formatted pidf is empty");
+            return null;
+        }
+
+        Reader reader = null;
+        try {
+            // Init the instance of the parser
+            XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+            reader = new StringReader(formattedPidf);
+            parser.setInput(reader);
+
+            // Start parsing
+            Presence presence = parsePidf(parser);
+
+            // Convert from the Presence to the RcsContactUceCapability
+            return convertToRcsContactUceCapability(presence);
+
+        } catch (XmlPullParserException | IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    private static Presence parsePidf(XmlPullParser parser) throws IOException,
+            XmlPullParserException {
+        Presence presence = null;
+        int nextType = parser.next();
+        do {
+            // Find the Presence start tag
+            if (nextType == XmlPullParser.START_TAG
+                    && Presence.ELEMENT_NAME.equals(parser.getName())) {
+                presence = new Presence();
+                presence.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        return presence;
+    }
+
+    /*
+     * Convert the given Presence to the RcsContactUceCapability
+     */
+    private static RcsContactUceCapability convertToRcsContactUceCapability(Presence presence) {
+        if (presence == null) {
+            Log.w(LOG_TAG, "convertToRcsContactUceCapability: The presence is null");
+            return null;
+        }
+        if (TextUtils.isEmpty(presence.getEntity())) {
+            Log.w(LOG_TAG, "convertToRcsContactUceCapability: The entity is empty");
+            return null;
+        }
+
+        PresenceBuilder presenceBuilder = new PresenceBuilder(Uri.parse(presence.getEntity()),
+                RcsContactUceCapability.SOURCE_TYPE_NETWORK,
+                RcsContactUceCapability.REQUEST_RESULT_FOUND);
+
+        // Add all the capability tuples of this contact
+        presence.getTupleList().forEach(tuple -> {
+            RcsContactPresenceTuple capabilityTuple = getRcsContactPresenceTuple(tuple);
+            if (capabilityTuple != null) {
+                presenceBuilder.addCapabilityTuple(capabilityTuple);
+            }
+        });
+
+        return presenceBuilder.build();
+    }
+
+    /*
+     * Get the RcsContactPresenceTuple from the giving tuple element.
+     */
+    private static RcsContactPresenceTuple getRcsContactPresenceTuple(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+
+        String status = RcsContactPresenceTuple.TUPLE_BASIC_STATUS_CLOSED;
+        if (Basic.OPEN.equals(PidfParserUtils.getTupleStatus(tuple))) {
+            status = RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN;
+        }
+
+        String serviceId = PidfParserUtils.getTupleServiceId(tuple);
+        String serviceVersion = PidfParserUtils.getTupleServiceVersion(tuple);
+        String serviceDescription = PidfParserUtils.getTupleServiceDescription(tuple);
+
+        RcsContactPresenceTuple.Builder builder = new RcsContactPresenceTuple.Builder(status,
+                serviceId, serviceVersion);
+
+        // Set contact uri
+        String contact = PidfParserUtils.getTupleContact(tuple);
+        if (!TextUtils.isEmpty(contact)) {
+            builder.setContactUri(Uri.parse(contact));
+        }
+
+        // Timestamp
+        String timestamp = PidfParserUtils.getTupleTimestamp(tuple);
+        if (!TextUtils.isEmpty(timestamp)) {
+            try {
+                Instant instant = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(
+                        timestamp, Instant::from);
+                builder.setTime(instant);
+            } catch (DateTimeParseException e) {
+                Log.w(LOG_TAG, "getRcsContactPresenceTuple: Parse timestamp failed " + e);
+            }
+        }
+
+        // Set service description
+        if (!TextUtils.isEmpty(serviceDescription)) {
+            builder.setServiceDescription(serviceDescription);
+        }
+
+        // Set service capabilities
+        ServiceCaps serviceCaps = tuple.getServiceCaps();
+        if (serviceCaps != null) {
+            List<ElementBase> serviceCapsList = serviceCaps.getElements();
+            if (serviceCapsList != null && !serviceCapsList.isEmpty()) {
+                boolean isAudioSupported = false;
+                boolean isVideoSupported = false;
+                List<String> supportedTypes = null;
+                List<String> notSupportedTypes = null;
+
+                for (ElementBase element : serviceCapsList) {
+                    if (element instanceof Audio) {
+                        isAudioSupported = ((Audio) element).isAudioSupported();
+                    } else if (element instanceof Video) {
+                        isVideoSupported = ((Video) element).isVideoSupported();
+                    } else if (element instanceof Duplex) {
+                        supportedTypes = ((Duplex) element).getSupportedTypes();
+                        notSupportedTypes = ((Duplex) element).getNotSupportedTypes();
+                    }
+                }
+
+                ServiceCapabilities.Builder capabilitiesBuilder
+                        = new ServiceCapabilities.Builder(isAudioSupported, isVideoSupported);
+
+                if (supportedTypes != null && !supportedTypes.isEmpty()) {
+                    for (String supportedType : supportedTypes) {
+                        capabilitiesBuilder.addSupportedDuplexMode(supportedType);
+                    }
+                }
+
+                if (notSupportedTypes != null && !notSupportedTypes.isEmpty()) {
+                    for (String notSupportedType : notSupportedTypes) {
+                        capabilitiesBuilder.addUnsupportedDuplexMode(notSupportedType);
+                    }
+                }
+                builder.setServiceCapabilities(capabilitiesBuilder.build());
+            }
+        }
+        return builder.build();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java
new file mode 100644
index 0000000..07fde38
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java
@@ -0,0 +1,39 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+/**
+ * The constant of the pidf
+ */
+public class PidfParserConstant {
+    /**
+     * The UTF-8 encoding format
+     */
+    public static final String ENCODING_UTF_8 = "utf-8";
+
+    /**
+     * The service id of the capabilities discovery via presence.
+     */
+    public static final String SERVICE_ID_CAPS_DISCOVERY =
+            "org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp";
+
+    /**
+     * The service id of the VoLTE voice and video call.
+     */
+    public static final String SERVICE_ID_IpCall =
+            "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java
new file mode 100644
index 0000000..f2b21bd
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java
@@ -0,0 +1,312 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.BasicStatus;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Audio;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Duplex;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Video;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Description;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceId;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Version;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Basic;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Contact;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Presence;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Status;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Timestamp;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Tuple;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The utils to help the PIDF parsing process.
+ */
+public class PidfParserUtils {
+
+    /*
+     * The resource terminated reason with NOT FOUND
+     */
+    private static String[] REQUEST_RESULT_REASON_NOT_FOUND = { "noresource", "rejected" };
+
+    /**
+     * Convert the given class RcsContactUceCapability to the class Presence.
+     */
+    static Presence getPresence(RcsContactUceCapability capabilities) {
+        // Create "presence" element which is the root element of the pidf
+        Presence presence = new Presence(capabilities.getContactUri());
+
+        List<RcsContactPresenceTuple> tupleList = capabilities.getCapabilityTuples();
+        if (tupleList == null || tupleList.isEmpty()) {
+            return presence;
+        }
+
+        for (RcsContactPresenceTuple presenceTuple : tupleList) {
+            Tuple tupleElement = getTupleElement(presenceTuple);
+            if (tupleElement != null) {
+                presence.addTuple(tupleElement);
+            }
+        }
+
+        return presence;
+    }
+
+    /**
+     * Convert the class from RcsContactPresenceTuple to the class Tuple
+     */
+    private static Tuple getTupleElement(RcsContactPresenceTuple presenceTuple) {
+        if (presenceTuple == null) {
+            return null;
+        }
+        Tuple tupleElement = new Tuple();
+
+        // status element
+        handleTupleStatusElement(tupleElement, presenceTuple.getStatus());
+
+        // service description element
+        handleTupleServiceDescriptionElement(tupleElement, presenceTuple.getServiceId(),
+                presenceTuple.getServiceVersion(), presenceTuple.getServiceDescription());
+
+        // service capabilities element
+        handleServiceCapsElement(tupleElement, presenceTuple.getServiceCapabilities());
+
+        // contact element
+        handleTupleContactElement(tupleElement, presenceTuple.getContactUri());
+
+        return tupleElement;
+    }
+
+    private static void handleTupleContactElement(Tuple tupleElement, Uri uri) {
+        if (uri == null) {
+            return;
+        }
+        Contact contactElement = new Contact();
+        contactElement.setContact(uri.toString());
+        tupleElement.setContact(contactElement);
+    }
+
+    private static void handleTupleStatusElement(Tuple tupleElement, @BasicStatus String status) {
+        if (TextUtils.isEmpty(status)) {
+            return;
+        }
+        Basic basicElement = new Basic(status);
+        Status statusElement = new Status();
+        statusElement.setBasic(basicElement);
+        tupleElement.setStatus(statusElement);
+    }
+
+    private static void handleTupleServiceDescriptionElement(Tuple tupleElement, String serviceId,
+            String version, String description) {
+        ServiceId serviceIdElement = null;
+        Version versionElement = null;
+        Description descriptionElement = null;
+
+        // init serviceId element
+        if (!TextUtils.isEmpty(serviceId)) {
+            serviceIdElement = new ServiceId(serviceId);
+        }
+
+        // init version element
+        if (!TextUtils.isEmpty(version)) {
+            String[] versionAry = version.split("\\.");
+            if (versionAry != null && versionAry.length == 2) {
+                int majorVersion = Integer.parseInt(versionAry[0]);
+                int minorVersion = Integer.parseInt(versionAry[1]);
+                versionElement = new Version(majorVersion, minorVersion);
+            }
+        }
+
+        // init description element
+        if (!TextUtils.isEmpty(description)) {
+            descriptionElement = new Description(description);
+        }
+
+        // Add the Service Description element into the tuple
+        if (serviceIdElement != null && versionElement != null) {
+            ServiceDescription serviceDescription = new ServiceDescription();
+            serviceDescription.setServiceId(serviceIdElement);
+            serviceDescription.setVersion(versionElement);
+            if (descriptionElement != null) {
+                serviceDescription.setDescription(descriptionElement);
+            }
+            tupleElement.setServiceDescription(serviceDescription);
+        }
+    }
+
+    private static void handleServiceCapsElement(Tuple tupleElement,
+            ServiceCapabilities serviceCaps) {
+        if (serviceCaps == null) {
+            return;
+        }
+
+        ServiceCaps servCapsElement = new ServiceCaps();
+
+        // Audio and Video element
+        Audio audioElement = new Audio(serviceCaps.isAudioCapable());
+        Video videoElement = new Video(serviceCaps.isVideoCapable());
+        servCapsElement.addElement(audioElement);
+        servCapsElement.addElement(videoElement);
+
+        // Duplex element
+        List<String> supportedDuplexModes = serviceCaps.getSupportedDuplexModes();
+        List<String> UnsupportedDuplexModes = serviceCaps.getUnsupportedDuplexModes();
+        if ((supportedDuplexModes != null && !supportedDuplexModes.isEmpty()) ||
+                (UnsupportedDuplexModes != null && !UnsupportedDuplexModes.isEmpty())) {
+            Duplex duplex = new Duplex();
+            if (!supportedDuplexModes.isEmpty()) {
+                duplex.addSupportedType(supportedDuplexModes.get(0));
+            }
+            if (!UnsupportedDuplexModes.isEmpty()) {
+                duplex.addNotSupportedType(UnsupportedDuplexModes.get(0));
+            }
+            servCapsElement.addElement(duplex);
+        }
+
+        tupleElement.setServiceCaps(servCapsElement);
+    }
+
+    /**
+     * Get the status from the given tuple.
+     */
+    public static String getTupleStatus(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+        Status status = tuple.getStatus();
+        if (status != null) {
+            Basic basic = status.getBasic();
+            if (basic != null) {
+                return basic.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the service Id from the given tuple.
+     */
+    public static String getTupleServiceId(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+        ServiceDescription servDescription = tuple.getServiceDescription();
+        if (servDescription != null) {
+            ServiceId serviceId = servDescription.getServiceId();
+            if (serviceId != null) {
+                return serviceId.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the service version from the given tuple.
+     */
+    public static String getTupleServiceVersion(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+        ServiceDescription servDescription = tuple.getServiceDescription();
+        if (servDescription != null) {
+            Version version = servDescription.getVersion();
+            if (version != null) {
+                return version.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the service description from the given tuple.
+     */
+    public static String getTupleServiceDescription(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+        ServiceDescription servDescription = tuple.getServiceDescription();
+        if (servDescription != null) {
+            Description description = servDescription.getDescription();
+            if (description != null) {
+                return description.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the contact from the given tuple.
+     */
+    public static String getTupleContact(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+        Contact contact = tuple.getContact();
+        if (contact != null) {
+            return contact.getContact();
+        }
+        return null;
+    }
+
+    /**
+     * Get the timestamp from the given tuple.
+     */
+    public static String getTupleTimestamp(Tuple tuple) {
+        if (tuple == null) {
+            return null;
+        }
+        Timestamp timestamp = tuple.getTimestamp();
+        if (timestamp != null) {
+            return timestamp.getValue();
+        }
+        return null;
+    }
+
+    /**
+     * Get the terminated capability which disable all the capabilities.
+     */
+    public static RcsContactUceCapability getTerminatedCapability(Uri contact, String reason) {
+        if (reason == null) reason = "";
+        int requestResult = (Arrays.stream(REQUEST_RESULT_REASON_NOT_FOUND)
+                    .anyMatch(reason::equalsIgnoreCase) == true) ?
+                            RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND :
+                                    RcsContactUceCapability.REQUEST_RESULT_UNKNOWN;
+
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(
+                        contact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+        return builder.build();
+    }
+
+    /**
+     * Get the RcsContactUceCapability instance which the request result is NOT FOUND.
+     */
+    public static RcsContactUceCapability getNotFoundContactCapabilities(Uri contact) {
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(contact,
+                        RcsContactUceCapability.SOURCE_TYPE_NETWORK,
+                        RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND);
+        return builder.build();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java
new file mode 100644
index 0000000..e3fe7ab
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "audio" element of the Capabilities namespace.
+ */
+public class Audio extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "audio";
+
+    private boolean mSupported;
+
+    public Audio() {
+    }
+
+    public Audio(boolean supported) {
+        mSupported = supported;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return CapsConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public boolean isAudioSupported() {
+        return mSupported;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        serializer.text(String.valueOf(isAudioSupported()));
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String isSupported = parser.getText();
+            if (!TextUtils.isEmpty(isSupported)) {
+                mSupported = Boolean.parseBoolean(isSupported);
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/CapsConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/CapsConstant.java
new file mode 100644
index 0000000..2b809b2
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/CapsConstant.java
@@ -0,0 +1,21 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+public class CapsConstant {
+    public static final String NAMESPACE = "urn:ietf:params:xml:ns:pidf:caps";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java
new file mode 100644
index 0000000..af55a42
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java
@@ -0,0 +1,182 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import android.annotation.StringDef;
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * The "duplex" element indicates how the communication service send and receive media. It can
+ * contain two elements: "supported" and "notsupported." The supported and
+ * nonsupported elements can contains four elements: "full", "half", "receive-only" and
+ * "send-only".
+ */
+public class Duplex extends ElementBase {
+    /** The name of the duplex element */
+    public static final String ELEMENT_NAME = "duplex";
+
+    /** The name of the supported element */
+    public static final String ELEMENT_SUPPORTED = "supported";
+
+    /** The name of the notsupported element */
+    public static final String ELEMENT_NOT_SUPPORTED = "notsupported";
+
+    /** The device can simultaneously send and receive media */
+    public static final String DUPLEX_FULL = "full";
+
+    /** The service can alternate between sending and receiving media.*/
+    public static final String DUPLEX_HALF = "half";
+
+    /** The service can only receive media */
+    public static final String DUPLEX_RECEIVE_ONLY = "receive-only";
+
+    /** The service can only send media */
+    public static final String DUPLEX_SEND_ONLY = "send-only";
+
+    @StringDef(value = {
+            DUPLEX_FULL,
+            DUPLEX_HALF,
+            DUPLEX_RECEIVE_ONLY,
+            DUPLEX_SEND_ONLY})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DuplexType {}
+
+    private final List<String> mSupportedTypeList = new ArrayList<>();
+    private final List<String> mNotSupportedTypeList = new ArrayList<>();
+
+    public Duplex() {
+    }
+
+    @Override
+    protected String initNamespace() {
+        return CapsConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public void addSupportedType(@DuplexType String type) {
+        mSupportedTypeList.add(type);
+    }
+
+    public List<String> getSupportedTypes() {
+        return Collections.unmodifiableList(mSupportedTypeList);
+    }
+
+    public void addNotSupportedType(@DuplexType String type) {
+        mNotSupportedTypeList.add(type);
+    }
+
+    public List<String> getNotSupportedTypes() {
+        return Collections.unmodifiableList(mNotSupportedTypeList);
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mSupportedTypeList.isEmpty() && mNotSupportedTypeList.isEmpty()) {
+            return;
+        }
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        for (String supportedType : mSupportedTypeList) {
+            serializer.startTag(namespace, ELEMENT_SUPPORTED);
+            serializer.startTag(namespace, supportedType);
+            serializer.endTag(namespace, supportedType);
+            serializer.endTag(namespace, ELEMENT_SUPPORTED);
+        }
+        for (String notSupportedType : mNotSupportedTypeList) {
+            serializer.startTag(namespace, ELEMENT_NOT_SUPPORTED);
+            serializer.startTag(namespace, notSupportedType);
+            serializer.endTag(namespace, notSupportedType);
+            serializer.endTag(namespace, ELEMENT_NOT_SUPPORTED);
+        }
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event.
+        int eventType = parser.next();
+
+        while(!(eventType == XmlPullParser.END_TAG
+                && getNamespace().equals(parser.getNamespace())
+                && getElementName().equals(parser.getName()))) {
+
+            if (eventType == XmlPullParser.START_TAG) {
+                String tagName = parser.getName();
+
+                if (ELEMENT_SUPPORTED.equals(tagName)) {
+                    String duplexType = getDuplexType(parser);
+                    if (!TextUtils.isEmpty(duplexType)) {
+                        addSupportedType(duplexType);
+                    }
+                } else if (ELEMENT_NOT_SUPPORTED.equals(tagName)) {
+                    String duplexType = getDuplexType(parser);
+                    if (!TextUtils.isEmpty(duplexType)) {
+                        addNotSupportedType(duplexType);
+                    }
+                }
+            }
+
+            eventType = parser.next();
+
+            // Leave directly if the event type is the end of the document.
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                return;
+            }
+        }
+    }
+
+    private String getDuplexType(XmlPullParser parser) throws IOException, XmlPullParserException {
+        // Move to the next event
+        int eventType = parser.next();
+
+        String name = parser.getName();
+        if (eventType == XmlPullParser.START_TAG) {
+            if (DUPLEX_FULL.equals(name) ||
+                    DUPLEX_HALF.equals(name) ||
+                    DUPLEX_RECEIVE_ONLY.equals(name) ||
+                    DUPLEX_SEND_ONLY.equals(name)) {
+                return name;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java
new file mode 100644
index 0000000..16b52cd
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java
@@ -0,0 +1,117 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The "servicecaps" element is the root element of service capabilities.
+ */
+public class ServiceCaps extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "servcaps";
+
+    // The elements in the "servcaps" element.
+    private final List<ElementBase> mElements = new ArrayList<>();
+
+    public ServiceCaps() {
+    }
+
+    @Override
+    protected String initNamespace() {
+        return CapsConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public void addElement(ElementBase element) {
+        mElements.add(element);
+    }
+
+    public List<ElementBase> getElements() {
+        return mElements;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mElements.isEmpty()) {
+            return;
+        }
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        for (ElementBase element : mElements) {
+            element.serialize(serializer);
+        }
+        serializer.endTag(namespace, elementName);
+
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event.
+        int eventType = parser.next();
+
+        while(!(eventType == XmlPullParser.END_TAG
+                && getNamespace().equals(parser.getNamespace())
+                && getElementName().equals(parser.getName()))) {
+
+            if (eventType == XmlPullParser.START_TAG) {
+                String tagName = parser.getName();
+
+                if (Audio.ELEMENT_NAME.equals(tagName)) {
+                    Audio audio = new Audio();
+                    audio.parse(parser);
+                    mElements.add(audio);
+                } else if (Video.ELEMENT_NAME.equals(tagName)) {
+                    Video video = new Video();
+                    video.parse(parser);
+                    mElements.add(video);
+                } else if (Duplex.ELEMENT_NAME.equals(tagName)) {
+                    Duplex duplex = new Duplex();
+                    duplex.parse(parser);
+                    mElements.add(duplex);
+                }
+            }
+
+            eventType = parser.next();
+
+            // Leave directly if the event type is the end of the document.
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                return;
+            }
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.java
new file mode 100644
index 0000000..290b614
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "video" element of the Capabilities namespace.
+ */
+public class Video extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "video";
+
+    private boolean mSupported;
+
+    public Video() {
+    }
+
+    public Video(boolean supported) {
+        mSupported = supported;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return CapsConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public boolean isVideoSupported() {
+        return mSupported;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        serializer.text(String.valueOf(isVideoSupported()));
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String isSupported = parser.getText();
+            if (!TextUtils.isEmpty(isSupported)) {
+                mSupported = Boolean.parseBoolean(isSupported);
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java
new file mode 100644
index 0000000..8b4b861
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "description" element of the pidf
+ */
+public class Description extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "description";
+
+    private String mDescription;
+
+    public Description() {
+    }
+
+    public Description(String description) {
+        mDescription = description;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return OmaPresConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getValue() {
+        return mDescription;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mDescription == null) {
+            return;
+        }
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        serializer.text(mDescription);
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String description = parser.getText();
+            if (!TextUtils.isEmpty(description)) {
+                mDescription = description;
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java
new file mode 100644
index 0000000..668a1b3
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java
@@ -0,0 +1,21 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+public class OmaPresConstant {
+    public static final String NAMESPACE = "urn:oma:xml:prs:pidf:oma-pres";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java
new file mode 100644
index 0000000..1a4eeda
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java
@@ -0,0 +1,137 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "service-description" element of the pidf.
+ */
+public class ServiceDescription extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "service-description";
+
+    private ServiceId mServiceId;
+    private Version mVersion;
+    private Description mDescription;
+
+    public ServiceDescription() {
+    }
+
+    @Override
+    protected String initNamespace() {
+        return OmaPresConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public void setServiceId(ServiceId serviceId) {
+        mServiceId = serviceId;
+    }
+
+    public ServiceId getServiceId() {
+        return mServiceId;
+    }
+
+    public void setVersion(Version version) {
+        mVersion = version;
+    }
+
+    public Version getVersion() {
+        return mVersion;
+    }
+
+    public void setDescription(Description description) {
+        mDescription = description;
+    }
+
+    public Description getDescription() {
+        return mDescription;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if(mServiceId == null && mVersion == null && mDescription == null) {
+            return;
+        }
+        final String namespace = getNamespace();
+        final String element = getElementName();
+        serializer.startTag(namespace, element);
+        if (mServiceId != null) {
+            mServiceId.serialize(serializer);
+        }
+        if (mVersion != null) {
+            mVersion.serialize(serializer);
+        }
+        if (mDescription != null) {
+            mDescription.serialize(serializer);
+        }
+        serializer.endTag(namespace, element);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event.
+        int eventType = parser.next();
+
+        while(!(eventType == XmlPullParser.END_TAG
+                && getNamespace().equals(parser.getNamespace())
+                && getElementName().equals(parser.getName()))) {
+
+            if (eventType == XmlPullParser.START_TAG) {
+                String tagName = parser.getName();
+
+                if (ServiceId.ELEMENT_NAME.equals(tagName)) {
+                    ServiceId serviceId = new ServiceId();
+                    serviceId.parse(parser);
+                    mServiceId = serviceId;
+                } else if (Version.ELEMENT_NAME.equals(tagName)) {
+                    Version version = new Version();
+                    version.parse(parser);
+                    mVersion = version;
+                } else if (Description.ELEMENT_NAME.equals(tagName)) {
+                    Description description = new Description();
+                    description.parse(parser);
+                    mDescription = description;
+                }
+            }
+
+            eventType = parser.next();
+
+            // Leave directly if the event type is the end of the document.
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                return;
+            }
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java
new file mode 100644
index 0000000..db821fb
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "service-id" element of the pidf.
+ */
+public class ServiceId extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "service-id";
+
+    private String mServiceId;
+
+    public ServiceId() {
+    }
+
+    public ServiceId(String serviceId) {
+        mServiceId = serviceId;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return OmaPresConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getValue() {
+        return mServiceId;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mServiceId == null) {
+            return;
+        }
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        serializer.text(mServiceId);
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String serviceId = parser.getText();
+            if (!TextUtils.isEmpty(serviceId)) {
+                mServiceId = serviceId;
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java
new file mode 100644
index 0000000..8e0a721
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java
@@ -0,0 +1,105 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "version" element of the pidf.
+ */
+public class Version extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "version";
+
+    private int mMajorVersion;
+    private int mMinorVersion;
+
+    public Version() {
+    }
+
+    public Version(int majorVersion, int minorVersion) {
+        mMajorVersion = majorVersion;
+        mMinorVersion = minorVersion;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return OmaPresConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getValue() {
+        StringBuilder builder = new StringBuilder();
+        builder.append(mMajorVersion).append(".").append(mMinorVersion);
+        return builder.toString();
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        serializer.text(getValue());
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String version = parser.getText();
+            handleParsedVersion(version);
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+
+    private void handleParsedVersion(String version) {
+        if (TextUtils.isEmpty(version)) {
+            return;
+        }
+
+        String[] versionAry = version.split("\\.");
+        if (versionAry != null && versionAry.length == 2) {
+            mMajorVersion = Integer.parseInt(versionAry[0]);
+            mMinorVersion = Integer.parseInt(versionAry[1]);
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java
new file mode 100644
index 0000000..a4f487a
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java
@@ -0,0 +1,118 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.annotation.StringDef;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The "basic" element of the pidf.
+ */
+public class Basic extends ElementBase {
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "Basic";
+
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "basic";
+
+    /** The value "open" of the Basic element */
+    public static final String OPEN = "open";
+
+    /** The value "closed" of the Basic element */
+    public static final String CLOSED = "closed";
+
+    @StringDef(value = {
+            OPEN,
+            CLOSED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BasicValue {}
+
+    private @BasicValue String mBasic;
+
+    public Basic() {
+    }
+
+    public Basic(@BasicValue String value) {
+        mBasic = value;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getValue() {
+        return mBasic;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mBasic == null) {
+            return;
+        }
+        final String namespace = getNamespace();
+        final String element = getElementName();
+        serializer.startTag(namespace, element);
+        serializer.text(mBasic);
+        serializer.endTag(namespace, element);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String basicValue = parser.getText();
+            if (OPEN.equals(basicValue)) {
+                mBasic = OPEN;
+            } else if (CLOSED.equals(basicValue)) {
+                mBasic = CLOSED;
+            } else {
+                mBasic = null;
+            }
+        } else {
+            Log.d(LOG_TAG, "The eventType is not TEXT=" + eventType);
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java
new file mode 100644
index 0000000..df5c800
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "contact" element of the pidf.
+ */
+public class Contact extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "contact";
+
+    private Double mPriority;
+    private String mContact;
+
+    public Contact() {
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public void setPriority(Double priority) {
+        mPriority = priority;
+    }
+
+    @VisibleForTesting
+    public Double getPriority() {
+        return mPriority;
+    }
+
+    public void setContact(String contact) {
+        mContact = contact;
+    }
+
+    public String getContact() {
+        return mContact;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mContact == null) {
+            return;
+        }
+        String noNamespace = XmlPullParser.NO_NAMESPACE;
+        String namespace = getNamespace();
+        String elementName = getElementName();
+        serializer.startTag(namespace, elementName);
+        if (mPriority != null) {
+            serializer.attribute(noNamespace, "priority", String.valueOf(mPriority));
+        }
+        serializer.text(mContact);
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        String priority = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "priority");
+        if (!TextUtils.isEmpty(priority)) {
+            mPriority = Double.parseDouble(priority);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String contact = parser.getText();
+            if (!TextUtils.isEmpty(contact)) {
+                mContact = contact;
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java
new file mode 100644
index 0000000..ef13b5b
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java
@@ -0,0 +1,95 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "note" element of the pidf. This element is usually used for a human readable comment.
+ * It may appear as a child element of "presence" or as a child element of the "tuple" element.
+ */
+public class Note extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "note";
+
+    private String mNote;
+
+    public Note() {
+    }
+
+    public Note(String note) {
+        mNote = note;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getNote() {
+        return mNote;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mNote == null) {
+            return;
+        }
+        final String namespace = getNamespace();
+        final String element = getElementName();
+        serializer.startTag(namespace, element);
+        serializer.text(mNote);
+        serializer.endTag(namespace, element);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String note = parser.getText();
+            if (!TextUtils.isEmpty(note)) {
+                mNote = note;
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java
new file mode 100644
index 0000000..ac9c9da
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java
@@ -0,0 +1,21 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+public class PidfConstant {
+    public static final String NAMESPACE = "urn:ietf:params:xml:ns:pidf";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java
new file mode 100644
index 0000000..e9a40a8
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java
@@ -0,0 +1,176 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The "present" element is the root element of an "application/pidf+xml" object.
+ */
+public class Presence extends ElementBase {
+    /**
+     * The presence element consists the following elements:
+     * 1: Any number (including 0) of <tuple> elements
+     * 2: Any number (including 0) of <note> elements
+     * 3: Any number of OPTIONAL extension elements from other namespaces.
+     */
+
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "presence";
+
+    private static final String ATTRIBUTE_NAME_ENTITY = "entity";
+
+    // The presence element must have an "entity" attribute.
+    private String mEntity;
+
+    // The presence element contains any number of <tuple> elements
+    private final List<Tuple> mTupleList = new ArrayList<>();
+
+    // The presence element contains any number of <note> elements;
+    private final List<Note> mNoteList = new ArrayList<>();
+
+    public Presence() {
+    }
+
+    public Presence(@NonNull Uri contact) {
+        initEntity(contact);
+    }
+
+    private void initEntity(Uri contact) {
+        mEntity = contact.toString();
+    }
+
+    @VisibleForTesting
+    public void setEntity(String entity) {
+        mEntity = entity;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getEntity() {
+        return mEntity;
+    }
+
+    public void addTuple(@NonNull Tuple tuple) {
+        mTupleList.add(tuple);
+    }
+
+    public @NonNull List<Tuple> getTupleList() {
+        return Collections.unmodifiableList(mTupleList);
+    }
+
+    public void addNote(@NonNull Note note) {
+        mNoteList.add(note);
+    }
+
+    public @NonNull List<Note> getNoteList() {
+        return Collections.unmodifiableList(mNoteList);
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        String namespace = getNamespace();
+        String elementName = getElementName();
+
+        serializer.startTag(namespace, elementName);
+        // entity attribute
+        serializer.attribute(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_ENTITY, mEntity);
+
+        // tuple elements
+        for (Tuple tuple : mTupleList) {
+            tuple.serialize(serializer);
+        }
+
+        // note elements
+        for (Note note : mNoteList) {
+            note.serialize(serializer);
+        }
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        mEntity = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_ENTITY);
+
+        // Move to the next event.
+        int eventType = parser.next();
+
+        while(!(eventType == XmlPullParser.END_TAG
+                && getNamespace().equals(parser.getNamespace())
+                && getElementName().equals(parser.getName()))) {
+
+            if (eventType == XmlPullParser.START_TAG) {
+                String tagName = parser.getName();
+
+                if (isTupleElement(eventType, tagName)) {
+                    Tuple tuple = new Tuple();
+                    tuple.parse(parser);
+                    mTupleList.add(tuple);
+                } else if (isNoteElement(eventType, tagName)) {
+                    Note note = new Note();
+                    note.parse(parser);
+                    mNoteList.add(note);
+                }
+            }
+
+            eventType = parser.next();
+
+            // Leave directly if the event type is the end of the document.
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                return;
+            }
+        }
+    }
+
+    private boolean isTupleElement(int eventType, String name) {
+        return (eventType == XmlPullParser.START_TAG && Tuple.ELEMENT_NAME.equals(name)) ?
+                true : false;
+    }
+
+    private boolean isNoteElement(int eventType, String name) {
+        return (eventType == XmlPullParser.START_TAG && Note.ELEMENT_NAME.equals(name)) ?
+                true : false;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java
new file mode 100644
index 0000000..92ad5d6
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java
@@ -0,0 +1,99 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "status" element of the pidf.
+ */
+public class Status extends ElementBase {
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "Status";
+
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "status";
+
+    // The "status" element contain one optional "basic" element.
+    private Basic mBasic;
+
+    public Status() {
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public void setBasic(Basic basic) {
+        mBasic = basic;
+    }
+
+    public Basic getBasic() {
+        return mBasic;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mBasic == null) {
+            return;
+        }
+        final String namespace = getNamespace();
+        final String element = getElementName();
+        serializer.startTag(namespace, element);
+        mBasic.serialize(serializer);
+        serializer.endTag(namespace, element);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next tag to get the Basic element.
+        int eventType = parser.nextTag();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.START_TAG) {
+            Basic basic = new Basic();
+            basic.parse(parser);
+            mBasic = basic;
+        } else {
+            Log.d(LOG_TAG, "The eventType is not START_TAG=" + eventType);
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java
new file mode 100644
index 0000000..4c0d810
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+public class Timestamp extends ElementBase {
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "timestamp";
+
+    private String mTimestamp;
+
+    public Timestamp() {
+    }
+
+    public Timestamp(String timestamp) {
+        mTimestamp = timestamp;
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getValue() {
+        return mTimestamp;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        if (mTimestamp == null) {
+            return;
+        }
+        final String namespace = getNamespace();
+        final String element = getElementName();
+        serializer.startTag(namespace, element);
+        serializer.text(mTimestamp);
+        serializer.endTag(namespace, element);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // Move to the next event to get the value.
+        int eventType = parser.next();
+
+        // Get the value if the event type is text.
+        if (eventType == XmlPullParser.TEXT) {
+            String timestamp = parser.getText();
+            if (!TextUtils.isEmpty(timestamp)) {
+                mTimestamp = timestamp;
+            }
+        }
+
+        // Move to the end tag.
+        moveToElementEndTag(parser, eventType);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.java
new file mode 100644
index 0000000..014dbed
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.java
@@ -0,0 +1,226 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The "tuple" element of the pidf.
+ */
+public class Tuple extends ElementBase {
+    /**
+     * The tuple element consists the following elements:
+     * 1: one "status" element
+     * 2: any number of optional extension elements
+     * 3: an optional "contact" element
+     * 4: any number of optional "note" elements
+     * 5: an optional "timestamp" element
+     */
+
+    /** The name of this element */
+    public static final String ELEMENT_NAME = "tuple";
+
+    private static final String ATTRIBUTE_NAME_TUPLE_ID = "id";
+
+    private static long sTupleId = 0;
+
+    private static final Object LOCK = new Object();
+
+    private String mId;
+    private Status mStatus;
+    private ServiceDescription mServiceDescription;
+    private ServiceCaps mServiceCaps;
+    private Contact mContact;
+    private List<Note> mNoteList = new ArrayList<>();
+    private Timestamp mTimestamp;
+
+    public Tuple() {
+        mId = getTupleId();
+    }
+
+    @Override
+    protected String initNamespace() {
+        return PidfConstant.NAMESPACE;
+    }
+
+    @Override
+    protected String initElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public void setStatus(Status status) {
+        mStatus = status;
+    }
+
+    public Status getStatus() {
+        return mStatus;
+    }
+
+    public void setServiceDescription(ServiceDescription servDescription) {
+        mServiceDescription = servDescription;
+    }
+
+    public ServiceDescription getServiceDescription() {
+        return mServiceDescription;
+    }
+
+    public void setServiceCaps(ServiceCaps serviceCaps) {
+        mServiceCaps = serviceCaps;
+    }
+
+    public ServiceCaps getServiceCaps() {
+        return mServiceCaps;
+    }
+
+    public void setContact(Contact contact) {
+        mContact = contact;
+    }
+
+    public Contact getContact() {
+        return mContact;
+    }
+
+    public void addNote(Note note) {
+        mNoteList.add(note);
+    }
+
+    public List<Note> getNoteList() {
+        return Collections.unmodifiableList(mNoteList);
+    }
+
+    public void setTimestamp(Timestamp timestamp) {
+        mTimestamp = timestamp;
+    }
+
+    public Timestamp getTimestamp() {
+        return mTimestamp;
+    }
+
+    @Override
+    public void serialize(XmlSerializer serializer) throws IOException {
+        String namespace = getNamespace();
+        String elementName = getElementName();
+
+        serializer.startTag(namespace, elementName);
+        // id attribute
+        serializer.attribute(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_TUPLE_ID, mId);
+
+        // status element
+        mStatus.serialize(serializer);
+
+        // Service description
+        if (mServiceDescription != null) {
+            mServiceDescription.serialize(serializer);
+        }
+
+        // Service capabilities
+        if (mServiceCaps != null) {
+            mServiceCaps.serialize(serializer);
+        }
+
+        // contact element
+        if (mContact != null) {
+            mContact.serialize(serializer);
+        }
+
+        // note element
+        for (Note note: mNoteList) {
+            note.serialize(serializer);
+        }
+
+        // Timestamp
+        if (mTimestamp != null) {
+            mTimestamp.serialize(serializer);
+        }
+        serializer.endTag(namespace, elementName);
+    }
+
+    @Override
+    public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String namespace = parser.getNamespace();
+        String name = parser.getName();
+
+        if (!verifyParsingElement(namespace, name)) {
+            throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+        }
+
+        // id attribute
+        mId = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_TUPLE_ID);
+
+        // Move to the next event.
+        int eventType = parser.next();
+
+        while(!(eventType == XmlPullParser.END_TAG
+                && getNamespace().equals(parser.getNamespace())
+                && getElementName().equals(parser.getName()))) {
+
+            if (eventType == XmlPullParser.START_TAG) {
+                String tagName = parser.getName();
+
+                if (Status.ELEMENT_NAME.equals(tagName)) {
+                    Status status = new Status();
+                    status.parse(parser);
+                    mStatus = status;
+                } else if (ServiceDescription.ELEMENT_NAME.equals(tagName)) {
+                    ServiceDescription serviceDescription = new ServiceDescription();
+                    serviceDescription.parse(parser);
+                    mServiceDescription = serviceDescription;
+                } else if (ServiceCaps.ELEMENT_NAME.equals(tagName)) {
+                    ServiceCaps serviceCaps = new ServiceCaps();
+                    serviceCaps.parse(parser);
+                    mServiceCaps = serviceCaps;
+                } else if (Contact.ELEMENT_NAME.equals(tagName)) {
+                    Contact contact = new Contact();
+                    contact.parse(parser);
+                    mContact = contact;
+                } else if (Note.ELEMENT_NAME.equals(tagName)) {
+                    Note note = new Note();
+                    note.parse(parser);
+                    mNoteList.add(note);
+                } else if (Timestamp.ELEMENT_NAME.equals(tagName)) {
+                    Timestamp timestamp = new Timestamp();
+                    timestamp.parse(parser);
+                    mTimestamp = timestamp;
+                }
+            }
+
+            eventType = parser.next();
+
+            // Leave directly if the event type is the end of the document.
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                return;
+            }
+        }
+    }
+
+    private String getTupleId() {
+        synchronized (LOCK) {
+            return "tid" + (sTupleId++);
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java
new file mode 100644
index 0000000..d63b973
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java
@@ -0,0 +1,568 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.TelecomManager;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.util.ArraySet;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Stores the device's capabilities information.
+ */
+public class DeviceCapabilityInfo {
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "DeviceCapabilityInfo";
+
+    private final int mSubId;
+
+    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+
+    // FT overrides to add to the IMS registration, which will be added to the existing
+    // capabilities.
+    private final Set<String> mOverrideAddFeatureTags = new ArraySet<>();
+
+    // FT overrides to remove from the existing IMS registration, which will remove the related
+    // capabilities.
+    private final Set<String> mOverrideRemoveFeatureTags = new ArraySet<>();
+
+    // Tracks capability status based on the IMS registration.
+    private PublishServiceDescTracker mServiceCapRegTracker;
+
+    // The feature tags associated with the last IMS registration update.
+    private Set<String> mLastRegistrationFeatureTags = Collections.emptySet();
+    // The feature tags associated with the last IMS registration update, which also include
+    // overrides
+    private Set<String> mLastRegistrationOverrideFeatureTags = Collections.emptySet();
+
+    // The mmtel feature is registered or not
+    private boolean mMmtelRegistered;
+
+    // The network type which ims mmtel registers on.
+    private int mMmtelNetworkRegType;
+
+    // The rcs feature is registered or not
+    private boolean mRcsRegistered;
+
+    // Whether or not presence is reported as capable
+    private boolean mPresenceCapable;
+
+    // The network type which ims rcs registers on.
+    private int mRcsNetworkRegType;
+
+    // The MMTel capabilities of this subscription Id
+    private MmTelFeature.MmTelCapabilities mMmTelCapabilities;
+
+    // Whether the settings are changed or not
+    private int mTtyPreferredMode;
+    private boolean mAirplaneMode;
+    private boolean mMobileData;
+    private boolean mVtSetting;
+
+    public DeviceCapabilityInfo(int subId, String[] capToRegistrationMap) {
+        mSubId = subId;
+        mServiceCapRegTracker = PublishServiceDescTracker.fromCarrierConfig(capToRegistrationMap);
+        reset();
+    }
+
+    /**
+     * Reset all the status.
+     */
+    public synchronized void reset() {
+        logd("reset");
+        mMmtelRegistered = false;
+        mMmtelNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+        mRcsRegistered = false;
+        mRcsNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+        mTtyPreferredMode = TelecomManager.TTY_MODE_OFF;
+        mAirplaneMode = false;
+        mMobileData = true;
+        mVtSetting = true;
+        mMmTelCapabilities = new MmTelCapabilities();
+    }
+
+    /**
+     * Update the capability registration tracker feature tag override mapping.
+     * @return if true, this has caused a change in the Feature Tags associated with the device
+     * and a new PUBLISH should be generated.
+     */
+    public synchronized boolean updateCapabilityRegistrationTrackerMap(String[] newMap) {
+        Set<String> oldTags = mServiceCapRegTracker.copyRegistrationFeatureTags();
+        mServiceCapRegTracker = PublishServiceDescTracker.fromCarrierConfig(newMap);
+        mServiceCapRegTracker.updateImsRegistration(mLastRegistrationOverrideFeatureTags);
+        boolean changed = !oldTags.equals(mServiceCapRegTracker.copyRegistrationFeatureTags());
+        if (changed) logi("Carrier Config Change resulted in associated FT list change");
+        return changed;
+    }
+
+    public synchronized boolean isImsRegistered() {
+        return mMmtelRegistered;
+    }
+
+    /**
+     * Update the status that IMS MMTEL is registered.
+     */
+    public synchronized void updateImsMmtelRegistered(int type) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("IMS MMTEL registered: original state=").append(mMmtelRegistered)
+                .append(", changes type from ").append(mMmtelNetworkRegType)
+                .append(" to ").append(type);
+        logi(builder.toString());
+
+        if (!mMmtelRegistered) {
+            mMmtelRegistered = true;
+        }
+
+        if (mMmtelNetworkRegType != type) {
+            mMmtelNetworkRegType = type;
+        }
+    }
+
+    /**
+     * Update the status that IMS MMTEL is unregistered.
+     */
+    public synchronized void updateImsMmtelUnregistered() {
+        logi("IMS MMTEL unregistered: original state=" + mMmtelRegistered);
+        if (mMmtelRegistered) {
+            mMmtelRegistered = false;
+        }
+        mMmtelNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+    }
+
+    public synchronized void updatePresenceCapable(boolean isCapable) {
+        mPresenceCapable = isCapable;
+    }
+
+    /**
+     * Update the status that IMS RCS is registered.
+     * @return true if the IMS registration status changed, false if it did not.
+     */
+    public synchronized boolean updateImsRcsRegistered(ImsRegistrationAttributes attr) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("IMS RCS registered: original state=").append(mRcsRegistered)
+                .append(", changes type from ").append(mRcsNetworkRegType)
+                .append(" to ").append(attr.getTransportType());
+        logi(builder.toString());
+
+        boolean changed = false;
+        if (!mRcsRegistered) {
+            mRcsRegistered = true;
+            changed = true;
+        }
+
+        if (mRcsNetworkRegType != attr.getTransportType()) {
+            mRcsNetworkRegType = attr.getTransportType();
+            changed = true;
+        }
+
+        mLastRegistrationFeatureTags = attr.getFeatureTags();
+        changed |= updateRegistration(mLastRegistrationFeatureTags);
+
+        return changed;
+    }
+
+    /**
+     * Update the status that IMS RCS is unregistered.
+     */
+    public synchronized boolean updateImsRcsUnregistered() {
+        logi("IMS RCS unregistered: original state=" + mRcsRegistered);
+        boolean changed = false;
+        if (mRcsRegistered) {
+            mRcsRegistered = false;
+            changed = true;
+        }
+        mRcsNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+        return changed;
+    }
+
+    public synchronized boolean addRegistrationOverrideCapabilities(Set<String> featureTags) {
+        logd("override - add: " + featureTags);
+        mOverrideRemoveFeatureTags.removeAll(featureTags);
+        mOverrideAddFeatureTags.addAll(featureTags);
+        // Call with the last feature tags so that the new ones will be potentially picked up.
+        return updateRegistration(mLastRegistrationFeatureTags);
+    };
+
+    public synchronized boolean removeRegistrationOverrideCapabilities(Set<String> featureTags) {
+        logd("override - remove: " + featureTags);
+        mOverrideAddFeatureTags.removeAll(featureTags);
+        mOverrideRemoveFeatureTags.addAll(featureTags);
+        // Call with the last feature tags so that the new ones will be potentially picked up.
+        return updateRegistration(mLastRegistrationFeatureTags);
+    };
+
+    public synchronized boolean clearRegistrationOverrideCapabilities() {
+        logd("override - clear");
+        mOverrideAddFeatureTags.clear();
+        mOverrideRemoveFeatureTags.clear();
+        // Call with the last feature tags so that base tags will be restored
+        return updateRegistration(mLastRegistrationFeatureTags);
+    };
+
+    /**
+     * Update the IMS registration tracked by the PublishServiceDescTracker if needed.
+     * @return true if the registration changed, else otherwise.
+     */
+    private boolean updateRegistration(Set<String> baseTags) {
+        Set<String> updatedTags = updateImsRegistrationFeatureTags(baseTags);
+        if (!mLastRegistrationOverrideFeatureTags.equals(updatedTags)) {
+            mLastRegistrationOverrideFeatureTags = updatedTags;
+            mServiceCapRegTracker.updateImsRegistration(updatedTags);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Combine IMS registration with overrides to produce a new feature tag Set.
+     * @return true if the IMS registration changed, false otherwise.
+     */
+    private synchronized Set<String> updateImsRegistrationFeatureTags(Set<String> featureTags) {
+        Set<String> tags = new ArraySet<>(featureTags);
+        tags.addAll(mOverrideAddFeatureTags);
+        tags.removeAll(mOverrideRemoveFeatureTags);
+        return tags;
+    }
+
+    /**
+     * Update the TTY preferred mode.
+     * @return {@code true} if tty preferred mode is changed, {@code false} otherwise.
+     */
+    public synchronized boolean updateTtyPreferredMode(int ttyMode) {
+        if (mTtyPreferredMode != ttyMode) {
+            logd("TTY preferred mode changes from " + mTtyPreferredMode + " to " + ttyMode);
+            mTtyPreferredMode = ttyMode;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Update airplane mode state.
+     * @return {@code true} if the airplane mode is changed, {@code false} otherwise.
+     */
+    public synchronized boolean updateAirplaneMode(boolean state) {
+        if (mAirplaneMode != state) {
+            logd("Airplane mode changes from " + mAirplaneMode + " to " + state);
+            mAirplaneMode = state;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Update mobile data setting.
+     * @return {@code true} if the mobile data setting is changed, {@code false} otherwise.
+     */
+    public synchronized boolean updateMobileData(boolean mobileData) {
+        if (mMobileData != mobileData) {
+            logd("Mobile data changes from " + mMobileData + " to " + mobileData);
+            mMobileData = mobileData;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Update VT setting.
+     * @return {@code true} if vt setting is changed, {@code false}.otherwise.
+     */
+    public synchronized boolean updateVtSetting(boolean vtSetting) {
+        if (mVtSetting != vtSetting) {
+            logd("VT setting changes from " + mVtSetting + " to " + vtSetting);
+            mVtSetting = vtSetting;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Update the MMTEL capabilities if the capabilities is changed.
+     * @return {@code true} if the mmtel capabilities are changed, {@code false} otherwise.
+     */
+    public synchronized boolean updateMmtelCapabilitiesChanged(MmTelCapabilities capabilities) {
+        if (capabilities == null) {
+            return false;
+        }
+        boolean oldVolteAvailable = isVolteAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+        boolean oldVoWifiAvailable = isVoWifiAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+        boolean oldVtAvailable = isVtAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+        boolean oldViWifiAvailable = isViWifiAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+        boolean oldCallComposerAvailable = isCallComposerAvailable(mMmTelCapabilities);
+
+        boolean volteAvailable = isVolteAvailable(mMmtelNetworkRegType, capabilities);
+        boolean voWifiAvailable = isVoWifiAvailable(mMmtelNetworkRegType, capabilities);
+        boolean vtAvailable = isVtAvailable(mMmtelNetworkRegType, capabilities);
+        boolean viWifiAvailable = isViWifiAvailable(mMmtelNetworkRegType, capabilities);
+        boolean callComposerAvailable = isCallComposerAvailable(capabilities);
+
+        logd("updateMmtelCapabilitiesChanged: from " + mMmTelCapabilities + " to " + capabilities);
+
+        // Update to the new mmtel capabilities
+        mMmTelCapabilities = deepCopyCapabilities(capabilities);
+
+        if (oldVolteAvailable != volteAvailable
+                || oldVoWifiAvailable != voWifiAvailable
+                || oldVtAvailable != vtAvailable
+                || oldViWifiAvailable != viWifiAvailable
+                || oldCallComposerAvailable != callComposerAvailable) {
+            return true;
+        }
+        return false;
+    }
+
+    public synchronized boolean isPresenceCapable() {
+        return mPresenceCapable;
+    }
+
+    private boolean isVolteAvailable(int networkRegType, MmTelCapabilities capabilities) {
+        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
+                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+    }
+
+    private boolean isVoWifiAvailable(int networkRegType, MmTelCapabilities capabilities) {
+        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
+                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+    }
+
+    private boolean isVtAvailable(int networkRegType, MmTelCapabilities capabilities) {
+        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
+                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+    }
+
+    private boolean isViWifiAvailable(int networkRegType, MmTelCapabilities capabilities) {
+        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
+                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+    }
+
+    private boolean isCallComposerAvailable(MmTelCapabilities capabilities) {
+        return capabilities.isCapable(
+                MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER);
+    }
+
+    /**
+     * Get the device's capabilities.
+     */
+    public synchronized RcsContactUceCapability getDeviceCapabilities(
+            @CapabilityMechanism int mechanism, Context context) {
+        switch (mechanism) {
+            case RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE:
+                return getPresenceCapabilities(context);
+            case RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS:
+                return getOptionsCapabilities(context);
+            default:
+                logw("getDeviceCapabilities: invalid mechanism " + mechanism);
+                return null;
+        }
+    }
+
+    // Get the device's capabilities with the PRESENCE mechanism.
+    private RcsContactUceCapability getPresenceCapabilities(Context context) {
+        Uri uri = PublishUtils.getDeviceContactUri(context, mSubId);
+        if (uri == null) {
+            logw("getPresenceCapabilities: uri is empty");
+            return null;
+        }
+        Set<ServiceDescription> capableFromReg =
+                mServiceCapRegTracker.copyRegistrationCapabilities();
+
+        PresenceBuilder presenceBuilder = new PresenceBuilder(uri,
+                RcsContactUceCapability.SOURCE_TYPE_CACHED,
+                RcsContactUceCapability.REQUEST_RESULT_FOUND);
+        // RCS presence tag (added to all presence documents)
+        ServiceDescription presDescription = getCustomizedDescription(
+                ServiceDescription.SERVICE_DESCRIPTION_PRESENCE, capableFromReg);
+        addCapability(presenceBuilder, presDescription.getTupleBuilder(), uri);
+        capableFromReg.remove(presDescription);
+
+        // mmtel
+        ServiceDescription voiceDescription = getCustomizedDescription(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE, capableFromReg);
+        ServiceDescription vtDescription = getCustomizedDescription(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, capableFromReg);
+        ServiceDescription descToUse = (hasVolteCapability() && hasVtCapability()) ?
+                vtDescription : voiceDescription;
+        ServiceCapabilities servCaps = new ServiceCapabilities.Builder(
+                hasVolteCapability(), hasVtCapability())
+                .addSupportedDuplexMode(ServiceCapabilities.DUPLEX_MODE_FULL).build();
+        addCapability(presenceBuilder, descToUse.getTupleBuilder()
+                .setServiceCapabilities(servCaps), uri);
+        capableFromReg.remove(voiceDescription);
+        capableFromReg.remove(vtDescription);
+
+        // call composer via mmtel
+        ServiceDescription composerDescription = getCustomizedDescription(
+                ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, capableFromReg);
+        if (hasCallComposerCapability()) {
+            addCapability(presenceBuilder, composerDescription.getTupleBuilder(), uri);
+        }
+        capableFromReg.remove(composerDescription);
+
+        // External features can only be found using registration states from other components.
+        // Count these features as capable and include in PIDF XML if they are registered.
+        for (ServiceDescription capability : capableFromReg) {
+            addCapability(presenceBuilder, capability.getTupleBuilder(), uri);
+        }
+
+        return presenceBuilder.build();
+    }
+
+    /**
+     * Search the refSet for the ServiceDescription that matches the service-id && version and
+     * return that or return the reference if there is no match.
+     */
+    private ServiceDescription getCustomizedDescription(ServiceDescription reference,
+            Set<ServiceDescription> refSet) {
+        return refSet.stream().filter(s -> s.serviceId.equals(reference.serviceId)
+                && s.version.equals(reference.version)).findFirst().orElse(reference);
+    }
+
+    // Get the device's capabilities with the OPTIONS mechanism.
+    private RcsContactUceCapability getOptionsCapabilities(Context context) {
+        Uri uri = PublishUtils.getDeviceContactUri(context, mSubId);
+        if (uri == null) {
+            logw("getOptionsCapabilities: uri is empty");
+            return null;
+        }
+
+        Set<String> capableFromReg = mServiceCapRegTracker.copyRegistrationFeatureTags();
+
+        OptionsBuilder optionsBuilder = new OptionsBuilder(uri, SOURCE_TYPE_CACHED);
+        optionsBuilder.setRequestResult(RcsContactUceCapability.REQUEST_RESULT_FOUND);
+        FeatureTags.addFeatureTags(optionsBuilder, hasVolteCapability(), hasVtCapability(),
+                isPresenceCapable(), hasCallComposerCapability(), capableFromReg);
+        return optionsBuilder.build();
+    }
+
+    private void addCapability(RcsContactUceCapability.PresenceBuilder presenceBuilder,
+            RcsContactPresenceTuple.Builder tupleBuilder, Uri contactUri) {
+        presenceBuilder.addCapabilityTuple(tupleBuilder.setContactUri(contactUri).build());
+    }
+
+    // Check if the device has the VoLTE capability
+    private synchronized boolean hasVolteCapability() {
+        return overrideCapability(FeatureTags.FEATURE_TAG_MMTEL, mMmTelCapabilities != null
+                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE));
+    }
+
+    // Check if the device has the VT capability
+    private synchronized boolean hasVtCapability() {
+        return overrideCapability(FeatureTags.FEATURE_TAG_VIDEO, mMmTelCapabilities != null
+                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO));
+    }
+
+    // Check if the device has the Call Composer capability
+    private synchronized boolean hasCallComposerCapability() {
+        return overrideCapability(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
+                mMmTelCapabilities != null && mMmTelCapabilities.isCapable(
+                        MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER));
+    }
+
+    /**
+     * @return the overridden value for the provided feature tag or the original capability if there
+     * is no override.
+     */
+    private synchronized boolean overrideCapability(String featureTag, boolean originalCap) {
+        if (mOverrideRemoveFeatureTags.contains(featureTag)) {
+            return false;
+        }
+
+        if (mOverrideAddFeatureTags.contains(featureTag)) {
+            return true;
+        }
+
+        return originalCap;
+    }
+
+    private synchronized MmTelCapabilities deepCopyCapabilities(MmTelCapabilities capabilities) {
+        MmTelCapabilities mmTelCapabilities = new MmTelCapabilities();
+        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE)) {
+            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+        }
+        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO)) {
+            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+        }
+        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT)) {
+            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_UT);
+        }
+        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS)) {
+            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_SMS);
+        }
+        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER)) {
+            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER);
+        }
+        return mmTelCapabilities;
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[D] " + log);
+    }
+
+    private void logi(String log) {
+        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[W] " + log);
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("DeviceCapabilityInfo :");
+        pw.increaseIndent();
+
+        mServiceCapRegTracker.dump(pw);
+
+        pw.println("Log:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java
new file mode 100644
index 0000000..ead078a
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java
@@ -0,0 +1,677 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.telecom.TelecomManager;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.AccessNetworkConstants.TransportType;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.util.HandlerExecutor;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+
+/**
+ * Listen to the device changes and notify the PublishController to publish the device's
+ * capabilities to the Presence server.
+ */
+public class DeviceCapabilityListener {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "DeviceCapListener";
+
+    private static final long REGISTER_IMS_CHANGED_DELAY = 15000L;  // 15 seconds
+
+    /**
+     * Used to inject ImsMmTelManager instances for testing.
+     */
+    @VisibleForTesting
+    public interface ImsMmTelManagerFactory {
+        ImsMmTelManager getImsMmTelManager(int subId);
+    }
+
+    /**
+     * Used to inject ImsRcsManager instances for testing.
+     */
+    @VisibleForTesting
+    public interface ImsRcsManagerFactory {
+        ImsRcsManager getImsRcsManager(int subId);
+    }
+
+    /**
+     * Used to inject ProvisioningManager instances for testing.
+     */
+    @VisibleForTesting
+    public interface ProvisioningManagerFactory {
+        ProvisioningManager getProvisioningManager(int subId);
+    }
+
+    /*
+     * Handle registering IMS callback and triggering the publish request because of the
+     * capabilities changed.
+     */
+    private class DeviceCapabilityHandler extends Handler {
+        private static final long TRIGGER_PUBLISH_REQUEST_DELAY_MS = 500L;
+
+        private static final int EVENT_REGISTER_IMS_CONTENT_CHANGE = 1;
+        private static final int EVENT_UNREGISTER_IMS_CHANGE = 2;
+        private static final int EVENT_REQUEST_PUBLISH = 3;
+
+        DeviceCapabilityHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            logd("handleMessage: " + msg.what);
+            if (mIsDestroyed) return;
+            switch (msg.what) {
+                case EVENT_REGISTER_IMS_CONTENT_CHANGE:
+                    registerImsProvisionCallback();
+                    break;
+                case EVENT_UNREGISTER_IMS_CHANGE:
+                    unregisterImsProvisionCallback();
+                    break;
+                case EVENT_REQUEST_PUBLISH:
+                    int triggerType = msg.arg1;
+                    mCallback.requestPublishFromInternal(triggerType);
+                    break;
+            }
+        }
+
+        public void sendRegisterImsContentChangedMessage(long delay) {
+            // Remove the existing message and send a new one with the delayed time.
+            removeMessages(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+            Message msg = obtainMessage(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+            sendMessageDelayed(msg, delay);
+        }
+
+        public void removeRegisterImsContentChangedMessage() {
+            removeMessages(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+        }
+
+        public void sendUnregisterImsCallbackMessage() {
+            removeMessages(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+            sendEmptyMessage(EVENT_UNREGISTER_IMS_CHANGE);
+        }
+
+        public void sendTriggeringPublishMessage(@PublishTriggerType int type) {
+            logd("sendTriggeringPublishMessage: type=" + type);
+            // Remove the existing message and resend a new message.
+            removeMessages(EVENT_REQUEST_PUBLISH);
+            Message message = obtainMessage();
+            message.what = EVENT_REQUEST_PUBLISH;
+            message.arg1 = type;
+            sendMessageDelayed(message, TRIGGER_PUBLISH_REQUEST_DELAY_MS);
+        }
+    }
+
+    private final int mSubId;
+    private final Context mContext;
+    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+    private volatile boolean mInitialized;
+    private volatile boolean mIsDestroyed;
+    private volatile boolean mIsRcsConnected;
+    private volatile boolean mIsImsCallbackRegistered;
+
+    // The callback to trigger the internal publish request
+    private final PublishControllerCallback mCallback;
+    private final DeviceCapabilityInfo mCapabilityInfo;
+    private final HandlerThread mHandlerThread;
+    private final DeviceCapabilityHandler mHandler;
+    private final HandlerExecutor mHandlerExecutor;
+
+    private ImsMmTelManager mImsMmTelManager;
+    private ImsMmTelManagerFactory mImsMmTelManagerFactory = (subId) -> getImsMmTelManager(subId);
+
+    private ImsRcsManager mImsRcsManager;
+    private ImsRcsManagerFactory mImsRcsManagerFactory = (subId) -> getImsRcsManager(subId);
+
+    private ProvisioningManager mProvisioningManager;
+    private ProvisioningManagerFactory mProvisioningMgrFactory = (subId)
+            -> ProvisioningManager.createForSubscriptionId(subId);
+
+    private ContentObserver mMobileDataObserver = null;
+    private ContentObserver mSimInfoContentObserver = null;
+
+    private final Object mLock = new Object();
+
+    public DeviceCapabilityListener(Context context, int subId, DeviceCapabilityInfo info,
+            PublishControllerCallback callback) {
+        mSubId = subId;
+        logi("create");
+
+        mContext = context;
+        mCallback = callback;
+        mCapabilityInfo = info;
+        mInitialized = false;
+
+        mHandlerThread = new HandlerThread("DeviceCapListenerThread");
+        mHandlerThread.start();
+        mHandler = new DeviceCapabilityHandler(mHandlerThread.getLooper());
+        mHandlerExecutor = new HandlerExecutor(mHandler);
+    }
+
+    /**
+     * Turn on the device capabilities changed listener
+     */
+    public void initialize() {
+        synchronized (mLock) {
+            if (mIsDestroyed) {
+                logw("initialize: This instance is already destroyed");
+                return;
+            }
+            if (mInitialized) return;
+
+            logi("initialize");
+            mImsMmTelManager = mImsMmTelManagerFactory.getImsMmTelManager(mSubId);
+            mImsRcsManager = mImsRcsManagerFactory.getImsRcsManager(mSubId);
+            mProvisioningManager = mProvisioningMgrFactory.getProvisioningManager(mSubId);
+            registerReceivers();
+            registerImsProvisionCallback();
+
+            mInitialized = true;
+        }
+    }
+
+    // The RcsFeature has been connected to the framework
+    public void onRcsConnected() {
+        mIsRcsConnected = true;
+        mHandler.sendRegisterImsContentChangedMessage(0L);
+    }
+
+    // The framework has lost the binding to the RcsFeature.
+    public void onRcsDisconnected() {
+        mIsRcsConnected = false;
+        mHandler.sendUnregisterImsCallbackMessage();
+    }
+
+    /**
+     * Notify the instance is destroyed
+     */
+    public void onDestroy() {
+        logi("onDestroy");
+        mIsDestroyed = true;
+        synchronized (mLock) {
+            if (!mInitialized) return;
+            logi("turnOffListener");
+            mInitialized = false;
+            unregisterReceivers();
+            unregisterImsProvisionCallback();
+            mHandlerThread.quit();
+        }
+    }
+
+    /*
+     * Register receivers to listen to the data changes.
+     */
+    private void registerReceivers() {
+        logd("registerReceivers");
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        filter.addAction(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+        mContext.registerReceiver(mReceiver, filter);
+
+        ContentResolver resolver = mContext.getContentResolver();
+        if (resolver != null) {
+            // Listen to the mobile data content changed.
+            resolver.registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.MOBILE_DATA), false,
+                    getMobileDataObserver());
+            // Listen to the SIM info content changed.
+            resolver.registerContentObserver(Telephony.SimInfo.CONTENT_URI, false,
+                    getSimInfoContentObserver());
+        }
+    }
+
+    private void unregisterReceivers() {
+        logd("unregisterReceivers");
+        mContext.unregisterReceiver(mReceiver);
+        ContentResolver resolver = mContext.getContentResolver();
+        if (resolver != null) {
+            resolver.unregisterContentObserver(getMobileDataObserver());
+            resolver.unregisterContentObserver(getSimInfoContentObserver());
+        }
+    }
+
+    private void registerImsProvisionCallback() {
+        if (mIsImsCallbackRegistered) {
+            logd("registerImsProvisionCallback: already registered.");
+            return;
+        }
+
+        logd("registerImsProvisionCallback");
+        try {
+            // Register mmtel callback
+            if (mImsMmTelManager != null) {
+                mImsMmTelManager.registerImsRegistrationCallback(mHandlerExecutor,
+                        mMmtelRegistrationCallback);
+                mImsMmTelManager.registerMmTelCapabilityCallback(mHandlerExecutor,
+                        mMmtelCapabilityCallback);
+            }
+
+            // Register rcs callback
+            if (mImsRcsManager != null) {
+                mImsRcsManager.registerImsRegistrationCallback(mHandlerExecutor,
+                        mRcsRegistrationCallback);
+            }
+
+            // Register provisioning changed callback
+            mProvisioningManager.registerProvisioningChangedCallback(mHandlerExecutor,
+                    mProvisionChangedCallback);
+
+            // Set the IMS callback is registered.
+            mIsImsCallbackRegistered = true;
+        } catch (ImsException e) {
+            logw("registerImsProvisionCallback error: " + e);
+            // Unregister the callback
+            unregisterImsProvisionCallback();
+
+            // Retry registering IMS callback only when the RCS is connected.
+            if (mIsRcsConnected) {
+                mHandler.sendRegisterImsContentChangedMessage(REGISTER_IMS_CHANGED_DELAY);
+            }
+        }
+    }
+
+    private void unregisterImsProvisionCallback() {
+        logd("unregisterImsProvisionCallback");
+
+        // Clear the registering IMS callback message from the handler thread
+        mHandler.removeRegisterImsContentChangedMessage();
+
+        // Unregister mmtel callback
+        if (mImsMmTelManager != null) {
+            try {
+                mImsMmTelManager.unregisterImsRegistrationCallback(mMmtelRegistrationCallback);
+            } catch (RuntimeException e) {
+                logw("unregister MMTel registration error: " + e.getMessage());
+            }
+            try {
+                mImsMmTelManager.unregisterMmTelCapabilityCallback(mMmtelCapabilityCallback);
+            } catch (RuntimeException e) {
+                logw("unregister MMTel capability error: " + e.getMessage());
+            }
+        }
+
+        // Unregister rcs callback
+        if (mImsRcsManager != null) {
+            try {
+                mImsRcsManager.unregisterImsRegistrationCallback(mRcsRegistrationCallback);
+            } catch (RuntimeException e) {
+                logw("unregister rcs capability error: " + e.getMessage());
+            }
+        }
+
+        try {
+            // Unregister provisioning changed callback
+            mProvisioningManager.unregisterProvisioningChangedCallback(mProvisionChangedCallback);
+        } catch (RuntimeException e) {
+            logw("unregister provisioning callback error: " + e.getMessage());
+        }
+
+        // Clear the IMS callback registered flag.
+        mIsImsCallbackRegistered = false;
+    }
+
+    @VisibleForTesting
+    public final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent == null || intent.getAction() == null) return;
+            switch (intent.getAction()) {
+                case TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED:
+                    int preferredMode = intent.getIntExtra(TelecomManager.EXTRA_TTY_PREFERRED_MODE,
+                            TelecomManager.TTY_MODE_OFF);
+                    handleTtyPreferredModeChanged(preferredMode);
+                    break;
+
+                case Intent.ACTION_AIRPLANE_MODE_CHANGED:
+                    boolean airplaneMode = intent.getBooleanExtra("state", false);
+                    handleAirplaneModeChanged(airplaneMode);
+                    break;
+            }
+        }
+    };
+
+    private ContentObserver getMobileDataObserver() {
+        synchronized (mLock) {
+            if (mMobileDataObserver == null) {
+                mMobileDataObserver = new ContentObserver(new Handler(mHandler.getLooper())) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        boolean isEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+                                Settings.Global.MOBILE_DATA, 1) == 1;
+                        handleMobileDataChanged(isEnabled);
+                    }
+                };
+            }
+            return mMobileDataObserver;
+        }
+    }
+
+    private ContentObserver getSimInfoContentObserver() {
+        synchronized (mLock) {
+            if (mSimInfoContentObserver == null) {
+                mSimInfoContentObserver = new ContentObserver(new Handler(mHandler.getLooper())) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        if (mImsMmTelManager == null) {
+                            logw("SimInfo change error: MmTelManager is null");
+                            return;
+                        }
+
+                        try {
+                            boolean isEnabled = mImsMmTelManager.isVtSettingEnabled();
+                            handleVtSettingChanged(isEnabled);
+                        } catch (RuntimeException e) {
+                            logw("SimInfo change error: " + e);
+                        }
+                    }
+                };
+            }
+            return mSimInfoContentObserver;
+        }
+    }
+
+    private ImsMmTelManager getImsMmTelManager(int subId) {
+        try {
+            ImsManager imsManager = mContext.getSystemService(
+                    android.telephony.ims.ImsManager.class);
+            return (imsManager == null) ? null : imsManager.getImsMmTelManager(subId);
+        } catch (IllegalArgumentException e) {
+            logw("getImsMmTelManager error: " + e.getMessage());
+            return null;
+        }
+    }
+
+    private ImsRcsManager getImsRcsManager(int subId) {
+        try {
+            ImsManager imsManager = mContext.getSystemService(
+                    android.telephony.ims.ImsManager.class);
+            return (imsManager == null) ? null : imsManager.getImsRcsManager(subId);
+        } catch (IllegalArgumentException e) {
+            logw("getImsRcsManager error: " + e.getMessage());
+            return null;
+        }
+    }
+
+    @VisibleForTesting
+    public final RegistrationManager.RegistrationCallback mRcsRegistrationCallback =
+            new RegistrationManager.RegistrationCallback() {
+                @Override
+                public void onRegistered(ImsRegistrationAttributes attributes) {
+                    synchronized (mLock) {
+                        logi("onRcsRegistered: " + attributes);
+                        if (!mIsImsCallbackRegistered) return;
+                        handleImsRcsRegistered(attributes);
+                    }
+                }
+
+                @Override
+                public void onUnregistered(ImsReasonInfo info) {
+                    synchronized (mLock) {
+                        logi("onRcsUnregistered: " + info);
+                        if (!mIsImsCallbackRegistered) return;
+                        handleImsRcsUnregistered();
+                    }
+                }
+    };
+
+    @VisibleForTesting
+    public final RegistrationManager.RegistrationCallback mMmtelRegistrationCallback =
+            new RegistrationManager.RegistrationCallback() {
+                @Override
+                public void onRegistered(@TransportType int transportType) {
+                    synchronized (mLock) {
+                        String type = AccessNetworkConstants.transportTypeToString(transportType);
+                        logi("onMmTelRegistered: " + type);
+                        if (!mIsImsCallbackRegistered) return;
+                        handleImsMmtelRegistered(transportType);
+                    }
+                }
+
+                @Override
+                public void onUnregistered(ImsReasonInfo info) {
+                    synchronized (mLock) {
+                        logi("onMmTelUnregistered: " + info);
+                        if (!mIsImsCallbackRegistered) return;
+                        handleImsMmtelUnregistered();
+                    }
+                }
+            };
+
+    @VisibleForTesting
+    public final ImsMmTelManager.CapabilityCallback mMmtelCapabilityCallback =
+            new CapabilityCallback() {
+                @Override
+                public void onCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
+                    if (capabilities == null) {
+                        logw("onCapabilitiesStatusChanged: parameter is null");
+                        return;
+                    }
+                    synchronized (mLock) {
+                        handleMmtelCapabilitiesStatusChanged(capabilities);
+                    }
+                }
+            };
+
+    @VisibleForTesting
+    public final ProvisioningManager.Callback mProvisionChangedCallback =
+            new ProvisioningManager.Callback() {
+                @Override
+                public void onProvisioningIntChanged(int item, int value) {
+                    logi("onProvisioningIntChanged: item=" + item + ", value=" + value);
+                    switch (item) {
+                        case ProvisioningManager.KEY_EAB_PROVISIONING_STATUS:
+                        case ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS:
+                        case ProvisioningManager.KEY_VT_PROVISIONING_STATUS:
+                            handleProvisioningChanged();
+                        case ProvisioningManager.KEY_RCS_PUBLISH_SOURCE_THROTTLE_MS:
+                            handlePublishThrottleChanged(value);
+                            break;
+                    }
+                }
+            };
+
+    private void handleTtyPreferredModeChanged(int preferredMode) {
+        boolean isChanged = mCapabilityInfo.updateTtyPreferredMode(preferredMode);
+        logi("TTY preferred mode changed: " + preferredMode + ", isChanged=" + isChanged);
+        if (isChanged) {
+            mHandler.sendTriggeringPublishMessage(
+                PublishController.PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE);
+        }
+    }
+
+    private void handleAirplaneModeChanged(boolean state) {
+        boolean isChanged = mCapabilityInfo.updateAirplaneMode(state);
+        logi("Airplane mode changed: " + state + ", isChanged="+ isChanged);
+        if (isChanged) {
+            mHandler.sendTriggeringPublishMessage(
+                    PublishController.PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE);
+        }
+    }
+
+    private void handleMobileDataChanged(boolean isEnabled) {
+        boolean isChanged = mCapabilityInfo.updateMobileData(isEnabled);
+        logi("Mobile data changed: " + isEnabled + ", isChanged=" + isChanged);
+        if (isChanged) {
+            mHandler.sendTriggeringPublishMessage(
+                    PublishController.PUBLISH_TRIGGER_MOBILE_DATA_CHANGE);
+        }
+    }
+
+    private void handleVtSettingChanged(boolean isEnabled) {
+        boolean isChanged = mCapabilityInfo.updateVtSetting(isEnabled);
+        logi("VT setting changed: " + isEnabled + ", isChanged=" + isChanged);
+        if (isChanged) {
+            mHandler.sendTriggeringPublishMessage(
+                    PublishController.PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+        }
+    }
+
+    /*
+     * This method is called when the MMTEL is registered.
+     */
+    private void handleImsMmtelRegistered(int imsTransportType) {
+        mCapabilityInfo.updateImsMmtelRegistered(imsTransportType);
+        mHandler.sendTriggeringPublishMessage(
+                PublishController.PUBLISH_TRIGGER_MMTEL_REGISTERED);
+    }
+
+    /*
+     * This method is called when the MMTEL is unregistered.
+     */
+    private void handleImsMmtelUnregistered() {
+        mCapabilityInfo.updateImsMmtelUnregistered();
+        mHandler.sendTriggeringPublishMessage(
+                PublishController.PUBLISH_TRIGGER_MMTEL_UNREGISTERED);
+    }
+
+    private void handleMmtelCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
+        boolean isChanged = mCapabilityInfo.updateMmtelCapabilitiesChanged(capabilities);
+        logi("MMTel capabilities status changed: isChanged=" + isChanged);
+        if (isChanged) {
+            mHandler.sendTriggeringPublishMessage(
+                    PublishController.PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE);
+        }
+    }
+
+    /*
+     * This method is called when RCS is registered.
+     */
+    private void handleImsRcsRegistered(ImsRegistrationAttributes attr) {
+        if (mCapabilityInfo.updateImsRcsRegistered(attr)) {
+            mHandler.sendTriggeringPublishMessage(PublishController.PUBLISH_TRIGGER_RCS_REGISTERED);
+        }
+    }
+
+    /*
+     * This method is called when RCS is unregistered.
+     */
+    private void handleImsRcsUnregistered() {
+        if (mCapabilityInfo.updateImsRcsUnregistered()) {
+            mHandler.sendTriggeringPublishMessage(
+                    PublishController.PUBLISH_TRIGGER_RCS_UNREGISTERED);
+        }
+    }
+
+    /*
+     * This method is called when the provisioning is changed
+     */
+    private void handleProvisioningChanged() {
+        mHandler.sendTriggeringPublishMessage(
+                PublishController.PUBLISH_TRIGGER_PROVISIONING_CHANGE);
+    }
+
+    /*
+     * Update the publish throttle.
+     */
+    private void handlePublishThrottleChanged(int value) {
+        mCallback.updatePublishThrottle(value);
+    }
+
+    @VisibleForTesting
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    public void setImsMmTelManagerFactory(ImsMmTelManagerFactory factory) {
+        mImsMmTelManagerFactory = factory;
+    }
+
+    @VisibleForTesting
+    public void setImsRcsManagerFactory(ImsRcsManagerFactory factory) {
+        mImsRcsManagerFactory = factory;
+    }
+
+    @VisibleForTesting
+    public void setProvisioningMgrFactory(ProvisioningManagerFactory factory) {
+        mProvisioningMgrFactory = factory;
+    }
+
+    @VisibleForTesting
+    public void setImsCallbackRegistered(boolean registered) {
+        mIsImsCallbackRegistered = registered;
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[D] " + log);
+    }
+
+    private void logi(String log) {
+        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[W] " + log);
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("DeviceCapListener" + "[subId: " + mSubId + "]:");
+        pw.increaseIndent();
+
+        mCapabilityInfo.dump(pw);
+
+        pw.println("Log:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        pw.println("---");
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java
new file mode 100644
index 0000000..eac31a7
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java
@@ -0,0 +1,224 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+
+import com.android.ims.rcs.uce.ControllerBase;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.Set;
+
+/**
+ * The interface related to the PUBLISH request.
+ */
+public interface PublishController extends ControllerBase {
+
+    /** Publish is triggered by the ImsService */
+    int PUBLISH_TRIGGER_SERVICE = 1;
+
+    /** Publish trigger type: retry */
+    int PUBLISH_TRIGGER_RETRY = 2;
+
+    /** Publish trigger type: TTY preferred changes */
+    int PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE = 3;
+
+    /** Publish trigger type: Airplane mode changes */
+    int PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE = 4;
+
+    /** Publish trigger type: Mobile data changes */
+    int PUBLISH_TRIGGER_MOBILE_DATA_CHANGE = 5;
+
+    /** Publish trigger type: VT setting changes */
+    int PUBLISH_TRIGGER_VT_SETTING_CHANGE = 6;
+
+    /** Publish trigger type: MMTEL registered */
+    int PUBLISH_TRIGGER_MMTEL_REGISTERED = 7;
+
+    /** Publish trigger type: MMTEL unregistered */
+    int PUBLISH_TRIGGER_MMTEL_UNREGISTERED = 8;
+
+    /** Publish trigger type: MMTEL capability changes */
+    int PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE = 9;
+
+    /** Publish trigger type: RCS registered */
+    int PUBLISH_TRIGGER_RCS_REGISTERED = 10;
+
+    /** Publish trigger type: RCS unregistered */
+    int PUBLISH_TRIGGER_RCS_UNREGISTERED = 11;
+
+    /** Publish trigger type: provisioning changes */
+    int PUBLISH_TRIGGER_PROVISIONING_CHANGE = 12;
+
+    /**The caps have been overridden for a test*/
+    int PUBLISH_TRIGGER_OVERRIDE_CAPS = 13;
+
+    /** The Carrier Config for the subscription has Changed **/
+    int PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED = 14;
+
+    @IntDef(value = {
+            PUBLISH_TRIGGER_SERVICE,
+            PUBLISH_TRIGGER_RETRY,
+            PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE,
+            PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE,
+            PUBLISH_TRIGGER_MOBILE_DATA_CHANGE,
+            PUBLISH_TRIGGER_VT_SETTING_CHANGE,
+            PUBLISH_TRIGGER_MMTEL_REGISTERED,
+            PUBLISH_TRIGGER_MMTEL_UNREGISTERED,
+            PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE,
+            PUBLISH_TRIGGER_RCS_REGISTERED,
+            PUBLISH_TRIGGER_RCS_UNREGISTERED,
+            PUBLISH_TRIGGER_PROVISIONING_CHANGE,
+            PUBLISH_TRIGGER_OVERRIDE_CAPS,
+            PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED
+    }, prefix="PUBLISH_TRIGGER_")
+    @Retention(RetentionPolicy.SOURCE)
+    @interface PublishTriggerType {}
+
+    /**
+     * Receive the callback from the sub-components which interact with PublishController.
+     */
+    interface PublishControllerCallback {
+        /**
+         * Request publish from local.
+         */
+        void requestPublishFromInternal(@PublishTriggerType int type);
+
+        /**
+         * Receive the command error callback of the request from ImsService.
+         */
+        void onRequestCommandError(PublishRequestResponse requestResponse);
+
+        /**
+         * Receive the network response callback fo the request from ImsService.
+         */
+        void onRequestNetworkResp(PublishRequestResponse requestResponse);
+
+        /**
+         * Set the timer to cancel the request. This timer is to prevent taking too long for
+         * waiting the response callback.
+         */
+        void setupRequestCanceledTimer(long taskId, long delay);
+
+        /**
+         * Clear the request canceled timer. This api will be called if the request is finished.
+         */
+        void clearRequestCanceledTimer();
+
+        /**
+         * Update the publish request result.
+         */
+        void updatePublishRequestResult(int publishState, Instant updatedTimestamp, String pidfXml);
+
+        /**
+         * Update the value of the publish throttle.
+         */
+        void updatePublishThrottle(int value);
+
+        /**
+         * Update the device state with the publish request result.
+         */
+        void refreshDeviceState(int SipCode, String reason);
+    }
+
+    /**
+     * Add new feature tags to the Set used to calculate the capabilities in PUBLISH.
+     * <p>
+     * Used for testing ONLY.
+     * @return the new capabilities that will be used for PUBLISH.
+     */
+    RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags);
+
+    /**
+     * Remove existing feature tags to the Set used to calculate the capabilities in PUBLISH.
+     * <p>
+     * Used for testing ONLY.
+     * @return the new capabilities that will be used for PUBLISH.
+     */
+    RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags);
+
+    /**
+     * Clear all overrides in the Set used to calculate the capabilities in PUBLISH.
+     * <p>
+     * Used for testing ONLY.
+     * @return the new capabilities that will be used for PUBLISH.
+     */
+    RcsContactUceCapability clearRegistrationOverrideCapabilities();
+
+    /**
+     * @return latest RcsContactUceCapability instance that will be used for PUBLISH.
+     */
+    RcsContactUceCapability getLatestRcsContactUceCapability();
+
+    /**
+     * Retrieve the RCS UCE Publish state.
+     */
+    @PublishState int getUcePublishState();
+
+    /**
+     * @return the last PIDF XML used for publish or {@code null} if the device is not published.
+     */
+    String getLastPidfXml();
+
+    /**
+     * Notify that the device's capabilities have been unpublished from the network.
+     */
+    void onUnpublish();
+
+    /**
+     * Retrieve the device's capabilities.
+     */
+    RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism);
+
+    /**
+     * Publish the device's capabilities to the Presence server.
+     */
+    void requestPublishCapabilitiesFromService(int triggerType);
+
+    /**
+     * Register a {@link PublishStateCallback} to listen to the published state changed.
+     */
+    void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c);
+
+    /**
+     * Removes an existing {@link PublishStateCallback}.
+     */
+    void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c);
+
+    /**
+     * Setup the timer to reset the device state.
+     */
+    void setupResetDeviceStateTimer(long resetAfterSec);
+
+    /**
+     * Clear the reset device state timer.
+     */
+    void clearResetDeviceStateTimer();
+
+    /**
+     * Dump the state of this PublishController to the printWriter.
+     */
+    void dump(PrintWriter printWriter);
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java
new file mode 100644
index 0000000..21baaea
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java
@@ -0,0 +1,971 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities.RcsImsCapabilityFlag;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The implementation of PublishController.
+ */
+public class PublishControllerImpl implements PublishController {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishController";
+
+    /**
+     * Used to inject PublishProcessor instances for testing.
+     */
+    @VisibleForTesting
+    public interface PublishProcessorFactory {
+        PublishProcessor createPublishProcessor(Context context, int subId,
+                DeviceCapabilityInfo capabilityInfo, PublishControllerCallback callback);
+    }
+
+    /**
+     * Used to inject DeviceCapabilityListener instances for testing.
+     */
+    @VisibleForTesting
+    public interface DeviceCapListenerFactory {
+        DeviceCapabilityListener createDeviceCapListener(Context context, int subId,
+                DeviceCapabilityInfo capInfo, PublishControllerCallback callback);
+    }
+
+    private final int mSubId;
+    private final Context mContext;
+    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+    private PublishHandler mPublishHandler;
+    private volatile boolean mIsDestroyedFlag;
+    private volatile boolean mReceivePublishFromService;
+    private volatile RcsFeatureManager mRcsFeatureManager;
+    private final UceControllerCallback mUceCtrlCallback;
+
+    // The device publish state
+    private @PublishState int mPublishState;
+    // The timestamp of updating the publish state
+    private Instant mPublishStateUpdatedTime = Instant.now();
+    // The last PIDF XML used in the publish
+    private String mPidfXml;
+
+    // The callbacks to notify publish state changed.
+    private RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
+
+    private final Object mPublishStateLock = new Object();
+
+    // The information of the device's capabilities.
+    private DeviceCapabilityInfo mDeviceCapabilityInfo;
+
+    // The processor of publishing device's capabilities.
+    private PublishProcessor mPublishProcessor;
+    private PublishProcessorFactory mPublishProcessorFactory = (context, subId, capInfo, callback)
+            -> new PublishProcessor(context, subId, capInfo, callback);
+
+    // The listener to listen to the device's capabilities changed.
+    private DeviceCapabilityListener mDeviceCapListener;
+    private DeviceCapListenerFactory mDeviceCapListenerFactory = (context, subId, capInfo, callback)
+            -> new DeviceCapabilityListener(context, subId, capInfo, callback);
+
+    // Listen to the RCS availability status changed.
+    private final IImsCapabilityCallback mRcsCapabilitiesCallback =
+            new IImsCapabilityCallback.Stub() {
+        @Override
+        public void onQueryCapabilityConfiguration(
+                int resultCapability, int resultRadioTech, boolean enabled) {
+        }
+        @Override
+        public void onCapabilitiesStatusChanged(@RcsImsCapabilityFlag int capabilities) {
+            logd("onCapabilitiesStatusChanged: " + capabilities);
+            mPublishHandler.sendRcsCapabilitiesStatusChangedMsg(capabilities);
+        }
+        @Override
+        public void onChangeCapabilityConfigurationError(int capability, int radioTech,
+                int reason) {
+        }
+    };
+
+    public PublishControllerImpl(Context context, int subId, UceControllerCallback callback,
+            Looper looper) {
+        mSubId = subId;
+        mContext = context;
+        mUceCtrlCallback = callback;
+        logi("create");
+        initPublishController(looper);
+    }
+
+    @VisibleForTesting
+    public PublishControllerImpl(Context context, int subId, UceControllerCallback c,
+            Looper looper, DeviceCapListenerFactory deviceCapFactory,
+            PublishProcessorFactory processorFactory) {
+        mSubId = subId;
+        mContext = context;
+        mUceCtrlCallback = c;
+        mDeviceCapListenerFactory = deviceCapFactory;
+        mPublishProcessorFactory = processorFactory;
+        initPublishController(looper);
+    }
+
+    private void initPublishController(Looper looper) {
+        mPublishState = RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
+        mPublishStateCallbacks = new RemoteCallbackList<>();
+        mPublishHandler = new PublishHandler(this, looper);
+
+        String[] serviceDescFeatureTagMap = getCarrierServiceDescriptionFeatureTagMap();
+        mDeviceCapabilityInfo = new DeviceCapabilityInfo(mSubId, serviceDescFeatureTagMap);
+
+        initPublishProcessor();
+        initDeviceCapabilitiesListener();
+
+        // Turn on the listener to listen to the device changes.
+        mDeviceCapListener.initialize();
+    }
+
+    private void initPublishProcessor() {
+        mPublishProcessor = mPublishProcessorFactory.createPublishProcessor(mContext, mSubId,
+                mDeviceCapabilityInfo, mPublishControllerCallback);
+    }
+
+    private void initDeviceCapabilitiesListener() {
+        mDeviceCapListener = mDeviceCapListenerFactory.createDeviceCapListener(mContext, mSubId,
+                mDeviceCapabilityInfo, mPublishControllerCallback);
+    }
+
+    @Override
+    public void onRcsConnected(RcsFeatureManager manager) {
+        logd("onRcsConnected");
+        mPublishHandler.sendRcsConnectedMsg(manager);
+    }
+
+    @Override
+    public void onRcsDisconnected() {
+        logd("onRcsDisconnected");
+        mPublishHandler.sendRcsDisconnectedMsg();
+    }
+
+    @Override
+    public void onDestroy() {
+        logi("onDestroy");
+        mPublishHandler.sendDestroyedMsg();
+    }
+
+    @Override
+    public void onCarrierConfigChanged() {
+        logi("onCarrierConfigChanged");
+        mPublishHandler.sendCarrierConfigChangedMsg();
+    }
+
+    @Override
+    public int getUcePublishState() {
+        synchronized (mPublishStateLock) {
+            return (!mIsDestroyedFlag) ? mPublishState : RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+        }
+    }
+
+    @Override
+    public RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags) {
+        if (mDeviceCapabilityInfo.addRegistrationOverrideCapabilities(featureTags)) {
+            mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
+        }
+        return mDeviceCapabilityInfo.getDeviceCapabilities(
+                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+    }
+
+    @Override
+    public RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags) {
+        if (mDeviceCapabilityInfo.removeRegistrationOverrideCapabilities(featureTags)) {
+            mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
+        }
+        return mDeviceCapabilityInfo.getDeviceCapabilities(
+                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+    }
+
+    @Override
+    public RcsContactUceCapability clearRegistrationOverrideCapabilities() {
+        if (mDeviceCapabilityInfo.clearRegistrationOverrideCapabilities()) {
+            mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
+        }
+        return mDeviceCapabilityInfo.getDeviceCapabilities(
+                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+    }
+
+    @Override
+    public RcsContactUceCapability getLatestRcsContactUceCapability() {
+        return mDeviceCapabilityInfo.getDeviceCapabilities(
+                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+    }
+
+    @Override
+    public String getLastPidfXml() {
+        return mPidfXml;
+    }
+
+    /**
+     * Register a {@link PublishStateCallback} to listen to the published state changed.
+     */
+    @Override
+    public void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+        synchronized (mPublishStateLock) {
+            if (mIsDestroyedFlag) return;
+            mPublishStateCallbacks.register(c);
+            logd("registerPublishStateCallback: size="
+                    + mPublishStateCallbacks.getRegisteredCallbackCount());
+        }
+        // Notify the current publish state
+        mPublishHandler.sendNotifyCurrentPublishStateMessage(c);
+    }
+
+    /**
+     * Removes an existing {@link PublishStateCallback}.
+     */
+    @Override
+    public void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+        synchronized (mPublishStateLock) {
+            if (mIsDestroyedFlag) return;
+            mPublishStateCallbacks.unregister(c);
+        }
+    }
+
+    @Override
+    public void setupResetDeviceStateTimer(long resetAfterSec) {
+        logd("setupResetDeviceStateTimer: resetAfterSec=" + resetAfterSec);
+        mPublishHandler.sendResetDeviceStateTimerMessage(resetAfterSec);
+    }
+
+    @Override
+    public void clearResetDeviceStateTimer() {
+        logd("clearResetDeviceStateTimer");
+        mPublishHandler.clearResetDeviceStateTimer();
+    }
+
+    // Clear all the publish state callbacks since the publish controller instance is destroyed.
+    private void clearPublishStateCallbacks() {
+        synchronized (mPublishStateLock) {
+            logd("clearPublishStateCallbacks");
+            final int lastIndex = mPublishStateCallbacks.getRegisteredCallbackCount() - 1;
+            for (int index = lastIndex; index >= 0; index--) {
+                IRcsUcePublishStateCallback callback =
+                        mPublishStateCallbacks.getRegisteredCallbackItem(index);
+                mPublishStateCallbacks.unregister(callback);
+            }
+        }
+    }
+
+    /**
+     * Notify that the device's capabilities has been unpublished from the network.
+     */
+    @Override
+    public void onUnpublish() {
+        logd("onUnpublish");
+        if (mIsDestroyedFlag) return;
+        mPublishHandler.sendPublishStateChangedMessage(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED,
+                Instant.now(), null /*pidfXml*/);
+    }
+
+    @Override
+    public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
+        return mDeviceCapabilityInfo.getDeviceCapabilities(mechanism, mContext);
+    }
+
+    // The local publish request from the sub-components which interact with PublishController.
+    private final PublishControllerCallback mPublishControllerCallback =
+            new PublishControllerCallback() {
+                @Override
+                public void requestPublishFromInternal(@PublishTriggerType int type) {
+                    logd("requestPublishFromInternal: type=" + type);
+                    mPublishHandler.sendPublishMessage(type);
+                }
+
+                @Override
+                public void onRequestCommandError(PublishRequestResponse requestResponse) {
+                    logd("onRequestCommandError: taskId=" + requestResponse.getTaskId()
+                            + ", time=" + requestResponse.getResponseTimestamp());
+                    mPublishHandler.sendRequestCommandErrorMessage(requestResponse);
+                }
+
+                @Override
+                public void onRequestNetworkResp(PublishRequestResponse requestResponse) {
+                    logd("onRequestNetworkResp: taskId=" + requestResponse.getTaskId()
+                            + ", time=" + requestResponse.getResponseTimestamp());
+                    mPublishHandler.sendRequestNetworkRespMessage(requestResponse);
+                }
+
+                @Override
+                public void setupRequestCanceledTimer(long taskId, long delay) {
+                    logd("setupRequestCanceledTimer: taskId=" + taskId + ", delay=" + delay);
+                    mPublishHandler.sendRequestCanceledTimerMessage(taskId, delay);
+                }
+
+                @Override
+                public void clearRequestCanceledTimer() {
+                    logd("clearRequestCanceledTimer");
+                    mPublishHandler.clearRequestCanceledTimer();
+                }
+
+                @Override
+                public void updatePublishRequestResult(@PublishState int state,
+                        Instant updatedTime, String pidfXml) {
+                    logd("updatePublishRequestResult: " + state + ", time=" + updatedTime);
+                    mPublishHandler.sendPublishStateChangedMessage(state, updatedTime, pidfXml);
+                }
+
+                @Override
+                public void updatePublishThrottle(int value) {
+                    logd("updatePublishThrottle: value=" + value);
+                    mPublishProcessor.updatePublishThrottle(value);
+                }
+
+                @Override
+                public void refreshDeviceState(int sipCode, String reason) {
+                    mUceCtrlCallback.refreshDeviceState(sipCode, reason);
+                }
+            };
+
+    /**
+     * Publish the device's capabilities to the network. This method is triggered by ImsService.
+     */
+    @Override
+    public void requestPublishCapabilitiesFromService(int triggerType) {
+        logi("Receive the publish request from service: service trigger type=" + triggerType);
+        mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_SERVICE);
+    }
+
+    private static class PublishHandler extends Handler {
+        private static final int MSG_RCS_CONNECTED = 1;
+        private static final int MSG_RCS_DISCONNECTED = 2;
+        private static final int MSG_DESTROYED = 3;
+        private static final int MSG_CARRIER_CONFIG_CHANGED = 4;
+        private static final int MSG_RCS_CAPABILITIES_CHANGED = 5;
+        private static final int MSG_PUBLISH_STATE_CHANGED = 6;
+        private static final int MSG_NOTIFY_CURRENT_PUBLISH_STATE = 7;
+        private static final int MSG_REQUEST_PUBLISH = 8;
+        private static final int MSG_REQUEST_CMD_ERROR = 9;
+        private static final int MSG_REQUEST_NETWORK_RESPONSE = 10;
+        private static final int MSG_REQUEST_CANCELED = 11;
+        private static final int MSG_RESET_DEVICE_STATE = 12;
+
+        private final WeakReference<PublishControllerImpl> mPublishControllerRef;
+
+        public PublishHandler(PublishControllerImpl publishController, Looper looper) {
+            super(looper);
+            mPublishControllerRef = new WeakReference<>(publishController);
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            publishCtrl.logd("handleMessage: " + EVENT_DESCRIPTION.get(message.what));
+            switch (message.what) {
+                case MSG_RCS_CONNECTED: {
+                    SomeArgs args = (SomeArgs) message.obj;
+                    RcsFeatureManager manager = (RcsFeatureManager) args.arg1;
+                    args.recycle();
+                    publishCtrl.handleRcsConnectedMessage(manager);
+                    break;
+                }
+                case MSG_RCS_DISCONNECTED:
+                    publishCtrl.handleRcsDisconnectedMessage();
+                    break;
+
+                case MSG_DESTROYED:
+                    publishCtrl.handleDestroyedMessage();
+                    break;
+
+                case MSG_CARRIER_CONFIG_CHANGED:
+                    publishCtrl.handleCarrierConfigChangedMessage();
+                    break;
+
+                case MSG_RCS_CAPABILITIES_CHANGED:
+                    int RcsCapabilities = message.arg1;
+                    publishCtrl.handleRcsCapabilitiesChangedMessage(RcsCapabilities);
+                    break;
+
+                case MSG_PUBLISH_STATE_CHANGED: {
+                    SomeArgs args = (SomeArgs) message.obj;
+                    int newPublishState = (Integer) args.arg1;
+                    Instant updatedTimestamp = (Instant) args.arg2;
+                    String pidfXml = (String) args.arg3;
+                    args.recycle();
+                    publishCtrl.handlePublishStateChangedMessage(newPublishState, updatedTimestamp,
+                            pidfXml);
+                    break;
+                }
+                case MSG_NOTIFY_CURRENT_PUBLISH_STATE:
+                    IRcsUcePublishStateCallback c = (IRcsUcePublishStateCallback) message.obj;
+                    publishCtrl.handleNotifyCurrentPublishStateMessage(c);
+                    break;
+
+                case MSG_REQUEST_PUBLISH:
+                    int type = message.arg1;
+                    publishCtrl.handleRequestPublishMessage(type);
+                    break;
+
+                case MSG_REQUEST_CMD_ERROR:
+                    PublishRequestResponse cmdErrorResponse = (PublishRequestResponse) message.obj;
+                    publishCtrl.mPublishProcessor.onCommandError(cmdErrorResponse);
+                    break;
+
+                case MSG_REQUEST_NETWORK_RESPONSE:
+                    PublishRequestResponse networkResponse = (PublishRequestResponse) message.obj;
+                    publishCtrl.mPublishProcessor.onNetworkResponse(networkResponse);
+                    break;
+
+                case MSG_REQUEST_CANCELED:
+                    long taskId = (Long) message.obj;
+                    publishCtrl.handleRequestCanceledMessage(taskId);
+                    break;
+
+                case MSG_RESET_DEVICE_STATE:
+                    publishCtrl.handleResetDeviceStateMessage();
+                    break;
+            }
+        }
+
+        /**
+         * Remove all the messages from the handler.
+         */
+        public void onDestroy() {
+            removeCallbacksAndMessages(null);
+        }
+
+        public void sendRcsConnectedMsg(RcsFeatureManager manager) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = manager;
+            Message message = obtainMessage();
+            message.what = MSG_RCS_CONNECTED;
+            message.obj = args;
+            sendMessage(message);
+        }
+
+        public void sendRcsDisconnectedMsg() {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_RCS_DISCONNECTED;
+            sendMessage(message);
+        }
+
+        public void sendDestroyedMsg() {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_DESTROYED;
+            sendMessage(message);
+        }
+
+        public void sendCarrierConfigChangedMsg() {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_CARRIER_CONFIG_CHANGED;
+            sendMessage(message);
+        }
+
+        public void sendRcsCapabilitiesStatusChangedMsg(@RcsImsCapabilityFlag int capabilities) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_RCS_CAPABILITIES_CHANGED;
+            message.arg1 = capabilities;
+            sendMessage(message);
+        }
+
+        /**
+         * Send the message to notify the publish state is changed.
+         */
+        public void sendPublishStateChangedMessage(@PublishState int publishState,
+                @NonNull Instant updatedTimestamp, String pidfXml) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = publishState;
+            args.arg2 = updatedTimestamp;
+            args.arg3 = pidfXml;
+            Message message = obtainMessage();
+            message.what = MSG_PUBLISH_STATE_CHANGED;
+            message.obj = args;
+            sendMessage(message);
+        }
+
+        /**
+         * Send the message to notify the new added callback of the latest publish state.
+         */
+        public void sendNotifyCurrentPublishStateMessage(
+                IRcsUcePublishStateCallback callback) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_NOTIFY_CURRENT_PUBLISH_STATE;
+            message.obj = callback;
+            sendMessage(message);
+        }
+
+        public void sendPublishMessage(@PublishTriggerType int type) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_REQUEST_PUBLISH;
+            message.arg1 = type;
+            sendMessage(message);
+        }
+
+        public void sendPublishMessage(@PublishTriggerType int type, long delay) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) return;
+            if (publishCtrl.mIsDestroyedFlag) return;
+
+            Message message = obtainMessage();
+            message.what = MSG_REQUEST_PUBLISH;
+            message.arg1 = type;
+            sendMessageDelayed(message, delay);
+        }
+
+        public void sendRequestCommandErrorMessage(PublishRequestResponse response) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            Message message = obtainMessage();
+            message.what = MSG_REQUEST_CMD_ERROR;
+            message.obj = response;
+            sendMessage(message);
+        }
+
+        public void sendRequestNetworkRespMessage(PublishRequestResponse response) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            Message message = obtainMessage();
+            message.what = MSG_REQUEST_NETWORK_RESPONSE;
+            message.obj = response;
+            sendMessage(message);
+        }
+
+        public void sendRequestCanceledTimerMessage(long taskId, long delay) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            removeMessages(MSG_REQUEST_CANCELED, (Long) taskId);
+
+            Message message = obtainMessage();
+            message.what = MSG_REQUEST_CANCELED;
+            message.obj = (Long) taskId;
+            sendMessageDelayed(message, delay);
+        }
+
+        public void clearRequestCanceledTimer() {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            removeMessages(MSG_REQUEST_CANCELED);
+        }
+
+        public void sendResetDeviceStateTimerMessage(long resetAfterSec) {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            // Remove old timer and setup the new timer.
+            removeMessages(MSG_RESET_DEVICE_STATE);
+            Message message = obtainMessage();
+            message.what = MSG_RESET_DEVICE_STATE;
+            sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(resetAfterSec));
+        }
+
+        public void clearResetDeviceStateTimer() {
+            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+            if (publishCtrl == null) {
+                return;
+            }
+            if (publishCtrl.mIsDestroyedFlag) return;
+            removeMessages(MSG_RESET_DEVICE_STATE);
+        }
+
+        private static Map<Integer, String> EVENT_DESCRIPTION = new HashMap<>();
+        static {
+            EVENT_DESCRIPTION.put(MSG_RCS_CONNECTED, "RCS_CONNECTED");
+            EVENT_DESCRIPTION.put(MSG_RCS_DISCONNECTED, "RCS_DISCONNECTED");
+            EVENT_DESCRIPTION.put(MSG_DESTROYED, "DESTROYED");
+            EVENT_DESCRIPTION.put(MSG_CARRIER_CONFIG_CHANGED, "CARRIER_CONFIG_CHANGED");
+            EVENT_DESCRIPTION.put(MSG_RCS_CAPABILITIES_CHANGED, "RCS_CAPABILITIES_CHANGED");
+            EVENT_DESCRIPTION.put(MSG_PUBLISH_STATE_CHANGED, "PUBLISH_STATE_CHANGED");
+            EVENT_DESCRIPTION.put(MSG_NOTIFY_CURRENT_PUBLISH_STATE, "NOTIFY_PUBLISH_STATE");
+            EVENT_DESCRIPTION.put(MSG_REQUEST_PUBLISH, "REQUEST_PUBLISH");
+            EVENT_DESCRIPTION.put(MSG_REQUEST_CMD_ERROR, "REQUEST_CMD_ERROR");
+            EVENT_DESCRIPTION.put(MSG_REQUEST_NETWORK_RESPONSE, "REQUEST_NETWORK_RESPONSE");
+            EVENT_DESCRIPTION.put(MSG_REQUEST_CANCELED, "REQUEST_CANCELED");
+            EVENT_DESCRIPTION.put(MSG_RESET_DEVICE_STATE, "RESET_DEVICE_STATE");
+        }
+    }
+
+    /**
+     * Check if the PUBLISH request is allowed.
+     */
+    private boolean isPublishRequestAllowed() {
+        // The PUBLISH request requires that the RCS PRESENCE is capable.
+        if (!mDeviceCapabilityInfo.isPresenceCapable()) {
+            logd("isPublishRequestAllowed: capability presence uce is not enabled.");
+            return false;
+        }
+
+        // The first PUBLISH request is required to be triggered from the service.
+        if (!mReceivePublishFromService) {
+            logd("isPublishRequestAllowed: Have not received the first PUBLISH from the service.");
+            return false;
+        }
+
+        // Check whether the device state is not allowed to execute the PUBLISH request.
+        DeviceStateResult deviceState = mUceCtrlCallback.getDeviceState();
+        if (deviceState.isRequestForbidden()) {
+            logd("isPublishRequestAllowed: The device state is disallowed. "
+                    + deviceState.getDeviceState());
+            return false;
+        }
+
+        // Check whether there is already a publish request running or not. When the running
+        // request is finished and there is a pending request, it will send a new request.
+        if (mPublishProcessor.isPublishingNow()) {
+            logd("isPublishRequestAllowed: There is already a publish request running now.");
+            return false;
+        }
+        return true;
+    }
+
+    private void handleRcsConnectedMessage(RcsFeatureManager manager) {
+        if (mIsDestroyedFlag) return;
+        mRcsFeatureManager = manager;
+        mDeviceCapListener.onRcsConnected();
+        mPublishProcessor.onRcsConnected(manager);
+        registerRcsAvailabilityChanged(manager);
+    }
+
+    private void handleRcsDisconnectedMessage() {
+        if (mIsDestroyedFlag) return;
+        mRcsFeatureManager = null;
+        onUnpublish();
+        mDeviceCapabilityInfo.updatePresenceCapable(false);
+        mDeviceCapListener.onRcsDisconnected();
+        mPublishProcessor.onRcsDisconnected();
+    }
+
+    private void handleDestroyedMessage() {
+        mIsDestroyedFlag = true;
+        mDeviceCapabilityInfo.updatePresenceCapable(false);
+        unregisterRcsAvailabilityChanged();
+        mDeviceCapListener.onDestroy();   // It will turn off the listener automatically.
+        mPublishHandler.onDestroy();
+        mPublishProcessor.onDestroy();
+        synchronized (mPublishStateLock) {
+            clearPublishStateCallbacks();
+        }
+    }
+
+    /*
+     * Register the availability callback to receive the RCS capabilities change. This method is
+     * called when the RCS is connected.
+     */
+    private void registerRcsAvailabilityChanged(RcsFeatureManager manager) {
+        try {
+            manager.registerRcsAvailabilityCallback(mSubId, mRcsCapabilitiesCallback);
+        } catch (ImsException e) {
+            logw("registerRcsAvailabilityChanged exception " + e);
+        }
+    }
+
+    /*
+     * Unregister the availability callback. This method is called when the PublishController
+     * instance is destroyed.
+     */
+    private void unregisterRcsAvailabilityChanged() {
+        RcsFeatureManager manager = mRcsFeatureManager;
+        if (manager == null) return;
+        try {
+            manager.unregisterRcsAvailabilityCallback(mSubId, mRcsCapabilitiesCallback);
+        } catch (Exception e) {
+            // Do not handle the exception
+        }
+    }
+
+    private void handleCarrierConfigChangedMessage() {
+        if (mIsDestroyedFlag) return;
+        String[] newMap = getCarrierServiceDescriptionFeatureTagMap();
+        if (mDeviceCapabilityInfo.updateCapabilityRegistrationTrackerMap(newMap)) {
+            mPublishHandler.sendPublishMessage(
+                    PublishController.PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED);
+        }
+    }
+
+    private String[] getCarrierServiceDescriptionFeatureTagMap() {
+        CarrierConfigManager manager = mContext.getSystemService(CarrierConfigManager.class);
+        PersistableBundle bundle = manager != null ? manager.getConfigForSubId(mSubId) :
+                CarrierConfigManager.getDefaultConfig();
+        return bundle.getStringArray(CarrierConfigManager.Ims.
+                KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY);
+    }
+
+    private void handleRcsCapabilitiesChangedMessage(int capabilities) {
+        logd("handleRcsCapabilitiesChangedMessage: " + capabilities);
+        if (mIsDestroyedFlag) return;
+        RcsImsCapabilities RcsImsCapabilities = new RcsImsCapabilities(capabilities);
+        mDeviceCapabilityInfo.updatePresenceCapable(
+                RcsImsCapabilities.isCapable(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE));
+        // Trigger a publish request if the RCS capabilities presence is enabled.
+        if (mDeviceCapabilityInfo.isPresenceCapable()) {
+            mPublishProcessor.checkAndSendPendingRequest();
+        }
+    }
+
+    /**
+     * Update the publish state and notify the publish state callback if the new state is different
+     * from original state.
+     */
+    private void handlePublishStateChangedMessage(@PublishState int newPublishState,
+            Instant updatedTimestamp, String pidfXml) {
+        synchronized (mPublishStateLock) {
+            if (mIsDestroyedFlag) return;
+            // Check if the time of the given publish state is not earlier than existing time.
+            if (updatedTimestamp == null || !updatedTimestamp.isAfter(mPublishStateUpdatedTime)) {
+                logd("handlePublishStateChangedMessage: updatedTimestamp is not allowed: "
+                        + mPublishStateUpdatedTime + " to " + updatedTimestamp
+                        + ", publishState=" + newPublishState);
+                return;
+            }
+            logd("publish state changes from " + mPublishState + " to " + newPublishState +
+                    ", time=" + updatedTimestamp);
+            if (mPublishState == newPublishState) return;
+            mPublishState = newPublishState;
+            mPublishStateUpdatedTime = updatedTimestamp;
+            mPidfXml = pidfXml;
+        }
+
+        // Trigger the publish state changed in handler thread since it may take time.
+        logd("Notify publish state changed: " + mPublishState);
+        mPublishStateCallbacks.broadcast(c -> {
+            try {
+                c.onPublishStateChanged(mPublishState);
+            } catch (RemoteException e) {
+                logw("Notify publish state changed error: " + e);
+            }
+        });
+        logd("Notify publish state changed: completed");
+    }
+
+    private void handleNotifyCurrentPublishStateMessage(IRcsUcePublishStateCallback callback) {
+        if (mIsDestroyedFlag || callback == null) return;
+        try {
+            callback.onPublishStateChanged(getUcePublishState());
+        } catch (RemoteException e) {
+            logw("handleCurrentPublishStateUpdateMessage exception: " + e);
+        }
+    }
+
+    private void handleRequestPublishMessage(@PublishTriggerType int type) {
+        if (mIsDestroyedFlag) return;
+
+        logd("handleRequestPublishMessage: type=" + type);
+
+        // Set the PUBLISH FROM SERVICE flag and reset the device state if the PUBLISH request is
+        // triggered by the ImsService.
+        if (type == PublishController.PUBLISH_TRIGGER_SERVICE) {
+            // Set the flag
+            if (!mReceivePublishFromService) {
+                mReceivePublishFromService = true;
+            }
+            // Reset device state
+            DeviceStateResult deviceState = mUceCtrlCallback.getDeviceState();
+            if (deviceState.isRequestForbidden()) {
+                mUceCtrlCallback.resetDeviceState();
+            }
+        }
+
+        // Set the pending flag and return if the request is not allowed.
+        if (!isPublishRequestAllowed()) {
+            logd("handleRequestPublishMessage: SKIP. The request is not allowed. type=" + type);
+            mPublishProcessor.setPendingRequest(type);
+            return;
+        }
+
+        // Update the latest PUBLISH allowed time according to the given trigger type.
+        mPublishProcessor.updatePublishingAllowedTime(type);
+
+        // Get the publish request delay time. If the delay is not present, the first
+        // PUBLISH is not allowed to be executed; If the delay time is 0, it means that
+        // this request can be executed immediately.
+        Optional<Long> delay = mPublishProcessor.getPublishingDelayTime();
+        if (!delay.isPresent()) {
+            logd("handleRequestPublishMessage: SKIP. The delay is empty. type=" + type);
+            mPublishProcessor.setPendingRequest(type);
+            return;
+        }
+
+        logd("handleRequestPublishMessage: " + type + ", delay=" + delay.get());
+        if (delay.get() == 0L) {
+            mPublishProcessor.doPublish(type);
+        } else {
+            mPublishHandler.sendPublishMessage(type, delay.get());
+        }
+    }
+
+    private void handleRequestCanceledMessage(long taskId) {
+        if (mIsDestroyedFlag) return;
+        mPublishProcessor.cancelPublishRequest(taskId);
+    }
+
+    private void handleResetDeviceStateMessage() {
+        if(mIsDestroyedFlag) return;
+        mUceCtrlCallback.resetDeviceState();
+    }
+
+    @VisibleForTesting
+    public void setPublishStateCallback(RemoteCallbackList<IRcsUcePublishStateCallback> list) {
+        mPublishStateCallbacks = list;
+    }
+
+    @VisibleForTesting
+    public PublishHandler getPublishHandler() {
+        return mPublishHandler;
+    }
+
+    @VisibleForTesting
+    public IImsCapabilityCallback getRcsCapabilitiesCallback() {
+        return mRcsCapabilitiesCallback;
+    }
+
+    @VisibleForTesting
+    public PublishControllerCallback getPublishControllerCallback() {
+        return mPublishControllerCallback;
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[D] " + log);
+    }
+
+    private void logi(String log) {
+        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[I] " + log);
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+        mLocalLog.log("[W] " + log);
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+
+    @Override
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("PublishControllerImpl" + "[subId: " + mSubId + "]:");
+        pw.increaseIndent();
+
+        pw.print("isPresenceCapable=");
+        pw.println(mDeviceCapabilityInfo.isPresenceCapable());
+        pw.print("mPublishState=");
+        pw.print(mPublishState);
+        pw.print(" at time ");
+        pw.println(mPublishStateUpdatedTime);
+        pw.println("Last PIDF XML:");
+        pw.increaseIndent();
+        pw.println(mPidfXml);
+        pw.decreaseIndent();
+
+        if (mPublishProcessor != null) {
+            mPublishProcessor.dump(pw);
+        } else {
+            pw.println("mPublishProcessor is null");
+        }
+
+        pw.println();
+        mDeviceCapListener.dump(pw);
+
+        pw.println("Log:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        pw.println("---");
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java
new file mode 100644
index 0000000..abeb441
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java
@@ -0,0 +1,492 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParser;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * Send the publish request and handle the response of the publish request result.
+ */
+public class PublishProcessor {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishProcessor";
+
+    // The length of time waiting for the response callback.
+    private static final long RESPONSE_CALLBACK_WAITING_TIME = 60000L;
+
+    private final int mSubId;
+    private final Context mContext;
+    private volatile boolean mIsDestroyed;
+    private volatile RcsFeatureManager mRcsFeatureManager;
+
+    // Manage the state of the publish processor.
+    private PublishProcessorState mProcessorState;
+
+    // The information of the device's capabilities.
+    private final DeviceCapabilityInfo mDeviceCapabilities;
+
+    // The callback of the PublishController
+    private final PublishControllerCallback mPublishCtrlCallback;
+
+    // The lock of processing the pending request.
+    private final Object mPendingRequestLock = new Object();
+
+    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+
+    public PublishProcessor(Context context, int subId, DeviceCapabilityInfo capabilityInfo,
+            PublishControllerCallback publishCtrlCallback) {
+        mSubId = subId;
+        mContext = context;
+        mDeviceCapabilities = capabilityInfo;
+        mPublishCtrlCallback = publishCtrlCallback;
+        mProcessorState = new PublishProcessorState(subId);
+    }
+
+    /**
+     * The RcsFeature has been connected to the framework.
+     */
+    public void onRcsConnected(RcsFeatureManager featureManager) {
+        mLocalLog.log("onRcsConnected");
+        logi("onRcsConnected");
+        mRcsFeatureManager = featureManager;
+        // Check if there is a pending request.
+        checkAndSendPendingRequest();
+    }
+
+    /**
+     * The framework has lost the binding to the RcsFeature.
+     */
+    public void onRcsDisconnected() {
+        mLocalLog.log("onRcsDisconnected");
+        logi("onRcsDisconnected");
+        mRcsFeatureManager = null;
+        mProcessorState.onRcsDisconnected();
+    }
+
+    /**
+     * Set the destroy flag
+     */
+    public void onDestroy() {
+        mLocalLog.log("onDestroy");
+        logi("onDestroy");
+        mIsDestroyed = true;
+    }
+
+    /**
+     * Execute the publish request. This method is called by the handler of the PublishController.
+     * @param triggerType The type of triggering the publish request.
+     */
+    public void doPublish(@PublishTriggerType int triggerType) {
+        mProcessorState.setPublishingFlag(true);
+        if (!doPublishInternal(triggerType)) {
+            // Reset the publishing flag if the request cannot be sent to the IMS service.
+            mProcessorState.setPublishingFlag(false);
+        }
+    }
+    /**
+     * Execute the publish request internally.
+     * @param triggerType The type of triggering the publish request.
+     * @return true if the publish is sent to the IMS service successfully, false otherwise.
+     */
+    private boolean doPublishInternal(@PublishTriggerType int triggerType) {
+        if (mIsDestroyed) return false;
+
+        mLocalLog.log("doPublishInternal: trigger type=" + triggerType);
+        logi("doPublishInternal: trigger type=" + triggerType);
+
+        // Return if this request is not allowed to be executed.
+        if (!isRequestAllowed(triggerType)) {
+            mLocalLog.log("doPublishInternal: The request is not allowed.");
+            return false;
+        }
+
+        // Get the latest device's capabilities.
+        RcsContactUceCapability deviceCapability =
+                mDeviceCapabilities.getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE, mContext);
+        if (deviceCapability == null) {
+            logw("doPublishInternal: device capability is null");
+            return false;
+        }
+
+        // Convert the device's capabilities to pidf format.
+        String pidfXml = PidfParser.convertToPidf(deviceCapability);
+        if (TextUtils.isEmpty(pidfXml)) {
+            logw("doPublishInternal: pidfXml is empty");
+            return false;
+        }
+
+        // Set the pending request and return if RCS is not connected. When the RCS is connected
+        // afterward, it will send a new request if there's a pending request.
+        RcsFeatureManager featureManager = mRcsFeatureManager;
+        if (featureManager == null) {
+            logw("doPublishInternal: RCS is not connected.");
+            setPendingRequest(triggerType);
+            return false;
+        }
+
+        // Publish to the Presence server.
+        return publishCapabilities(featureManager, pidfXml);
+    }
+
+    /*
+     * According to the given trigger type, check whether the request is allowed to be executed or
+     * not.
+     */
+    private boolean isRequestAllowed(@PublishTriggerType int triggerType) {
+        // Check if the instance is destroyed.
+        if (mIsDestroyed) {
+            logd("isPublishAllowed: This instance is already destroyed");
+            return false;
+        }
+
+        // Check if it has provisioned. When the provisioning changes, a new publish request will
+        // be triggered.
+        if (!UceUtils.isEabProvisioned(mContext, mSubId)) {
+            logd("isPublishAllowed: NOT provisioned");
+            return false;
+        }
+
+        // Do not request publish if the IMS is not registered. When the IMS is registered
+        // afterward, a new publish request will be triggered.
+        if (!mDeviceCapabilities.isImsRegistered()) {
+            logd("isPublishAllowed: IMS is not registered");
+            return false;
+        }
+
+        // Skip this request if the PUBLISH is not allowed at current time. Resend the PUBLISH
+        // request and it will be triggered with an appropriate delay time.
+        if (!mProcessorState.isPublishAllowedAtThisTime()) {
+            logd("isPublishAllowed: Current time is not allowed, resend this request");
+            mPublishCtrlCallback.requestPublishFromInternal(triggerType);
+            return false;
+        }
+        return true;
+    }
+
+    // Publish the device capabilities with the given pidf.
+    private boolean publishCapabilities(@NonNull RcsFeatureManager featureManager,
+            @NonNull String pidfXml) {
+        PublishRequestResponse requestResponse = null;
+        try {
+            // Clear the pending flag because it is going to send the latest device's capabilities.
+            clearPendingRequest();
+
+            // Generate a unique taskId to track this request.
+            long taskId = mProcessorState.generatePublishTaskId();
+            requestResponse = new PublishRequestResponse(mPublishCtrlCallback, taskId, pidfXml);
+
+            mLocalLog.log("publish capabilities: taskId=" + taskId);
+            logi("publishCapabilities: taskId=" + taskId);
+
+            // request publication
+            featureManager.requestPublication(pidfXml, requestResponse.getResponseCallback());
+
+            // Send a request canceled timer to avoid waiting too long for the response callback.
+            mPublishCtrlCallback.setupRequestCanceledTimer(taskId, RESPONSE_CALLBACK_WAITING_TIME);
+            return true;
+        } catch (RemoteException e) {
+            mLocalLog.log("publish capability exception: " + e.getMessage());
+            logw("publishCapabilities: exception=" + e.getMessage());
+            // Exception occurred, end this request.
+            setRequestEnded(requestResponse);
+            checkAndSendPendingRequest();
+            return false;
+        }
+   }
+
+    /**
+     * Handle the command error callback of the publish request. This method is called by the
+     * handler of the PublishController.
+     */
+    public void onCommandError(PublishRequestResponse requestResponse) {
+        if (!checkRequestRespValid(requestResponse)) {
+            mLocalLog.log("Command error callback is invalid");
+            logw("onCommandError: request response is invalid");
+            setRequestEnded(requestResponse);
+            checkAndSendPendingRequest();
+            return;
+        }
+
+        mLocalLog.log("Receive command error code=" + requestResponse.getCmdErrorCode());
+        logd("onCommandError: " + requestResponse.toString());
+
+        if (requestResponse.needRetry() && !mProcessorState.isReachMaximumRetries()) {
+            handleRequestRespWithRetry(requestResponse);
+        } else {
+            handleRequestRespWithoutRetry(requestResponse);
+        }
+    }
+
+    /**
+     * Handle the network response callback of the publish request. This method is called by the
+     * handler of the PublishController.
+     */
+    public void onNetworkResponse(PublishRequestResponse requestResponse) {
+        if (!checkRequestRespValid(requestResponse)) {
+            mLocalLog.log("Network response callback is invalid");
+            logw("onNetworkResponse: request response is invalid");
+            setRequestEnded(requestResponse);
+            checkAndSendPendingRequest();
+            return;
+        }
+
+        mLocalLog.log("Receive network response code=" + requestResponse.getNetworkRespSipCode());
+        logd("onNetworkResponse: " + requestResponse.toString());
+
+        if (requestResponse.needRetry() && !mProcessorState.isReachMaximumRetries()) {
+            handleRequestRespWithRetry(requestResponse);
+        } else {
+            handleRequestRespWithoutRetry(requestResponse);
+        }
+    }
+
+    // Check if the request response callback is valid.
+    private boolean checkRequestRespValid(PublishRequestResponse requestResponse) {
+        if (requestResponse == null) {
+            logd("checkRequestRespValid: request response is null");
+            return false;
+        }
+
+        if (!mProcessorState.isPublishingNow()) {
+            logd("checkRequestRespValid: the request is finished");
+            return false;
+        }
+
+        // Abandon this response callback if the current taskId is different to the response
+        // callback taskId. This response callback is obsoleted.
+        long taskId = mProcessorState.getCurrentTaskId();
+        long responseTaskId = requestResponse.getTaskId();
+        if (taskId != responseTaskId) {
+            logd("checkRequestRespValid: invalid taskId! current taskId=" + taskId
+                    + ", response callback taskId=" + responseTaskId);
+            return false;
+        }
+
+        if (mIsDestroyed) {
+            logd("checkRequestRespValid: is already destroyed! taskId=" + taskId);
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * Handle the publishing request with retry. This method is called when it receives a failed
+     * request response and need to retry.
+     */
+    private void handleRequestRespWithRetry(PublishRequestResponse requestResponse) {
+        // Increase the retry count
+        mProcessorState.increaseRetryCount();
+
+        // Reset the pending flag because it is going to resend a request.
+        clearPendingRequest();
+
+        // Finish this request and resend a new publish request
+        setRequestEnded(requestResponse);
+        mPublishCtrlCallback.requestPublishFromInternal(PublishController.PUBLISH_TRIGGER_RETRY);
+    }
+
+    /*
+     * Handle the publishing request without retry. This method is called when it receives the
+     * request response and it does not need to retry.
+     */
+    private void handleRequestRespWithoutRetry(PublishRequestResponse requestResponse) {
+        Instant responseTime = requestResponse.getResponseTimestamp();
+
+        // Record the time when the request is successful and reset the retry count.
+        if (requestResponse.isRequestSuccess()) {
+            mProcessorState.setLastPublishedTime(responseTime);
+            mProcessorState.resetRetryCount();
+        }
+
+        // Update the publish state after the request has finished.
+        int publishState = requestResponse.getPublishState();
+        String pidfXml = requestResponse.getPidfXml();
+        mPublishCtrlCallback.updatePublishRequestResult(publishState, responseTime, pidfXml);
+
+        // Refresh the device state with the publish request result.
+        requestResponse.getResponseSipCode().ifPresent(sipCode -> {
+            String reason = requestResponse.getResponseReason().orElse("");
+            mPublishCtrlCallback.refreshDeviceState(sipCode, reason);
+        });
+
+        // Finish the request and check if there is pending request.
+        setRequestEnded(requestResponse);
+        checkAndSendPendingRequest();
+    }
+
+    /**
+     * Cancel the publishing request since it has token too long for waiting the response callback.
+     * This method is called by the handler of the PublishController.
+     */
+    public void cancelPublishRequest(long taskId) {
+        mLocalLog.log("cancel publish request: taskId=" + taskId);
+        logd("cancelPublishRequest: taskId=" + taskId);
+        setRequestEnded(null);
+        checkAndSendPendingRequest();
+    }
+
+    /*
+     * Finish the publishing request. This method is required to be called before the publishing
+     * request is finished.
+     */
+    private void setRequestEnded(PublishRequestResponse requestResponse) {
+        long taskId = -1L;
+        if (requestResponse != null) {
+            requestResponse.onDestroy();
+            taskId = requestResponse.getTaskId();
+        }
+        mProcessorState.setPublishingFlag(false);
+        mPublishCtrlCallback.clearRequestCanceledTimer();
+
+        mLocalLog.log("Set request ended: taskId=" + taskId);
+        logd("setRequestEnded: taskId=" + taskId);
+    }
+
+    /*
+     * Set the pending flag when it cannot be executed now.
+     */
+    public void setPendingRequest(@PublishTriggerType int triggerType) {
+        synchronized (mPendingRequestLock) {
+            mProcessorState.setPendingRequest(triggerType);
+        }
+    }
+
+    /**
+     * Check and trigger a new publish request if there is a pending request.
+     */
+    public void checkAndSendPendingRequest() {
+        synchronized (mPendingRequestLock) {
+            if (mIsDestroyed) return;
+            if (mProcessorState.hasPendingRequest()) {
+                // Retrieve the trigger type of the pending request
+                int type = mProcessorState.getPendingRequestTriggerType()
+                        .orElse(PublishController.PUBLISH_TRIGGER_RETRY);
+                logd("checkAndSendPendingRequest: send pending request, type=" + type);
+
+                // Clear the pending flag because it is going to send a PUBLISH request.
+                mProcessorState.clearPendingRequest();
+                mPublishCtrlCallback.requestPublishFromInternal(type);
+            }
+        }
+    }
+
+    /**
+     * Clear the pending request. It means that the publish request is triggered and this flag can
+     * be removed.
+     */
+    private void clearPendingRequest() {
+        synchronized (mPendingRequestLock) {
+            mProcessorState.clearPendingRequest();
+        }
+    }
+
+    /**
+     * Update the publishing allowed time with the given trigger type. This method wil be called
+     * before adding a PUBLISH request to the handler.
+     * @param triggerType The trigger type of this PUBLISH request
+     */
+    public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
+        mProcessorState.updatePublishingAllowedTime(triggerType);
+    }
+
+    /**
+     * @return The delay time to allow to execute the PUBLISH request. This method will be called
+     * to determine the delay time before adding a PUBLISH request to the handler.
+     */
+    public Optional<Long> getPublishingDelayTime() {
+        return mProcessorState.getPublishingDelayTime();
+    }
+
+    /**
+     * Update the publish throttle.
+     */
+    public void updatePublishThrottle(int publishThrottle) {
+        mProcessorState.updatePublishThrottle(publishThrottle);
+    }
+
+    /**
+     * @return true if the publish request is running now.
+     */
+    public boolean isPublishingNow() {
+        return mProcessorState.isPublishingNow();
+    }
+
+    @VisibleForTesting
+    public void setProcessorState(PublishProcessorState processorState) {
+        mProcessorState = processorState;
+    }
+
+    private void logd(String log) {
+       Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private void logi(String log) {
+       Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("PublishProcessor" + "[subId: " + mSubId + "]:");
+        pw.increaseIndent();
+
+        pw.print("ProcessorState: isPublishing=");
+        pw.print(mProcessorState.isPublishingNow());
+        pw.print(", hasReachedMaxRetries=");
+        pw.print(mProcessorState.isReachMaximumRetries());
+        pw.print(", delayTimeToAllowPublish=");
+        pw.println(mProcessorState.getPublishingDelayTime().orElse(-1L));
+
+        pw.println("Log:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        pw.println("---");
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java
new file mode 100644
index 0000000..40d901f
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java
@@ -0,0 +1,399 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The helper class to manage the publish request parameters.
+ */
+public class PublishProcessorState {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishProcessorState";
+
+    /*
+     * Manager the pending request flag and the trigger type of this pending request.
+     */
+    private static class PendingRequest {
+        private boolean mPendingFlag;
+        private Optional<Integer> mTriggerType;
+        private final Object mLock = new Object();
+
+        public PendingRequest() {
+            mTriggerType = Optional.empty();
+        }
+
+        // Set the flag to indicate there is a pending request.
+        public void setPendingRequest(@PublishTriggerType int triggerType) {
+            synchronized (mLock) {
+                mPendingFlag = true;
+                mTriggerType = Optional.of(triggerType);
+            }
+        }
+
+        // Clear the flag. The publish request is triggered and this flag can be cleared.
+        public void clearPendingRequest() {
+            synchronized (mLock) {
+                mPendingFlag = false;
+                mTriggerType = Optional.empty();
+            }
+        }
+
+        // Check if there is pending request need to be executed.
+        public boolean hasPendingRequest() {
+            synchronized (mLock) {
+                return mPendingFlag;
+            }
+        }
+
+        // Get the trigger type of the pending request.
+        public Optional<Integer> getPendingRequestTriggerType() {
+            synchronized (mLock) {
+                return mTriggerType;
+            }
+        }
+    }
+
+    /**
+     * Manager when the PUBLISH request can be executed.
+     */
+    private static class PublishThrottle {
+        // The unit time interval of the request retry.
+        private static final int RETRY_BASE_PERIOD_MIN = 1;
+
+        // The maximum number of the publication retries.
+        private static final int PUBLISH_MAXIMUM_NUM_RETRIES = 3;
+
+        // Get the minimum time that allows two PUBLISH requests can be executed continuously.
+        // It is one of the calculation conditions for the next publish allowed time.
+        private long mRcsPublishThrottle;
+
+        // The number of times the PUBLISH failed to retry. It is one of the calculation conditions
+        // for the next publish allowed time.
+        private int mRetryCount;
+
+        // The subscription ID associated with this throttle helper.
+        private int mSubId;
+
+        // The time when the last PUBLISH request is success. It is one of the calculation
+        // conditions for the next publish allowed time.
+        private Optional<Instant> mLastPublishedTime;
+
+        // The time to allow to execute the publishing request.
+        private Optional<Instant> mPublishAllowedTime;
+
+        public PublishThrottle(int subId) {
+            mSubId = subId;
+            resetState();
+        }
+
+        // Set the time of the last successful PUBLISH request.
+        public void setLastPublishedTime(Instant lastPublishedTime) {
+            mLastPublishedTime = Optional.of(lastPublishedTime);
+        }
+
+        // Increase the retry count when the PUBLISH has failed and need to be retried.
+        public void increaseRetryCount() {
+            if (mRetryCount < PUBLISH_MAXIMUM_NUM_RETRIES) {
+                mRetryCount++;
+            }
+            // Adjust the publish allowed time.
+            calcLatestPublishAllowedTime();
+        }
+
+        // Reset the retry count when the PUBLISH request is success or it does not need to retry.
+        public void resetRetryCount() {
+            mRetryCount = 0;
+            // Adjust the publish allowed time.
+            calcLatestPublishAllowedTime();
+        }
+
+        // In the case that the ImsService is disconnected, reset state for when the service
+        // reconnects
+        public void resetState() {
+            mLastPublishedTime = Optional.empty();
+            mPublishAllowedTime = Optional.empty();
+            mRcsPublishThrottle = UceUtils.getRcsPublishThrottle(mSubId);
+            Log.d(LOG_TAG, "RcsPublishThrottle=" + mRcsPublishThrottle);
+        }
+
+        // Check if it has reached the maximum retries.
+        public boolean isReachMaximumRetries() {
+            return (mRetryCount >= PUBLISH_MAXIMUM_NUM_RETRIES) ? true : false;
+        }
+
+        // Update the RCS publish throttle
+        public void updatePublishThrottle(int publishThrottle) {
+            mRcsPublishThrottle = publishThrottle;
+            calcLatestPublishAllowedTime();
+        }
+
+        // Check if the PUBLISH request can be executed now.
+        public boolean isPublishAllowedAtThisTime() {
+            // If the allowed time has not been set, it means that it is not ready to PUBLISH.
+            // It means that it has not received the publish request from the service.
+            if (!mPublishAllowedTime.isPresent()) {
+                return false;
+            }
+
+            // Check whether the current time has exceeded the allowed PUBLISH.
+            return (Instant.now().isBefore(mPublishAllowedTime.get())) ? false : true;
+        }
+
+        // Update the PUBLISH allowed time with the given trigger type.
+        public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
+            if (triggerType == PublishController.PUBLISH_TRIGGER_SERVICE) {
+                // If the request is triggered by service, reset the retry count and allow to
+                // execute the PUBLISH immediately.
+                mRetryCount = 0;
+                mPublishAllowedTime = Optional.of(Instant.now());
+            } else if (triggerType != PublishController.PUBLISH_TRIGGER_RETRY) {
+                // If the trigger type is not RETRY, it means that the device capabilities have
+                // changed, reset the retry cout.
+                resetRetryCount();
+            }
+        }
+
+        // Get the delay time to allow to execute the PUBLISH request.
+        public Optional<Long> getPublishingDelayTime() {
+            // If the allowed time has not been set, it means that it is not ready to PUBLISH.
+            // It means that it has not received the publish request from the service.
+            if (!mPublishAllowedTime.isPresent()) {
+                return Optional.empty();
+            }
+
+            // Setup the delay to the time which publish request is allowed to be executed.
+            long delayTime = ChronoUnit.MILLIS.between(Instant.now(), mPublishAllowedTime.get());
+            if (delayTime < 0) {
+                delayTime = 0L;
+            }
+            return Optional.of(delayTime);
+        }
+
+        // Calculate the latest time allowed to PUBLISH
+        private void calcLatestPublishAllowedTime() {
+            final long retryDelay = getNextRetryDelayTime();
+            if (!mLastPublishedTime.isPresent()) {
+                // If the publish request has not been successful before, it does not need to
+                // consider the PUBLISH throttle. The publish allowed time is decided by the retry
+                // delay.
+                mPublishAllowedTime = Optional.of(
+                        Instant.now().plus(Duration.ofMillis(retryDelay)));
+                Log.d(LOG_TAG, "calcLatestPublishAllowedTime: The last published time is empty");
+            } else {
+                // The default allowed time is the last published successful time plus the
+                // PUBLISH throttle.
+                Instant lastPublishedTime = mLastPublishedTime.get();
+                Instant defaultAllowedTime = lastPublishedTime.plus(
+                        Duration.ofMillis(mRcsPublishThrottle));
+
+                if (retryDelay == 0) {
+                    // If there is no delay time, the default allowed time is used.
+                    mPublishAllowedTime = Optional.of(defaultAllowedTime);
+                } else {
+                    // When the retry count is updated and there is delay time, it needs to compare
+                    // the default time and the retry delay time. The later time will be the
+                    // final decision value.
+                    Instant retryDelayTime = Instant.now().plus(Duration.ofMillis(retryDelay));
+                    mPublishAllowedTime = Optional.of(
+                            (retryDelayTime.isAfter(defaultAllowedTime))
+                                    ? retryDelayTime : defaultAllowedTime);
+                }
+            }
+            Log.d(LOG_TAG, "calcLatestPublishAllowedTime: " + mPublishAllowedTime.get());
+        }
+
+        // Get the milliseconds of the next retry delay.
+        private long getNextRetryDelayTime() {
+            // If the current retry count is zero, the delay time is also zero.
+            if (mRetryCount == 0) return 0L;
+            // Next retry delay time (minute)
+            int power = mRetryCount - 1;
+            Double delayTime = RETRY_BASE_PERIOD_MIN * Math.pow(2, power);
+            // Convert to millis
+            return TimeUnit.MINUTES.toMillis(delayTime.longValue());
+        }
+    }
+
+
+    private long mTaskId;
+
+    // Used to check whether the publish request is running now.
+    private volatile boolean mIsPublishing;
+
+    // Control the pending request flag.
+    private final PendingRequest mPendingRequest;
+
+    // Control the publish throttle
+    private final PublishThrottle mPublishThrottle;
+
+    private final Object mLock = new Object();
+
+    public PublishProcessorState(int subId) {
+        mPendingRequest = new PendingRequest();
+        mPublishThrottle = new PublishThrottle(subId);
+    }
+
+    /**
+     * @return A unique task Id for this request.
+     */
+    public long generatePublishTaskId() {
+        synchronized (mLock) {
+            mTaskId = UceUtils.generateTaskId();
+            return mTaskId;
+        }
+    }
+
+    /**
+     * @return The current valid PUBLISH task ID.
+     */
+    public long getCurrentTaskId() {
+        synchronized (mLock) {
+            return mTaskId;
+        }
+    }
+
+    /**
+     * Set the publishing flag to indicate whether it is executing a PUBLISH request or not.
+     */
+    public void setPublishingFlag(boolean flag) {
+        mIsPublishing = flag;
+    }
+
+    /**
+     * @return true if it is executing a PUBLISH request now.
+     */
+    public boolean isPublishingNow() {
+        return mIsPublishing;
+    }
+
+    /**
+     * Set the flag to indicate there is a pending request waiting to be executed.
+     */
+    public void setPendingRequest(@PublishTriggerType int triggerType) {
+        mPendingRequest.setPendingRequest(triggerType);
+    }
+
+    /**
+     * Clear the flag. It means a new publish request is triggered and the pending request flag
+     * can be cleared.
+     */
+    public void clearPendingRequest() {
+        mPendingRequest.clearPendingRequest();
+    }
+
+    /**
+     * @return true if there is pending request to be executed.
+     */
+    public boolean hasPendingRequest() {
+        return mPendingRequest.hasPendingRequest();
+    }
+
+    /**
+     * @return The trigger type of the pending request. If there is no pending request, it will
+     * return Optional.empty
+     */
+    public Optional<Integer> getPendingRequestTriggerType() {
+        return mPendingRequest.getPendingRequestTriggerType();
+    }
+
+    /**
+     * Set the time of the last successful PUBLISH request.
+     * @param lastPublishedTime The time when the last PUBLISH request is success
+     */
+    public void setLastPublishedTime(Instant lastPublishedTime) {
+        synchronized (mLock) {
+            mPublishThrottle.setLastPublishedTime(lastPublishedTime);
+        }
+    }
+
+    /**
+     * Increase the retry count when the PUBLISH has failed and need to retry.
+     */
+    public void increaseRetryCount() {
+        synchronized (mLock) {
+            mPublishThrottle.increaseRetryCount();
+        }
+    }
+
+    /**
+     * Reset the retry count when the PUBLISH request is success or it does not need to retry.
+     */
+    public void resetRetryCount() {
+        synchronized (mLock) {
+            mPublishThrottle.resetRetryCount();
+        }
+    }
+
+    /*
+     * Check if it has reached the maximum retry count.
+     */
+    public boolean isReachMaximumRetries() {
+        synchronized (mLock) {
+            return mPublishThrottle.isReachMaximumRetries();
+        }
+    }
+
+    /*
+     * Check if the PUBLISH can be executed now.
+     */
+    public boolean isPublishAllowedAtThisTime() {
+        synchronized (mLock) {
+            return mPublishThrottle.isPublishAllowedAtThisTime();
+        }
+    }
+
+    /**
+     * Update the PUBLISH allowed time with the given trigger type.
+     * @param triggerType The trigger type of this PUBLISH request
+     */
+    public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
+        synchronized (mLock) {
+            mPublishThrottle.updatePublishingAllowedTime(triggerType);
+        }
+    }
+
+    // Get the delay time to allow to execute the PUBLISH request.
+    public Optional<Long> getPublishingDelayTime() {
+        synchronized (mLock) {
+            return mPublishThrottle.getPublishingDelayTime();
+        }
+    }
+
+    public void updatePublishThrottle(int publishThrottle) {
+        synchronized (mLock) {
+            mPublishThrottle.updatePublishThrottle(publishThrottle);
+        }
+    }
+
+    public void onRcsDisconnected() {
+        synchronized (mLock) {
+            setPublishingFlag(false /*isPublishing*/);
+            clearPendingRequest();
+            mPublishThrottle.resetState();
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java
new file mode 100644
index 0000000..82969fc
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java
@@ -0,0 +1,339 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.Nullable;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IPublishResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * Receiving the result callback of the publish request.
+ */
+public class PublishRequestResponse {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishRequestResp";
+
+    private final long mTaskId;
+    private final String mPidfXml;
+    private volatile boolean mNeedRetry;
+    private volatile PublishControllerCallback mPublishCtrlCallback;
+
+    private Optional<Integer> mCmdErrorCode;
+    private Optional<Integer> mNetworkRespSipCode;
+    private Optional<String> mReasonPhrase;
+    private Optional<Integer> mReasonHeaderCause;
+    private Optional<String> mReasonHeaderText;
+
+    // The timestamp when receive the response from the network.
+    private Instant mResponseTimestamp;
+
+    public PublishRequestResponse(PublishControllerCallback publishCtrlCallback, long taskId,
+            String pidfXml) {
+        mTaskId = taskId;
+        mPidfXml = pidfXml;
+        mPublishCtrlCallback = publishCtrlCallback;
+        mCmdErrorCode = Optional.empty();
+        mNetworkRespSipCode = Optional.empty();
+        mReasonPhrase = Optional.empty();
+        mReasonHeaderCause = Optional.empty();
+        mReasonHeaderText = Optional.empty();
+    }
+
+    // The result callback of the publish capability request.
+    private IPublishResponseCallback mResponseCallback = new IPublishResponseCallback.Stub() {
+        @Override
+        public void onCommandError(int code) {
+            PublishRequestResponse.this.onCommandError(code);
+        }
+
+        @Override
+        public void onNetworkResponse(int code, String reason) {
+            PublishRequestResponse.this.onNetworkResponse(code, reason);
+        }
+
+        @Override
+        public void onNetworkRespHeader(int code, String reasonPhrase, int reasonHeaderCause,
+                String reasonHeaderText) {
+            PublishRequestResponse.this.onNetworkResponse(code, reasonPhrase, reasonHeaderCause,
+                    reasonHeaderText);
+        }
+    };
+
+    public IPublishResponseCallback getResponseCallback() {
+        return mResponseCallback;
+    }
+
+    public long getTaskId() {
+        return mTaskId;
+    }
+
+    /**
+     * Retrieve the command error code which received from the network.
+     */
+    public Optional<Integer> getCmdErrorCode() {
+        return mCmdErrorCode;
+    }
+
+    /**
+     * Retrieve the network response sip code which received from the network.
+     */
+    public Optional<Integer> getNetworkRespSipCode() {
+        return mNetworkRespSipCode;
+    }
+
+    /**
+     * Retrieve the reason phrase of the network response which received from the network.
+     */
+    public Optional<String> getReasonPhrase() {
+        return mReasonPhrase;
+    }
+
+    /**
+     * Retrieve the reason header from the network response.
+     */
+    public Optional<Integer> getReasonHeaderCause() {
+        return mReasonHeaderCause;
+    }
+
+    /**
+     * Retrieve the description of the reason header.
+     */
+    public Optional<String> getReasonHeaderText() {
+        return mReasonHeaderText;
+    }
+
+    /**
+     * Retrieve the SIP code from the network response. It will get the value from the Reason
+     * Header first. If the ReasonHeader is not present, it will get the value from the Network
+     * response instead.
+     */
+    public Optional<Integer> getResponseSipCode() {
+        return (mReasonHeaderCause.isPresent()) ? mReasonHeaderCause : mNetworkRespSipCode;
+    }
+
+    /**
+     * Retrieve the REASON from the network response. It will get the value from the Reason Header
+     * first. If the ReasonHeader is not present, it will get the value from the Network response
+     * instead.
+     */
+    public Optional<String> getResponseReason() {
+        return (mReasonHeaderText.isPresent()) ? mReasonHeaderText : mReasonPhrase;
+    }
+
+    /**
+     * Get the timestamp of receiving the network response callback.
+     */
+    public @Nullable Instant getResponseTimestamp() {
+        return mResponseTimestamp;
+    }
+
+    /**
+     * @return the PIDF XML sent during this request.
+     */
+    public String getPidfXml() {
+        return mPidfXml;
+    }
+
+    public void onDestroy() {
+        mPublishCtrlCallback = null;
+    }
+
+    private void onCommandError(int errorCode) {
+        mResponseTimestamp = Instant.now();
+        mCmdErrorCode = Optional.of(errorCode);
+        updateRetryFlagByCommandError();
+
+        PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
+        if (ctrlCallback != null) {
+            ctrlCallback.onRequestCommandError(this);
+        } else {
+            Log.d(LOG_TAG, "onCommandError: already destroyed. error code=" + errorCode);
+        }
+    }
+
+    private void onNetworkResponse(int sipCode, String reason) {
+        mResponseTimestamp = Instant.now();
+        mNetworkRespSipCode = Optional.of(sipCode);
+        mReasonPhrase = Optional.ofNullable(reason);
+        updateRetryFlagByNetworkResponse();
+
+        PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
+        if (ctrlCallback != null) {
+            ctrlCallback.onRequestNetworkResp(this);
+        } else {
+            Log.d(LOG_TAG, "onNetworkResponse: already destroyed. sip code=" + sipCode);
+        }
+    }
+
+    private void onNetworkResponse(int sipCode, String reasonPhrase, int reasonHeaderCause,
+            String reasonHeaderText) {
+        mResponseTimestamp = Instant.now();
+        mNetworkRespSipCode = Optional.of(sipCode);
+        mReasonPhrase = Optional.ofNullable(reasonPhrase);
+        mReasonHeaderCause = Optional.of(reasonHeaderCause);
+        mReasonHeaderText = Optional.ofNullable(reasonHeaderText);
+        updateRetryFlagByNetworkResponse();
+
+        PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
+        if (ctrlCallback != null) {
+            ctrlCallback.onRequestNetworkResp(this);
+        } else {
+            Log.d(LOG_TAG, "onNetworkResponse: already destroyed. sipCode=" + sipCode +
+                    ", reasonHeader=" + reasonHeaderCause);
+        }
+    }
+
+    private void updateRetryFlagByCommandError() {
+        switch(getCmdErrorCode().orElse(-1)) {
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_INSUFFICIENT_MEMORY:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_LOST_NETWORK_CONNECTION:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE:
+                mNeedRetry = true;
+                break;
+        }
+    }
+
+    private void updateRetryFlagByNetworkResponse() {
+        // Disable retry flag because the retry mechanism is implemented in the ImsService.
+        mNeedRetry = false;
+    }
+
+    /*
+     * Check whether the publishing request is successful.
+     */
+    public boolean isRequestSuccess() {
+        if (isCommandError()) {
+            return false;
+        }
+        // The result of the request was treated as successful if the command error code is present
+        // and its value is COMMAND_CODE_NO_CHANGE.
+        if (isCommandCodeNoChange()) {
+            return true;
+        }
+
+        final int sipCodeOk = NetworkSipCode.SIP_CODE_OK;
+        if (getNetworkRespSipCode().filter(c -> c == sipCodeOk).isPresent() &&
+                (!getReasonHeaderCause().isPresent()
+                        || getReasonHeaderCause().filter(c -> c == sipCodeOk).isPresent())) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check if the PUBLISH request is failed with receiving the command error.
+     * @return true if the command is failure.
+     */
+    private boolean isCommandError() {
+        // The request is failed if the command error code is present and its value is not
+        // COMMAND_CODE_NO_CHANGE.
+        if (getCmdErrorCode().isPresent() && !isCommandCodeNoChange()) {
+            return true;
+        }
+        return false;
+    }
+
+    // @return true If it received the command code COMMAND_CODE_NO_CHANGE
+    private boolean isCommandCodeNoChange() {
+        if (getCmdErrorCode().filter(code ->
+                code == RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE).isPresent()) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check whether the publishing request needs to be retried.
+     */
+    public boolean needRetry() {
+        return mNeedRetry;
+    }
+
+    /**
+     * @return The publish state when the publish request is finished.
+     */
+     public int getPublishState() {
+         if (isCommandError()) {
+             return getPublishStateByCmdErrorCode();
+         } else {
+             return getPublishStateByNetworkResponse();
+         }
+     }
+
+    /**
+     * Convert the command error code to the publish state
+     */
+    private int getPublishStateByCmdErrorCode() {
+        if (getCmdErrorCode().orElse(-1) ==
+                RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT) {
+            return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
+        }
+        return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+    }
+
+    /**
+     * Convert the network sip code to the publish state
+     */
+    private int getPublishStateByNetworkResponse() {
+        int respSipCode;
+        if (isCommandCodeNoChange()) {
+            // If the command code is COMMAND_CODE_NO_CHANGE, it should be treated as successful.
+            respSipCode = NetworkSipCode.SIP_CODE_OK;
+        } else if (getReasonHeaderCause().isPresent()) {
+            respSipCode = getReasonHeaderCause().get();
+        } else {
+            respSipCode = getNetworkRespSipCode().orElse(-1);
+        }
+
+        switch (respSipCode) {
+            case NetworkSipCode.SIP_CODE_OK:
+                return RcsUceAdapter.PUBLISH_STATE_OK;
+            case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT:
+                return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
+            default:
+                return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+        }
+    }
+
+    /**
+     * Get the information of the publish request response.
+     */
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("taskId=").append(mTaskId)
+                .append(", CmdErrorCode=").append(getCmdErrorCode().orElse(-1))
+                .append(", NetworkRespSipCode=").append(getNetworkRespSipCode().orElse(-1))
+                .append(", ReasonPhrase=").append(getReasonPhrase().orElse(""))
+                .append(", ReasonHeaderCause=").append(getReasonHeaderCause().orElse(-1))
+                .append(", ReasonHeaderText=").append(getReasonHeaderText().orElse(""))
+                .append(", ResponseTimestamp=").append(mResponseTimestamp)
+                .append(", isRequestSuccess=").append(isRequestSuccess())
+                .append(", needRetry=").append(mNeedRetry);
+        return builder.toString();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java
new file mode 100644
index 0000000..ff4b72c
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
+
+import android.telephony.CarrierConfigManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Parses the Android Carrier Configuration for service-description -> feature tag mappings and
+ * tracks the IMS registration to pass in the
+ * to determine capabilities for features that the framework does not manage.
+ *
+ * @see CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY for
+ * more information on the format of this key.
+ */
+public class PublishServiceDescTracker {
+    private static final String TAG = "PublishServiceDescTracker";
+
+    /**
+     * Map from (service-id, version) to the feature tags required in registration required in order
+     * for the RCS feature to be considered "capable".
+     * <p>
+     * See {@link
+     * CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY}
+     * for more information on how this can be overridden/extended.
+     */
+    private static final Map<ServiceDescription, Set<String>> DEFAULT_SERVICE_DESCRIPTION_MAP;
+    static {
+        ArrayMap<ServiceDescription, Set<String>> map = new ArrayMap<>(19);
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_IM,
+                Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_IM));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_SESSION,
+                Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_SESSION));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_FT,
+                Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_FT_SMS,
+                Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_PRESENCE,
+                Collections.singleton(FeatureTags.FEATURE_TAG_PRESENCE));
+        // Same service-ID & version for MMTEL, but different description.
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE,
+                Collections.singleton(FeatureTags.FEATURE_TAG_MMTEL));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, new ArraySet<>(
+                Arrays.asList(FeatureTags.FEATURE_TAG_MMTEL, FeatureTags.FEATURE_TAG_VIDEO)));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH,
+                Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH_SMS,
+                Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH_VIA_SMS));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER,
+                Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL,
+                Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_POST_CALL,
+                Collections.singleton(FeatureTags.FEATURE_TAG_POST_CALL));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_MAP,
+                Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_MAP));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_SKETCH,
+                Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_SKETCH));
+        // Feature tags defined twice for chatbot session because we want v1 and v2 based on bot
+        // version
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>(
+                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
+                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2, new ArraySet<>(
+                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
+                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+        // Feature tags defined twice for chatbot sa session because we want v1 and v2 based on bot
+        // version
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION, new ArraySet<>(
+                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
+                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2, new ArraySet<>(
+                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
+                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE,
+                Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE));
+        DEFAULT_SERVICE_DESCRIPTION_MAP = Collections.unmodifiableMap(map);
+    }
+
+    // Maps from ServiceDescription to the set of feature tags required to consider the feature
+    // capable for PUBLISH.
+    private final Map<ServiceDescription, Set<String>> mServiceDescriptionFeatureTagMap;
+    // Handles cases where multiple ServiceDescriptions match a subset of the same feature tags.
+    // This will be used to only include the feature tags where the
+    private final Set<ServiceDescription> mServiceDescriptionPartialMatches = new ArraySet<>();
+    // The capabilities calculated based off of the last IMS registration.
+    private final Set<ServiceDescription> mRegistrationCapabilities = new ArraySet<>();
+    // Contains the feature tags used in the last update to IMS registration.
+    private Set<String> mRegistrationFeatureTags = new ArraySet<>();
+
+    /**
+     * Create a new instance, which incorporates any carrier config overrides of the default
+     * mapping.
+     */
+    public static PublishServiceDescTracker fromCarrierConfig(String[] carrierConfig) {
+        Map<ServiceDescription, Set<String>> elements = new ArrayMap<>();
+        for (Map.Entry<ServiceDescription, Set<String>> entry :
+                DEFAULT_SERVICE_DESCRIPTION_MAP.entrySet()) {
+
+            elements.put(entry.getKey(), entry.getValue().stream()
+                    .map(PublishServiceDescTracker::removeInconsistencies)
+                    .collect(Collectors.toSet()));
+        }
+        if (carrierConfig != null) {
+            for (String entry : carrierConfig) {
+                String[] serviceDesc = entry.split("\\|");
+                if (serviceDesc.length < 4) {
+                    Log.w(TAG, "fromCarrierConfig: error parsing " + entry);
+                    continue;
+                }
+                elements.put(new ServiceDescription(serviceDesc[0].trim(), serviceDesc[1].trim(),
+                        serviceDesc[2].trim()), parseFeatureTags(serviceDesc[3]));
+            }
+        }
+        return new PublishServiceDescTracker(elements);
+    }
+
+    /**
+     * Parse the feature tags in the string, which will be separated by ";".
+     */
+    private static Set<String> parseFeatureTags(String featureTags) {
+        // First, split feature tags into individual params
+        String[] featureTagSplit = featureTags.split(";");
+        if (featureTagSplit.length == 0) {
+            return Collections.emptySet();
+        }
+        ArraySet<String> tags = new ArraySet<>(featureTagSplit.length);
+        // Add each tag, first trying to remove inconsistencies in string matching that may cause
+        // it to fail.
+        for (String tag : featureTagSplit) {
+            tags.add(removeInconsistencies(tag));
+        }
+        return tags;
+    }
+
+    private PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap) {
+        mServiceDescriptionFeatureTagMap = serviceFeatureTagMap;
+        Set<ServiceDescription> keySet = mServiceDescriptionFeatureTagMap.keySet();
+        // Go through and collect any ServiceDescriptions that have the same service-id & version
+        // (but not the same description) and add them to a "partial match" list.
+        for (ServiceDescription c : keySet) {
+            mServiceDescriptionPartialMatches.addAll(keySet.stream()
+                    .filter(s -> !Objects.equals(s, c) && isSimilar(c , s))
+                    .collect(Collectors.toList()));
+        }
+    }
+
+    /**
+     * Update the IMS registration associated with this tracker.
+     * @param imsRegistration A List of feature tags that were associated with the last IMS
+     *                        registration.
+     */
+    public void updateImsRegistration(Set<String> imsRegistration) {
+        Set<String> sanitizedTags = imsRegistration.stream()
+                // Ensure formatting passed in is the same as format stored here.
+                .map(PublishServiceDescTracker::parseFeatureTags)
+                // Each entry should only contain one feature tag.
+                .map(s -> s.iterator().next()).collect(Collectors.toSet());
+
+        // For aliased service descriptions (service-id && version is the same, but desc is
+        // different), Keep a "score" of the number of feature tags that the service description
+        // has associated with it. If another is found with a higher score, replace this one.
+        Map<ServiceDescription, Integer> aliasedServiceDescScore = new ArrayMap<>();
+        synchronized (mRegistrationCapabilities) {
+            mRegistrationFeatureTags = imsRegistration;
+            mRegistrationCapabilities.clear();
+            for (Map.Entry<ServiceDescription, Set<String>> desc :
+                    mServiceDescriptionFeatureTagMap.entrySet()) {
+                boolean found = true;
+                for (String tag : desc.getValue()) {
+                    if (!sanitizedTags.contains(tag)) {
+                        found = false;
+                        break;
+                    }
+                }
+                if (found) {
+                    // There may be ambiguity with multiple entries having the same service-id &&
+                    // version, but not the same description. In this case, we need to find any
+                    // other entries with the same id & version and replace it with the new entry
+                    // if it matches more "completely", i.e. match "mmtel;video" over "mmtel" if the
+                    // registration set includes "mmtel;video". Skip putting that in for now and
+                    // instead track the match with the most feature tags associated with it that
+                    // are all found in the IMS registration.
+                    if (mServiceDescriptionPartialMatches.contains(desc.getKey())) {
+                        ServiceDescription aliasedDesc = aliasedServiceDescScore.keySet().stream()
+                                .filter(s -> isSimilar(s, desc.getKey()))
+                                .findFirst().orElse(null);
+                        if (aliasedDesc != null) {
+                            Integer prevEntrySize = aliasedServiceDescScore.get(aliasedDesc);
+                            if (prevEntrySize != null
+                                    // Overrides are added below the original map, so prefer those.
+                                    && (prevEntrySize <= desc.getValue().size())) {
+                                aliasedServiceDescScore.remove(aliasedDesc);
+                                aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
+                            }
+                        } else {
+                            aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
+                        }
+                    } else {
+                        mRegistrationCapabilities.add(desc.getKey());
+                    }
+                }
+            }
+            // Collect the highest "scored" ServiceDescriptions and add themto registration caps.
+            mRegistrationCapabilities.addAll(aliasedServiceDescScore.keySet());
+        }
+    }
+
+    /**
+     * @return A copy of the service-description pairs (service-id, version) that are associated
+     * with the last IMS registration update in {@link #updateImsRegistration(Set)}
+     */
+    public Set<ServiceDescription> copyRegistrationCapabilities() {
+        synchronized (mRegistrationCapabilities) {
+            return new ArraySet<>(mRegistrationCapabilities);
+        }
+    }
+
+    /**
+     * @return A copy of the last update to the IMS feature tags via {@link #updateImsRegistration}.
+     */
+    public Set<String> copyRegistrationFeatureTags() {
+        synchronized (mRegistrationCapabilities) {
+            return new ArraySet<>(mRegistrationFeatureTags);
+        }
+    }
+
+    /**
+     * Dumps the current state of this tracker.
+     */
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
+        pw.println("PublishServiceDescTracker");
+        pw.increaseIndent();
+
+        pw.println("ServiceDescription -> Feature Tag Map:");
+        pw.increaseIndent();
+        for (Map.Entry<ServiceDescription, Set<String>> entry :
+                mServiceDescriptionFeatureTagMap.entrySet()) {
+            pw.print(entry.getKey());
+            pw.print("->");
+            pw.println(entry.getValue());
+        }
+        pw.println();
+        pw.decreaseIndent();
+
+        if (!mServiceDescriptionPartialMatches.isEmpty()) {
+            pw.println("Similar ServiceDescriptions:");
+            pw.increaseIndent();
+            for (ServiceDescription entry : mServiceDescriptionPartialMatches) {
+                pw.println(entry);
+            }
+            pw.decreaseIndent();
+        } else {
+            pw.println("No Similar ServiceDescriptions:");
+        }
+        pw.println();
+
+        pw.println("Last IMS registration update:");
+        pw.increaseIndent();
+        for (String entry : mRegistrationFeatureTags) {
+            pw.println(entry);
+        }
+        pw.println();
+        pw.decreaseIndent();
+
+        pw.println("Capabilities:");
+        pw.increaseIndent();
+        for (ServiceDescription entry : mRegistrationCapabilities) {
+            pw.println(entry);
+        }
+        pw.println();
+        pw.decreaseIndent();
+
+        pw.decreaseIndent();
+    }
+
+    /**
+     * Test if two ServiceDescriptions are similar, meaning service-id && version are equal.
+     */
+    private static boolean isSimilar(ServiceDescription a, ServiceDescription b) {
+        return (a.serviceId.equals(b.serviceId) && a.version.equals(b.version));
+    }
+
+    /**
+     * Remove any formatting inconsistencies that could make string matching difficult.
+     */
+    private static String removeInconsistencies(String tag) {
+        tag = tag.toLowerCase();
+        tag = tag.replaceAll("\\s+", "");
+        return tag;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java
new file mode 100644
index 0000000..de7305b
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.UceUtils;
+
+/**
+ * The util class of publishing device's capabilities.
+ */
+class PublishUtils {
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishUtils";
+
+    private static final String SCHEME_SIP = "sip";
+    private static final String SCHEME_TEL = "tel";
+    private static final String DOMAIN_SEPARATOR = "@";
+
+    public static Uri getDeviceContactUri(Context context, int subId) {
+        TelephonyManager telephonyManager = getTelephonyManager(context, subId);
+        if (telephonyManager == null) {
+            Log.w(LOG_TAG, "getDeviceContactUri: TelephonyManager is null");
+            return null;
+        }
+
+        // Get the contact uri from ISIM.
+        Uri contactUri = getContactUriFromIsim(telephonyManager);
+        if (contactUri != null) {
+            Log.d(LOG_TAG, "getDeviceContactUri: impu");
+            return contactUri;
+        } else {
+            Log.d(LOG_TAG, "getDeviceContactUri: line number");
+            return getContactUriFromLine1Number(telephonyManager);
+        }
+    }
+
+    private static Uri getContactUriFromIsim(TelephonyManager telephonyManager) {
+        // Get the home network domain and the array of the public user identities
+        String domain = telephonyManager.getIsimDomain();
+        String[] impus = telephonyManager.getIsimImpu();
+
+        if (TextUtils.isEmpty(domain) || impus == null) {
+            Log.d(LOG_TAG, "getContactUriFromIsim: domain is null=" + TextUtils.isEmpty(domain));
+            Log.d(LOG_TAG, "getContactUriFromIsim: impu is null=" +
+                    ((impus == null || impus.length == 0) ? "true" : "false"));
+            return null;
+        }
+
+        for (String impu : impus) {
+            if (TextUtils.isEmpty(impu)) continue;
+            Uri impuUri = Uri.parse(impu);
+            String scheme = impuUri.getScheme();
+            String schemeSpecificPart = impuUri.getSchemeSpecificPart();
+            if (SCHEME_SIP.equals(scheme) && !TextUtils.isEmpty(schemeSpecificPart) &&
+                    schemeSpecificPart.endsWith(domain)) {
+                return impuUri;
+            }
+        }
+        Log.d(LOG_TAG, "getContactUriFromIsim: there is no impu matching the domain");
+        return null;
+    }
+
+    private static Uri getContactUriFromLine1Number(TelephonyManager telephonyManager) {
+        String phoneNumber = formatPhoneNumber(telephonyManager.getLine1Number());
+        if (TextUtils.isEmpty(phoneNumber)) {
+            Log.w(LOG_TAG, "Cannot get the phone number");
+            return null;
+        }
+
+        String domain = telephonyManager.getIsimDomain();
+        if (!TextUtils.isEmpty(domain)) {
+            return Uri.fromParts(SCHEME_SIP, phoneNumber + DOMAIN_SEPARATOR + domain, null);
+        } else {
+            return Uri.fromParts(SCHEME_TEL, phoneNumber, null);
+        }
+    }
+
+    private static String formatPhoneNumber(final String phoneNumber) {
+        if (TextUtils.isEmpty(phoneNumber)) {
+            Log.w(LOG_TAG, "formatPhoneNumber: phone number is empty");
+            return null;
+        }
+        String number = PhoneNumberUtils.stripSeparators(phoneNumber);
+        return PhoneNumberUtils.normalizeNumber(number);
+    }
+
+    private static TelephonyManager getTelephonyManager(Context context, int subId) {
+        TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+        if (telephonyManager == null) {
+            return null;
+        } else {
+            return telephonyManager.createForSubscriptionId(subId);
+        }
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java b/src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java
new file mode 100644
index 0000000..f0db7d9
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Represents the "service-description" element in the PIDF XML for SIP PUBLISH of RCS capabilities.
+ */
+public class ServiceDescription {
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHAT_IM = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_CHAT_V1,
+            "1.0" /*version*/,
+            null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHAT_SESSION =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CHAT_V2,
+                    "2.0" /*version*/,
+                    null /*description*/
+            );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_FT = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_FT,
+            "1.0" /*version*/,
+            null /*description*/
+            );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_FT_SMS = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_FT_OVER_SMS,
+            "1.0" /*version*/,
+            null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_PRESENCE = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_PRESENCE,
+            "1.0" /*version*/,
+            "Capabilities Discovery Service" /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_MMTEL_VOICE = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_MMTEL,
+            "1.0" /*version*/,
+            "Voice Service" /*description*/
+    );
+
+    // No change except for description (service capabilities generated elsewhere).
+    public static final ServiceDescription SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_MMTEL,
+                    "1.0" /*version*/,
+                    "Voice and Video Service" /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_GEOPUSH = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH,
+            "1.0" /*version*/,
+            null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_GEOPUSH_SMS = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH_VIA_SMS,
+            "1.0" /*version*/,
+            null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CALL_COMPOSER =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER,
+                    "1.0" /*version*/,
+                    null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER,
+                    "2.0" /*version*/,
+                    null /*description*/
+            );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_POST_CALL = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_POST_CALL,
+            "1.0" /*version*/,
+            null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_SHARED_MAP = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_SHARED_MAP,
+            "1.0" /*version*/,
+            null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_SHARED_SKETCH =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_SHARED_SKETCH,
+                    "1.0" /*version*/,
+                    null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SESSION =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CHATBOT,
+                    "1.0" /*version*/,
+                    null /*description*/
+    );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SESSION_V2 =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CHATBOT,
+                    "2.0" /*version*/,
+                    null /*description*/
+            );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SA_SESSION =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CHATBOT_STANDALONE,
+                    "1.0" /*version*/,
+                    null /*description*/
+            );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2 =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CHATBOT_STANDALONE,
+                    "2.0" /*version*/,
+                    null /*description*/
+            );
+
+    public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_ROLE =
+            new ServiceDescription(
+                    RcsContactPresenceTuple.SERVICE_ID_CHATBOT_ROLE,
+                    "1.0" /*version*/,
+                    null /*description*/
+            );
+
+    /** Mandatory "service-id" element */
+    public final @NonNull String serviceId;
+    /** Mandatory "version" element */
+    public final @NonNull String version;
+    /** Optional "description" element */
+    public final @Nullable String description;
+
+    public ServiceDescription(String serviceId, String version, String description) {
+        this.serviceId = serviceId;
+        this.version = version;
+        this.description = description;
+    }
+
+    public RcsContactPresenceTuple.Builder getTupleBuilder() {
+        RcsContactPresenceTuple.Builder b = new RcsContactPresenceTuple.Builder(
+                RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN, serviceId, version);
+        if (!TextUtils.isEmpty(description)) {
+            b.setServiceDescription(description);
+        }
+        return b;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ServiceDescription that = (ServiceDescription) o;
+        return serviceId.equals(that.serviceId)
+                && version.equals(that.version)
+                && Objects.equals(description, that.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(serviceId, version, description);
+    }
+
+    @Override
+    public String toString() {
+        return "(id=" + serviceId + ", v=" + version + ", d=" + description + ')';
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java
new file mode 100644
index 0000000..83e864b
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java
@@ -0,0 +1,38 @@
+/*
+ * 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.ims.rcs.uce.presence.subscribe;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+
+import com.android.ims.rcs.uce.ControllerBase;
+
+import java.util.List;
+
+/**
+ * The interface related to the SUBSCRIBE request
+ */
+public interface SubscribeController extends ControllerBase {
+    /**
+     * Request the cached capabilities for the requested contacts if they exist. If not, perform
+     * a capability request on the network for the capabilities of these contacts.
+     */
+    void requestCapabilities(@NonNull List<Uri> contactUris, @NonNull ISubscribeResponseCallback c)
+            throws RemoteException;
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java
new file mode 100644
index 0000000..be4bd74
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java
@@ -0,0 +1,85 @@
+/*
+ * 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.ims.rcs.uce.presence.subscribe;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.List;
+
+/**
+ * The implementation of the SubscribeController.
+ */
+public class SubscribeControllerImpl implements SubscribeController {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "SubscribeController";
+
+    private final int mSubId;
+    private final Context mContext;
+    private volatile boolean mIsDestroyedFlag;
+    private volatile RcsFeatureManager mRcsFeatureManager;
+
+    public SubscribeControllerImpl(Context context, int subId) {
+        mSubId = subId;
+        mContext = context;
+    }
+
+    @Override
+    public void onRcsConnected(RcsFeatureManager manager) {
+        mRcsFeatureManager = manager;
+    }
+
+    @Override
+    public void onRcsDisconnected() {
+        mRcsFeatureManager = null;
+    }
+
+    @Override
+    public void onDestroy() {
+        mIsDestroyedFlag = true;
+    }
+
+    @Override
+    public void onCarrierConfigChanged() {
+        // Nothing Required Here.
+    }
+
+    @Override
+    public void requestCapabilities(List<Uri> contactUris, ISubscribeResponseCallback c)
+            throws RemoteException {
+
+        if (mIsDestroyedFlag) {
+            throw new RemoteException("Subscribe controller is destroyed");
+        }
+
+        RcsFeatureManager featureManager = mRcsFeatureManager;
+        if (featureManager == null) {
+            Log.w(LOG_TAG, "requestCapabilities: Service is unavailable");
+            c.onCommandError(RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE);
+            return;
+        }
+
+        featureManager.requestCapabilities(contactUris, c);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java b/src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java
new file mode 100644
index 0000000..f7a4acc
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import android.net.Uri;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsContactUceCapability;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * The base class of the UCE request to request the capabilities from the carrier network.
+ */
+public abstract class CapabilityRequest implements UceRequest {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "CapabilityRequest";
+
+    protected final int mSubId;
+    protected final long mTaskId;
+    protected final List<Uri> mUriList;
+    protected final @UceRequestType int mRequestType;
+    protected final RequestManagerCallback mRequestManagerCallback;
+    protected final CapabilityRequestResponse mRequestResponse;
+
+    protected volatile long mCoordinatorId;
+    protected volatile boolean mIsFinished;
+    protected volatile boolean mSkipGettingFromCache;
+
+    public CapabilityRequest(int subId, @UceRequestType int type, RequestManagerCallback callback) {
+        mSubId = subId;
+        mRequestType = type;
+        mUriList = new ArrayList<>();
+        mRequestManagerCallback = callback;
+        mRequestResponse = new CapabilityRequestResponse();
+        mTaskId = UceUtils.generateTaskId();
+    }
+
+    @VisibleForTesting
+    public CapabilityRequest(int subId, @UceRequestType int type, RequestManagerCallback callback,
+            CapabilityRequestResponse requestResponse) {
+        mSubId = subId;
+        mRequestType = type;
+        mUriList = new ArrayList<>();
+        mRequestManagerCallback = callback;
+        mRequestResponse = requestResponse;
+        mTaskId = UceUtils.generateTaskId();
+    }
+
+    @Override
+    public void setRequestCoordinatorId(long coordinatorId) {
+        mCoordinatorId = coordinatorId;
+    }
+
+    @Override
+    public long getRequestCoordinatorId() {
+        return mCoordinatorId;
+    }
+
+    @Override
+    public long getTaskId() {
+        return mTaskId;
+    }
+
+    @Override
+    public void onFinish() {
+        mIsFinished = true;
+        // Remove the timeout timer of this request
+        mRequestManagerCallback.removeRequestTimeoutTimer(mTaskId);
+    }
+
+    @Override
+    public void setContactUri(List<Uri> uris) {
+        mUriList.addAll(uris);
+        mRequestResponse.setRequestContacts(uris);
+    }
+
+    public List<Uri> getContactUri() {
+        return Collections.unmodifiableList(mUriList);
+    }
+
+    /**
+     * Set to check if this request should be getting the capabilities from the cache. The flag is
+     * set when the request is triggered by the capability polling service. The contacts from the
+     * capability polling service are already expired, skip checking from the cache.
+     */
+    public void setSkipGettingFromCache(boolean skipFromCache) {
+        mSkipGettingFromCache = skipFromCache;
+    }
+
+    /**
+     * Return if the capabilities request should skip getting from the cache. The flag is set when
+     * the request is triggered by the capability polling service and the request doesn't need to
+     * check the cache again.
+     */
+    private boolean isSkipGettingFromCache() {
+        return mSkipGettingFromCache;
+    }
+
+    /**
+     * @return The RequestResponse instance associated with this request.
+     */
+    public CapabilityRequestResponse getRequestResponse() {
+        return mRequestResponse;
+    }
+
+    /**
+     * Start executing this request.
+     */
+    @Override
+    public void executeRequest() {
+        // Return if this request is not allowed to be executed.
+        if (!isRequestAllowed()) {
+            logd("executeRequest: The request is not allowed.");
+            mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+            return;
+        }
+
+        // Get the capabilities from the cache.
+        final List<RcsContactUceCapability> cachedCapList
+                = isSkipGettingFromCache() ? Collections.EMPTY_LIST : getCapabilitiesFromCache();
+        mRequestResponse.addCachedCapabilities(cachedCapList);
+
+        logd("executeRequest: cached capabilities size=" + cachedCapList.size());
+
+        // Notify that the cached capabilities are updated.
+        if (!cachedCapList.isEmpty()) {
+            mRequestManagerCallback.notifyCachedCapabilitiesUpdated(mCoordinatorId, mTaskId);
+        }
+
+        // Get the rest contacts which need to request capabilities from the network.
+        final List<Uri> requestCapUris = getRequestingFromNetworkUris(cachedCapList);
+
+        logd("executeRequest: requestCapUris size=" + requestCapUris.size());
+
+        // Notify that it doesn't need to request capabilities from the network when all the
+        // requested capabilities can be retrieved from cache. Otherwise, it needs to request
+        // capabilities from the network for those contacts which cannot retrieve capabilities from
+        // the cache.
+        if (requestCapUris.isEmpty()) {
+            mRequestManagerCallback.notifyNoNeedRequestFromNetwork(mCoordinatorId, mTaskId);
+        } else {
+            requestCapabilities(requestCapUris);
+        }
+    }
+
+    // Check whether this request is allowed to be executed or not.
+    private boolean isRequestAllowed() {
+        if (mUriList == null || mUriList.isEmpty()) {
+            logw("isRequestAllowed: uri is empty");
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            return false;
+        }
+
+        if (mIsFinished) {
+            logw("isRequestAllowed: This request is finished");
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            return false;
+        }
+
+        DeviceStateResult deviceStateResult = mRequestManagerCallback.getDeviceState();
+        if (deviceStateResult.isRequestForbidden()) {
+            logw("isRequestAllowed: The device is disallowed.");
+            mRequestResponse.setRequestInternalError(
+                    deviceStateResult.getErrorCode().orElse(RcsUceAdapter.ERROR_GENERIC_FAILURE));
+            return false;
+        }
+        return true;
+    }
+
+    // Get the cached capabilities by the given request type.
+    private List<RcsContactUceCapability> getCapabilitiesFromCache() {
+        List<EabCapabilityResult> resultList = null;
+        if (mRequestType == REQUEST_TYPE_CAPABILITY) {
+            resultList = mRequestManagerCallback.getCapabilitiesFromCache(mUriList);
+        } else if (mRequestType == REQUEST_TYPE_AVAILABILITY) {
+            // Always get the first element if the request type is availability.
+            Uri uri = mUriList.get(0);
+            EabCapabilityResult eabResult = mRequestManagerCallback.getAvailabilityFromCache(uri);
+            resultList = new ArrayList<>();
+            resultList.add(eabResult);
+        }
+        if (resultList == null) {
+            return Collections.emptyList();
+        }
+        return resultList.stream()
+                .filter(Objects::nonNull)
+                .filter(result -> result.getStatus() == EabCapabilityResult.EAB_QUERY_SUCCESSFUL)
+                .map(EabCapabilityResult::getContactCapabilities)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Get the contact uris which cannot retrieve capabilities from the cache.
+     * @param cachedCapList The capabilities which are already stored in the cache.
+     */
+    private List<Uri> getRequestingFromNetworkUris(List<RcsContactUceCapability> cachedCapList) {
+        return mUriList.stream()
+                .filter(uri -> cachedCapList.stream()
+                        .noneMatch(cap -> cap.getContactUri().equals(uri)))
+                        .collect(Collectors.toList());
+    }
+
+    /**
+     * Set the timeout timer of this request.
+     */
+    protected void setupRequestTimeoutTimer() {
+        long timeoutAfterMs = UceUtils.getCapRequestTimeoutAfterMillis();
+        logd("setupRequestTimeoutTimer(ms): " + timeoutAfterMs);
+        mRequestManagerCallback.setRequestTimeoutTimer(mCoordinatorId, mTaskId, timeoutAfterMs);
+    }
+
+    /*
+     * Requests capabilities from IMS. The inherited request is required to override this method
+     * to define the behavior of requesting capabilities.
+     */
+    protected abstract void requestCapabilities(List<Uri> requestCapUris);
+
+    protected void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    protected void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    protected void logi(String log) {
+        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId).append("][taskId=").append(mTaskId).append("] ");
+        return builder;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java b/src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java
new file mode 100644
index 0000000..817db46
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java
@@ -0,0 +1,446 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactTerminatedReason;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.ErrorCode;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * The container of the result of the capabilities request.
+ */
+public class CapabilityRequestResponse {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "CapabilityRequestResp";
+
+    // The error code when the request encounters internal errors.
+    private @ErrorCode Optional<Integer> mRequestInternalError;
+
+    // The command error code of the request. It is assigned by the callback "onCommandError"
+    private @CommandCode Optional<Integer> mCommandError;
+
+    // The SIP code and reason of the network response.
+    private Optional<Integer> mNetworkRespSipCode;
+    private Optional<String> mReasonPhrase;
+
+    // The SIP code and the phrase read from the reason header
+    private Optional<Integer> mReasonHeaderCause;
+    private Optional<String> mReasonHeaderText;
+
+    // The reason why the this request was terminated and how long after it can be retried.
+    // This value is assigned by the callback "onTerminated"
+    private Optional<String> mTerminatedReason;
+    private Optional<Long> mRetryAfterMillis;
+
+    // The list of the valid capabilities which is retrieved from the cache.
+    private List<RcsContactUceCapability> mCachedCapabilityList;
+
+    // The list of the updated capabilities. This is assigned by the callback
+    // "onNotifyCapabilitiesUpdate"
+    private List<RcsContactUceCapability> mUpdatedCapabilityList;
+
+    // The list of the terminated resource. This is assigned by the callback
+    // "onResourceTerminated"
+    private List<RcsContactUceCapability> mTerminatedResource;
+
+    // The list of the remote contact's capability.
+    private Set<String> mRemoteCaps;
+
+    // The collection to record whether the request contacts have received the capabilities updated.
+    private Map<Uri, Boolean> mContactCapsReceived;
+
+    public CapabilityRequestResponse() {
+        mRequestInternalError = Optional.empty();
+        mCommandError = Optional.empty();
+        mNetworkRespSipCode = Optional.empty();
+        mReasonPhrase = Optional.empty();
+        mReasonHeaderCause = Optional.empty();
+        mReasonHeaderText = Optional.empty();
+        mTerminatedReason = Optional.empty();
+        mRetryAfterMillis = Optional.of(0L);
+        mTerminatedResource = new ArrayList<>();
+        mCachedCapabilityList = new ArrayList<>();
+        mUpdatedCapabilityList = new ArrayList<>();
+        mRemoteCaps = new HashSet<>();
+        mContactCapsReceived = new HashMap<>();
+    }
+
+    /**
+     * Set the request contacts which is expected to receive the capabilities updated.
+     */
+    public synchronized void setRequestContacts(List<Uri> contactUris) {
+        contactUris.stream().forEach(contact -> mContactCapsReceived.put(contact, Boolean.FALSE));
+        Log.d(LOG_TAG, "setRequestContacts: size=" + mContactCapsReceived.size());
+    }
+
+    /**
+     * Set the request contacts which is expected to receive the capabilities updated.
+     */
+    public synchronized boolean haveAllRequestCapsUpdatedBeenReceived() {
+        return !(mContactCapsReceived.containsValue(Boolean.FALSE));
+    }
+
+    /**
+     * Set the error code when the request encounters internal unexpected errors.
+     * @param errorCode the error code of the internal request error.
+     */
+    public synchronized void setRequestInternalError(@ErrorCode int errorCode) {
+        mRequestInternalError = Optional.of(errorCode);
+    }
+
+    /**
+     * Get the request internal error code.
+     */
+    public synchronized Optional<Integer> getRequestInternalError() {
+        return mRequestInternalError;
+    }
+
+    /**
+     * Set the command error code which is sent from ImsService and set the capability error code.
+     */
+    public synchronized void setCommandError(@CommandCode int commandError) {
+        mCommandError = Optional.of(commandError);
+    }
+
+    /**
+     * Get the command error codeof this request.
+     */
+    public synchronized Optional<Integer> getCommandError() {
+        return mCommandError;
+    }
+
+    /**
+     * Set the network response of this request which is sent by the network.
+     */
+    public synchronized void setNetworkResponseCode(int sipCode, String reason) {
+        mNetworkRespSipCode = Optional.of(sipCode);
+        mReasonPhrase = Optional.ofNullable(reason);
+    }
+
+    /**
+     * Set the network response of this request which is sent by the network.
+     */
+    public synchronized void setNetworkResponseCode(int sipCode, String reasonPhrase,
+            int reasonHeaderCause, String reasonHeaderText) {
+        mNetworkRespSipCode = Optional.of(sipCode);
+        mReasonPhrase = Optional.ofNullable(reasonPhrase);
+        mReasonHeaderCause = Optional.of(reasonHeaderCause);
+        mReasonHeaderText = Optional.ofNullable(reasonHeaderText);
+    }
+
+    // Get the sip code of the network response.
+    public synchronized Optional<Integer> getNetworkRespSipCode() {
+        return mNetworkRespSipCode;
+    }
+
+    // Get the reason of the network response.
+    public synchronized Optional<String> getReasonPhrase() {
+        return mReasonPhrase;
+    }
+
+    // Get the response sip code from the reason header.
+    public synchronized Optional<Integer> getReasonHeaderCause() {
+        return mReasonHeaderCause;
+    }
+
+    // Get the response phrae from the reason header.
+    public synchronized Optional<String> getReasonHeaderText() {
+        return mReasonHeaderText;
+    }
+
+    public Optional<Integer> getResponseSipCode() {
+        if (mReasonHeaderCause.isPresent()) {
+            return mReasonHeaderCause;
+        } else {
+            return mNetworkRespSipCode;
+        }
+    }
+
+    public Optional<String> getResponseReason() {
+        if (mReasonPhrase.isPresent()) {
+            return mReasonPhrase;
+        } else {
+            return mReasonHeaderText;
+        }
+    }
+
+    /**
+     * Set the reason and retry-after info when the callback onTerminated is called.
+     * @param reason The reason why this request is terminated.
+     * @param retryAfterMillis How long to wait before retry this request.
+     */
+    public synchronized void setTerminated(String reason, long retryAfterMillis) {
+        mTerminatedReason = Optional.ofNullable(reason);
+        mRetryAfterMillis = Optional.of(retryAfterMillis);
+    }
+
+    /**
+     * @return The reason of terminating the subscription request. empty string if it has not
+     * been given.
+     */
+    public synchronized String getTerminatedReason() {
+        return mTerminatedReason.orElse("");
+    }
+
+    /**
+     * @return Return the retryAfterMillis, 0L if the value is not present.
+     */
+    public synchronized long getRetryAfterMillis() {
+        return mRetryAfterMillis.orElse(0L);
+    }
+
+    /**
+     * Add the capabilities which are retrieved from the cache.
+     */
+    public synchronized void addCachedCapabilities(List<RcsContactUceCapability> capabilityList) {
+        mCachedCapabilityList.addAll(capabilityList);
+
+        // Record which contact has received the capabilities updated.
+        capabilityList.stream().forEach(cap ->
+            mContactCapsReceived.computeIfPresent(cap.getContactUri(), (k, v) -> Boolean.TRUE));
+    }
+
+    /**
+     * Clear the cached capabilities when the cached capabilities have been sent to client.
+     */
+    public synchronized void removeCachedContactCapabilities() {
+        mCachedCapabilityList.clear();
+    }
+
+    /**
+     * @return the cached capabilities.
+     */
+    public synchronized List<RcsContactUceCapability> getCachedContactCapability() {
+        return Collections.unmodifiableList(mCachedCapabilityList);
+    }
+
+    /**
+     * Add the updated contact capabilities which sent from ImsService.
+     */
+    public synchronized void addUpdatedCapabilities(List<RcsContactUceCapability> capabilityList) {
+        mUpdatedCapabilityList.addAll(capabilityList);
+
+        // Record which contact has received the capabilities updated.
+        capabilityList.stream().forEach(cap ->
+                mContactCapsReceived.computeIfPresent(cap.getContactUri(), (k, v) -> Boolean.TRUE));
+    }
+
+    /**
+     * Remove the given capabilities from the UpdatedCapabilityList when these capabilities have
+     * updated to the requester.
+     */
+    public synchronized void removeUpdatedCapabilities(List<RcsContactUceCapability> capList) {
+        mUpdatedCapabilityList.removeAll(capList);
+    }
+
+    /**
+     * Get all the updated capabilities to trigger the capability receive callback.
+     */
+    public synchronized List<RcsContactUceCapability> getUpdatedContactCapability() {
+        return Collections.unmodifiableList(mUpdatedCapabilityList);
+    }
+
+    /**
+     * Add the terminated resources which sent from ImsService.
+     */
+    public synchronized void addTerminatedResource(List<RcsContactTerminatedReason> resourceList) {
+        // Convert the RcsContactTerminatedReason to RcsContactUceCapability
+        List<RcsContactUceCapability> capabilityList = resourceList.stream()
+                .filter(Objects::nonNull)
+                .map(reason -> PidfParserUtils.getTerminatedCapability(
+                        reason.getContactUri(), reason.getReason())).collect(Collectors.toList());
+
+        // Save the terminated resource.
+        mTerminatedResource.addAll(capabilityList);
+
+        // Record which contact has received the capabilities updated.
+        capabilityList.stream().forEach(cap ->
+                mContactCapsReceived.computeIfPresent(cap.getContactUri(), (k, v) -> Boolean.TRUE));
+    }
+
+    /*
+     * Remove the given capabilities from the mTerminatedResource when these capabilities have
+     * updated to the requester.
+     */
+    public synchronized void removeTerminatedResources(List<RcsContactUceCapability> resourceList) {
+        mTerminatedResource.removeAll(resourceList);
+    }
+
+    /**
+     * Get the terminated resources which sent from ImsService.
+     */
+    public synchronized List<RcsContactUceCapability> getTerminatedResources() {
+        return Collections.unmodifiableList(mTerminatedResource);
+    }
+
+    /**
+     * Set the remote's capabilities which are sent from the network.
+     */
+    public synchronized void setRemoteCapabilities(Set<String> remoteCaps) {
+        if (remoteCaps != null) {
+            remoteCaps.stream().filter(Objects::nonNull).forEach(capability ->
+                    mRemoteCaps.add(capability));
+        }
+    }
+
+    /**
+     * Get the remote capability feature tags.
+     */
+    public synchronized Set<String> getRemoteCapability() {
+        return Collections.unmodifiableSet(mRemoteCaps);
+    }
+
+    /**
+     * Check if the network response is success.
+     * @return true if the network response code is OK or Accepted and the Reason header cause
+     * is either not present or OK.
+     */
+    public synchronized boolean isNetworkResponseOK() {
+        final int sipCodeOk = NetworkSipCode.SIP_CODE_OK;
+        final int sipCodeAccepted = NetworkSipCode.SIP_CODE_ACCEPTED;
+        Optional<Integer> respSipCode = getNetworkRespSipCode();
+        if (respSipCode.filter(c -> (c == sipCodeOk || c == sipCodeAccepted)).isPresent()
+                && (!getReasonHeaderCause().isPresent()
+                        || getReasonHeaderCause().filter(c -> c == sipCodeOk).isPresent())) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check whether the request is forbidden or not.
+     * @return true if the Reason header sip code is 403(Forbidden) or the response sip code is 403.
+     */
+    public synchronized boolean isRequestForbidden() {
+        final int sipCodeForbidden = NetworkSipCode.SIP_CODE_FORBIDDEN;
+        if (getReasonHeaderCause().isPresent()) {
+            return getReasonHeaderCause().filter(c -> c == sipCodeForbidden).isPresent();
+        } else {
+            return getNetworkRespSipCode().filter(c -> c == sipCodeForbidden).isPresent();
+        }
+    }
+
+    /**
+     * Check the contacts of the request is not found.
+     * @return true if the sip code of the network response is NOT_FOUND(404) or
+     * DOES_NOT_EXIST_ANYWHERE(604)
+     */
+    public synchronized boolean isNotFound() {
+        final int notFound = NetworkSipCode.SIP_CODE_NOT_FOUND;
+        final int notExistAnywhere = NetworkSipCode.SIP_CODE_DOES_NOT_EXIST_ANYWHERE;
+        Optional<Integer> reasonHeaderCause = getReasonHeaderCause();
+        Optional<Integer> respSipCode = getNetworkRespSipCode();
+        if (reasonHeaderCause.filter(c -> c == notFound || c == notExistAnywhere).isPresent() ||
+                respSipCode.filter(c -> c == notFound || c == notExistAnywhere).isPresent()) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * This method convert from the command error code which are defined in the
+     * RcsCapabilityExchangeImplBase to the Capabilities error code which are defined in the
+     * RcsUceAdapter.
+     */
+    public static int getCapabilityErrorFromCommandError(@CommandCode int cmdError) {
+        int uceError;
+        switch (cmdError) {
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNKNOWN:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_INVALID_PARAM:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_FETCH_ERROR:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED:
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE:
+                uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+                break;
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_FOUND:
+                uceError = RcsUceAdapter.ERROR_NOT_FOUND;
+                break;
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT:
+                uceError = RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
+                break;
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_INSUFFICIENT_MEMORY:
+                uceError = RcsUceAdapter.ERROR_INSUFFICIENT_MEMORY;
+                break;
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_LOST_NETWORK_CONNECTION:
+                uceError = RcsUceAdapter.ERROR_LOST_NETWORK;
+                break;
+            case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE:
+                uceError = RcsUceAdapter.ERROR_SERVER_UNAVAILABLE;
+                break;
+            default:
+                uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+                break;
+        }
+        return uceError;
+    }
+
+    /**
+     * Convert the SIP error code which sent by ImsService to the capability error code.
+     */
+    public static int getCapabilityErrorFromSipCode(CapabilityRequestResponse response) {
+        int sipError;
+        String respReason;
+        // Check the sip code in the Reason header first if the Reason Header is present.
+        if (response.getReasonHeaderCause().isPresent()) {
+            sipError = response.getReasonHeaderCause().get();
+            respReason = response.getReasonHeaderText().orElse("");
+        } else {
+            sipError = response.getNetworkRespSipCode().orElse(-1);
+            respReason = response.getReasonPhrase().orElse("");
+        }
+        return NetworkSipCode.getCapabilityErrorFromSipCode(sipError, respReason);
+    }
+
+    @Override
+    public synchronized String toString() {
+        StringBuilder builder = new StringBuilder();
+        return builder.append("RequestInternalError=").append(mRequestInternalError.orElse(-1))
+                .append(", CommandErrorCode=").append(mCommandError.orElse(-1))
+                .append(", NetworkResponseCode=").append(mNetworkRespSipCode.orElse(-1))
+                .append(", NetworkResponseReason=").append(mReasonPhrase.orElse(""))
+                .append(", ReasonHeaderCause=").append(mReasonHeaderCause.orElse(-1))
+                .append(", ReasonHeaderText=").append(mReasonHeaderText.orElse(""))
+                .append(", TerminatedReason=").append(mTerminatedReason.orElse(""))
+                .append(", RetryAfterMillis=").append(mRetryAfterMillis.orElse(0L))
+                .append(", Terminated resource size=" + mTerminatedResource.size())
+                .append(", cached capability size=" + mCachedCapabilityList.size())
+                .append(", Updated capability size=" + mUpdatedCapabilityList.size())
+                .append(", RemoteCaps size=" + mRemoteCaps.size())
+                .toString();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/OptionsRequest.java b/src/java/com/android/ims/rcs/uce/request/OptionsRequest.java
new file mode 100644
index 0000000..df5cebb
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/OptionsRequest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_NETWORK;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode;
+
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The UceRequest to request the capabilities when the OPTIONS mechanism is supported by the
+ * network.
+ */
+public class OptionsRequest extends CapabilityRequest {
+
+    // The result callback of the capabilities request from the IMS service.
+    private IOptionsResponseCallback mResponseCallback = new IOptionsResponseCallback.Stub() {
+        @Override
+        public void onCommandError(int code) {
+            OptionsRequest.this.onCommandError(code);
+        }
+
+        @Override
+        public void onNetworkResponse(int sipCode, String reason, List<String> remoteCaps) {
+            OptionsRequest.this.onNetworkResponse(sipCode, reason, remoteCaps);
+        }
+    };
+
+    private Uri mContactUri;
+    private OptionsController mOptionsController;
+
+    public OptionsRequest(int subId, @UceRequestType int requestType,
+            RequestManagerCallback taskMgrCallback, OptionsController optionsController) {
+        super(subId, requestType, taskMgrCallback);
+        mOptionsController = optionsController;
+        logd("OptionsRequest created");
+    }
+
+    @VisibleForTesting
+    public OptionsRequest(int subId, @UceRequestType int requestType,
+            RequestManagerCallback taskMgrCallback, OptionsController optionsController,
+            CapabilityRequestResponse requestResponse) {
+        super(subId, requestType, taskMgrCallback, requestResponse);
+        mOptionsController = optionsController;
+    }
+
+    @Override
+    public void onFinish() {
+        mOptionsController = null;
+        super.onFinish();
+        logd("OptionsRequest finish");
+    }
+
+    @Override
+    public void requestCapabilities(@NonNull List<Uri> requestCapUris) {
+        OptionsController optionsController = mOptionsController;
+        if (optionsController == null) {
+            logw("requestCapabilities: request is finished");
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+            return;
+        }
+
+        // Get the device's capabilities to send to the remote client.
+        RcsContactUceCapability deviceCap = mRequestManagerCallback.getDeviceCapabilities(
+                RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS);
+        if (deviceCap == null) {
+            logw("requestCapabilities: Cannot get device capabilities");
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+            return;
+        }
+
+        mContactUri = requestCapUris.get(0);
+        Set<String> featureTags = deviceCap.getFeatureTags();
+
+        logi("requestCapabilities: featureTag size=" + featureTags.size());
+        try {
+            // Send the capabilities request.
+            optionsController.sendCapabilitiesRequest(mContactUri, featureTags, mResponseCallback);
+            // Setup the timeout timer.
+            setupRequestTimeoutTimer();
+        } catch (RemoteException e) {
+            logw("requestCapabilities exception: " + e);
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+        }
+    }
+
+    // Receive the command error callback which is triggered by IOptionsResponseCallback.
+    private void onCommandError(@CommandCode int cmdError) {
+        logd("onCommandError: error code=" + cmdError);
+        if (mIsFinished) {
+            logw("onCommandError: The request is already finished");
+            return;
+        }
+        mRequestResponse.setCommandError(cmdError);
+        mRequestManagerCallback.notifyCommandError(mCoordinatorId, mTaskId);
+    }
+
+    // Receive the network response callback which is triggered by IOptionsResponseCallback.
+    private void onNetworkResponse(int sipCode, String reason, List<String> remoteCaps) {
+        logd("onNetworkResponse: sipCode=" + sipCode + ", reason=" + reason
+                + ", remoteCap size=" + ((remoteCaps == null) ? "null" : remoteCaps.size()));
+        if (mIsFinished) {
+            logw("onNetworkResponse: The request is already finished");
+            return;
+        }
+
+        if (remoteCaps == null) {
+            remoteCaps = Collections.EMPTY_LIST;
+        }
+
+        // Set the all the results to the request response.
+        mRequestResponse.setNetworkResponseCode(sipCode, reason);
+        mRequestResponse.setRemoteCapabilities(new HashSet<>(remoteCaps));
+        RcsContactUceCapability contactCapabilities = getContactCapabilities(mContactUri, sipCode,
+                new HashSet<>(remoteCaps));
+        mRequestResponse.addUpdatedCapabilities(Collections.singletonList(contactCapabilities));
+
+        // Notify that the network response is received.
+        mRequestManagerCallback.notifyNetworkResponse(mCoordinatorId, mTaskId);
+    }
+
+    /**
+     * Convert the remote capabilities from string list type to RcsContactUceCapability.
+     */
+    private RcsContactUceCapability getContactCapabilities(Uri contact, int sipCode,
+            Set<String> featureTags) {
+        int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+        if (!mRequestResponse.isNetworkResponseOK()) {
+            switch (sipCode) {
+                case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT:
+                    // Intentional fallthrough
+                case NetworkSipCode.SIP_CODE_TEMPORARILY_UNAVAILABLE:
+                    requestResult = RcsContactUceCapability.REQUEST_RESULT_NOT_ONLINE;
+                    break;
+                case NetworkSipCode.SIP_CODE_NOT_FOUND:
+                    // Intentional fallthrough
+                case NetworkSipCode.SIP_CODE_DOES_NOT_EXIST_ANYWHERE:
+                    requestResult = RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+                    break;
+                default:
+                    requestResult = RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+                    break;
+            }
+        }
+
+        RcsContactUceCapability.OptionsBuilder optionsBuilder
+                = new RcsContactUceCapability.OptionsBuilder(contact, SOURCE_TYPE_NETWORK);
+        optionsBuilder.setRequestResult(requestResult);
+        optionsBuilder.addFeatureTags(featureTags);
+        return optionsBuilder.build();
+    }
+
+    @VisibleForTesting
+    public IOptionsResponseCallback getResponseCallback() {
+        return mResponseCallback;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java b/src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java
new file mode 100644
index 0000000..a150dd6
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.stub.RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE;
+
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Responsible for the communication and interaction between OptionsRequests and triggering
+ * the callback to notify the result of the capabilities request.
+ */
+public class OptionsRequestCoordinator extends UceRequestCoordinator {
+    /**
+     * The builder of the OptionsRequestCoordinator.
+     */
+    public static final class Builder {
+        private OptionsRequestCoordinator mRequestCoordinator;
+
+        public Builder(int subId, Collection<UceRequest> requests,
+                RequestManagerCallback callback) {
+            mRequestCoordinator = new OptionsRequestCoordinator(subId, requests, callback);
+        }
+
+        public Builder setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+            mRequestCoordinator.setCapabilitiesCallback(callback);
+            return this;
+        }
+
+        public OptionsRequestCoordinator build() {
+            return mRequestCoordinator;
+        }
+    }
+
+    /**
+     * Different request updated events will create different {@link RequestResult}. Define the
+     * interface to get the {@link RequestResult} instance according to the given task ID and
+     * {@link CapabilityRequestResponse}.
+     */
+    @FunctionalInterface
+    private interface RequestResultCreator {
+        RequestResult createRequestResult(long taskId, CapabilityRequestResponse response);
+    }
+
+    // The RequestResult creator of the request error.
+    private static final RequestResultCreator sRequestErrorCreator = (taskId, response) -> {
+        int errorCode = response.getRequestInternalError().orElse(DEFAULT_ERROR_CODE);
+        long retryAfter = response.getRetryAfterMillis();
+        return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+    };
+
+    // The RequestResult creator of the request command error.
+    private static final RequestResultCreator sCommandErrorCreator = (taskId, response) -> {
+        int cmdError = response.getCommandError().orElse(COMMAND_CODE_GENERIC_FAILURE);
+        int errorCode = CapabilityRequestResponse.getCapabilityErrorFromCommandError(cmdError);
+        long retryAfter = response.getRetryAfterMillis();
+        return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+    };
+
+    // The RequestResult creator of the network response.
+    private static final RequestResultCreator sNetworkRespCreator = (taskId, response) -> {
+        if (response.isNetworkResponseOK()) {
+            return RequestResult.createSuccessResult(taskId);
+        } else {
+            int errorCode = CapabilityRequestResponse.getCapabilityErrorFromSipCode(response);
+            long retryAfter = response.getRetryAfterMillis();
+            return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+        }
+    };
+
+    // The RequestResult creator for does not need to request from the network.
+    private static final RequestResultCreator sNotNeedRequestFromNetworkCreator =
+            (taskId, response) -> RequestResult.createSuccessResult(taskId);
+
+    // The RequestResult creator of the request timeout.
+    private static final RequestResultCreator sRequestTimeoutCreator =
+            (taskId, response) -> RequestResult.createFailedResult(taskId,
+                    RcsUceAdapter.ERROR_REQUEST_TIMEOUT, 0L);
+
+    // The callback to notify the result of the capabilities request.
+    private IRcsUceControllerCallback mCapabilitiesCallback;
+
+    private OptionsRequestCoordinator(int subId, Collection<UceRequest> requests,
+            RequestManagerCallback requestMgrCallback) {
+        super(subId, requests, requestMgrCallback);
+        logd("OptionsRequestCoordinator: created");
+    }
+
+    private void setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+        mCapabilitiesCallback = callback;
+    }
+
+    @Override
+    public void onFinish() {
+        logd("OptionsRequestCoordinator: onFinish");
+        mCapabilitiesCallback = null;
+        super.onFinish();
+    }
+
+    @Override
+    public void onRequestUpdated(long taskId, @UceRequestUpdate int event) {
+        if (mIsFinished) return;
+        OptionsRequest request = (OptionsRequest) getUceRequest(taskId);
+        if (request == null) {
+            logw("onRequestUpdated: Cannot find OptionsRequest taskId=" + taskId);
+            return;
+        }
+
+        logd("onRequestUpdated(OptionsRequest): taskId=" + taskId + ", event=" +
+                REQUEST_EVENT_DESC.get(event));
+
+        switch (event) {
+            case REQUEST_UPDATE_ERROR:
+                handleRequestError(request);
+                break;
+            case REQUEST_UPDATE_COMMAND_ERROR:
+                handleCommandError(request);
+                break;
+            case REQUEST_UPDATE_NETWORK_RESPONSE:
+                handleNetworkResponse(request);
+                break;
+            case REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE:
+                handleCachedCapabilityUpdated(request);
+                break;
+            case REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK:
+                handleNoNeedRequestFromNetwork(request);
+                break;
+            case REQUEST_UPDATE_TIMEOUT:
+                handleRequestTimeout(request);
+                break;
+            default:
+                logw("onRequestUpdated(OptionsRequest): invalid event " + event);
+                break;
+        }
+
+        // End this instance if all the UceRequests in the coordinator are finished.
+        checkAndFinishRequestCoordinator();
+    }
+
+    /**
+     * Finish the OptionsRequest because it has encountered error.
+     */
+    private void handleRequestError(OptionsRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleRequestError: " + request.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sRequestErrorCreator.createRequestResult(taskId, response);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the given OptionsRequest received the onCommandError callback
+     * from the ImsService.
+     */
+    private void handleCommandError(OptionsRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleCommandError: " + request.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sCommandErrorCreator.createRequestResult(taskId, response);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the given OptionsRequest received the onNetworkResponse
+     * callback from the ImsService.
+     */
+    private void handleNetworkResponse(OptionsRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleNetworkResponse: " + response.toString());
+
+        List<RcsContactUceCapability> updatedCapList = response.getUpdatedContactCapability();
+        if (!updatedCapList.isEmpty()) {
+            // Save the capabilities and trigger the capabilities callback
+            mRequestManagerCallback.saveCapabilities(updatedCapList);
+            triggerCapabilitiesReceivedCallback(updatedCapList);
+            response.removeUpdatedCapabilities(updatedCapList);
+        }
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sNetworkRespCreator.createRequestResult(taskId, response);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the OptionsRequest retrieves the capabilities from cache.
+     */
+    private void handleCachedCapabilityUpdated(OptionsRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        Long taskId = request.getTaskId();
+        List<RcsContactUceCapability> cachedCapList = response.getCachedContactCapability();
+        logd("handleCachedCapabilityUpdated: taskId=" + taskId + ", CapRequestResp=" + response);
+
+        if (cachedCapList.isEmpty()) {
+            return;
+        }
+
+        // Trigger the capabilities updated callback.
+        triggerCapabilitiesReceivedCallback(cachedCapList);
+        response.removeCachedContactCapabilities();
+    }
+
+    /**
+     * This method is called when all the capabilities can be retrieved from the cached and it does
+     * not need to request capabilities from the network.
+     */
+    private void handleNoNeedRequestFromNetwork(OptionsRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleNoNeedRequestFromNetwork: " + response.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        long taskId = request.getTaskId();
+        RequestResult requestResult = sNotNeedRequestFromNetworkCreator.createRequestResult(taskId,
+                response);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the framework does not receive receive the result for
+     * capabilities request.
+     */
+    private void handleRequestTimeout(OptionsRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleRequestTimeout: " + response.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        long taskId = request.getTaskId();
+        RequestResult requestResult = sRequestTimeoutCreator.createRequestResult(taskId,
+                response);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * Trigger the capabilities updated callback.
+     */
+    private void triggerCapabilitiesReceivedCallback(List<RcsContactUceCapability> capList) {
+        try {
+            logd("triggerCapabilitiesCallback: size=" + capList.size());
+            mCapabilitiesCallback.onCapabilitiesReceived(capList);
+        } catch (RemoteException e) {
+            logw("triggerCapabilitiesCallback exception: " + e);
+        } finally {
+            logd("triggerCapabilitiesCallback: done");
+        }
+    }
+
+    /**
+     * Trigger the onComplete callback to notify the request is completed.
+     */
+    private void triggerCompletedCallback() {
+        try {
+            logd("triggerCompletedCallback");
+            mCapabilitiesCallback.onComplete();
+        } catch (RemoteException e) {
+            logw("triggerCompletedCallback exception: " + e);
+        } finally {
+            logd("triggerCompletedCallback: done");
+        }
+    }
+
+    /**
+     * Trigger the onError callback to notify the request is failed.
+     */
+    private void triggerErrorCallback(int errorCode, long retryAfterMillis) {
+        try {
+            logd("triggerErrorCallback: errorCode=" + errorCode + ", retry=" + retryAfterMillis);
+            mCapabilitiesCallback.onError(errorCode, retryAfterMillis);
+        } catch (RemoteException e) {
+            logw("triggerErrorCallback exception: " + e);
+        } finally {
+            logd("triggerErrorCallback: done");
+        }
+    }
+
+    private void checkAndFinishRequestCoordinator() {
+        synchronized (mCollectionLock) {
+            // Return because there are requests running.
+            if (!mActivatedRequests.isEmpty()) {
+                return;
+            }
+
+            // All the requests has finished, find the request which has the max retryAfter time.
+            // If the result is empty, it means all the request are success.
+            Optional<RequestResult> optRequestResult =
+                    mFinishedRequests.values().stream()
+                            .filter(result -> !result.isRequestSuccess())
+                            .max(Comparator.comparingLong(result ->
+                                    result.getRetryMillis().orElse(-1L)));
+
+            // Trigger the callback
+            if (optRequestResult.isPresent()) {
+                RequestResult result = optRequestResult.get();
+                int errorCode = result.getErrorCode().orElse(DEFAULT_ERROR_CODE);
+                long retryAfter = result.getRetryMillis().orElse(0L);
+                triggerErrorCallback(errorCode, retryAfter);
+            } else {
+                triggerCompletedCallback();
+            }
+
+            // Notify UceRequestManager to remove this instance from the collection.
+            mRequestManagerCallback.notifyRequestCoordinatorFinished(mCoordinatorId);
+
+            logd("checkAndFinishRequestCoordinator(OptionsRequest) done, id=" + mCoordinatorId);
+        }
+    }
+
+    @VisibleForTesting
+    public Collection<UceRequest> getActivatedRequest() {
+        return mActivatedRequests.values();
+    }
+
+    @VisibleForTesting
+    public Collection<RequestResult> getFinishedRequest() {
+        return mFinishedRequests.values();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java
new file mode 100644
index 0000000..c8aa3f7
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.util.NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR;
+import static com.android.ims.rcs.uce.util.NetworkSipCode.SIP_SERVICE_UNAVAILABLE;
+
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+
+import com.android.ims.rcs.uce.request.RemoteOptionsRequest.RemoteOptResponse;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+
+/**
+ * Responsible for the manager the remote options request and triggering the callback to notify
+ * the result of the request.
+ */
+public class RemoteOptionsCoordinator extends UceRequestCoordinator {
+    /**
+     * The builder of the RemoteOptionsCoordinator.
+     */
+    public static final class Builder {
+        RemoteOptionsCoordinator mRemoteOptionsCoordinator;
+
+        public Builder(int subId, Collection<UceRequest> requests, RequestManagerCallback c) {
+            mRemoteOptionsCoordinator = new RemoteOptionsCoordinator(subId, requests, c);
+        }
+
+        public Builder setOptionsRequestCallback(IOptionsRequestCallback callback) {
+            mRemoteOptionsCoordinator.setOptionsRequestCallback(callback);
+            return this;
+        }
+
+        public RemoteOptionsCoordinator build() {
+            return mRemoteOptionsCoordinator;
+        }
+    }
+
+    /**
+     * Different request updated events will create different {@link RequestResult}. Define the
+     * interface to get the {@link RequestResult} instance according to the given task ID and
+     * {@link RemoteOptResponse}.
+     */
+    @FunctionalInterface
+    private interface RequestResultCreator {
+        RequestResult createRequestResult(long taskId, RemoteOptResponse response);
+    }
+
+    // The RequestResult creator of the remote options response.
+    private static final RequestResultCreator sRemoteResponseCreator = (taskId, response) -> {
+        RcsContactUceCapability capability = response.getRcsContactCapability();
+        if (capability != null) {
+            return RequestResult.createSuccessResult(taskId);
+        } else {
+            int errorCode = response.getErrorSipCode().orElse(SIP_CODE_SERVER_INTERNAL_ERROR);
+            return RequestResult.createFailedResult(taskId, errorCode, 0L);
+        }
+    };
+
+    // The callback to notify the result of the remote options request.
+    private IOptionsRequestCallback mOptionsReqCallback;
+
+    private RemoteOptionsCoordinator(int subId, Collection<UceRequest> requests,
+            RequestManagerCallback requestMgrCallback) {
+        super(subId, requests, requestMgrCallback);
+        logd("RemoteOptionsCoordinator: created");
+    }
+
+    public void setOptionsRequestCallback(IOptionsRequestCallback callback) {
+        mOptionsReqCallback = callback;
+    }
+
+    @Override
+    public void onFinish() {
+        logd("RemoteOptionsCoordinator: onFinish");
+        mOptionsReqCallback = null;
+        super.onFinish();
+    }
+
+    @Override
+    public void onRequestUpdated(long taskId, int event) {
+        if (mIsFinished) return;
+        RemoteOptionsRequest request = (RemoteOptionsRequest) getUceRequest(taskId);
+        if (request == null) {
+            logw("onRequestUpdated: Cannot find RemoteOptionsRequest taskId=" + taskId);
+            return;
+        }
+
+        logd("onRequestUpdated: taskId=" + taskId + ", event=" + REQUEST_EVENT_DESC.get(event));
+        switch (event) {
+            case REQUEST_UPDATE_REMOTE_REQUEST_DONE:
+                handleRemoteRequestDone(request);
+                break;
+            default:
+                logw("onRequestUpdated: invalid event " + event);
+                break;
+        }
+
+        // End this instance if all the UceRequests in the coordinator are finished.
+        checkAndFinishRequestCoordinator();
+    }
+
+    private void handleRemoteRequestDone(RemoteOptionsRequest request) {
+        // Trigger the options request callback
+        RemoteOptResponse response = request.getRemoteOptResponse();
+        RcsContactUceCapability capability = response.getRcsContactCapability();
+        if (capability != null) {
+            boolean isNumberBlocked = response.isNumberBlocked();
+            triggerOptionsReqCallback(capability, isNumberBlocked);
+        } else {
+            int errorCode = response.getErrorSipCode().orElse(SIP_CODE_SERVER_INTERNAL_ERROR);
+            String reason = response.getErrorReason().orElse(SIP_SERVICE_UNAVAILABLE);
+            triggerOptionsReqWithErrorCallback(errorCode, reason);
+        }
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sRemoteResponseCreator.createRequestResult(taskId, response);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    private void triggerOptionsReqCallback(RcsContactUceCapability deviceCaps,
+            boolean isRemoteNumberBlocked) {
+        try {
+            logd("triggerOptionsReqCallback: start");
+            mOptionsReqCallback.respondToCapabilityRequest(deviceCaps, isRemoteNumberBlocked);
+        } catch (RemoteException e) {
+            logw("triggerOptionsReqCallback exception: " + e);
+        } finally {
+            logd("triggerOptionsReqCallback: done");
+        }
+    }
+
+    private void triggerOptionsReqWithErrorCallback(int errorCode, String reason) {
+        try {
+            logd("triggerOptionsReqWithErrorCallback: start");
+            mOptionsReqCallback.respondToCapabilityRequestWithError(errorCode, reason);
+        } catch (RemoteException e) {
+            logw("triggerOptionsReqWithErrorCallback exception: " + e);
+        } finally {
+            logd("triggerOptionsReqWithErrorCallback: done");
+        }
+    }
+
+    private void checkAndFinishRequestCoordinator() {
+        synchronized (mCollectionLock) {
+            // Return because there are requests running.
+            if (!mActivatedRequests.isEmpty()) {
+                return;
+            }
+            // Notify UceRequestManager to remove this instance from the collection.
+            mRequestManagerCallback.notifyRequestCoordinatorFinished(mCoordinatorId);
+            logd("checkAndFinishRequestCoordinator: id=" + mCoordinatorId);
+        }
+    }
+
+    @VisibleForTesting
+    public Collection<UceRequest> getActivatedRequest() {
+        return mActivatedRequests.values();
+    }
+
+    @VisibleForTesting
+    public Collection<RequestResult> getFinishedRequest() {
+        return mFinishedRequests.values();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java
new file mode 100644
index 0000000..17e59ef
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_NETWORK;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Handle the OPTIONS request from the network.
+ */
+public class RemoteOptionsRequest implements UceRequest {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "RemoteOptRequest";
+
+    /**
+     * The response of the remote capability request.
+     */
+    public static class RemoteOptResponse {
+        private boolean mIsNumberBlocked;
+        private RcsContactUceCapability mRcsContactCapability;
+        private Optional<Integer> mErrorSipCode;
+        private Optional<String> mErrorReason;
+
+        public RemoteOptResponse() {
+            mErrorSipCode = Optional.empty();
+            mErrorReason = Optional.empty();
+        }
+
+        void setRespondToRequest(RcsContactUceCapability capability, boolean isBlocked) {
+            mIsNumberBlocked = isBlocked;
+            mRcsContactCapability = capability;
+        }
+
+        void setRespondToRequestWithError(int code, String reason) {
+            mErrorSipCode = Optional.of(code);
+            mErrorReason = Optional.of(reason);
+        }
+
+        public boolean isNumberBlocked() {
+            return mIsNumberBlocked;
+        }
+
+        public RcsContactUceCapability getRcsContactCapability() {
+            return mRcsContactCapability;
+        }
+
+        public Optional<Integer> getErrorSipCode() {
+            return mErrorSipCode;
+        }
+
+        public Optional<String> getErrorReason() {
+            return mErrorReason;
+        }
+    }
+
+    private final int mSubId;
+    private final long mTaskId;
+    private volatile long mCoordinatorId;
+    private volatile boolean mIsFinished;
+    private volatile boolean mIsRemoteNumberBlocked;
+
+    private List<Uri> mUriList;
+    private final List<String> mRemoteFeatureTags;
+    private final RemoteOptResponse mRemoteOptResponse;
+    private final RequestManagerCallback mRequestManagerCallback;
+
+    public RemoteOptionsRequest(int subId, RequestManagerCallback requestMgrCallback) {
+        mSubId = subId;
+        mTaskId = UceUtils.generateTaskId();
+        mRemoteFeatureTags = new ArrayList<>();
+        mRemoteOptResponse = new RemoteOptResponse();
+        mRequestManagerCallback = requestMgrCallback;
+        logd("created");
+    }
+
+    @Override
+    public void setRequestCoordinatorId(long coordinatorId) {
+        mCoordinatorId = coordinatorId;
+    }
+
+    @Override
+    public long getRequestCoordinatorId() {
+        return mCoordinatorId;
+    }
+
+    @Override
+    public long getTaskId() {
+        return mTaskId;
+    }
+
+    @Override
+    public void onFinish() {
+        mIsFinished = true;
+    }
+
+    @Override
+    public void setContactUri(List<Uri> uris) {
+        mUriList = uris;
+    }
+
+    public void setRemoteFeatureTags(List<String> remoteFeatureTags) {
+        remoteFeatureTags.forEach(mRemoteFeatureTags::add);
+    }
+
+    public void setIsRemoteNumberBlocked(boolean isBlocked) {
+        mIsRemoteNumberBlocked = isBlocked;
+    }
+
+    /**
+     * @return The response of this request.
+     */
+    public RemoteOptResponse getRemoteOptResponse() {
+        return mRemoteOptResponse;
+    }
+
+    @Override
+    public void executeRequest() {
+        logd("executeRequest");
+        try {
+            executeRequestInternal();
+        } catch (Exception e) {
+            logw("executeRequest: exception " + e);
+            setResponseWithError(NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR,
+                    NetworkSipCode.SIP_INTERNAL_SERVER_ERROR);
+        } finally {
+            mRequestManagerCallback.notifyRemoteRequestDone(mCoordinatorId, mTaskId);
+        }
+    }
+
+    private void executeRequestInternal() {
+        if (mUriList == null || mUriList.isEmpty()) {
+            logw("executeRequest: uri is empty");
+            setResponseWithError(NetworkSipCode.SIP_CODE_BAD_REQUEST,
+                    NetworkSipCode.SIP_BAD_REQUEST);
+            return;
+        }
+
+        if (mIsFinished) {
+            logw("executeRequest: This request is finished");
+            setResponseWithError(NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE,
+                    NetworkSipCode.SIP_SERVICE_UNAVAILABLE);
+            return;
+        }
+
+        // Store the remote capabilities
+        Uri contactUri = mUriList.get(0);
+        RcsContactUceCapability remoteCaps = FeatureTags.getContactCapability(contactUri,
+                SOURCE_TYPE_NETWORK, mRemoteFeatureTags);
+        mRequestManagerCallback.saveCapabilities(Collections.singletonList(remoteCaps));
+
+        // Get the device's capabilities and trigger the request callback
+        RcsContactUceCapability deviceCaps = mRequestManagerCallback.getDeviceCapabilities(
+                CAPABILITY_MECHANISM_OPTIONS);
+        if (deviceCaps == null) {
+            logw("executeRequest: The device's capabilities is empty");
+            setResponseWithError(NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR,
+                    NetworkSipCode.SIP_INTERNAL_SERVER_ERROR);
+        } else {
+            logd("executeRequest: Respond to capability request, blocked="
+                    + mIsRemoteNumberBlocked);
+            setResponse(deviceCaps, mIsRemoteNumberBlocked);
+        }
+    }
+
+    private void setResponse(RcsContactUceCapability deviceCaps,
+            boolean isRemoteNumberBlocked) {
+        mRemoteOptResponse.setRespondToRequest(deviceCaps, isRemoteNumberBlocked);
+    }
+
+    private void setResponseWithError(int errorCode, String reason) {
+        mRemoteOptResponse.setRespondToRequestWithError(errorCode, reason);
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private void logw(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId).append("][taskId=").append(mTaskId).append("] ");
+        return builder;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java b/src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java
new file mode 100644
index 0000000..eb36839
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactTerminatedReason;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode;
+
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParser;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * The UceRequest to request the capabilities when the presence mechanism is supported by the
+ * network.
+ */
+public class SubscribeRequest extends CapabilityRequest {
+
+    // The result callback of the capabilities request from IMS service.
+    private final ISubscribeResponseCallback mResponseCallback =
+            new ISubscribeResponseCallback.Stub() {
+                @Override
+                public void onCommandError(int code) {
+                    SubscribeRequest.this.onCommandError(code);
+                }
+                @Override
+                public void onNetworkResponse(int code, String reason) {
+                    SubscribeRequest.this.onNetworkResponse(code, reason);
+                }
+                @Override
+                public void onNetworkRespHeader(int code, String reasonPhrase,
+                        int reasonHeaderCause, String reasonHeaderText) {
+                    SubscribeRequest.this.onNetworkResponse(code, reasonPhrase, reasonHeaderCause,
+                            reasonHeaderText);
+                }
+                @Override
+                public void onNotifyCapabilitiesUpdate(List<String> pidfXmls) {
+                    SubscribeRequest.this.onCapabilitiesUpdate(pidfXmls);
+                }
+                @Override
+                public void onResourceTerminated(List<RcsContactTerminatedReason> terminatedList) {
+                    SubscribeRequest.this.onResourceTerminated(terminatedList);
+                }
+                @Override
+                public void onTerminated(String reason, long retryAfterMillis) {
+                    SubscribeRequest.this.onTerminated(reason, retryAfterMillis);
+                }
+            };
+
+    private SubscribeController mSubscribeController;
+
+    public SubscribeRequest(int subId, @UceRequestType int requestType,
+            RequestManagerCallback taskMgrCallback, SubscribeController subscribeController) {
+        super(subId, requestType, taskMgrCallback);
+        mSubscribeController = subscribeController;
+        logd("SubscribeRequest created");
+    }
+
+    @VisibleForTesting
+    public SubscribeRequest(int subId, @UceRequestType int requestType,
+            RequestManagerCallback taskMgrCallback, SubscribeController subscribeController,
+            CapabilityRequestResponse requestResponse) {
+        super(subId, requestType, taskMgrCallback, requestResponse);
+        mSubscribeController = subscribeController;
+    }
+
+    @Override
+    public void onFinish() {
+        mSubscribeController = null;
+        super.onFinish();
+        logd("SubscribeRequest finish");
+    }
+
+    @Override
+    public void requestCapabilities(@NonNull List<Uri> requestCapUris) {
+        SubscribeController subscribeController = mSubscribeController;
+        if (subscribeController == null) {
+            logw("requestCapabilities: request is finished");
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+            return;
+        }
+
+        logi("requestCapabilities: size=" + requestCapUris.size());
+        try {
+            // Send the capabilities request.
+            subscribeController.requestCapabilities(requestCapUris, mResponseCallback);
+            // Setup the timeout timer.
+            setupRequestTimeoutTimer();
+        } catch (RemoteException e) {
+            logw("requestCapabilities exception: " + e);
+            mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+            mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+        }
+    }
+
+    // Receive the command error callback which is triggered by ISubscribeResponseCallback.
+    private void onCommandError(@CommandCode int cmdError) {
+        logd("onCommandError: error code=" + cmdError);
+        if (mIsFinished) {
+            logw("onCommandError: request is already finished");
+            return;
+        }
+        mRequestResponse.setCommandError(cmdError);
+        mRequestManagerCallback.notifyCommandError(mCoordinatorId, mTaskId);
+    }
+
+    // Receive the network response callback which is triggered by ISubscribeResponseCallback.
+    private void onNetworkResponse(int sipCode, String reason) {
+        logd("onNetworkResponse: code=" + sipCode + ", reason=" + reason);
+        if (mIsFinished) {
+            logw("onNetworkResponse: request is already finished");
+            return;
+        }
+        mRequestResponse.setNetworkResponseCode(sipCode, reason);
+        mRequestManagerCallback.notifyNetworkResponse(mCoordinatorId, mTaskId);
+    }
+
+    // Receive the network response callback which is triggered by ISubscribeResponseCallback.
+    private void onNetworkResponse(int sipCode, String reasonPhrase,
+        int reasonHeaderCause, String reasonHeaderText) {
+        logd("onNetworkResponse: code=" + sipCode + ", reasonPhrase=" + reasonPhrase +
+                ", reasonHeaderCause=" + reasonHeaderCause +
+                ", reasonHeaderText=" + reasonHeaderText);
+        if (mIsFinished) {
+            logw("onNetworkResponse: request is already finished");
+            return;
+        }
+        mRequestResponse.setNetworkResponseCode(sipCode, reasonPhrase, reasonHeaderCause,
+                reasonHeaderText);
+        mRequestManagerCallback.notifyNetworkResponse(mCoordinatorId, mTaskId);
+    }
+
+    // Receive the resource terminated callback which is triggered by ISubscribeResponseCallback.
+    private void onResourceTerminated(List<RcsContactTerminatedReason> terminatedResource) {
+        if (mIsFinished) {
+            logw("onResourceTerminated: request is already finished");
+            return;
+        }
+
+        if (terminatedResource == null) {
+            logw("onResourceTerminated: the parameter is null");
+            terminatedResource = Collections.emptyList();
+        }
+
+        logd("onResourceTerminated: size=" + terminatedResource.size());
+
+        // Add the terminated resource into the RequestResponse and notify the RequestManager
+        // to process the RcsContactUceCapabilities update.
+        mRequestResponse.addTerminatedResource(terminatedResource);
+        mRequestManagerCallback.notifyResourceTerminated(mCoordinatorId, mTaskId);
+    }
+
+    // Receive the capabilities update callback which is triggered by ISubscribeResponseCallback.
+    private void onCapabilitiesUpdate(List<String> pidfXml) {
+        if (mIsFinished) {
+            logw("onCapabilitiesUpdate: request is already finished");
+            return;
+        }
+
+        if (pidfXml == null) {
+            logw("onCapabilitiesUpdate: The parameter is null");
+            pidfXml = Collections.EMPTY_LIST;
+        }
+
+        // Convert from the pidf xml to the list of RcsContactUceCapability
+        List<RcsContactUceCapability> capabilityList = pidfXml.stream()
+                .map(pidf -> PidfParser.getRcsContactUceCapability(pidf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        logd("onCapabilitiesUpdate: PIDF size=" + pidfXml.size()
+                + ", contact capability size=" + capabilityList.size());
+
+        // Add these updated RcsContactUceCapability into the RequestResponse and notify
+        // the RequestManager to process the RcsContactUceCapabilities updated.
+        mRequestResponse.addUpdatedCapabilities(capabilityList);
+        mRequestManagerCallback.notifyCapabilitiesUpdated(mCoordinatorId, mTaskId);
+    }
+
+    // Receive the terminated callback which is triggered by ISubscribeResponseCallback.
+    private void onTerminated(String reason, long retryAfterMillis) {
+        logd("onTerminated: reason=" + reason + ", retryAfter=" + retryAfterMillis);
+        if (mIsFinished) {
+            logd("onTerminated: This request is already finished");
+            return;
+        }
+        mRequestResponse.setTerminated(reason, retryAfterMillis);
+        mRequestManagerCallback.notifyTerminated(mCoordinatorId, mTaskId);
+    }
+
+    @VisibleForTesting
+    public ISubscribeResponseCallback getResponseCallback() {
+        return mResponseCallback;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java b/src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java
new file mode 100644
index 0000000..54aa5cc
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.stub.RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE;
+
+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 com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.request.SubscriptionTerminatedHelper.TerminatedResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Responsible for the communication and interaction between SubscribeRequests and triggering
+ * the callback to notify the result of the capabilities request.
+ */
+public class SubscribeRequestCoordinator extends UceRequestCoordinator {
+    /**
+     * The builder of the SubscribeRequestCoordinator.
+     */
+    public static final class Builder {
+        private SubscribeRequestCoordinator mRequestCoordinator;
+
+        /**
+         * The builder of the SubscribeRequestCoordinator class.
+         */
+        public Builder(int subId, Collection<UceRequest> requests, RequestManagerCallback c) {
+            mRequestCoordinator = new SubscribeRequestCoordinator(subId, requests, c);
+        }
+
+        /**
+         * Set the callback to receive the request updated.
+         */
+        public Builder setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+            mRequestCoordinator.setCapabilitiesCallback(callback);
+            return this;
+        }
+
+        /**
+         * Get the SubscribeRequestCoordinator instance.
+         */
+        public SubscribeRequestCoordinator build() {
+            return mRequestCoordinator;
+        }
+    }
+
+    /**
+     * Different request updated events will create different {@link RequestResult}. Define the
+     * interface to get the {@link RequestResult} instance according to the given task ID and
+     * {@link CapabilityRequestResponse}.
+     */
+    @FunctionalInterface
+    private interface RequestResultCreator {
+        RequestResult createRequestResult(long taskId, CapabilityRequestResponse response,
+                RequestManagerCallback requestMgrCallback);
+    }
+
+    // The RequestResult creator of the request error.
+    private static final RequestResultCreator sRequestErrorCreator = (taskId, response,
+            requestMgrCallback) -> {
+        int errorCode = response.getRequestInternalError().orElse(DEFAULT_ERROR_CODE);
+        long retryAfter = response.getRetryAfterMillis();
+        return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+    };
+
+    // The RequestResult creator of the command error.
+    private static final RequestResultCreator sCommandErrorCreator = (taskId, response,
+            requestMgrCallback) -> {
+        int cmdError = response.getCommandError().orElse(COMMAND_CODE_GENERIC_FAILURE);
+        int errorCode = CapabilityRequestResponse.getCapabilityErrorFromCommandError(cmdError);
+        long retryAfter = response.getRetryAfterMillis();
+        return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+    };
+
+    // The RequestResult creator of the network response error.
+    private static final RequestResultCreator sNetworkRespErrorCreator = (taskId, response,
+            requestMgrCallback) -> {
+        DeviceStateResult deviceState = requestMgrCallback.getDeviceState();
+        if (deviceState.isRequestForbidden()) {
+            int errorCode = deviceState.getErrorCode().orElse(RcsUceAdapter.ERROR_FORBIDDEN);
+            long retryAfter = deviceState.getRequestRetryAfterMillis();
+            return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+        } else {
+            int errorCode = CapabilityRequestResponse.getCapabilityErrorFromSipCode(response);
+            long retryAfter = response.getRetryAfterMillis();
+            return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+        }
+    };
+
+    // The RequestResult creator of the request terminated.
+    private static final RequestResultCreator sTerminatedCreator = (taskId, response,
+            requestMgrCallback) -> {
+        // Check the given terminated reason to determine whether clients should retry or not.
+        TerminatedResult terminatedResult = SubscriptionTerminatedHelper.getAnalysisResult(
+                response.getTerminatedReason(), response.getRetryAfterMillis(),
+                response.haveAllRequestCapsUpdatedBeenReceived());
+        if (terminatedResult.getErrorCode().isPresent()) {
+            // If the terminated error code is present, it means that the request is failed.
+            int errorCode = terminatedResult.getErrorCode().get();
+            long terminatedRetry = terminatedResult.getRetryAfterMillis();
+            return RequestResult.createFailedResult(taskId, errorCode, terminatedRetry);
+        } else if (!response.isNetworkResponseOK() || response.getRetryAfterMillis() > 0L) {
+            // If the network response is failed or the retryAfter is not 0, this request is failed.
+            long retryAfterMillis = response.getRetryAfterMillis();
+            int errorCode = CapabilityRequestResponse.getCapabilityErrorFromSipCode(response);
+            return RequestResult.createFailedResult(taskId, errorCode, retryAfterMillis);
+        } else {
+            return RequestResult.createSuccessResult(taskId);
+        }
+    };
+
+    // The RequestResult creator for does not need to request from the network.
+    private static final RequestResultCreator sNotNeedRequestFromNetworkCreator =
+            (taskId, response, requestMgrCallback) -> RequestResult.createSuccessResult(taskId);
+
+    // The RequestResult creator of the request timeout.
+    private static final RequestResultCreator sRequestTimeoutCreator =
+            (taskId, response, requestMgrCallback) -> RequestResult.createFailedResult(taskId,
+                    RcsUceAdapter.ERROR_REQUEST_TIMEOUT, 0L);
+
+    // The callback to notify the result of the capabilities request.
+    private volatile IRcsUceControllerCallback mCapabilitiesCallback;
+
+    private SubscribeRequestCoordinator(int subId, Collection<UceRequest> requests,
+            RequestManagerCallback requestMgrCallback) {
+        super(subId, requests, requestMgrCallback);
+        logd("SubscribeRequestCoordinator: created");
+    }
+
+    private void setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+        mCapabilitiesCallback = callback;
+    }
+
+    @Override
+    public void onFinish() {
+        logd("SubscribeRequestCoordinator: onFinish");
+        mCapabilitiesCallback = null;
+        super.onFinish();
+    }
+
+    @Override
+    public void onRequestUpdated(long taskId, @UceRequestUpdate int event) {
+        if (mIsFinished) return;
+        SubscribeRequest request = (SubscribeRequest) getUceRequest(taskId);
+        if (request == null) {
+            logw("onRequestUpdated: Cannot find SubscribeRequest taskId=" + taskId);
+            return;
+        }
+
+        logd("onRequestUpdated(SubscribeRequest): taskId=" + taskId + ", event=" +
+                REQUEST_EVENT_DESC.get(event));
+
+        switch (event) {
+            case REQUEST_UPDATE_ERROR:
+                handleRequestError(request);
+                break;
+            case REQUEST_UPDATE_COMMAND_ERROR:
+                handleCommandError(request);
+                break;
+            case REQUEST_UPDATE_NETWORK_RESPONSE:
+                handleNetworkResponse(request);
+                break;
+            case REQUEST_UPDATE_CAPABILITY_UPDATE:
+                handleCapabilitiesUpdated(request);
+                break;
+            case REQUEST_UPDATE_RESOURCE_TERMINATED:
+                handleResourceTerminated(request);
+                break;
+            case REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE:
+                handleCachedCapabilityUpdated(request);
+                break;
+            case REQUEST_UPDATE_TERMINATED:
+                handleTerminated(request);
+                break;
+            case REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK:
+                handleNoNeedRequestFromNetwork(request);
+                break;
+            case REQUEST_UPDATE_TIMEOUT:
+                handleRequestTimeout(request);
+                break;
+            default:
+                logw("onRequestUpdated(SubscribeRequest): invalid event " + event);
+                break;
+        }
+
+        // End this instance if all the UceRequests in the coordinator are finished.
+        checkAndFinishRequestCoordinator();
+    }
+
+    /**
+     * Finish the SubscribeRequest because it has encountered error.
+     */
+    private void handleRequestError(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleRequestError: " + request.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sRequestErrorCreator.createRequestResult(taskId, response,
+                mRequestManagerCallback);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the given SubscribeRequest received the onCommandError callback
+     * from the ImsService.
+     */
+    private void handleCommandError(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleCommandError: " + request.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sCommandErrorCreator.createRequestResult(taskId, response,
+                mRequestManagerCallback);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the given SubscribeRequest received the onNetworkResponse
+     * callback from the ImsService.
+     */
+    private void handleNetworkResponse(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleNetworkResponse: " + response.toString());
+
+        // Refresh the device state with the request result.
+        response.getResponseSipCode().ifPresent(sipCode -> {
+            String reason = response.getResponseReason().orElse("");
+            mRequestManagerCallback.refreshDeviceState(sipCode, reason);
+        });
+
+        // When the network response is unsuccessful, there is no subsequent callback for this
+        // request. Check the forbidden state and finish this request. Otherwise, keep waiting for
+        // the subsequent callback of this request.
+        if (!response.isNetworkResponseOK()) {
+            Long taskId = request.getTaskId();
+            RequestResult requestResult = sNetworkRespErrorCreator.createRequestResult(taskId,
+                    response, mRequestManagerCallback);
+
+            // handle forbidden and not found case.
+            handleNetworkResponseFailed(request, requestResult);
+
+            // Trigger capabilities updated callback if there is any.
+            List<RcsContactUceCapability> updatedCapList = response.getUpdatedContactCapability();
+            if (!updatedCapList.isEmpty()) {
+                mRequestManagerCallback.saveCapabilities(updatedCapList);
+                triggerCapabilitiesReceivedCallback(updatedCapList);
+                response.removeUpdatedCapabilities(updatedCapList);
+            }
+
+            // Finish this request.
+            request.onFinish();
+
+            // Remove this request from the activated collection and notify RequestManager.
+            moveRequestToFinishedCollection(taskId, requestResult);
+        }
+    }
+
+    private void handleNetworkResponseFailed(SubscribeRequest request, RequestResult result) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+
+        if (response.isNotFound()) {
+            List<Uri> uriList = request.getContactUri();
+            List<RcsContactUceCapability> capabilityList = uriList.stream().map(uri ->
+                    PidfParserUtils.getNotFoundContactCapabilities(uri))
+                    .collect(Collectors.toList());
+            response.addUpdatedCapabilities(capabilityList);
+        }
+    }
+
+    /**
+     * This method is called when the given SubscribeRequest received the onNotifyCapabilitiesUpdate
+     * callback from the ImsService.
+     */
+    private void handleCapabilitiesUpdated(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        Long taskId = request.getTaskId();
+        List<RcsContactUceCapability> updatedCapList = response.getUpdatedContactCapability();
+        logd("handleCapabilitiesUpdated: taskId=" + taskId + ", size=" + updatedCapList.size());
+
+        if (updatedCapList.isEmpty()) {
+            return;
+        }
+
+        // Save the updated capabilities to the cache.
+        mRequestManagerCallback.saveCapabilities(updatedCapList);
+
+        // Trigger the capabilities updated callback and remove the given capabilities that have
+        // executed the callback onCapabilitiesReceived.
+        triggerCapabilitiesReceivedCallback(updatedCapList);
+        response.removeUpdatedCapabilities(updatedCapList);
+    }
+
+    /**
+     * This method is called when the given SubscribeRequest received the onResourceTerminated
+     * callback from the ImsService.
+     */
+    private void handleResourceTerminated(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        Long taskId = request.getTaskId();
+        List<RcsContactUceCapability> terminatedResources = response.getTerminatedResources();
+        logd("handleResourceTerminated: taskId=" + taskId + ", size=" + terminatedResources.size());
+
+        if (terminatedResources.isEmpty()) {
+            return;
+        }
+
+        // Save the terminated capabilities to the cache.
+        mRequestManagerCallback.saveCapabilities(terminatedResources);
+
+        // Trigger the capabilities updated callback and remove the given capabilities from the
+        // resource terminated list.
+        triggerCapabilitiesReceivedCallback(terminatedResources);
+        response.removeTerminatedResources(terminatedResources);
+    }
+
+    /**
+     * This method is called when the given SubscribeRequest retrieve the cached capabilities.
+     */
+    private void handleCachedCapabilityUpdated(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        Long taskId = request.getTaskId();
+        List<RcsContactUceCapability> cachedCapList = response.getCachedContactCapability();
+        logd("handleCachedCapabilityUpdated: taskId=" + taskId + ", size=" + cachedCapList.size());
+
+        if (cachedCapList.isEmpty()) {
+            return;
+        }
+
+        // Trigger the capabilities updated callback.
+        triggerCapabilitiesReceivedCallback(cachedCapList);
+        response.removeCachedContactCapabilities();
+    }
+
+    /**
+     * This method is called when the given SubscribeRequest received the onTerminated callback
+     * from the ImsService.
+     */
+    private void handleTerminated(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleTerminated: " + response.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        Long taskId = request.getTaskId();
+        RequestResult requestResult = sTerminatedCreator.createRequestResult(taskId, response,
+                mRequestManagerCallback);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when all the capabilities can be retrieved from the cached and it does
+     * not need to request capabilities from the network.
+     */
+    private void handleNoNeedRequestFromNetwork(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleNoNeedRequestFromNetwork: " + response.toString());
+
+        // Finish this request.
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        long taskId = request.getTaskId();
+        RequestResult requestResult = sNotNeedRequestFromNetworkCreator.createRequestResult(taskId,
+                response, mRequestManagerCallback);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    /**
+     * This method is called when the framework does not receive receive the result for
+     * capabilities request.
+     */
+    private void handleRequestTimeout(SubscribeRequest request) {
+        CapabilityRequestResponse response = request.getRequestResponse();
+        logd("handleRequestTimeout: " + response);
+
+        // Finish this request
+        request.onFinish();
+
+        // Remove this request from the activated collection and notify RequestManager.
+        long taskId = request.getTaskId();
+        RequestResult requestResult = sRequestTimeoutCreator.createRequestResult(taskId,
+                response, mRequestManagerCallback);
+        moveRequestToFinishedCollection(taskId, requestResult);
+    }
+
+    private void checkAndFinishRequestCoordinator() {
+        synchronized (mCollectionLock) {
+            // Return because there are requests running.
+            if (!mActivatedRequests.isEmpty()) {
+                return;
+            }
+
+            // All the requests has finished, find the request which has the max retryAfter time.
+            // If the result is empty, it means all the request are success.
+            Optional<RequestResult> optRequestResult =
+                    mFinishedRequests.values().stream()
+                        .filter(result -> !result.isRequestSuccess())
+                        .max(Comparator.comparingLong(result ->
+                                result.getRetryMillis().orElse(-1L)));
+
+            // Trigger the callback
+            if (optRequestResult.isPresent()) {
+                RequestResult result = optRequestResult.get();
+                int errorCode = result.getErrorCode().orElse(DEFAULT_ERROR_CODE);
+                long retryAfter = result.getRetryMillis().orElse(0L);
+                triggerErrorCallback(errorCode, retryAfter);
+            } else {
+                triggerCompletedCallback();
+            }
+
+            // Notify UceRequestManager to remove this instance from the collection.
+            mRequestManagerCallback.notifyRequestCoordinatorFinished(mCoordinatorId);
+
+            logd("checkAndFinishRequestCoordinator(SubscribeRequest) done, id=" + mCoordinatorId);
+        }
+    }
+
+    /**
+     * Trigger the capabilities updated callback.
+     */
+    private void triggerCapabilitiesReceivedCallback(List<RcsContactUceCapability> capList) {
+        try {
+            logd("triggerCapabilitiesCallback: size=" + capList.size());
+            mCapabilitiesCallback.onCapabilitiesReceived(capList);
+        } catch (RemoteException e) {
+            logw("triggerCapabilitiesCallback exception: " + e);
+        } finally {
+            logd("triggerCapabilitiesCallback: done");
+        }
+    }
+
+    /**
+     * Trigger the onComplete callback to notify the request is completed.
+     */
+    private void triggerCompletedCallback() {
+        try {
+            logd("triggerCompletedCallback");
+            mCapabilitiesCallback.onComplete();
+        } catch (RemoteException e) {
+            logw("triggerCompletedCallback exception: " + e);
+        } finally {
+            logd("triggerCompletedCallback: done");
+        }
+    }
+
+    /**
+     * Trigger the onError callback to notify the request is failed.
+     */
+    private void triggerErrorCallback(int errorCode, long retryAfterMillis) {
+        try {
+            logd("triggerErrorCallback: errorCode=" + errorCode + ", retry=" + retryAfterMillis);
+            mCapabilitiesCallback.onError(errorCode, retryAfterMillis);
+        } catch (RemoteException e) {
+            logw("triggerErrorCallback exception: " + e);
+        } finally {
+            logd("triggerErrorCallback: done");
+        }
+    }
+
+    @VisibleForTesting
+    public Collection<UceRequest> getActivatedRequest() {
+        return mActivatedRequests.values();
+    }
+
+    @VisibleForTesting
+    public Collection<RequestResult> getFinishedRequest() {
+        return mFinishedRequests.values();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java b/src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java
new file mode 100644
index 0000000..074d6e5
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.ErrorCode;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.Optional;
+
+/**
+ * The helper class to analyze the result of the callback onTerminated to determine whether the
+ * subscription request should be retried or not.
+ */
+public class SubscriptionTerminatedHelper {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "SubscriptionTerminated";
+
+    // The terminated reasons defined in RFC 3265 3.2.4
+    private static final String REASON_DEACTIVATED = "deactivated";
+    private static final String REASON_PROBATION = "probation";
+    private static final String REASON_REJECTED = "rejected";
+    private static final String REASON_TIMEOUT = "timeout";
+    private static final String REASON_GIVEUP = "giveup";
+    private static final String REASON_NORESOURCE = "noresource";
+
+    /**
+     * The analysis result of the callback onTerminated.
+     */
+    static class TerminatedResult {
+        private final @ErrorCode Optional<Integer> mErrorCode;
+        private final long mRetryAfterMillis;
+
+        public TerminatedResult(@ErrorCode Optional<Integer> errorCode, long retryAfterMillis) {
+            mErrorCode = errorCode;
+            mRetryAfterMillis = retryAfterMillis;
+        }
+
+        /**
+         * @return the error code when the request is failed. Optional.empty if the request is
+         * successful.
+         */
+        public Optional<Integer> getErrorCode() {
+            return mErrorCode;
+        }
+
+        public long getRetryAfterMillis() {
+            return mRetryAfterMillis;
+        }
+
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("TerminatedResult: ")
+                    .append("errorCode=").append(mErrorCode)
+                    .append(", retryAfterMillis=").append(mRetryAfterMillis);
+            return builder.toString();
+        }
+    }
+
+    /**
+     * According to the RFC 3265, Check the given reason to see whether clients should retry the
+     * subscribe request.
+     * <p>
+     * See RFC 3265 3.2.4 for the detail.
+     *
+     * @param reason The reason why the subscribe request is terminated. The reason is given by the
+     * network and it could be empty.
+     * @param retryAfterMillis How long should clients wait before retrying.
+     * @param allCapsHaveReceived Whether all the request contact capabilities have been received.
+     */
+    public static TerminatedResult getAnalysisResult(String reason, long retryAfterMillis,
+            boolean allCapsHaveReceived) {
+        TerminatedResult result = null;
+        if (TextUtils.isEmpty(reason)) {
+            /*
+             * When the value of retryAfterMillis is larger then zero, the client should retry.
+             */
+            if (retryAfterMillis > 0L) {
+                result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE),
+                        retryAfterMillis);
+            }
+        } else if (REASON_DEACTIVATED.equalsIgnoreCase(reason)) {
+            /*
+             * When the reason is "deactivated", clients should retry immediately.
+             */
+            long retry = getRequestRetryAfterMillis(retryAfterMillis);
+            result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
+        } else if (REASON_PROBATION.equalsIgnoreCase(reason)) {
+            /*
+             * When the reason is "probation", it means that the subscription has been terminated,
+             * but the client should retry at some later time.
+             */
+            long retry = getRequestRetryAfterMillis(retryAfterMillis);
+            result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
+        } else if (REASON_REJECTED.equalsIgnoreCase(reason)) {
+            /*
+             * When the reason is "rejected", it means that the subscription has been terminated
+             * due to chang in authorization policy. Clients should NOT retry.
+             */
+            result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_AUTHORIZED), 0L);
+        } else if (REASON_TIMEOUT.equalsIgnoreCase(reason)) {
+            if (retryAfterMillis > 0L) {
+                /*
+                 * When the parameter "retryAfterMillis" is greater than zero, it means that the
+                 * ImsService requires clients should retry later.
+                 */
+                long retry = getRequestRetryAfterMillis(retryAfterMillis);
+                result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_REQUEST_TIMEOUT),
+                        retry);
+            } else if (!allCapsHaveReceived) {
+                /*
+                 * The ImsService does not require to retry when the parameter "retryAfterMillis"
+                 * is zero. However, the request is still failed because it has not received all
+                 * the capabilities updated from the network.
+                 */
+                result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_REQUEST_TIMEOUT), 0L);
+            } else {
+                /*
+                 * The subscribe request is successfully when the parameter retryAfter is zero and
+                 * all the request capabilities have been received.
+                 */
+                result = new TerminatedResult(Optional.empty(), 0L);
+            }
+        } else if (REASON_GIVEUP.equalsIgnoreCase(reason)) {
+            /*
+             * The subscription has been terminated because the notifier could no obtain
+             * authorization in a timely fashion. Clients could retry the subscribe request.
+             */
+            long retry = getRequestRetryAfterMillis(retryAfterMillis);
+            result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_AUTHORIZED), retry);
+        } else if (REASON_NORESOURCE.equalsIgnoreCase(reason)) {
+            /*
+             * The subscription has been terminated because the resource is no longer exists.
+             * Clients should NOT retry.
+             */
+            result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_FOUND), 0L);
+        } else if (retryAfterMillis > 0L) {
+            /*
+             * Even if the reason is not listed above, clients should retry the request as long as
+             * the value of retry is non-zero.
+             */
+            long retry = getRequestRetryAfterMillis(retryAfterMillis);
+            result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
+        }
+
+        // The request should be successful. when the terminated is not in the above cases
+        if (result == null) {
+            result = new TerminatedResult(Optional.empty(), 0L);
+        }
+
+        Log.d(LOG_TAG, "getAnalysisResult: reason=" + reason + ", retry=" + retryAfterMillis +
+                ", allCapsHaveReceived=" + allCapsHaveReceived + ", " + result);
+        return result;
+    }
+
+    /*
+     * Get the appropriated retryAfterMillis for the subscribe request.
+     */
+    private static long getRequestRetryAfterMillis(long retryAfterMillis) {
+        // Return the minimum retry after millis if the given retryAfterMillis is less than the
+        // minimum value.
+        long minRetryAfterMillis = UceUtils.getMinimumRequestRetryAfterMillis();
+        return (retryAfterMillis < minRetryAfterMillis) ? minRetryAfterMillis : retryAfterMillis;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequest.java b/src/java/com/android/ims/rcs/uce/request/UceRequest.java
new file mode 100644
index 0000000..197f4ba
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.annotation.IntDef;
+import android.net.Uri;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * The interface of the UCE request to request the capabilities from the carrier network.
+ */
+public interface UceRequest {
+    /** The request type: CAPABILITY */
+    int REQUEST_TYPE_CAPABILITY = 1;
+
+    /** The request type: AVAILABILITY */
+    int REQUEST_TYPE_AVAILABILITY = 2;
+
+    /**@hide*/
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "REQUEST_TYPE_", value = {
+            REQUEST_TYPE_CAPABILITY,
+            REQUEST_TYPE_AVAILABILITY
+    })
+    @interface UceRequestType {}
+
+    /**
+     * Set the UceRequestCoordinator ID associated with this request.
+     */
+    void setRequestCoordinatorId(long coordinatorId);
+
+    /**
+     * @return Return the UceRequestCoordinator ID associated with this request.
+     */
+    long getRequestCoordinatorId();
+
+    /**
+     * @return Return the task ID of this request.
+     */
+    long getTaskId();
+
+    /**
+     * Notify that the request is finish.
+     */
+    void onFinish();
+
+    /**
+     * Set the contact URIs associated with this request.
+     */
+    void setContactUri(List<Uri> uris);
+
+    /**
+     * Execute the request.
+     */
+    void executeRequest();
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java b/src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java
new file mode 100644
index 0000000..eea4fbe
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.ims.RcsUceAdapter;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * The base class that is responsible for the communication and interaction between the UceRequests.
+ */
+public abstract class UceRequestCoordinator {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "ReqCoordinator";
+
+    /**
+     * The UceRequest encountered error.
+     */
+    public static final int REQUEST_UPDATE_ERROR = 0;
+
+    /**
+     * The UceRequest received the onCommandError callback.
+     */
+    public static final int REQUEST_UPDATE_COMMAND_ERROR = 1;
+
+    /**
+     * The UceRequest received the onNetworkResponse callback.
+     */
+    public static final int REQUEST_UPDATE_NETWORK_RESPONSE = 2;
+
+    /**
+     * The UceRequest received the onNotifyCapabilitiesUpdate callback.
+     */
+    public static final int REQUEST_UPDATE_CAPABILITY_UPDATE = 3;
+
+    /**
+     * The UceRequest received the onResourceTerminated callback.
+     */
+    public static final int REQUEST_UPDATE_RESOURCE_TERMINATED = 4;
+
+    /**
+     * The UceRequest retrieve the valid capabilities from the cache.
+     */
+    public static final int REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE = 5;
+
+    /**
+     * The UceRequest receive the onTerminated callback.
+     */
+    public static final int REQUEST_UPDATE_TERMINATED = 6;
+
+    /**
+     * The UceRequest does not need to request capabilities to network because all the capabilities
+     * can be retrieved from the cache.
+     */
+    public static final int REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK = 7;
+
+    /**
+     * The remote options request is done.
+     */
+    public static final int REQUEST_UPDATE_REMOTE_REQUEST_DONE = 8;
+
+    /**
+     * The capabilities request is timeout.
+     */
+    public static final int REQUEST_UPDATE_TIMEOUT = 9;
+
+    @IntDef(value = {
+            REQUEST_UPDATE_ERROR,
+            REQUEST_UPDATE_COMMAND_ERROR,
+            REQUEST_UPDATE_NETWORK_RESPONSE,
+            REQUEST_UPDATE_TERMINATED,
+            REQUEST_UPDATE_RESOURCE_TERMINATED,
+            REQUEST_UPDATE_CAPABILITY_UPDATE,
+            REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE,
+            REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK,
+            REQUEST_UPDATE_REMOTE_REQUEST_DONE,
+            REQUEST_UPDATE_TIMEOUT,
+    }, prefix="REQUEST_UPDATE_")
+    @Retention(RetentionPolicy.SOURCE)
+    @interface UceRequestUpdate {}
+
+    protected static Map<Integer, String> REQUEST_EVENT_DESC = new HashMap<>();
+    static {
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_ERROR, "REQUEST_ERROR");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_COMMAND_ERROR, "RETRIEVE_COMMAND_ERROR");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_NETWORK_RESPONSE, "REQUEST_NETWORK_RESPONSE");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_TERMINATED, "REQUEST_TERMINATED");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_RESOURCE_TERMINATED, "REQUEST_RESOURCE_TERMINATED");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_CAPABILITY_UPDATE, "REQUEST_CAPABILITY_UPDATE");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE, "REQUEST_CACHE_CAP_UPDATE");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK, "NO_NEED_REQUEST");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_REMOTE_REQUEST_DONE, "REMOTE_REQUEST_DONE");
+        REQUEST_EVENT_DESC.put(REQUEST_UPDATE_TIMEOUT, "REQUEST_TIMEOUT");
+    }
+
+    /**
+     * The result of the UceRequest. This is the used by the RequestCoordinator to record the
+     * result of each sub-requests.
+     */
+    static class RequestResult {
+        /**
+         * Create a RequestResult that successfully completes the request.
+         * @param taskId the task id of the UceRequest
+         */
+        public static RequestResult createSuccessResult(long taskId) {
+            return new RequestResult(taskId);
+        }
+
+        /**
+         * Create a RequestResult for the failed request.
+         * @param taskId the task id of the UceRequest
+         * @param errorCode the error code of the failed request
+         * @param retry When the request can be retried.
+         */
+        public static RequestResult createFailedResult(long taskId, int errorCode, long retry) {
+            return new RequestResult(taskId, errorCode, retry);
+        }
+
+        private final Long mTaskId;
+        private final Boolean mIsSuccess;
+        private final Optional<Integer> mErrorCode;
+        private final Optional<Long> mRetryMillis;
+
+        /**
+         * The private constructor for the successful request.
+         */
+        private RequestResult(long taskId) {
+            mTaskId = taskId;
+            mIsSuccess = true;
+            mErrorCode = Optional.empty();
+            mRetryMillis = Optional.empty();
+        }
+
+        /**
+         * The private constructor for the failed request.
+         */
+        private RequestResult(long taskId, int errorCode, long retryMillis) {
+            mTaskId = taskId;
+            mIsSuccess = false;
+            mErrorCode = Optional.of(errorCode);
+            mRetryMillis = Optional.of(retryMillis);
+        }
+
+        public long getTaskId() {
+            return mTaskId;
+        }
+
+        public boolean isRequestSuccess() {
+            return mIsSuccess;
+        }
+
+        public Optional<Integer> getErrorCode() {
+            return mErrorCode;
+        }
+
+        public Optional<Long> getRetryMillis() {
+            return mRetryMillis;
+        }
+    }
+
+    // The default capability error code.
+    protected static final int DEFAULT_ERROR_CODE = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+
+    protected final int mSubId;
+    protected final long mCoordinatorId;
+    protected volatile boolean mIsFinished;
+
+    // The collection of activated requests.
+    protected final Map<Long, UceRequest> mActivatedRequests;
+    // The collection of the finished requests.
+    protected final Map<Long, RequestResult> mFinishedRequests;
+    // The lock of the activated and finished collection.
+    protected final Object mCollectionLock = new Object();
+
+    // The callback to communicate with UceRequestManager
+    protected final RequestManagerCallback mRequestManagerCallback;
+
+    public UceRequestCoordinator(int subId, Collection<UceRequest> requests,
+            RequestManagerCallback requestMgrCallback) {
+        mSubId = subId;
+        mCoordinatorId = UceUtils.generateRequestCoordinatorId();
+        mRequestManagerCallback = requestMgrCallback;
+
+        // Set the coordinatorId to all the given UceRequests
+        requests.forEach(request -> request.setRequestCoordinatorId(mCoordinatorId));
+
+        // All the given requests are put in the activated request at the beginning.
+        mFinishedRequests = new HashMap<>();
+        mActivatedRequests = requests.stream().collect(
+                Collectors.toMap(UceRequest::getTaskId, request -> request));
+    }
+
+    /**
+     * @return Get the request coordinator ID.
+     */
+    public long getCoordinatorId() {
+        return mCoordinatorId;
+    }
+
+    /**
+     * @return Get the collection of task ID of all the activated requests.
+     */
+    public @NonNull List<Long> getActivatedRequestTaskIds() {
+        synchronized (mCollectionLock) {
+            return mActivatedRequests.values().stream()
+                    .map(request -> request.getTaskId())
+                    .collect(Collectors.toList());
+        }
+    }
+
+    /**
+     * @return Get the UceRequest associated with the given taskId from the activated requests.
+     */
+    public @Nullable UceRequest getUceRequest(Long taskId) {
+        synchronized (mCollectionLock) {
+            return mActivatedRequests.get(taskId);
+        }
+    }
+
+    /**
+     * Remove the UceRequest associated with the given taskId from the activated collection and
+     * add the {@link RequestResult} into the finished request collection. This method is called by
+     * the coordinator instance when it receives the request updated event and judges this request
+     * is finished.
+     */
+    protected void moveRequestToFinishedCollection(Long taskId, RequestResult requestResult) {
+        synchronized (mCollectionLock) {
+            mActivatedRequests.remove(taskId);
+            mFinishedRequests.put(taskId, requestResult);
+            mRequestManagerCallback.notifyUceRequestFinished(getCoordinatorId(), taskId);
+        }
+    }
+
+    /**
+     * Notify this coordinator instance is finished. This method sets the finish flag and clear all
+     * the UceRequest collections and it can be used anymore after the method is called.
+     */
+    public void onFinish() {
+        mIsFinished = true;
+        synchronized (mCollectionLock) {
+            mActivatedRequests.forEach((taskId, request) -> request.onFinish());
+            mActivatedRequests.clear();
+            mFinishedRequests.clear();
+        }
+    }
+
+    /**
+     * Notify the UceRequest associated with the given taskId in the coordinator is updated.
+     */
+    public abstract void onRequestUpdated(long taskId, @UceRequestUpdate int event);
+
+    protected void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    protected void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId).append("][coordId=").append(mCoordinatorId).append("] ");
+        return builder;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java b/src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java
new file mode 100644
index 0000000..76bde85
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import android.util.Log;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Calculate network carry capabilities and dispatcher the UceRequests.
+ */
+public class UceRequestDispatcher {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "RequestDispatcher";
+
+    /**
+     * Record the request timestamp.
+     */
+    private static class Request {
+        private final long mTaskId;
+        private final long mCoordinatorId;
+        private Optional<Instant> mExecutingTime;
+
+        public Request(long coordinatorId, long taskId) {
+            mTaskId = taskId;
+            mCoordinatorId = coordinatorId;
+            mExecutingTime = Optional.empty();
+        }
+
+        public long getCoordinatorId() {
+            return mCoordinatorId;
+        }
+
+        public long getTaskId() {
+            return mTaskId;
+        }
+
+        public void setExecutingTime(Instant instant) {
+            mExecutingTime = Optional.of(instant);
+        }
+
+        public Optional<Instant> getExecutingTime() {
+            return mExecutingTime;
+        }
+    }
+
+    private final int mSubId;
+
+    // The interval milliseconds for each request.
+    private long mIntervalTime = 100;
+
+    // The number of requests that the network can process at the same time.
+    private int mMaxConcurrentNum = 1;
+
+    // The collection of all requests waiting to be executed.
+    private final List<Request> mWaitingRequests = new ArrayList<>();
+
+    // The collection of all executing requests.
+    private final List<Request> mExecutingRequests = new ArrayList<>();
+
+    // The callback to communicate with UceRequestManager
+    private RequestManagerCallback mRequestManagerCallback;
+
+    public UceRequestDispatcher(int subId, RequestManagerCallback callback) {
+        mSubId = subId;
+        mRequestManagerCallback = callback;
+    }
+
+    /**
+     * Clear all the collections when the instance is destroyed.
+     */
+    public synchronized void onDestroy() {
+        mWaitingRequests.clear();
+        mExecutingRequests.clear();
+        mRequestManagerCallback = null;
+    }
+
+    /**
+     * Add new requests to the waiting collection and trigger sending request if the network is
+     * capable of processing the given requests.
+     */
+    public synchronized void addRequest(long coordinatorId, List<Long> taskIds) {
+        taskIds.stream().forEach(taskId -> {
+            Request request = new Request(coordinatorId, taskId);
+            mWaitingRequests.add(request);
+        });
+        onRequestUpdated();
+    }
+
+    /**
+     * Notify that the request with the given taskId is finished.
+     */
+    public synchronized void onRequestFinished(Long taskId) {
+        logd("onRequestFinished: taskId=" + taskId);
+        mExecutingRequests.removeIf(request -> request.getTaskId() == taskId);
+        onRequestUpdated();
+    }
+
+    private synchronized void onRequestUpdated() {
+        logd("onRequestUpdated: waiting=" + mWaitingRequests.size()
+                + ", executing=" + mExecutingRequests.size());
+
+        // Return if there is no waiting request.
+        if (mWaitingRequests.isEmpty()) {
+            return;
+        }
+
+        // Check how many more requests can be executed and return if the size of executing
+        // requests have reached the maximum number.
+        int numCapacity = mMaxConcurrentNum - mExecutingRequests.size();
+        if (numCapacity <= 0) {
+            return;
+        }
+
+        List<Request> requestList = getRequestFromWaitingCollection(numCapacity);
+        if (!requestList.isEmpty()) {
+            notifyStartOfRequest(requestList);
+        }
+    }
+
+    /*
+     * Retrieve the given number of requests from the WaitingRequestList.
+     */
+    private List<Request> getRequestFromWaitingCollection(int numCapacity) {
+        // The number of the requests cannot more than the waiting requests.
+        int numRequests = (numCapacity < mWaitingRequests.size()) ?
+                numCapacity : mWaitingRequests.size();
+
+        List<Request> requestList = new ArrayList<>();
+        for (int i = 0; i < numRequests; i++) {
+            requestList.add(mWaitingRequests.get(i));
+        }
+
+        mWaitingRequests.removeAll(requestList);
+        return requestList;
+    }
+
+    /**
+     * Notify start of the UceRequest.
+     */
+    private void notifyStartOfRequest(List<Request> requestList) {
+        RequestManagerCallback callback = mRequestManagerCallback;
+        if (callback == null) {
+            logd("notifyStartOfRequest: The instance is destroyed");
+            return;
+        }
+
+        Instant lastRequestTime = getLastRequestTime();
+        Instant baseTime;
+        if (lastRequestTime.plusMillis(mIntervalTime).isAfter(Instant.now())) {
+            baseTime = lastRequestTime.plusMillis(mIntervalTime);
+        } else {
+            baseTime = Instant.now();
+        }
+
+        StringBuilder builder = new StringBuilder("notifyStartOfRequest: taskId=");
+        for (int i = 0; i < requestList.size(); i++) {
+            Instant startExecutingTime = baseTime.plusMillis((mIntervalTime * i));
+            Request request = requestList.get(i);
+            request.setExecutingTime(startExecutingTime);
+
+            // Add the request to the executing collection
+            mExecutingRequests.add(request);
+
+            // Notify RequestManager to execute this task.
+            long taskId = request.getTaskId();
+            long coordId = request.getCoordinatorId();
+            long delayTime = getDelayTime(startExecutingTime);
+            mRequestManagerCallback.notifySendingRequest(coordId, taskId, delayTime);
+
+            builder.append(request.getTaskId() + ", ");
+        }
+        builder.append("ExecutingRequests size=" + mExecutingRequests.size());
+        logd(builder.toString());
+    }
+
+    private Instant getLastRequestTime() {
+        if (mExecutingRequests.isEmpty()) {
+            return Instant.MIN;
+        }
+
+        Instant lastTime = Instant.MIN;
+        for (Request request : mExecutingRequests) {
+            if (!request.getExecutingTime().isPresent()) continue;
+            Instant executingTime = request.getExecutingTime().get();
+            if (executingTime.isAfter(lastTime)) {
+                lastTime = executingTime;
+            }
+        }
+        return lastTime;
+    }
+
+    private long getDelayTime(Instant executingTime) {
+        long delayTime = Duration.between(executingTime, Instant.now()).toMillis();
+        if (delayTime < 0L) {
+            delayTime = 0;
+        }
+        return delayTime;
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+}
+
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestManager.java b/src/java/com/android/ims/rcs/uce/request/UceRequestManager.java
new file mode 100644
index 0000000..5e428e9
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestManager.java
@@ -0,0 +1,826 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequest.UceRequestType;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.UceRequestUpdate;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Managers the capabilities requests and the availability requests from UceController.
+ */
+public class UceRequestManager {
+
+    private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceRequestManager";
+
+    /**
+     * Testing interface used to mock UceUtils in testing.
+     */
+    @VisibleForTesting
+    public interface UceUtilsProxy {
+        /**
+         * The interface for {@link UceUtils#isPresenceCapExchangeEnabled(Context, int)} used for
+         * testing.
+         */
+        boolean isPresenceCapExchangeEnabled(Context context, int subId);
+
+        /**
+         * The interface for {@link UceUtils#isPresenceSupported(Context, int)} used for testing.
+         */
+        boolean isPresenceSupported(Context context, int subId);
+
+        /**
+         * The interface for {@link UceUtils#isSipOptionsSupported(Context, int)} used for testing.
+         */
+        boolean isSipOptionsSupported(Context context, int subId);
+
+        /**
+         * @return true when the Presence group subscribe is enabled.
+         */
+        boolean isPresenceGroupSubscribeEnabled(Context context, int subId);
+
+        /**
+         * Retrieve the maximum number of contacts that can be included in a request.
+         */
+        int getRclMaxNumberEntries(int subId);
+
+        /**
+         * @return true if the given phone number is blocked by the network.
+         */
+        boolean isNumberBlocked(Context context, String phoneNumber);
+    }
+
+    private static UceUtilsProxy sUceUtilsProxy = new UceUtilsProxy() {
+        @Override
+        public boolean isPresenceCapExchangeEnabled(Context context, int subId) {
+            return UceUtils.isPresenceCapExchangeEnabled(context, subId);
+        }
+
+        @Override
+        public boolean isPresenceSupported(Context context, int subId) {
+            return UceUtils.isPresenceSupported(context, subId);
+        }
+
+        @Override
+        public boolean isSipOptionsSupported(Context context, int subId) {
+            return UceUtils.isSipOptionsSupported(context, subId);
+        }
+
+        @Override
+        public boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
+            return UceUtils.isPresenceGroupSubscribeEnabled(context, subId);
+        }
+
+        @Override
+        public int getRclMaxNumberEntries(int subId) {
+            return UceUtils.getRclMaxNumberEntries(subId);
+        }
+
+        @Override
+        public boolean isNumberBlocked(Context context, String phoneNumber) {
+            return UceUtils.isNumberBlocked(context, phoneNumber);
+        }
+    };
+
+    @VisibleForTesting
+    public void setsUceUtilsProxy(UceUtilsProxy uceUtilsProxy) {
+        sUceUtilsProxy = uceUtilsProxy;
+    }
+
+    /**
+     * The callback interface to receive the request and the result from the UceRequest.
+     */
+    public interface RequestManagerCallback {
+        /**
+         * Notify sending the UceRequest
+         */
+        void notifySendingRequest(long coordinator, long taskId, long delayTimeMs);
+
+        /**
+         * Retrieve the contact capabilities from the cache.
+         */
+        List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uriList);
+
+        /**
+         * Retrieve the contact availability from the cache.
+         */
+        EabCapabilityResult getAvailabilityFromCache(Uri uri);
+
+        /**
+         * Store the given contact capabilities to the cache.
+         */
+        void saveCapabilities(List<RcsContactUceCapability> contactCapabilities);
+
+        /**
+         * Retrieve the device's capabilities.
+         */
+        RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int capMechanism);
+
+        /**
+         * Get the device state to check whether the device is disallowed by the network or not.
+         */
+        DeviceStateResult getDeviceState();
+
+        /**
+         * Refresh the device state. It is called when receive the UCE request response.
+         */
+        void refreshDeviceState(int sipCode, String reason);
+
+        /**
+         * Notify that the UceRequest associated with the given taskId encounters error.
+         */
+        void notifyRequestError(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that the UceRequest received the onCommandError callback from the ImsService.
+         */
+        void notifyCommandError(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that the UceRequest received the onNetworkResponse callback from the ImsService.
+         */
+        void notifyNetworkResponse(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that the UceRequest received the onTerminated callback from the ImsService.
+         */
+        void notifyTerminated(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that some contacts are not RCS anymore. It will updated the cached capabilities
+         * and trigger the callback IRcsUceControllerCallback#onCapabilitiesReceived
+         */
+        void notifyResourceTerminated(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that the capabilities updates. It will update the cached and trigger the callback
+         * IRcsUceControllerCallback#onCapabilitiesReceived
+         */
+        void notifyCapabilitiesUpdated(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that some of the request capabilities can be retrieved from the cached.
+         */
+        void notifyCachedCapabilitiesUpdated(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that all the requested capabilities can be retrieved from the cache. It does not
+         * need to request capabilities from the network.
+         */
+        void notifyNoNeedRequestFromNetwork(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that the remote options request is done. This is sent by RemoteOptionsRequest and
+         * it will notify the RemoteOptionsCoordinator to handle it.
+         */
+        void notifyRemoteRequestDone(long requestCoordinatorId, long taskId);
+
+        /**
+         * Set the timer for the request timeout. It will cancel the request when the time is up.
+         */
+        void setRequestTimeoutTimer(long requestCoordinatorId, long taskId, long timeoutAfterMs);
+
+        /**
+         * Remove the timeout timer of the capabilities request.
+         */
+        void removeRequestTimeoutTimer(long taskId);
+
+        /**
+         * Notify that the UceRequest has finished. This is sent by UceRequestCoordinator.
+         */
+        void notifyUceRequestFinished(long requestCoordinatorId, long taskId);
+
+        /**
+         * Notify that the RequestCoordinator has finished. This is sent by UceRequestCoordinator
+         * to remove the coordinator from the UceRequestRepository.
+         */
+        void notifyRequestCoordinatorFinished(long requestCoordinatorId);
+    }
+
+    private RequestManagerCallback mRequestMgrCallback = new RequestManagerCallback() {
+        @Override
+        public void notifySendingRequest(long coordinatorId, long taskId, long delayTimeMs) {
+            mHandler.sendRequestMessage(coordinatorId, taskId, delayTimeMs);
+        }
+
+        @Override
+        public List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uriList) {
+            return mControllerCallback.getCapabilitiesFromCache(uriList);
+        }
+
+        @Override
+        public EabCapabilityResult getAvailabilityFromCache(Uri uri) {
+            return mControllerCallback.getAvailabilityFromCache(uri);
+        }
+
+        @Override
+        public void saveCapabilities(List<RcsContactUceCapability> contactCapabilities) {
+            mControllerCallback.saveCapabilities(contactCapabilities);
+        }
+
+        @Override
+        public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
+            return mControllerCallback.getDeviceCapabilities(mechanism);
+        }
+
+        @Override
+        public DeviceStateResult getDeviceState() {
+            return mControllerCallback.getDeviceState();
+        }
+
+        @Override
+        public void refreshDeviceState(int sipCode, String reason) {
+            mControllerCallback.refreshDeviceState(sipCode, reason);
+        }
+
+        @Override
+        public void notifyRequestError(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_ERROR);
+        }
+
+        @Override
+        public void notifyCommandError(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR);
+        }
+
+        @Override
+        public void notifyNetworkResponse(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE);
+        }
+        @Override
+        public void notifyTerminated(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_TERMINATED);
+        }
+        @Override
+        public void notifyResourceTerminated(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED);
+        }
+        @Override
+        public void notifyCapabilitiesUpdated(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE);
+        }
+
+        @Override
+        public void notifyCachedCapabilitiesUpdated(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
+        }
+
+        @Override
+        public void notifyNoNeedRequestFromNetwork(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
+        }
+
+        @Override
+        public void notifyRemoteRequestDone(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+                    UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE);
+        }
+
+        @Override
+        public void setRequestTimeoutTimer(long coordinatorId, long taskId, long timeoutAfterMs) {
+            mHandler.sendRequestTimeoutTimerMessage(coordinatorId, taskId, timeoutAfterMs);
+        }
+
+        @Override
+        public void removeRequestTimeoutTimer(long taskId) {
+            mHandler.removeRequestTimeoutTimer(taskId);
+        }
+
+        @Override
+        public void notifyUceRequestFinished(long requestCoordinatorId, long taskId) {
+            mHandler.sendRequestFinishedMessage(requestCoordinatorId, taskId);
+        }
+
+        @Override
+        public void notifyRequestCoordinatorFinished(long requestCoordinatorId) {
+            mHandler.sendRequestCoordinatorFinishedMessage(requestCoordinatorId);
+        }
+    };
+
+    private final int mSubId;
+    private final Context mContext;
+    private final UceRequestHandler mHandler;
+    private final UceRequestRepository mRequestRepository;
+    private volatile boolean mIsDestroyed;
+
+    private OptionsController mOptionsCtrl;
+    private SubscribeController mSubscribeCtrl;
+    private UceControllerCallback mControllerCallback;
+
+    public UceRequestManager(Context context, int subId, Looper looper, UceControllerCallback c) {
+        mSubId = subId;
+        mContext = context;
+        mControllerCallback = c;
+        mHandler = new UceRequestHandler(this, looper);
+        mRequestRepository = new UceRequestRepository(subId, mRequestMgrCallback);
+        logi("create");
+    }
+
+    @VisibleForTesting
+    public UceRequestManager(Context context, int subId, Looper looper, UceControllerCallback c,
+            UceRequestRepository requestRepository) {
+        mSubId = subId;
+        mContext = context;
+        mControllerCallback = c;
+        mHandler = new UceRequestHandler(this, looper);
+        mRequestRepository = requestRepository;
+    }
+
+    /**
+     * Set the OptionsController for requestiong capabilities by OPTIONS mechanism.
+     */
+    public void setOptionsController(OptionsController controller) {
+        mOptionsCtrl = controller;
+    }
+
+    /**
+     * Set the SubscribeController for requesting capabilities by Subscribe mechanism.
+     */
+    public void setSubscribeController(SubscribeController controller) {
+        mSubscribeCtrl = controller;
+    }
+
+    /**
+     * Notify that the request manager instance is destroyed.
+     */
+    public void onDestroy() {
+        logi("onDestroy");
+        mIsDestroyed = true;
+        mHandler.onDestroy();
+        mRequestRepository.onDestroy();
+    }
+
+    /**
+     * Send a new capability request. It is called by UceController.
+     */
+    public void sendCapabilityRequest(List<Uri> uriList, boolean skipFromCache,
+            IRcsUceControllerCallback callback) throws RemoteException {
+        if (mIsDestroyed) {
+            callback.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+            return;
+        }
+        sendRequestInternal(UceRequest.REQUEST_TYPE_CAPABILITY, uriList, skipFromCache, callback);
+    }
+
+    /**
+     * Send a new availability request. It is called by UceController.
+     */
+    public void sendAvailabilityRequest(Uri uri, IRcsUceControllerCallback callback)
+            throws RemoteException {
+        if (mIsDestroyed) {
+            callback.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+            return;
+        }
+        sendRequestInternal(UceRequest.REQUEST_TYPE_AVAILABILITY,
+                Collections.singletonList(uri), false /* skipFromCache */, callback);
+    }
+
+    private void sendRequestInternal(@UceRequestType int type, List<Uri> uriList,
+            boolean skipFromCache, IRcsUceControllerCallback callback) throws RemoteException {
+        UceRequestCoordinator requestCoordinator = null;
+        if (sUceUtilsProxy.isPresenceCapExchangeEnabled(mContext, mSubId) &&
+                sUceUtilsProxy.isPresenceSupported(mContext, mSubId)) {
+            requestCoordinator = createSubscribeRequestCoordinator(type, uriList, skipFromCache,
+                    callback);
+        } else if (sUceUtilsProxy.isSipOptionsSupported(mContext, mSubId)) {
+            requestCoordinator = createOptionsRequestCoordinator(type, uriList, callback);
+        }
+
+        if (requestCoordinator == null) {
+            logw("sendRequestInternal: Neither Presence nor OPTIONS are supported");
+            callback.onError(RcsUceAdapter.ERROR_NOT_ENABLED, 0L);
+            return;
+        }
+
+        StringBuilder builder = new StringBuilder("sendRequestInternal: ");
+        builder.append("requestType=").append(type)
+                .append(", requestCoordinatorId=").append(requestCoordinator.getCoordinatorId())
+                .append(", taskId={")
+                .append(requestCoordinator.getActivatedRequestTaskIds().stream()
+                        .map(Object::toString).collect(Collectors.joining(","))).append("}");
+        logd(builder.toString());
+
+        // Add this RequestCoordinator to the UceRequestRepository.
+        addRequestCoordinator(requestCoordinator);
+    }
+
+    private UceRequestCoordinator createSubscribeRequestCoordinator(final @UceRequestType int type,
+            final List<Uri> uriList, boolean skipFromCache, IRcsUceControllerCallback callback) {
+        SubscribeRequestCoordinator.Builder builder;
+
+        if (!sUceUtilsProxy.isPresenceGroupSubscribeEnabled(mContext, mSubId)) {
+            // When the group subscribe is disabled, each contact is required to be encapsulated
+            // into individual UceRequest.
+            List<UceRequest> requestList = new ArrayList<>();
+            uriList.forEach(uri -> {
+                List<Uri> individualUri = Collections.singletonList(uri);
+                UceRequest request = createSubscribeRequest(type, individualUri, skipFromCache);
+                requestList.add(request);
+            });
+            builder = new SubscribeRequestCoordinator.Builder(mSubId, requestList,
+                    mRequestMgrCallback);
+            builder.setCapabilitiesCallback(callback);
+        } else {
+            // Even when the group subscribe is supported by the network, the number of contacts in
+            // a UceRequest still cannot exceed the maximum.
+            List<UceRequest> requestList = new ArrayList<>();
+            final int rclMaxNumber = sUceUtilsProxy.getRclMaxNumberEntries(mSubId);
+            int numRequestCoordinators = uriList.size() / rclMaxNumber;
+            for (int count = 0; count < numRequestCoordinators; count++) {
+                List<Uri> subUriList = new ArrayList<>();
+                for (int index = 0; index < rclMaxNumber; index++) {
+                    subUriList.add(uriList.get(count * rclMaxNumber + index));
+                }
+                requestList.add(createSubscribeRequest(type, subUriList, skipFromCache));
+            }
+
+            List<Uri> subUriList = new ArrayList<>();
+            for (int i = numRequestCoordinators * rclMaxNumber; i < uriList.size(); i++) {
+                subUriList.add(uriList.get(i));
+            }
+            requestList.add(createSubscribeRequest(type, subUriList, skipFromCache));
+
+            builder = new SubscribeRequestCoordinator.Builder(mSubId, requestList,
+                    mRequestMgrCallback);
+            builder.setCapabilitiesCallback(callback);
+        }
+        return builder.build();
+    }
+
+    private UceRequestCoordinator createOptionsRequestCoordinator(@UceRequestType int type,
+            List<Uri> uriList, IRcsUceControllerCallback callback) {
+        OptionsRequestCoordinator.Builder builder;
+        List<UceRequest> requestList = new ArrayList<>();
+        uriList.forEach(uri -> {
+            List<Uri> individualUri = Collections.singletonList(uri);
+            UceRequest request = createOptionsRequest(type, individualUri, false);
+            requestList.add(request);
+        });
+        builder = new OptionsRequestCoordinator.Builder(mSubId, requestList, mRequestMgrCallback);
+        builder.setCapabilitiesCallback(callback);
+        return builder.build();
+    }
+
+    private CapabilityRequest createSubscribeRequest(int type, List<Uri> uriList,
+            boolean skipFromCache) {
+        CapabilityRequest request = new SubscribeRequest(mSubId, type, mRequestMgrCallback,
+                mSubscribeCtrl);
+        request.setContactUri(uriList);
+        request.setSkipGettingFromCache(skipFromCache);
+        return request;
+    }
+
+    private CapabilityRequest createOptionsRequest(int type, List<Uri> uriList,
+            boolean skipFromCache) {
+        CapabilityRequest request = new OptionsRequest(mSubId, type, mRequestMgrCallback,
+                mOptionsCtrl);
+        request.setContactUri(uriList);
+        request.setSkipGettingFromCache(skipFromCache);
+        return request;
+    }
+
+    /**
+     * Retrieve the device's capabilities. This request is from the ImsService to send the
+     * capabilities to the remote side.
+     */
+    public void retrieveCapabilitiesForRemote(Uri contactUri, List<String> remoteCapabilities,
+            IOptionsRequestCallback requestCallback) {
+        RemoteOptionsRequest request = new RemoteOptionsRequest(mSubId, mRequestMgrCallback);
+        request.setContactUri(Collections.singletonList(contactUri));
+        request.setRemoteFeatureTags(remoteCapabilities);
+
+        // If the remote number is blocked, do not send capabilities back.
+        String number = getNumberFromUri(contactUri);
+        if (!TextUtils.isEmpty(number)) {
+            request.setIsRemoteNumberBlocked(sUceUtilsProxy.isNumberBlocked(mContext, number));
+        }
+
+        // Create the RemoteOptionsCoordinator instance
+        RemoteOptionsCoordinator.Builder CoordBuilder = new RemoteOptionsCoordinator.Builder(
+                mSubId, Collections.singletonList(request), mRequestMgrCallback);
+        CoordBuilder.setOptionsRequestCallback(requestCallback);
+        RemoteOptionsCoordinator requestCoordinator = CoordBuilder.build();
+
+        StringBuilder builder = new StringBuilder("retrieveCapabilitiesForRemote: ");
+        builder.append("requestCoordinatorId ").append(requestCoordinator.getCoordinatorId())
+                .append(", taskId={")
+                .append(requestCoordinator.getActivatedRequestTaskIds().stream()
+                        .map(Object::toString).collect(Collectors.joining(","))).append("}");
+        logd(builder.toString());
+
+        // Add this RequestCoordinator to the UceRequestRepository.
+        addRequestCoordinator(requestCoordinator);
+    }
+
+    private static class UceRequestHandler extends Handler {
+        private static final int EVENT_EXECUTE_REQUEST = 1;
+        private static final int EVENT_REQUEST_UPDATED = 2;
+        private static final int EVENT_REQUEST_TIMEOUT = 3;
+        private static final int EVENT_REQUEST_FINISHED = 4;
+        private static final int EVENT_COORDINATOR_FINISHED = 5;
+
+        private final Map<Long, SomeArgs> mRequestTimeoutTimers;
+        private final WeakReference<UceRequestManager> mUceRequestMgrRef;
+
+        public UceRequestHandler(UceRequestManager requestManager, Looper looper) {
+            super(looper);
+            mRequestTimeoutTimers = new HashMap<>();
+            mUceRequestMgrRef = new WeakReference<>(requestManager);
+        }
+
+        /**
+         * Send the capabilities request message.
+         */
+        public void sendRequestMessage(Long coordinatorId, Long taskId, long delayTimeMs) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = coordinatorId;
+            args.arg2 = taskId;
+
+            Message message = obtainMessage();
+            message.what = EVENT_EXECUTE_REQUEST;
+            message.obj = args;
+            sendMessageDelayed(message, delayTimeMs);
+        }
+
+        /**
+         * Send the Uce request updated message.
+         */
+        public void sendRequestUpdatedMessage(Long coordinatorId, Long taskId,
+                @UceRequestUpdate int requestEvent) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = coordinatorId;
+            args.arg2 = taskId;
+            args.argi1 = requestEvent;
+
+            Message message = obtainMessage();
+            message.what = EVENT_REQUEST_UPDATED;
+            message.obj = args;
+            sendMessage(message);
+        }
+
+        /**
+         * Set the timeout timer to cancel the capabilities request.
+         */
+        public void sendRequestTimeoutTimerMessage(Long coordId, Long taskId, Long timeoutAfterMs) {
+            synchronized (mRequestTimeoutTimers) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = coordId;
+                args.arg2 = taskId;
+
+                // Add the message object to the collection. It can be used to find this message
+                // when the request is completed and remove the timeout timer.
+                mRequestTimeoutTimers.put(taskId, args);
+
+                Message message = obtainMessage();
+                message.what = EVENT_REQUEST_TIMEOUT;
+                message.obj = args;
+                sendMessageDelayed(message, timeoutAfterMs);
+            }
+        }
+
+        /**
+         * Remove the timeout timer because the capabilities request is finished.
+         */
+        public void removeRequestTimeoutTimer(Long taskId) {
+            synchronized (mRequestTimeoutTimers) {
+                SomeArgs args = mRequestTimeoutTimers.remove(taskId);
+                if (args == null) {
+                    return;
+                }
+                Log.d(LOG_TAG, "removeRequestTimeoutTimer: taskId=" + taskId);
+                removeMessages(EVENT_REQUEST_TIMEOUT, args);
+                args.recycle();
+            }
+        }
+
+        public void sendRequestFinishedMessage(Long coordinatorId, Long taskId) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = coordinatorId;
+            args.arg2 = taskId;
+
+            Message message = obtainMessage();
+            message.what = EVENT_REQUEST_FINISHED;
+            message.obj = args;
+            sendMessage(message);
+        }
+
+        /**
+         * Finish the UceRequestCoordinator associated with the given id.
+         */
+        public void sendRequestCoordinatorFinishedMessage(Long coordinatorId) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = coordinatorId;
+
+            Message message = obtainMessage();
+            message.what = EVENT_COORDINATOR_FINISHED;
+            message.obj = args;
+            sendMessage(message);
+        }
+
+        /**
+         * Remove all the messages from the handler
+         */
+        public void onDestroy() {
+            removeCallbacksAndMessages(null);
+            // Recycle all the arguments in the mRequestTimeoutTimers
+            synchronized (mRequestTimeoutTimers) {
+                mRequestTimeoutTimers.forEach((taskId, args) -> {
+                    try {
+                        args.recycle();
+                    } catch (Exception e) {}
+                });
+                mRequestTimeoutTimers.clear();
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            UceRequestManager requestManager = mUceRequestMgrRef.get();
+            if (requestManager == null) {
+                return;
+            }
+            SomeArgs args = (SomeArgs) msg.obj;
+            final Long coordinatorId = (Long) args.arg1;
+            final Long taskId = (Long) Optional.ofNullable(args.arg2).orElse(-1L);
+            final Integer requestEvent = Optional.of(args.argi1).orElse(-1);
+            args.recycle();
+
+            requestManager.logd("handleMessage: " + EVENT_DESCRIPTION.get(msg.what)
+                    + ", coordinatorId=" + coordinatorId + ", taskId=" + taskId);
+            switch (msg.what) {
+                case EVENT_EXECUTE_REQUEST: {
+                    UceRequest request = requestManager.getUceRequest(taskId);
+                    if (request == null) {
+                        requestManager.logw("handleMessage: cannot find request, taskId=" + taskId);
+                        return;
+                    }
+                    request.executeRequest();
+                    break;
+                }
+                case EVENT_REQUEST_UPDATED: {
+                    UceRequestCoordinator requestCoordinator =
+                            requestManager.getRequestCoordinator(coordinatorId);
+                    if (requestCoordinator == null) {
+                        requestManager.logw("handleMessage: cannot find UceRequestCoordinator");
+                        return;
+                    }
+                    requestCoordinator.onRequestUpdated(taskId, requestEvent);
+                    break;
+                }
+                case EVENT_REQUEST_TIMEOUT: {
+                    UceRequestCoordinator requestCoordinator =
+                            requestManager.getRequestCoordinator(coordinatorId);
+                    if (requestCoordinator == null) {
+                        requestManager.logw("handleMessage: cannot find UceRequestCoordinator");
+                        return;
+                    }
+                    // The timeout timer is triggered, remove this record from the collection.
+                    synchronized (mRequestTimeoutTimers) {
+                        mRequestTimeoutTimers.remove(taskId);
+                    }
+                    // Notify that the request is timeout.
+                    requestCoordinator.onRequestUpdated(taskId,
+                            UceRequestCoordinator.REQUEST_UPDATE_TIMEOUT);
+                    break;
+                }
+                case EVENT_REQUEST_FINISHED: {
+                    // Notify the repository that the request is finished.
+                    requestManager.notifyRepositoryRequestFinished(taskId);
+                    break;
+                }
+                case EVENT_COORDINATOR_FINISHED: {
+                    UceRequestCoordinator requestCoordinator =
+                            requestManager.removeRequestCoordinator(coordinatorId);
+                    if (requestCoordinator != null) {
+                        requestCoordinator.onFinish();
+                    }
+                    break;
+                }
+                default: {
+                    break;
+                }
+            }
+        }
+
+        private static Map<Integer, String> EVENT_DESCRIPTION = new HashMap<>();
+        static {
+            EVENT_DESCRIPTION.put(EVENT_EXECUTE_REQUEST, "EXECUTE_REQUEST");
+            EVENT_DESCRIPTION.put(EVENT_REQUEST_UPDATED, "REQUEST_UPDATE");
+            EVENT_DESCRIPTION.put(EVENT_REQUEST_TIMEOUT, "REQUEST_TIMEOUT");
+            EVENT_DESCRIPTION.put(EVENT_REQUEST_FINISHED, "REQUEST_FINISHED");
+            EVENT_DESCRIPTION.put(EVENT_COORDINATOR_FINISHED, "REMOVE_COORDINATOR");
+        }
+    }
+
+    private void addRequestCoordinator(UceRequestCoordinator coordinator) {
+        mRequestRepository.addRequestCoordinator(coordinator);
+    }
+
+    private UceRequestCoordinator removeRequestCoordinator(Long coordinatorId) {
+        return mRequestRepository.removeRequestCoordinator(coordinatorId);
+    }
+
+    private UceRequestCoordinator getRequestCoordinator(Long coordinatorId) {
+        return mRequestRepository.getRequestCoordinator(coordinatorId);
+    }
+
+    private UceRequest getUceRequest(Long taskId) {
+        return mRequestRepository.getUceRequest(taskId);
+    }
+
+    private void notifyRepositoryRequestFinished(Long taskId) {
+        mRequestRepository.notifyRequestFinished(taskId);
+    }
+
+    @VisibleForTesting
+    public UceRequestHandler getUceRequestHandler() {
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    public RequestManagerCallback getRequestManagerCallback() {
+        return mRequestMgrCallback;
+    }
+
+    private void logi(String log) {
+        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+    }
+
+    private StringBuilder getLogPrefix() {
+        StringBuilder builder = new StringBuilder("[");
+        builder.append(mSubId);
+        builder.append("] ");
+        return builder;
+    }
+
+    private String getNumberFromUri(Uri uri) {
+        if (uri == null) return null;
+        String number = uri.getSchemeSpecificPart();
+        String[] numberParts = number.split("[@;:]");
+
+        if (numberParts.length == 0) {
+            return null;
+        }
+        return numberParts[0];
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java b/src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java
new file mode 100644
index 0000000..1d2c1e8
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class is responsible for storing the capabilities request.
+ */
+public class UceRequestRepository {
+
+    // Dispatch the UceRequest to be executed.
+    private final UceRequestDispatcher mDispatcher;
+
+    // Store all the capabilities requests
+    private final Map<Long, UceRequestCoordinator> mRequestCoordinators;
+
+    private volatile boolean mDestroyed = false;
+
+    public UceRequestRepository(int subId, RequestManagerCallback callback) {
+        mRequestCoordinators = new HashMap<>();
+        mDispatcher = new UceRequestDispatcher(subId, callback);
+    }
+
+    /**
+     * Clear the collection when the instance is destroyed.
+     */
+    public synchronized void onDestroy() {
+        mDestroyed = true;
+        mDispatcher.onDestroy();
+        mRequestCoordinators.forEach((taskId, requestCoord) -> requestCoord.onFinish());
+        mRequestCoordinators.clear();
+    }
+
+    /**
+     * Add new UceRequestCoordinator and notify the RequestDispatcher to check whether the given
+     * requests can be executed or not.
+     */
+    public synchronized void addRequestCoordinator(UceRequestCoordinator coordinator) {
+        if (mDestroyed) return;
+        mRequestCoordinators.put(coordinator.getCoordinatorId(), coordinator);
+        mDispatcher.addRequest(coordinator.getCoordinatorId(),
+                coordinator.getActivatedRequestTaskIds());
+    }
+
+    /**
+     * Remove the RequestCoordinator from the RequestCoordinator collection.
+     */
+    public synchronized UceRequestCoordinator removeRequestCoordinator(Long coordinatorId) {
+        return mRequestCoordinators.remove(coordinatorId);
+
+    }
+
+    /**
+     * Retrieve the RequestCoordinator associated with the given coordinatorId.
+     */
+    public synchronized UceRequestCoordinator getRequestCoordinator(Long coordinatorId) {
+        return mRequestCoordinators.get(coordinatorId);
+    }
+
+    public synchronized UceRequest getUceRequest(Long taskId) {
+        for (UceRequestCoordinator coordinator : mRequestCoordinators.values()) {
+            UceRequest request = coordinator.getUceRequest(taskId);
+            if (request != null) {
+                return request;
+            }
+        }
+        return null;
+    }
+
+    // Notify that the task is finished.
+    public synchronized void notifyRequestFinished(Long taskId) {
+        mDispatcher.onRequestFinished(taskId);
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/util/FeatureTags.java b/src/java/com/android/ims/rcs/uce/util/FeatureTags.java
new file mode 100644
index 0000000..bba51fb
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/util/FeatureTags.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.util;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.SourceType;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The util class of the feature tags.
+ */
+public class FeatureTags {
+
+    public static final String FEATURE_TAG_STANDALONE_MSG =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-"
+                    + "service.ims.icsi.oma.cpm.msg,urn%3Aurn-7%3A3gpp-"
+                    + "service.ims.icsi.oma.cpm.largemsg,urn%3Aurn-7%3A3gpp-"
+                    + "service.ims.icsi.oma.cpm.deferred\";+g.gsma.rcs.cpm.pager-large";
+
+    public static final String FEATURE_TAG_CHAT_IM =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcse.im\"";
+
+    public static final String FEATURE_TAG_CHAT_SESSION =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+
+    public static final String FEATURE_TAG_FILE_TRANSFER =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+
+    public static final String FEATURE_TAG_FILE_TRANSFER_VIA_SMS =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.ftsms\"";
+
+    public static final String FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.callcomposer\"";
+
+    public static final String FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY = "+g.gsma.callcomposer";
+
+    public static final String FEATURE_TAG_POST_CALL =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.callunanswered\"";
+
+    public static final String FEATURE_TAG_SHARED_MAP =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.sharedmap\"";
+
+    public static final String FEATURE_TAG_SHARED_SKETCH =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.sharedsketch\"";
+
+    public static final String FEATURE_TAG_GEO_PUSH =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.geopush\"";
+
+    public static final String FEATURE_TAG_GEO_PUSH_VIA_SMS =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.geosms\"";
+
+    public static final String FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot\"";
+
+    public static final String FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot.sa\"";
+
+    public static final String FEATURE_TAG_CHATBOT_VERSION_SUPPORTED =
+            "+g.gsma.rcs.botversion=\"#=1,#=2\"";
+
+    public static final String FEATURE_TAG_CHATBOT_ROLE = "+g.gsma.rcs.isbot";
+
+    public static final String FEATURE_TAG_MMTEL =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";
+
+    public static final String FEATURE_TAG_VIDEO = "video";
+
+    public static final String FEATURE_TAG_PRESENCE =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcse.dp\"";
+
+    /**
+     * Add the feature tags to the given RcsContactUceCapability OPTIONS builder.
+     * @param optionsBuilder The OptionsBuilder to add the feature tags
+     * @param mmtelAudioSupport If the audio capability is supported
+     * @param mmtelVideoSupport If the video capability is supported
+     * @param presenceSupport If presence is also supported
+     * @param callComposerSupport If call composer via telephony is supported
+     * @param registrationTags The other feature tags included in the IMS registration.
+     */
+    public static void addFeatureTags(final OptionsBuilder optionsBuilder,
+            boolean mmtelAudioSupport, boolean mmtelVideoSupport,
+            boolean presenceSupport, boolean callComposerSupport, Set<String> registrationTags) {
+        if (presenceSupport) {
+            registrationTags.add(FEATURE_TAG_PRESENCE);
+        } else {
+            registrationTags.remove(FEATURE_TAG_PRESENCE);
+        }
+        if (mmtelAudioSupport && mmtelVideoSupport) {
+            registrationTags.add(FEATURE_TAG_MMTEL);
+            registrationTags.add(FEATURE_TAG_VIDEO);
+        } else if (mmtelAudioSupport) {
+            registrationTags.add(FEATURE_TAG_MMTEL);
+            registrationTags.remove(FEATURE_TAG_VIDEO);
+        } else {
+            registrationTags.remove(FEATURE_TAG_MMTEL);
+            registrationTags.remove(FEATURE_TAG_VIDEO);
+        }
+        if (callComposerSupport) {
+            registrationTags.add(FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY);
+        } else {
+            registrationTags.remove(FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY);
+        }
+        if (!registrationTags.isEmpty()) {
+            optionsBuilder.addFeatureTags(registrationTags);
+        }
+    }
+
+    /**
+     * Get RcsContactUceCapabilities from the given feature tags.
+     */
+    public static RcsContactUceCapability getContactCapability(Uri contact,
+            @SourceType int sourceType, List<String> featureTags) {
+        OptionsBuilder builder = new OptionsBuilder(contact, sourceType);
+        builder.setRequestResult(RcsContactUceCapability.REQUEST_RESULT_FOUND);
+        featureTags.forEach(feature -> builder.addFeatureTag(feature));
+        return builder.build();
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java b/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java
new file mode 100644
index 0000000..931976a
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java
@@ -0,0 +1,96 @@
+/*
+ * 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.ims.rcs.uce.util;
+
+import android.telephony.ims.RcsUceAdapter;
+
+/**
+ * Define the network sip code and the reason.
+ */
+public class NetworkSipCode {
+    public static final int SIP_CODE_OK = 200;
+    public static final int SIP_CODE_ACCEPTED = 202;
+    public static final int SIP_CODE_BAD_REQUEST = 400;
+    public static final int SIP_CODE_FORBIDDEN = 403;
+    public static final int SIP_CODE_NOT_FOUND = 404;
+    public static final int SIP_CODE_REQUEST_TIMEOUT = 408;
+    public static final int SIP_CODE_INTERVAL_TOO_BRIEF = 423;
+    public static final int SIP_CODE_TEMPORARILY_UNAVAILABLE = 480;
+    public static final int SIP_CODE_BAD_EVENT = 489;
+    public static final int SIP_CODE_BUSY = 486;
+    public static final int SIP_CODE_SERVER_INTERNAL_ERROR = 500;
+    public static final int SIP_CODE_SERVICE_UNAVAILABLE = 503;
+    public static final int SIP_CODE_SERVER_TIMEOUT = 504;
+    public static final int SIP_CODE_BUSY_EVERYWHERE = 600;
+    public static final int SIP_CODE_DECLINE = 603;
+    public static final int SIP_CODE_DOES_NOT_EXIST_ANYWHERE = 604;
+
+    public static final String SIP_OK = "OK";
+    public static final String SIP_ACCEPTED = "Accepted";
+    public static final String SIP_BAD_REQUEST = "Bad Request";
+    public static final String SIP_SERVICE_UNAVAILABLE = "Service Unavailable";
+    public static final String SIP_INTERNAL_SERVER_ERROR = "Internal Server Error";
+    public static final String SIP_NOT_REGISTERED = "User not registered";
+    public static final String SIP_NOT_AUTHORIZED_FOR_PRESENCE = "not authorized for presence";
+
+    /**
+     * Convert the given SIP CODE to the Contact uce capabilities error.
+     * @param sipCode The SIP code of the request response.
+     * @param reason The reason of the request response.
+     * @return The RCS contact UCE capabilities error which is defined in RcsUceAdapter.
+     */
+    public static int getCapabilityErrorFromSipCode(int sipCode, String reason) {
+        int uceError;
+        switch (sipCode) {
+            case NetworkSipCode.SIP_CODE_FORBIDDEN:   // 403
+                if (NetworkSipCode.SIP_NOT_REGISTERED.equalsIgnoreCase(reason)) {
+                    // Not registered with IMS. Device shall register to IMS.
+                    uceError = RcsUceAdapter.ERROR_NOT_REGISTERED;
+                } else if (NetworkSipCode.SIP_NOT_AUTHORIZED_FOR_PRESENCE.equalsIgnoreCase(
+                        reason)) {
+                    // Not provisioned for EAB. Device shall not retry.
+                    uceError = RcsUceAdapter.ERROR_NOT_AUTHORIZED;
+                } else {
+                    // The network has responded SIP 403 error with no reason.
+                    uceError = RcsUceAdapter.ERROR_FORBIDDEN;
+                }
+                break;
+            case NetworkSipCode.SIP_CODE_NOT_FOUND:              // 404
+                uceError = RcsUceAdapter.ERROR_NOT_FOUND;
+                break;
+            case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT:        // 408
+                uceError = RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
+                break;
+            case NetworkSipCode.SIP_CODE_INTERVAL_TOO_BRIEF:     // 423
+                // Rejected by the network because the requested expiry interval is too short.
+                uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+                break;
+            case NetworkSipCode.SIP_CODE_BAD_EVENT:
+                uceError = RcsUceAdapter.ERROR_FORBIDDEN;        // 489
+                break;
+            case NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR:  // 500
+            case NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE:    // 503
+                // The network is temporarily unavailable or busy.
+                uceError = RcsUceAdapter.ERROR_SERVER_UNAVAILABLE;
+                break;
+            default:
+                uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+                break;
+        }
+        return uceError;
+    }
+}
diff --git a/src/java/com/android/ims/rcs/uce/util/UceUtils.java b/src/java/com/android/ims/rcs/uce/util/UceUtils.java
new file mode 100644
index 0000000..6db0d23
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/util/UceUtils.java
@@ -0,0 +1,379 @@
+/*
+ * 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.ims.rcs.uce.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.PersistableBundle;
+import android.preference.PreferenceManager;
+import android.provider.BlockedNumberContract;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ProvisioningManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+public class UceUtils {
+
+    public static final int LOG_SIZE = 20;
+    private static final String LOG_PREFIX = "RcsUce.";
+    private static final String LOG_TAG = LOG_PREFIX + "UceUtils";
+
+    private static final String SHARED_PREF_DEVICE_STATE_KEY = "UceDeviceState";
+
+    private static final int DEFAULT_RCL_MAX_NUM_ENTRIES = 100;
+    private static final long DEFAULT_RCS_PUBLISH_SOURCE_THROTTLE_MS = 60000L;
+    private static final long DEFAULT_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC =
+            TimeUnit.DAYS.toSeconds(30);
+    private static final long DEFAULT_REQUEST_RETRY_INTERVAL_MS = TimeUnit.MINUTES.toMillis(20);
+    private static final long DEFAULT_MINIMUM_REQUEST_RETRY_AFTER_MS = TimeUnit.SECONDS.toMillis(3);
+
+    private static final long DEFAULT_CAP_REQUEST_TIMEOUT_AFTER_MS = TimeUnit.MINUTES.toMillis(1);
+    private static Optional<Long> OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS = Optional.empty();
+
+    // The task ID of the UCE request
+    private static long TASK_ID = 0L;
+
+    // The request coordinator ID
+    private static long REQUEST_COORDINATOR_ID = 0;
+
+    /**
+     * Get the log prefix of RCS UCE
+     */
+    public static String getLogPrefix() {
+        return LOG_PREFIX;
+    }
+
+    /**
+     * Generate the unique UCE request task id.
+     */
+    public static synchronized long generateTaskId() {
+        return ++TASK_ID;
+    }
+
+    /**
+     * Generate the unique request coordinator id.
+     */
+    public static synchronized long generateRequestCoordinatorId() {
+        return ++REQUEST_COORDINATOR_ID;
+    }
+
+    public static boolean isEabProvisioned(Context context, int subId) {
+        boolean isProvisioned = false;
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            Log.w(LOG_TAG, "isEabProvisioned: invalid subscriptionId " + subId);
+            return false;
+        }
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager != null) {
+            PersistableBundle config = configManager.getConfigForSubId(subId);
+            if (config != null && !config.getBoolean(
+                    CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONED_BOOL)) {
+                return true;
+            }
+        }
+        try {
+            ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
+            isProvisioned = manager.getProvisioningIntValue(
+                    ProvisioningManager.KEY_EAB_PROVISIONING_STATUS)
+                    == ProvisioningManager.PROVISIONING_VALUE_ENABLED;
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "isEabProvisioned: exception=" + e.getMessage());
+        }
+        return isProvisioned;
+    }
+
+    /**
+     * Check whether or not this carrier supports the exchange of phone numbers with the carrier's
+     * presence server.
+     */
+    public static boolean isPresenceCapExchangeEnabled(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return false;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return false;
+        }
+        return config.getBoolean(
+                CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_CAPABILITY_EXCHANGE_BOOL);
+    }
+
+    /**
+     * Check if Presence is supported by the carrier.
+     */
+    public static boolean isPresenceSupported(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return false;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return false;
+        }
+        return config.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_PUBLISH_BOOL);
+    }
+
+    /**
+     * Check if SIP OPTIONS is supported by the carrier.
+     */
+    public static boolean isSipOptionsSupported(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return false;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return false;
+        }
+        return config.getBoolean(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL);
+    }
+
+    /**
+     * Check whether the PRESENCE group subscribe is enabled or not.
+     *
+     * @return true when the Presence group subscribe is enabled, false otherwise.
+     */
+    public static boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return false;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return false;
+        }
+        return config.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_GROUP_SUBSCRIBE_BOOL);
+    }
+
+    /**
+     *  Returns {@code true} if {@code phoneNumber} is blocked.
+     *
+     * @param context the context of the caller.
+     * @param phoneNumber the number to check.
+     * @return true if the number is blocked, false otherwise.
+     */
+    public static boolean isNumberBlocked(Context context, String phoneNumber) {
+        int blockStatus;
+        try {
+            blockStatus = BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
+                    context, phoneNumber, null /*extras*/);
+        } catch (Exception e) {
+            return false;
+        }
+        return blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED;
+    }
+
+    /**
+     * Get the minimum time that allow two PUBLISH requests can be executed continuously.
+     *
+     * @param subId The subscribe ID
+     * @return The milliseconds that allowed two consecutive publish request.
+     */
+    public static long getRcsPublishThrottle(int subId) {
+        long throttle = DEFAULT_RCS_PUBLISH_SOURCE_THROTTLE_MS;
+        try {
+            ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
+            long provisioningValue = manager.getProvisioningIntValue(
+                    ProvisioningManager.KEY_RCS_PUBLISH_SOURCE_THROTTLE_MS);
+            if (provisioningValue > 0) {
+                throttle = provisioningValue;
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "getRcsPublishThrottle: exception=" + e.getMessage());
+        }
+        return throttle;
+    }
+
+    /**
+     * Retrieve the maximum number of contacts that is in one Request Contained List(RCL)
+     *
+     * @param subId The subscribe ID
+     * @return The maximum number of contacts.
+     */
+    public static int getRclMaxNumberEntries(int subId) {
+        int maxNumEntries = DEFAULT_RCL_MAX_NUM_ENTRIES;
+        try {
+            ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
+            int provisioningValue = manager.getProvisioningIntValue(
+                    ProvisioningManager.KEY_RCS_MAX_NUM_ENTRIES_IN_RCL);
+            if (provisioningValue > 0) {
+                maxNumEntries = provisioningValue;
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "getRclMaxNumberEntries: exception=" + e.getMessage());
+        }
+        return maxNumEntries;
+    }
+
+    public static long getNonRcsCapabilitiesCacheExpiration(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return DEFAULT_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return DEFAULT_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC;
+        }
+        return config.getInt(
+                CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT);
+    }
+
+    public static boolean isRequestForbiddenBySip489(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return false;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return false;
+        }
+        return config.getBoolean(
+                CarrierConfigManager.Ims.KEY_RCS_REQUEST_FORBIDDEN_BY_SIP_489_BOOL);
+    }
+
+    public static long getRequestRetryInterval(Context context, int subId) {
+        CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+        if (configManager == null) {
+            return DEFAULT_REQUEST_RETRY_INTERVAL_MS;
+        }
+        PersistableBundle config = configManager.getConfigForSubId(subId);
+        if (config == null) {
+            return DEFAULT_REQUEST_RETRY_INTERVAL_MS;
+        }
+        return config.getLong(
+                CarrierConfigManager.Ims.KEY_RCS_REQUEST_RETRY_INTERVAL_MILLIS_LONG);
+    }
+
+    public static boolean saveDeviceStateToPreference(Context context, int subId,
+            DeviceStateResult deviceState) {
+        SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        SharedPreferences.Editor editor = sharedPreferences.edit();
+        editor.putString(getDeviceStateSharedPrefKey(subId),
+                getDeviceStateSharedPrefValue(deviceState));
+        return editor.commit();
+    }
+
+    public static Optional<DeviceStateResult> restoreDeviceState(Context context, int subId) {
+        SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        final String sharedPrefKey = getDeviceStateSharedPrefKey(subId);
+        String sharedPrefValue = sharedPreferences.getString(sharedPrefKey, "");
+        if (TextUtils.isEmpty(sharedPrefValue)) {
+            return Optional.empty();
+        }
+        String[] valueAry = sharedPrefValue.split(",");
+        if (valueAry == null || valueAry.length != 4) {
+            return Optional.empty();
+        }
+        try {
+            int deviceState = Integer.valueOf(valueAry[0]);
+            Optional<Integer> errorCode = (Integer.valueOf(valueAry[1]) == -1L) ?
+                    Optional.empty() : Optional.of(Integer.valueOf(valueAry[1]));
+
+            long retryTimeMillis = Long.valueOf(valueAry[2]);
+            Optional<Instant> retryTime = (retryTimeMillis == -1L) ?
+                    Optional.empty() : Optional.of(Instant.ofEpochMilli(retryTimeMillis));
+
+            long exitStateTimeMillis = Long.valueOf(valueAry[3]);
+            Optional<Instant> exitStateTime = (exitStateTimeMillis == -1L) ?
+                    Optional.empty() : Optional.of(Instant.ofEpochMilli(exitStateTimeMillis));
+
+            return Optional.of(new DeviceStateResult(deviceState, errorCode, retryTime,
+                    exitStateTime));
+        } catch (Exception e) {
+            Log.d(LOG_TAG, "restoreDeviceState: exception " + e);
+            return Optional.empty();
+        }
+    }
+
+    public static boolean removeDeviceStateFromPreference(Context context, int subId) {
+        SharedPreferences sharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        SharedPreferences.Editor editor = sharedPreferences.edit();
+        editor.remove(getDeviceStateSharedPrefKey(subId));
+        return editor.commit();
+    }
+
+    private static String getDeviceStateSharedPrefKey(int subId) {
+        return SHARED_PREF_DEVICE_STATE_KEY + subId;
+    }
+
+    /**
+     * Build the device state preference value.
+     */
+    private static String getDeviceStateSharedPrefValue(DeviceStateResult deviceState) {
+        StringBuilder builder = new StringBuilder();
+        builder.append(deviceState.getDeviceState())  // device state
+                .append(",").append(deviceState.getErrorCode().orElse(-1));  // error code
+
+        long retryTimeMillis = -1L;
+        Optional<Instant> retryTime = deviceState.getRequestRetryTime();
+        if (retryTime.isPresent()) {
+            retryTimeMillis = retryTime.get().toEpochMilli();
+        }
+        builder.append(",").append(retryTimeMillis);   // retryTime
+
+        long exitStateTimeMillis = -1L;
+        Optional<Instant> exitStateTime = deviceState.getExitStateTime();
+        if (exitStateTime.isPresent()) {
+            exitStateTimeMillis = exitStateTime.get().toEpochMilli();
+        }
+        builder.append(",").append(exitStateTimeMillis);   // exit state time
+        return builder.toString();
+    }
+
+    /**
+     * Get the minimum value of the capabilities request retry after.
+     */
+    public static long getMinimumRequestRetryAfterMillis() {
+        return DEFAULT_MINIMUM_REQUEST_RETRY_AFTER_MS;
+    }
+
+    /**
+     * Override the capability request timeout to the millisecond value specified. Sending a
+     * value <= 0 will reset the capabilities.
+     */
+    public static synchronized void setCapRequestTimeoutAfterMillis(long timeoutAfterMs) {
+        if (timeoutAfterMs <= 0L) {
+            OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS = Optional.empty();
+        } else {
+            OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS = Optional.of(timeoutAfterMs);
+        }
+    }
+
+    /**
+     * Get the milliseconds of the capabilities request timed out.
+     * @return the time in milliseconds before a pending capabilities request will time out.
+     */
+    public static synchronized long getCapRequestTimeoutAfterMillis() {
+        if(OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS.isPresent()) {
+            return OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS.get();
+        } else {
+            return DEFAULT_CAP_REQUEST_TIMEOUT_AFTER_MS;
+        }
+    }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 0d0440b..82c303d 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -14,6 +14,10 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 android_test {
     name: "ImsCommonTests",
 
@@ -25,11 +29,17 @@
     libs: [
         "ims-common",
         "android.test.runner",
+        "android.test.mock",
         "android.test.base",
     ],
 
     static_libs: [
+        "androidx.test.ext.junit",
         "androidx.test.rules",
         "mockito-target-minus-junit4",
     ],
+
+    test_suites: [
+        "device-tests"
+    ]
 }
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 706035a..88831aa 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,6 +18,11 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ims.tests">
 
+    <!--  For EabBulkCapabilityUpdaterTest, EabBulkCapabilityUpdater will register content
+     observer to contact provider but currently there is no better way to mock contact provider
+     (registerContentObserver() is final), so require the read_contacts permission to test APK.-->
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
     <application android:label="@string/app_name">
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
new file mode 100644
index 0000000..4610122
--- /dev/null
+++ b/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<configuration description="Runs Frameworks IMS Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="ImsCommonTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="ImsCommonTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.ims.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/src/com/android/ims/ContextFixture.java b/tests/src/com/android/ims/ContextFixture.java
new file mode 100644
index 0000000..e987b38
--- /dev/null
+++ b/tests/src/com/android/ims/ContextFixture.java
@@ -0,0 +1,168 @@
+/*
+ * 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.ims;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.os.PersistableBundle;
+import android.telecom.TelecomManager;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsManager;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+import org.mockito.stubbing.Answer;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+public class ContextFixture {
+
+    private final Context mContext = spy(new FakeContext());
+
+    private final TelephonyManager mTelephonyManager = mock(TelephonyManager.class);
+    private final ConnectivityManager mConnectivityManager = mock(ConnectivityManager.class);
+    private final CarrierConfigManager mCarrierConfigManager = mock(CarrierConfigManager.class);
+    private final PackageManager mPackageManager = mock(PackageManager.class);
+    private final SubscriptionManager mSubscriptionManager = mock(SubscriptionManager.class);
+    private final ImsManager mImsManager = mock(ImsManager.class);
+    private final Resources mResources = mock(Resources.class);
+
+    private final PersistableBundle mBundle = new PersistableBundle();
+    private final HashSet<String> mSystemFeatures = new HashSet<>();
+    private final MockContentResolver mMockContentResolver = new MockContentResolver();
+
+    public ContextFixture() throws Exception {
+        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
+        doReturn(mBundle).when(mCarrierConfigManager).getConfig();
+
+        doAnswer((Answer<Boolean>)
+                invocation -> mSystemFeatures.contains((String) invocation.getArgument(0)))
+                .when(mPackageManager).hasSystemFeature(any());
+
+        doReturn(mResources).when(mPackageManager).getResourcesForApplication(anyString());
+        doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
+    }
+
+    public void destroy() {
+    }
+
+    public class FakeContext extends MockContext {
+        @Override
+        public Resources getResources() {
+            return mResources;
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            switch (name) {
+                case Context.TELEPHONY_SERVICE:
+                    return mTelephonyManager;
+                case Context.CARRIER_CONFIG_SERVICE:
+                    return mCarrierConfigManager;
+                case Context.CONNECTIVITY_SERVICE:
+                    return mConnectivityManager;
+                case Context.TELEPHONY_SUBSCRIPTION_SERVICE:
+                    return mSubscriptionManager;
+                case Context.TELEPHONY_IMS_SERVICE:
+                    return mImsManager;
+                default:
+                    return null;
+            }
+        }
+
+        @Override
+        public String getSystemServiceName(Class<?> serviceClass) {
+            if (serviceClass == SubscriptionManager.class) {
+                return Context.TELEPHONY_SUBSCRIPTION_SERVICE;
+            } else if (serviceClass == TelecomManager.class) {
+                return Context.TELECOM_SERVICE;
+            } else if (serviceClass == ConnectivityManager.class) {
+                return Context.CONNECTIVITY_SERVICE;
+            } else if (serviceClass == TelephonyManager.class) {
+                return Context.TELEPHONY_SERVICE;
+            } else if (serviceClass == ImsManager.class) {
+                return Context.TELEPHONY_IMS_SERVICE;
+            } else if (serviceClass == CarrierConfigManager.class) {
+                return Context.CARRIER_CONFIG_SERVICE;
+            }
+            return super.getSystemServiceName(serviceClass);
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+            return null;
+        }
+
+        @Override
+        public void unregisterReceiver(BroadcastReceiver receiver) {
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mMockContentResolver;
+        }
+
+        @Override
+        public Executor getMainExecutor() {
+            return Runnable::run;
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return mContext;
+        }
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public PersistableBundle getTestCarrierConfigBundle() {
+        return mBundle;
+    }
+
+    public void addSystemFeature(String feature) {
+        mSystemFeatures.add(feature);
+    }
+
+    public void removeSystemFeature(String feature) {
+        mSystemFeatures.remove(feature);
+    }
+}
diff --git a/tests/src/com/android/ims/FeatureConnectionTest.java b/tests/src/com/android/ims/FeatureConnectionTest.java
new file mode 100644
index 0000000..d7a9134
--- /dev/null
+++ b/tests/src/com/android/ims/FeatureConnectionTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.ims;
+
+import junit.framework.AssertionFailedError;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+
+@RunWith(AndroidJUnit4.class)
+public class FeatureConnectionTest extends ImsTestBase {
+
+    private class TestFeatureConnection extends FeatureConnection {
+        private Integer mFeatureState = ImsFeature.STATE_READY;
+
+        public boolean isFeatureCreatedCalled = false;
+        public boolean isFeatureRemovedCalled = false;
+        public int mNewStatus = ImsFeature.STATE_UNAVAILABLE;
+        public long mCapabilities;
+
+        TestFeatureConnection(Context context, int slotId) {
+            super(context, slotId, mConfigBinder, mRegistrationBinder, mSipTransportBinder);
+            if (!ImsManager.isImsSupportedOnDevice(context)) {
+                sImsSupportedOnDevice = false;
+            }
+        }
+
+        @Override
+        public void checkServiceIsReady() throws RemoteException {
+            super.checkServiceIsReady();
+        }
+
+        @Override
+        protected Integer retrieveFeatureState() {
+            return mFeatureState;
+        }
+
+        @Override
+        protected void onFeatureCapabilitiesUpdated(long capabilities) {
+            mCapabilities = capabilities;
+        }
+
+        public void setFeatureState(int state) {
+            mFeatureState = state;
+        }
+    };
+
+    private TestFeatureConnection mTestFeatureConnection;
+    @Mock IBinder mBinder;
+    @Mock IImsRegistration mRegistrationBinder;
+    @Mock IImsConfig mConfigBinder;
+    @Mock ISipTransport mSipTransportBinder;
+
+    public static final int PHONE_ID = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        doReturn(null).when(mContext).getMainLooper();
+        mContextFixture.addSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+
+        mTestFeatureConnection = new TestFeatureConnection(mContext, PHONE_ID);
+        mTestFeatureConnection.setBinder(mBinder);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Test service is ready when binder is alive and IMS status is ready.
+     */
+    @Test
+    @SmallTest
+    public void testServiceIsReady() {
+        when(mBinder.isBinderAlive()).thenReturn(true);
+        mTestFeatureConnection.setFeatureState(ImsFeature.STATE_READY);
+
+        try {
+            mTestFeatureConnection.checkServiceIsReady();
+        } catch (RemoteException e) {
+            throw new AssertionFailedError("Exception in testServiceIsReady: " + e);
+        }
+    }
+
+    /**
+     * Test service is not ready when binder is not alive or status is not ready.
+     */
+    @Test
+    @SmallTest
+    public void testServiceIsNotReady() {
+        // Binder is not alive
+        when(mBinder.isBinderAlive()).thenReturn(false);
+
+        try {
+            mTestFeatureConnection.checkServiceIsReady();
+            throw new AssertionFailedError("testServiceIsNotReady: binder isn't alive");
+        } catch (RemoteException e) {
+            // expected result
+        }
+
+        // IMS feature status is unavailable
+        when(mBinder.isBinderAlive()).thenReturn(true);
+        mTestFeatureConnection.setFeatureState(ImsFeature.STATE_UNAVAILABLE);
+
+        try {
+            mTestFeatureConnection.checkServiceIsReady();
+            throw new AssertionFailedError("testServiceIsNotReady: status unavailable");
+        } catch (RemoteException e) {
+            // expected result
+        }
+    }
+
+    /**
+     * Test registration tech callbacks.
+     */
+    @Test
+    @SmallTest
+    public void testRegistrationTech() throws Exception {
+        when(mRegistrationBinder.getRegistrationTechnology()).thenReturn(
+                ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+        assertEquals(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
+                mTestFeatureConnection.getRegistrationTech());
+
+    }
+
+    /**
+     * Test registration tech callbacks.
+     */
+    @Test
+    @SmallTest
+    public void testUpdateCapabilities() throws Exception {
+        long testCaps = 1;
+        assertEquals(0 /*base state*/, mTestFeatureConnection.mCapabilities);
+        mTestFeatureConnection.updateFeatureCapabilities(testCaps);
+        assertEquals(testCaps, mTestFeatureConnection.mCapabilities);
+
+    }
+}
diff --git a/tests/src/com/android/ims/FeatureConnectorTest.java b/tests/src/com/android/ims/FeatureConnectorTest.java
new file mode 100644
index 0000000..e560ae6
--- /dev/null
+++ b/tests/src/com/android/ims/FeatureConnectorTest.java
@@ -0,0 +1,422 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+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 static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class FeatureConnectorTest extends ImsTestBase {
+
+    private static class TestFeatureConnection extends FeatureConnection {
+
+        public TestFeatureConnection(Context context, int slotId, IImsConfig c,
+                IImsRegistration r, ISipTransport s) {
+            super(context, slotId, c, r, s);
+        }
+
+        @Override
+        protected Integer retrieveFeatureState() {
+            return null;
+        }
+
+        @Override
+        protected void onFeatureCapabilitiesUpdated(long capabilities) {
+        }
+    }
+
+    private static class TestManager implements FeatureUpdates {
+
+        public IImsServiceFeatureCallback callback;
+        public TestFeatureConnection connection;
+        private Context mContext;
+        private int mPhoneId;
+
+
+        public TestManager(Context context, int phoneId) {
+            mContext = context;
+            mPhoneId = phoneId;
+        }
+
+        @Override
+        public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
+            callback = cb;
+        }
+
+        @Override
+        public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
+            callback = null;
+        }
+
+        @Override
+        public void associate(ImsFeatureContainer c) {
+            connection = new TestFeatureConnection(mContext, mPhoneId, c.imsConfig,
+                    c.imsRegistration, c.sipTransport);
+            connection.setBinder(c.imsFeature);
+        }
+
+        @Override
+        public void invalidate() {
+            connection = null;
+        }
+
+        @Override
+        public void updateFeatureState(int state) {
+            assertNotNull(connection);
+            connection.updateFeatureState(state);
+        }
+
+        @Override
+        public void updateFeatureCapabilities(long capabilities) {
+            connection.updateFeatureCapabilities(capabilities);
+        }
+    }
+
+    private FeatureConnector<TestManager> mFeatureConnector;
+    private TestManager mTestManager;
+    @Mock private FeatureConnector.Listener<TestManager> mListener;
+    @Mock private IBinder feature;
+    @Mock private IImsRegistration reg;
+    @Mock private IImsConfig config;
+    @Mock private ISipTransport transport;
+
+    private static final int PHONE_ID = 1;
+    private static final long TEST_CAPS = ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        setImsSupportedFeature(true);
+        mTestManager = new TestManager(mContext, PHONE_ID);
+        when(feature.isBinderAlive()).thenReturn(true);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testConnect() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        assertNotNull(mTestManager.connection);
+        assertEquals(TEST_CAPS, mTestManager.connection.getFeatureCapabilties());
+        verify(mListener, never()).connectionReady(any());
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectNotSupported() {
+        createFeatureConnector();
+        // set not supported
+        setImsSupportedFeature(false);
+
+        mFeatureConnector.connect();
+        assertNull("connect should not the callback registration if not supported",
+                mTestManager.callback);
+        verify(mListener).connectionUnavailable(
+                FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectReadyNotReady() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_UNAVAILABLE);
+        assertNotNull("When not ready, the callback should still be registered",
+                mTestManager.callback);
+        assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_NOT_READY);
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectReadyAndInitializing() throws Exception {
+        ArrayList<Integer> filterList = new ArrayList<>();
+        filterList.add(ImsFeature.STATE_READY);
+        filterList.add(ImsFeature.STATE_INITIALIZING);
+        createFeatureConnector(filterList);
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        verify(mListener, never()).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_INITIALIZING);
+        assertNotNull("When not ready, the callback should still be registered",
+                mTestManager.callback);
+        assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        assertNotNull("When not ready, the callback should still be registered",
+                mTestManager.callback);
+        assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+        // Should not notify ready multiple times
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectReadyAndUnavailable() throws Exception {
+        ArrayList<Integer> filterList = new ArrayList<>();
+        filterList.add(ImsFeature.STATE_READY);
+        filterList.add(ImsFeature.STATE_INITIALIZING);
+        filterList.add(ImsFeature.STATE_UNAVAILABLE);
+        createFeatureConnector(filterList);
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_UNAVAILABLE);
+        assertNotNull("When not ready, the callback should still be registered",
+                mTestManager.callback);
+        assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_INITIALIZING);
+        assertNotNull("When not ready, the callback should still be registered",
+                mTestManager.callback);
+        assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+        // Should not notify ready multiple times
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        // Should not notify ready multiple times
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testCantConnectToServer() throws Exception {
+        ArrayList<Integer> filterList = new ArrayList<>();
+        filterList.add(ImsFeature.STATE_READY);
+        filterList.add(ImsFeature.STATE_INITIALIZING);
+        filterList.add(ImsFeature.STATE_UNAVAILABLE);
+        createFeatureConnector(filterList);
+
+        mFeatureConnector.connect();
+        mTestManager.callback.imsFeatureRemoved(
+                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+        verify(mListener).connectionUnavailable(
+                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+
+        // Clear callback and ensure that the second connect tries to register a callback.
+        mTestManager.registerFeatureCallback(PHONE_ID, null);
+        mFeatureConnector.connect();
+        assertNotNull("The register request should happen the second time as well.",
+                mTestManager.callback);
+        mTestManager.callback.imsFeatureRemoved(
+                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+        // In the special case that UNAVAILABLE_REASON_SERVER_UNAVAILABLE is returned, we should get
+        // an unavailable callback every time because it will require connect to be called again.
+        verify(mListener,times(2)).connectionUnavailable(
+                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectReadyRemovedReady() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener, never()).connectionUnavailable(anyInt());
+
+        mTestManager.callback.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+        assertNotNull("When not ready, the callback should still be registered",
+                mTestManager.callback);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        verify(mListener, times(2)).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectDisconnect() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        IImsServiceFeatureCallback oldCb = mTestManager.callback;
+        TestFeatureConnection testFc = mTestManager.connection;
+
+        mFeatureConnector.disconnect();
+        assertNull(mTestManager.callback);
+        assertNull(mTestManager.connection);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+        // make sure status/caps updates do not trigger more events after disconnect
+        oldCb.imsStatusChanged(ImsFeature.STATE_READY);
+        oldCb.imsStatusChanged(ImsFeature.STATE_UNAVAILABLE);
+        oldCb.updateCapabilities(0);
+        assertEquals(TEST_CAPS, testFc.getFeatureCapabilties());
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectDisconnectConnect() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+
+        mFeatureConnector.disconnect();
+        assertNull(mTestManager.callback);
+        assertNull(mTestManager.connection);
+        verify(mListener).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+        mFeatureConnector.connect();
+        assertNotNull(mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        assertNotNull(mTestManager.connection);
+        verify(mListener, times(2)).connectionReady(mTestManager);
+        verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+    }
+
+    @Test
+    @SmallTest
+    public void testUpdateCapabilities() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        assertEquals(TEST_CAPS, mTestManager.connection.getFeatureCapabilties());
+        mTestManager.callback.updateCapabilities(0);
+        assertEquals(0, mTestManager.connection.getFeatureCapabilties());
+    }
+
+    @Test
+    @SmallTest
+    public void testUpdateStatus() throws Exception {
+        createFeatureConnector();
+        mFeatureConnector.connect();
+        assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+        // simulate callback from ImsResolver
+        mTestManager.callback.imsFeatureCreated(createContainer());
+        mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+        assertEquals(ImsFeature.STATE_READY, mTestManager.connection.getFeatureState());
+    }
+
+    private void setImsSupportedFeature(boolean isSupported) {
+        if(isSupported) {
+            mContextFixture.addSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+        } else {
+            mContextFixture.removeSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+        }
+    }
+
+    private ImsFeatureContainer createContainer() {
+        ImsFeatureContainer c =  new ImsFeatureContainer(feature, config, reg, transport,
+                TEST_CAPS);
+        c.setState(ImsFeature.STATE_UNAVAILABLE);
+        return c;
+    }
+
+    private void createFeatureConnector() {
+        ArrayList<Integer> filter = new ArrayList<>();
+        filter.add(ImsFeature.STATE_READY);
+        createFeatureConnector(filter);
+    }
+
+    private void createFeatureConnector(List<Integer> featureReadyFilter) {
+        mFeatureConnector = new FeatureConnector<>(mContext, PHONE_ID,
+                (c, p) -> mTestManager, "Test", featureReadyFilter, mListener, Runnable::run);
+    }
+}
diff --git a/tests/src/com/android/ims/ImsConfigTest.java b/tests/src/com/android/ims/ImsConfigTest.java
index 63d1433..7ce26dd 100644
--- a/tests/src/com/android/ims/ImsConfigTest.java
+++ b/tests/src/com/android/ims/ImsConfigTest.java
@@ -20,9 +20,9 @@
 import static org.mockito.Mockito.verify;
 
 import android.telephony.ims.aidl.IImsConfig;
-import android.test.suitebuilder.annotation.SmallTest;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java b/tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java
new file mode 100644
index 0000000..273d1dc
--- /dev/null
+++ b/tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+
+import androidx.test.filters.SmallTest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsFeatureBinderRepositoryTest extends ImsTestBase {
+
+    private static final int TEST_PHONE_ID_1 = 1;
+    private static final int TEST_PHONE_ID_2 = 2;
+    private static final long TEST_SERVICE_CAPS = ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL;
+
+    @Mock IBinder mMockMmTelFeatureA;
+    @Mock IBinder mMockMmTelFeatureB;
+    @Mock IBinder mMockRcsFeatureA;
+    @Mock IImsConfig mMockImsConfig;
+    @Mock IImsRegistration mMockImsRegistration;
+    @Mock ISipTransport mMockSipTransport;
+
+    @Mock IImsServiceFeatureCallback mConnectionCallback;
+    @Mock IBinder mConnectionCallbackBinder;
+    @Mock IImsServiceFeatureCallback mConnectionCallback2;
+    @Mock IBinder mConnectionCallback2Binder;
+
+    private ImsFeatureBinderRepository mRepository;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mRepository = new ImsFeatureBinderRepository();
+        when(mConnectionCallbackBinder.isBinderAlive()).thenReturn(true);
+        when(mConnectionCallback2Binder.isBinderAlive()).thenReturn(true);
+        when(mConnectionCallback.asBinder()).thenReturn(mConnectionCallbackBinder);
+        when(mConnectionCallback2.asBinder()).thenReturn(mConnectionCallback2Binder);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testGetInterfaceExists() throws Exception {
+        ImsFeatureContainer fc =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fc);
+        ImsFeatureContainer resultFc =
+                mRepository.getIfExists(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL).orElse(null);
+        assertNotNull("returned connection should not be null!", resultFc);
+        assertEquals("returned connection does not match the set connection",
+                fc, resultFc);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetInterfaceDoesntExist() throws Exception {
+        ImsFeatureContainer fc =
+                mRepository.getIfExists(TEST_PHONE_ID_1,
+                ImsFeature.FEATURE_MMTEL).orElse(null);
+        assertNull("returned connection should be null!", fc);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetInterfaceRemoveDoesntExist() throws Exception {
+        ImsFeatureContainer fc =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fc);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, null);
+        ImsFeatureContainer resultFc =
+                mRepository.getIfExists(TEST_PHONE_ID_1,
+                ImsFeature.FEATURE_MMTEL).orElse(null);
+        assertNull("returned connection should be null!", resultFc);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetInterfaceUpdateExists() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        ImsFeatureContainer fcB =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcB);
+        ImsFeatureContainer resultFc =
+                mRepository.getIfExists(TEST_PHONE_ID_1,
+                        ImsFeature.FEATURE_MMTEL).orElse(null);
+        assertNotNull("returned connection should not be null!", resultFc);
+        assertEquals("returned connection does not match the set connection",
+                fcB, resultFc);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetMultipleInterfacesExists() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        ImsFeatureContainer fcB =
+                getFeatureContainer(mMockRcsFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_RCS, fcB);
+        ImsFeatureContainer resultFcA =
+                mRepository.getIfExists(TEST_PHONE_ID_1,
+                        ImsFeature.FEATURE_MMTEL).orElse(null);
+        ImsFeatureContainer resultFcB =
+                mRepository.getIfExists(TEST_PHONE_ID_1,
+                        ImsFeature.FEATURE_RCS).orElse(null);
+        assertNotNull("returned connection should not be null!", resultFcA);
+        assertNotNull("returned connection should not be null!", resultFcB);
+        assertEquals("returned connection does not match the set connection",
+                fcA, resultFcA);
+        assertEquals("returned connection does not match the set connection",
+                fcB, resultFcB);
+    }
+
+    @Test
+    @SmallTest
+    public void testListenForUpdate() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testListenNoUpdateForStaleListener() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+        when(mConnectionCallbackBinder.isBinderAlive()).thenReturn(false);
+        // Listener is "dead", so we should not get this update
+        mRepository.notifyFeatureStateChanged(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                ImsFeature.STATE_READY);
+        verify(mConnectionCallback, never()).imsStatusChanged(ImsFeature.STATE_READY);
+    }
+
+    @Test
+    @SmallTest
+    public void testListenForUpdateStateChanged() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        ImsFeatureContainer resultFc =
+                mRepository.getIfExists(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL).orElse(null);
+        assertNotNull(resultFc);
+        assertEquals(ImsFeature.STATE_UNAVAILABLE, resultFc.getState());
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+        verify(mConnectionCallback, never()).imsStatusChanged(anyInt());
+
+        mRepository.notifyFeatureStateChanged(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                ImsFeature.STATE_READY);
+        verify(mConnectionCallback).imsStatusChanged(ImsFeature.STATE_READY);
+        assertEquals(ImsFeature.STATE_READY, resultFc.getState());
+    }
+
+    @Test
+    @SmallTest
+    public void testListenForUpdateCapsChanged() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        ImsFeatureContainer resultFc =
+                mRepository.getIfExists(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL).orElse(null);
+        assertNotNull(resultFc);
+        assertEquals(TEST_SERVICE_CAPS, resultFc.getCapabilities());
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+        mRepository.notifyFeatureCapabilitiesChanged(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, 0);
+        verify(mConnectionCallback).updateCapabilities(0);
+        assertEquals(0, resultFc.getCapabilities());
+    }
+
+
+    @Test
+    @SmallTest
+    public void testRemoveCallback() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        ImsFeatureContainer fcB =
+                getFeatureContainer(mMockMmTelFeatureB, TEST_SERVICE_CAPS);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.unregisterForConnectionUpdates(mConnectionCallback);
+
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcB);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verifyFeatureCreatedCalled(0 /*times*/, mConnectionCallback, fcB);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testAddSameCallback() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testListenAfterUpdate() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testListenNoUpdate() throws Exception {
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        verify(mConnectionCallback, never()).imsFeatureCreated(any());
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testListenNull() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        mRepository.removeConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback).imsFeatureRemoved(
+                FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+    }
+
+    @Test
+    @SmallTest
+    public void testMultipleListeners() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        ImsFeatureContainer fcB =
+                getFeatureContainer(mMockRcsFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_RCS,
+                mConnectionCallback2, Runnable::run);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+        verify(mConnectionCallback2, never()).imsFeatureCreated(any());
+        verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_RCS, fcB);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback2, fcB);
+        verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testMultiplePhones() throws Exception {
+        ImsFeatureContainer fcA =
+                getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+        ImsFeatureContainer fcB =
+                getFeatureContainer(mMockRcsFeatureA, TEST_SERVICE_CAPS);
+        mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+                mConnectionCallback, Runnable::run);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+        mRepository.registerForConnectionUpdates(TEST_PHONE_ID_2, ImsFeature.FEATURE_RCS,
+                mConnectionCallback2, Runnable::run);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+        verify(mConnectionCallback2, never()).imsFeatureCreated(any());
+        verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+
+        mRepository.addConnection(TEST_PHONE_ID_2, ImsFeature.FEATURE_RCS, fcB);
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+        verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+        verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback2, fcB);
+        verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+    }
+
+    private void verifyFeatureCreatedCalled(int timesCalled, IImsServiceFeatureCallback cb,
+            ImsFeatureContainer fc) throws Exception {
+        verify(cb, times(timesCalled)).imsFeatureCreated(fc);
+    }
+
+    private ImsFeatureContainer getFeatureContainer(IBinder feature, long caps) {
+        return new ImsFeatureContainer(feature, mMockImsConfig,
+                mMockImsRegistration, mMockSipTransport, caps);
+    }
+}
diff --git a/tests/src/com/android/ims/ImsFeatureContainerTest.java b/tests/src/com/android/ims/ImsFeatureContainerTest.java
new file mode 100644
index 0000000..e6a5997
--- /dev/null
+++ b/tests/src/com/android/ims/ImsFeatureContainerTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipDelegateStateCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsConfigImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsFeatureContainerTest {
+
+    // Use real objects here as I'm not sure how mock IBinders/IInterfaces would parcel.
+    private MmTelFeature mMmTelFeature = new MmTelFeature();
+    private ImsConfigImplBase mImsConfig = new ImsConfigImplBase();
+    private ImsRegistrationImplBase mImsReg = new ImsRegistrationImplBase();
+    private ISipTransport mSipTransport = new ISipTransport.Stub() {
+        @Override
+        public void createSipDelegate(int subId, DelegateRequest request,
+                ISipDelegateStateCallback dc, ISipDelegateMessageCallback mc) {
+        }
+
+        @Override
+        public void destroySipDelegate(ISipDelegate delegate, int reason) {
+        }
+    };
+
+    @Test
+    @SmallTest
+    public void testParcelUnparcel() throws Exception {
+        final int state = ImsFeature.STATE_READY;
+        final long caps = ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL;
+        ImsFeatureContainer c = new ImsFeatureContainer(mMmTelFeature.getBinder().asBinder(),
+                mImsConfig.getIImsConfig(), mImsReg.getBinder(), mSipTransport, caps);
+        c.setState(state);
+
+        ImsFeatureContainer result = parcelUnparcel(c);
+
+        assertEquals(mMmTelFeature.getBinder().asBinder(), result.imsFeature);
+        assertEquals(mImsConfig.getIImsConfig(), result.imsConfig);
+        assertEquals(mImsReg.getBinder(), result.imsRegistration);
+        assertEquals(state, result.getState());
+        assertEquals(caps, result.getCapabilities());
+
+        assertEquals(c, result);
+    }
+
+    public ImsFeatureContainer parcelUnparcel(ImsFeatureContainer data) {
+        Parcel parcel = Parcel.obtain();
+        data.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ImsFeatureContainer unparceledData =
+                ImsFeatureContainer.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        return unparceledData;
+    }
+}
diff --git a/tests/src/com/android/ims/ImsManagerTest.java b/tests/src/com/android/ims/ImsManagerTest.java
new file mode 100644
index 0000000..3db8025
--- /dev/null
+++ b/tests/src/com/android/ims/ImsManagerTest.java
@@ -0,0 +1,907 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.anyInt;
+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 static org.mockito.Mockito.when;
+
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsConfigImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.os.SomeArgs;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Hashtable;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsManagerTest extends ImsTestBase {
+    private static final boolean ENHANCED_4G_MODE_DEFAULT_VAL = true;
+    private static final boolean ENHANCED_4G_MODE_EDITABLE = true;
+    private static final boolean WFC_IMS_ENABLE_DEFAULT_VAL = false;
+    private static final boolean WFC_IMS_ROAMING_ENABLE_DEFAULT_VAL = true;
+    private static final boolean VT_IMS_ENABLE_DEFAULT_VAL = true;
+    private static final boolean WFC_IMS_EDITABLE_VAL = true;
+    private static final boolean WFC_IMS_NOT_EDITABLE_VAL = false;
+    private static final boolean WFC_IMS_ROAMING_EDITABLE_VAL = true;
+    private static final boolean WFC_IMS_ROAMING_NOT_EDITABLE_VAL = false;
+    private static final int WFC_IMS_MODE_DEFAULT_VAL =
+            ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED;
+    private static final int WFC_IMS_ROAMING_MODE_DEFAULT_VAL =
+            ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED;
+    private static final boolean WFC_USE_HOME_MODE_FOR_ROAMING_VAL = true;
+    private static final boolean WFC_NOT_USE_HOME_MODE_FOR_ROAMING_VAL = false;
+
+    PersistableBundle mBundle;
+    @Mock ImsConfigImplBase mImsConfigImplBaseMock;
+    Hashtable<Integer, Integer> mProvisionedIntVals = new Hashtable<>();
+    ImsConfigImplBase.ImsConfigStub mImsConfigStub;
+    @Mock MmTelFeatureConnection mMmTelFeatureConnection;
+    @Mock IBinder mMmTelFeature;
+    @Mock IImsConfig mImsConfig;
+    @Mock IImsRegistration mImsReg;
+    @Mock ISipTransport mSipTransport;
+    @Mock ImsManager.SubscriptionManagerProxy mSubscriptionManagerProxy;
+    @Mock ImsManager.SettingsProxy mSettingsProxy;
+
+    private final int[] mSubId = {0};
+    private final int mPhoneId = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mBundle = mContextFixture.getTestCarrierConfigBundle();
+        // Force MmTelFeatureConnection to create an executor using Looper.myLooper().
+        doReturn(null).when(mContext).getMainLooper();
+
+        doReturn(true).when(mMmTelFeatureConnection).isBinderAlive();
+        mContextFixture.addSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+
+        doReturn(true).when(mSubscriptionManagerProxy).isValidSubscriptionId(anyInt());
+        doReturn(mSubId).when(mSubscriptionManagerProxy).getSubscriptionIds(eq(mPhoneId));
+        doReturn(mSubId).when(mSubscriptionManagerProxy).getActiveSubscriptionIdList();
+        doReturn(mPhoneId).when(mSubscriptionManagerProxy).getDefaultVoicePhoneId();
+        doReturn(-1).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(anyInt(),
+                anyString(), anyInt());
+
+
+        setDefaultValues();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private void setDefaultValues() {
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL,
+                ENHANCED_4G_MODE_EDITABLE);
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL,
+                WFC_IMS_EDITABLE_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL,
+                WFC_IMS_ROAMING_EDITABLE_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ENABLED_BOOL,
+                WFC_IMS_ENABLE_DEFAULT_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL,
+                WFC_IMS_ROAMING_ENABLE_DEFAULT_VAL);
+        mBundle.putInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT,
+                WFC_IMS_MODE_DEFAULT_VAL);
+        mBundle.putInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT,
+                WFC_IMS_ROAMING_MODE_DEFAULT_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL,
+                ENHANCED_4G_MODE_DEFAULT_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, true);
+        mBundle.putBoolean(
+                CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL,
+                WFC_NOT_USE_HOME_MODE_FOR_ROAMING_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL, true);
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL, true);
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false);
+
+    }
+
+    @Test @SmallTest
+    public void testGetDefaultValues() {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        assertEquals(WFC_IMS_ENABLE_DEFAULT_VAL, imsManager.isWfcEnabledByUser());
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ENABLED),
+                anyInt());
+
+        assertEquals(WFC_IMS_ROAMING_ENABLE_DEFAULT_VAL, imsManager.isWfcRoamingEnabledByUser());
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                anyInt());
+
+        assertEquals(ENHANCED_4G_MODE_DEFAULT_VAL,
+                imsManager.isEnhanced4gLteModeSettingEnabledByUser());
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.ENHANCED_4G_MODE_ENABLED),
+                anyInt());
+
+        assertEquals(WFC_IMS_MODE_DEFAULT_VAL, imsManager.getWfcMode(false));
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                anyInt());
+
+        assertEquals(WFC_IMS_ROAMING_MODE_DEFAULT_VAL, imsManager.getWfcMode(true));
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                anyInt());
+
+        assertEquals(VT_IMS_ENABLE_DEFAULT_VAL, imsManager.isVtEnabledByUser());
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.VT_IMS_ENABLED),
+                anyInt());
+    }
+
+    @SmallTest
+    @Test
+    public void testImsStats() {
+        setWfcEnabledByUser(true);
+        SomeArgs args = SomeArgs.obtain();
+        ImsManager.setImsStatsCallback(mPhoneId, new ImsManager.ImsStatsCallback() {
+            @Override
+            public void onEnabledMmTelCapabilitiesChanged(int capability, int regTech,
+                    boolean isEnabled) {
+                args.arg1 = capability;
+                args.arg2 = regTech;
+                args.arg3 = isEnabled;
+            }
+        });
+        mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL,
+                false);
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+        // Assert that the IMS stats callback is called properly when a setting changes.
+        imsManager.setWfcSetting(true);
+        assertEquals(args.arg1, MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+        assertEquals(args.arg2, ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+        assertEquals(args.arg3, true);
+        args.recycle();
+    }
+
+    @Test @SmallTest
+    public void testSetValues() {
+        setWfcEnabledByUser(true);
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        imsManager.setWfcMode(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                eq(mSubId[0]),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                eq("1"));
+
+        imsManager.setWfcMode(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED, true);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                eq(mSubId[0]),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                eq("1"));
+
+        imsManager.setVtSetting(false);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                eq(mSubId[0]),
+                eq(SubscriptionManager.VT_IMS_ENABLED),
+                eq("0"));
+
+        // enhanced 4g mode must be editable to use setEnhanced4gLteModeSetting
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL,
+                ENHANCED_4G_MODE_EDITABLE);
+        imsManager.setEnhanced4gLteModeSetting(true);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                eq(mSubId[0]),
+                eq(SubscriptionManager.ENHANCED_4G_MODE_ENABLED),
+                eq("1"));
+
+        imsManager.setWfcSetting(true);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                eq(mSubId[0]),
+                eq(SubscriptionManager.WFC_IMS_ENABLED),
+                eq("1"));
+    }
+    @Test
+    public void testGetProvisionedValues() throws Exception {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        assertEquals(true, imsManager.isWfcProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+        assertEquals(true, imsManager.isVtProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.LVC_SETTING_ENABLED));
+
+        assertEquals(true, imsManager.isVolteProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.VLT_SETTING_ENABLED));
+
+        // If we call get again, times should still be one because the value should be fetched
+        // from cache.
+        assertEquals(true, imsManager.isWfcProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+        assertEquals(true, imsManager.isVtProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.LVC_SETTING_ENABLED));
+
+        assertEquals(true, imsManager.isVolteProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.VLT_SETTING_ENABLED));
+
+        assertEquals(true, imsManager.isEabProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+    }
+
+    @Test
+    public void testSetProvisionedValues() throws Exception {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        assertEquals(true, imsManager.isWfcProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+        imsManager.getConfigInterface().setProvisionedValue(
+                ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED,
+                ImsConfig.FeatureValueConstants.OFF);
+
+        assertEquals(0, (int) mProvisionedIntVals.get(
+                ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+        assertEquals(false, imsManager.isWfcProvisionedOnDevice());
+
+        verify(mImsConfigImplBaseMock, times(1)).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED),
+                eq(0));
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+    }
+
+    @Test
+    public void testEabSetProvisionedValues() throws Exception {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        assertEquals(true, imsManager.isEabProvisionedOnDevice());
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+
+        imsManager.getConfigInterface().setProvisionedValue(
+                ImsConfig.ConfigConstants.EAB_SETTING_ENABLED,
+                ImsConfig.FeatureValueConstants.OFF);
+
+        assertEquals(0, (int) mProvisionedIntVals.get(
+                ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+
+        assertEquals(false, imsManager.isEabProvisionedOnDevice());
+
+        verify(mImsConfigImplBaseMock, times(1)).setConfig(
+                eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED),
+                eq(0));
+        verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+                eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+    }
+
+    /**
+     * Tests that when WFC is enabled/disabled for home/roaming, that setting is sent to the
+     * ImsService correctly.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcSetting_true_shouldSetWfcModeWrtRoamingState() throws Exception {
+        setWfcEnabledByUser(true);
+        // First, Set WFC home/roaming mode that is not the Carrier Config default.
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                        anyInt(),
+                        eq(SubscriptionManager.WFC_IMS_MODE),
+                        anyInt());
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                        anyInt(),
+                        eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                        anyInt());
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // Roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+        // Turn on WFC
+        imsManager.setWfcSetting(true);
+        // Roaming mode (CELLULAR_PREFERRED) should be set.
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+                eq(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED));
+        // WFC is enabled, so we should set user roaming setting
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+
+        // Not roaming
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+        // Turn on WFC
+        imsManager.setWfcSetting(true);
+        // Home mode (WIFI_PREFERRED) should be set.
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+                eq(ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED));
+        // WFC is enabled, so we should set user roaming setting
+        verify(mImsConfigImplBaseMock, times(2)).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+
+
+        // Turn off WFC and ensure that roaming setting is disabled.
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+        // mock Subscription DB change due to WFC setting being set to false
+        setWfcEnabledByUser(false);
+        imsManager.setWfcSetting(false);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ENABLED),
+                eq("0" /*false*/));
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                eq(ProvisioningManager.PROVISIONING_VALUE_DISABLED));
+    }
+
+
+    /**
+     * Tests that when user changed WFC setting while NOT roaming, the home WFC mode is sent to the
+     * modem and the roaming enabled configuration is pushed.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcSetting_shouldSetWfcModeRoamingDisabledUserEnabled() throws Exception {
+        setWfcEnabledByUser(true);
+        // The user has previously enabled "WFC while roaming" setting in UI and then turned WFC
+        // off.
+        doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                anyInt());
+
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // We are currently on the home network, not roaming.
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+
+        // User enables WFC from UI
+        imsManager.setWfcSetting(true /*enabled*/);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                // Should be enabled because the user enabled the "WFC while roaming" setting
+                // independent of whether or not we are roaming.
+                eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+    }
+
+    /**
+     * Tests that when user changed WFC setting while roaming, that the correct user setting
+     * is sent to the ImsService when changing the roaming mode.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcSetting_shouldSetWfcModeRoamingEnabledUserEnabled() throws Exception {
+        setWfcEnabledByUser(true);
+        // The user has previously enabled "WFC while roaming" setting in UI and then turned WFC
+        // off.
+        doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                anyInt());
+
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        //  The device is currently roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+        // The user has enabled WFC in the UI while the device is roaming.
+        imsManager.setWfcSetting(true /*enabled*/);
+
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                // Default for roaming is WFC_IMS_ROAMING_MODE_DEFAULT_VAL
+                eq(ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED));
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                // Should be enabled because user enabled the setting in the UI previously.
+                eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+    }
+
+    /**
+     * Tests that when a WFC mode is updated for home, that setting is sent to the
+     * ImsService correctly or ignored if the roaming mode is changed.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcMode_shouldSetWfcModeRoamingDisabled() throws Exception {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // the device is not currently roaming
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+
+        // set the WFC roaming mode while the device is not roaming, so any changes to roaming mode
+        // should be ignored
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                anyInt());
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+
+        // set home WFC mode setting while not roaming, the configuration should be set correctly.
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, false /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+        // WiFi Roaming enabled setting is not related to WFC mode
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+    }
+
+    /**
+     * Tests that when a WFC mode is updated for roaming while WFC is enabled, that setting is sent
+     * to the ImsService correctly when changing the roaming mode or ignored if the home setting is
+     * changed.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcMode_wfcEnabledShouldSetWfcModeRoamingEnabled() throws Exception {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // The user has previously enabled WFC in the settings UI.
+        doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ENABLED),
+                anyInt());
+
+        // The device is roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+        // The carrier app has changed the WFC mode for roaming while the device is home. The
+        // result of this operation is that the neither the WFC mode or the roaming enabled
+        // configuration should change.
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, false /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                anyInt());
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+
+        //  The carrier app has set the WFC mode for roaming while the device is roaming. The
+        // WFC mode should be updated to reflect the roaming setting and the roaming enabled
+        // configuration should be changed to enabled.
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+        // WiFi Roaming enabled setting is not related to WFC mode
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+    }
+
+    /**
+     * Tests that when a WFC mode is updated for roaming while WFC is disabled, the WFC roaming
+     * setting is always set to disabled.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcMode_WfcDisabledShouldNotSetWfcModeRoamingEnabled() throws Exception {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // The user has previously disabled WFC in the settings UI.
+        doReturn(0 /*false*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ENABLED),
+                anyInt());
+
+        // The device is roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+        // WFC is disabled and the carrier app has set the WFC mode for roaming while the device is
+        // roaming. The WFC mode should be updated to reflect the roaming setting and the roaming
+        // enabled configuration should be disabled because WFC is disabled.
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+        // WiFi Roaming enabled setting is not related to WFC mode
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+    }
+
+    /**
+     * Tests that when user changed WFC mode while not roaming, the new mode is sent to the modem
+     * and roaming enabled indication is sent to the ImsService correctly when changing the roaming
+     * mode.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcMode_shouldSetWfcModeRoamingDisabledUserEnabled() throws Exception {
+        // The user has enabled the WFC setting in the UI.
+        doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ENABLED),
+                anyInt());
+        // The user has enabled the "WFC while roaming" setting in the UI while WFC was enabled
+        doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                anyInt());
+
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // The device is currently on the home network
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+
+        // The user has changed the WFC mode in the UI for the non-roaming configuration
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, false /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                // ensure that the correct cellular preferred config change is sent
+                eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+        // WiFi Roaming enabled setting is not related to WFC mode
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+    }
+
+    /**
+     * Tests that when user changed WFC mode while roaming, that setting is sent to the
+     * ImsService correctly when changing the roaming mode.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+     */
+    @Test @SmallTest
+    public void testSetWfcMode_shouldSetWfcModeRoamingEnabledUserDisabled() throws Exception {
+        // The user disabled "WFC while roaming" setting in the UI
+        doReturn(0 /*false*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                anyInt());
+
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // the device is currently roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+        // The carrier app has changed the WFC mode while roaming, so we must set the WFC mode
+        // to the new configuration.
+        imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+                eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+        // WiFi Roaming enabled setting is not related to WFC mode
+        verify(mImsConfigImplBaseMock, never()).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                anyInt());
+    }
+
+    /**
+     * Tests that the settings for WFC mode are ignored if the Carrier sets the settings to not
+     * editable.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = false
+     */
+    @Test @SmallTest
+    public void testSetWfcSetting_wfcNotEditable() throws Exception {
+        setWfcEnabledByUser(true);
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL,
+                WFC_IMS_NOT_EDITABLE_VAL);
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL,
+                WFC_IMS_ROAMING_NOT_EDITABLE_VAL);
+        // Set some values that are different than the defaults for WFC mode.
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                anyInt());
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                anyInt());
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // Roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+        // Turn on WFC
+        imsManager.setWfcSetting(true);
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+                eq(WFC_IMS_ROAMING_MODE_DEFAULT_VAL));
+
+        // Not roaming
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+        // Turn on WFC
+        imsManager.setWfcSetting(true);
+        // Default Home mode (CELLULAR_PREFERRED) should be set.
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+                eq(WFC_IMS_MODE_DEFAULT_VAL));
+    }
+
+    /**
+     * Tests that the CarrierConfig defaults will be used if no setting is set in the Subscription
+     * Manager.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT = Carrier preferred
+     *  - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT = WiFi preferred
+     */
+    @Test @SmallTest
+    public void testSetWfcSetting_noUserSettingSet() throws Exception {
+        setWfcEnabledByUser(true);
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // Roaming
+        doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+        // Turn on WFC
+        imsManager.setWfcSetting(true);
+
+        // Default Roaming mode (WIFI_PREFERRED) for carrier should be set. With 1000 ms timeout.
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+                eq(WFC_IMS_ROAMING_MODE_DEFAULT_VAL));
+
+        // Not roaming
+        doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+        // Turn on WFC
+        imsManager.setWfcSetting(true);
+
+        // Default Home mode (CELLULAR_PREFERRED) for carrier should be set. With 1000 ms timeout.
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+                eq(WFC_IMS_MODE_DEFAULT_VAL));
+    }
+
+    /**
+     * Tests the operation of getWfcMode when the configuration to use the home network mode when
+     * roaming for WFC is false. First, it checks that the user setting for WFC_IMS_ROAMING_MODE is
+     * returned when WFC roaming is set to editable. Then, it switches the WFC roaming mode to not
+     * editable and ensures that the default WFC roaming mode is returned.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL = false
+     */
+    @Test @SmallTest
+    public void getWfcMode_useWfcHomeModeConfigFalse_shouldUseWfcRoamingMode() {
+        // Set some values that are different than the defaults for WFC mode.
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                anyInt());
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                anyInt());
+
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // Check that use the WFC roaming network mode.
+        assertEquals(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED,
+                imsManager.getWfcMode(true));
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                anyInt());
+
+        // Set WFC roaming network mode to not editable.
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL,
+                WFC_IMS_ROAMING_NOT_EDITABLE_VAL);
+
+        // Check that use the default WFC roaming network mode.
+        assertEquals(WFC_IMS_ROAMING_MODE_DEFAULT_VAL, imsManager.getWfcMode(true));
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                anyInt());
+    }
+
+    /**
+     * Tests the operation of getWfcMode when the configuration to use the home network mode when
+     * roaming for WFC is true independent of whether or not the WFC roaming mode is editable.
+     *
+     * Preconditions:
+     *  - CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL = true
+     */
+    @Test @SmallTest
+    public void getWfcMode_useWfcHomeModeConfigTrue_shouldUseWfcHomeMode() {
+        // Set some values that are different than the defaults for WFC mode.
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                anyInt());
+        doReturn(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)
+                .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+                anyInt());
+
+        // Set to use WFC home network mode in roaming network.
+        mBundle.putBoolean(
+                CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL,
+                WFC_USE_HOME_MODE_FOR_ROAMING_VAL);
+
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        // Check that use the WFC home network mode.
+        assertEquals(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY, imsManager.getWfcMode(true));
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                anyInt());
+
+        // Set WFC home network mode to not editable.
+        mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL,
+                WFC_IMS_NOT_EDITABLE_VAL);
+
+        // Check that use the default WFC home network mode.
+        assertEquals(WFC_IMS_MODE_DEFAULT_VAL, imsManager.getWfcMode(true));
+        verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_MODE),
+                anyInt());
+    }
+
+    /**
+     * Tests the operation of setWfcRoamingSetting and ensures that the user setting for WFC roaming
+     * and the ImsConfig setting are both called properly.
+     */
+    @Test @SmallTest
+    public void setWfcRoamingSettingTest() {
+        ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+        imsManager.setWfcRoamingSetting(true);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                eq("1"));
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+
+        imsManager.setWfcRoamingSetting(false);
+        verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+                anyInt(),
+                eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+                eq("0"));
+        verify(mImsConfigImplBaseMock).setConfig(
+                eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+                eq(ProvisioningManager.PROVISIONING_VALUE_DISABLED));
+
+    }
+
+    private ImsManager getImsManagerAndInitProvisionedValues() {
+        when(mImsConfigImplBaseMock.getConfigInt(anyInt()))
+                .thenAnswer(invocation ->  {
+                    return getProvisionedInt((Integer) (invocation.getArguments()[0]));
+                });
+
+        when(mImsConfigImplBaseMock.setConfig(anyInt(), anyInt()))
+                .thenAnswer(invocation ->  {
+                    mProvisionedIntVals.put((Integer) (invocation.getArguments()[0]),
+                            (Integer) (invocation.getArguments()[1]));
+                    return ImsConfig.OperationStatusConstants.SUCCESS;
+                });
+
+
+        // Configure ImsConfigStub
+        mImsConfigStub = new ImsConfigImplBase.ImsConfigStub(mImsConfigImplBaseMock);
+        doReturn(mImsConfigStub).when(mMmTelFeatureConnection).getConfig();
+
+        ImsManager mgr = new ImsManager(mContext, mPhoneId,
+                (context, phoneId, feature, c, r, s) -> mMmTelFeatureConnection,
+                mSubscriptionManagerProxy, mSettingsProxy);
+        ImsFeatureContainer c = new ImsFeatureContainer(mMmTelFeature, mImsConfig, mImsReg,
+                mSipTransport, 0 /*caps*/);
+        mgr.associate(c);
+        // Enabled WFC by default
+        setWfcEnabledByPlatform(true);
+        return mgr;
+    }
+
+    private void setWfcEnabledByPlatform(boolean isEnabled) {
+        Resources res = mContext.getResources();
+        doReturn(isEnabled).when(res).getBoolean(
+                com.android.internal.R.bool.config_device_wfc_ims_available);
+    }
+
+    private void setWfcEnabledByUser(boolean isEnabled) {
+        // The user has previously enabled WFC in the settings UI.
+        doReturn(isEnabled ? 1 /*true*/ : 0).when(mSubscriptionManagerProxy)
+                .getIntegerSubscriptionProperty(anyInt(), eq(SubscriptionManager.WFC_IMS_ENABLED),
+                        anyInt());
+    }
+
+    // If the value is ever set, return the set value. If not, return a constant value 1000.
+    private int getProvisionedInt(int item) {
+        if (mProvisionedIntVals.containsKey(item)) {
+            return mProvisionedIntVals.get(item);
+        } else {
+            return ImsConfig.FeatureValueConstants.ON;
+        }
+    }
+}
diff --git a/tests/src/com/android/ims/ImsTestBase.java b/tests/src/com/android/ims/ImsTestBase.java
index 32e57a3..8e9064e 100644
--- a/tests/src/com/android/ims/ImsTestBase.java
+++ b/tests/src/com/android/ims/ImsTestBase.java
@@ -19,8 +19,8 @@
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
-
-import androidx.test.InstrumentationRegistry;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 
 import org.mockito.MockitoAnnotations;
 
@@ -32,11 +32,21 @@
  */
 public class ImsTestBase {
 
+    protected ContextFixture mContextFixture;
     protected Context mContext;
 
+    protected TelephonyManager mTelephonyManager;
+    protected SubscriptionManager mSubscriptionManager;
+
     public void setUp() throws Exception {
-        mContext = InstrumentationRegistry.getTargetContext();
         MockitoAnnotations.initMocks(this);
+        mContextFixture = new ContextFixture();
+        mContext = mContextFixture.getContext();
+
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        mSubscriptionManager = (SubscriptionManager) mContext.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+
         // 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/ims/ImsUtTest.java b/tests/src/com/android/ims/ImsUtTest.java
new file mode 100644
index 0000000..634b4d9
--- /dev/null
+++ b/tests/src/com/android/ims/ImsUtTest.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.ims;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.ims.ImsSsInfo;
+import android.telephony.ims.ImsUtListener;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.internal.IImsUt;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsUtTest extends ImsTestBase {
+
+    private static final int MSG_QUERY = 1;
+    private static final int TEST_TIMEOUT_MS = 5000;
+
+    private static class TestHandler extends Handler {
+
+        TestHandler(Looper looper) {
+            super(looper);
+        }
+
+        private final LinkedBlockingQueue<ImsSsInfo> mPendingSsInfos = new LinkedBlockingQueue<>(1);
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_QUERY) {
+                AsyncResult ar = (AsyncResult) msg.obj;
+                mPendingSsInfos.offer((ImsSsInfo) ar.result);
+            }
+        }
+        public ImsSsInfo getPendingImsSsInfo() {
+            try {
+                return mPendingSsInfos.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                fail("test interrupted!");
+            }
+            return null;
+        }
+    }
+
+    @Mock IImsUt mImsUtBinder;
+
+    private TestHandler mHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mHandler = new TestHandler(Looper.getMainLooper());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        waitForHandlerAction(mHandler, 1000/*ms*/);
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testClirConversionCompat() throws Exception {
+        ArgumentCaptor<ImsUt.IImsUtListenerProxy> captor =
+                ArgumentCaptor.forClass(ImsUt.IImsUtListenerProxy.class);
+        ImsUt mImsUt = new ImsUt(mImsUtBinder);
+        verify(mImsUtBinder).setListener(captor.capture());
+        ImsUt.IImsUtListenerProxy proxy = captor.getValue();
+        assertNotNull(proxy);
+
+        doReturn(2).when(mImsUtBinder).queryCLIR();
+        mImsUt.queryCLIR(Message.obtain(mHandler, MSG_QUERY));
+
+        Bundle result = new Bundle();
+        result.putIntArray(ImsUtListener.BUNDLE_KEY_CLIR, new int[] {
+                ImsSsInfo.CLIR_OUTGOING_INVOCATION, ImsSsInfo.CLIR_STATUS_PROVISIONED_PERMANENT});
+        // This is deprecated, will be converted from Bundle -> ImsSsInfo
+        proxy.utConfigurationQueried(null, 2 /*id*/, result);
+        waitForHandlerAction(mHandler, 1000/*ms*/);
+
+
+        ImsSsInfo info = mHandler.getPendingImsSsInfo();
+        assertNotNull(info);
+        assertEquals(ImsSsInfo.CLIR_OUTGOING_INVOCATION, info.getClirOutgoingState());
+        assertEquals(ImsSsInfo.CLIR_STATUS_PROVISIONED_PERMANENT,
+                info.getClirInterrogationStatus());
+    }
+
+    @Test
+    @SmallTest
+    public void testClipConversionCompat() throws Exception {
+        ArgumentCaptor<ImsUt.IImsUtListenerProxy> captor =
+                ArgumentCaptor.forClass(ImsUt.IImsUtListenerProxy.class);
+        ImsUt mImsUt = new ImsUt(mImsUtBinder);
+        verify(mImsUtBinder).setListener(captor.capture());
+        ImsUt.IImsUtListenerProxy proxy = captor.getValue();
+        assertNotNull(proxy);
+
+        doReturn(2).when(mImsUtBinder).queryCLIP();
+        mImsUt.queryCLIP(Message.obtain(mHandler, MSG_QUERY));
+
+        ImsSsInfo info = new ImsSsInfo.Builder(ImsSsInfo.ENABLED).setProvisionStatus(
+                ImsSsInfo.CLIR_STATUS_PROVISIONED_PERMANENT).build();
+        Bundle result = new Bundle();
+        result.putParcelable(ImsUtListener.BUNDLE_KEY_SSINFO, info);
+        // This is deprecated, will be converted from Bundle -> ImsSsInfo
+        proxy.utConfigurationQueried(null, 2 /*id*/, result);
+        waitForHandlerAction(mHandler, 1000/*ms*/);
+
+        ImsSsInfo resultInfo = mHandler.getPendingImsSsInfo();
+        assertNotNull(resultInfo);
+        assertEquals(info.getStatus(), resultInfo.getStatus());
+        assertEquals(info.getProvisionStatus(), resultInfo.getProvisionStatus());
+    }
+}
diff --git a/tests/src/com/android/ims/MmTelFeatureConnectionTest.java b/tests/src/com/android/ims/MmTelFeatureConnectionTest.java
new file mode 100644
index 0000000..620fa23
--- /dev/null
+++ b/tests/src/com/android/ims/MmTelFeatureConnectionTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+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.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class MmTelFeatureConnectionTest extends ImsTestBase {
+
+    private class TestCallback extends Binder implements IInterface {
+
+        @Override
+        public IBinder asBinder() {
+            return this;
+        }
+    }
+
+    private class CallbackManagerTest extends
+            ImsCallbackAdapterManager<TestCallback> {
+
+        List<TestCallback> mCallbacks = new ArrayList<>();
+
+        CallbackManagerTest(Context context, Object lock) {
+            super(context, lock, 0 /*slotId*/);
+        }
+
+        // A callback has been registered. Register that callback with the MmTelFeature.
+        @Override
+        public void registerCallback(TestCallback localCallback) {
+            mCallbacks.add(localCallback);
+        }
+
+        // A callback has been removed, unregister that callback with the MmTelFeature.
+        @Override
+        public void unregisterCallback(TestCallback localCallback) {
+            mCallbacks.remove(localCallback);
+        }
+
+        public boolean doesCallbackExist(TestCallback callback) {
+            return mCallbacks.contains(callback);
+        }
+    }
+    private CallbackManagerTest mCallbackManagerUT;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCallbackManagerUT = new CallbackManagerTest(mContext, this);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mCallbackManagerUT = null;
+        super.tearDown();
+    }
+
+    /**
+     * Basic test of deprecated functionality, ensure that adding the callback directly triggers the
+     * appropriate registerCallback and unregisterCallback calls.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_addAndRemoveCallback() throws Exception {
+        TestCallback testCallback = new TestCallback();
+        mCallbackManagerUT.addCallback(testCallback);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback));
+        // The subscriptions changed listener should only be added for callbacks that are being
+        // linked to a subscription.
+        verify(mSubscriptionManager, never()).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        mCallbackManagerUT.removeCallback(testCallback);
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback));
+        // The subscriptions changed listener should only be removed for callbacks that are
+        // linked to a subscription.
+        verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    /**
+     * Ensure that adding the callback and linking subId triggers the appropriate registerCallback
+     * and unregisterCallback calls as well as the subscriptionChanged listener.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_addAndRemoveCallbackForSub() throws Exception {
+        TestCallback testCallback = new TestCallback();
+        int testSub = 1;
+        mCallbackManagerUT.addCallbackForSubscription(testCallback, testSub);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback));
+        verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        mCallbackManagerUT.removeCallbackForSubscription(testCallback, testSub);
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback));
+        verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    /**
+     * Ensure that adding the callback and linking multiple subIds trigger the appropriate
+     * registerCallback and unregisterCallback calls as well as the subscriptionChanged listener.
+     * When removing the callbacks, the subscriptionChanged listener shoud only be removed when all
+     * callbacks have been removed.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_addAndRemoveCallbackForMultipleSubs() throws Exception {
+        TestCallback testCallback1 = new TestCallback();
+        TestCallback testCallback2 = new TestCallback();
+        int testSub1 = 1;
+        int testSub2 = 2;
+        mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        mCallbackManagerUT.addCallbackForSubscription(testCallback2, testSub2);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        // This should only happen once.
+        verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        mCallbackManagerUT.removeCallbackForSubscription(testCallback1, testSub1);
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        // removing the listener should not happen until the second callback is removed.
+        verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        mCallbackManagerUT.removeCallbackForSubscription(testCallback2, testSub2);
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    /**
+     * The subscriptions have changed, ensure that the callbacks registered to the original
+     * subscription testSub1 are removed, while keeping the callbacks for testSub2, since it was not
+     * removed.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_onSubscriptionsChangedMultipleSubs() throws Exception {
+        TestCallback testCallback1 = new TestCallback();
+        TestCallback testCallback2 = new TestCallback();
+        int testSub1 = 1;
+        int testSub2 = 2;
+        int testSub3 = 3;
+        mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        mCallbackManagerUT.addCallbackForSubscription(testCallback2, testSub2);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        // Simulate subscriptions changed, where testSub1 is no longer active
+        doReturn(createSubscriptionInfoList(new int[] {testSub2, testSub3}))
+                .when(mSubscriptionManager).getActiveSubscriptionInfoList(anyBoolean());
+        mCallbackManagerUT.mSubChangedListener.onSubscriptionsChanged();
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        // verify that the subscription changed listener is not removed, since we still have a
+        // callback on testSub2
+        verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    /**
+     * The active subscription has changed, ensure that the callback registered to the original
+     * subscription testSub1 are removed as well as the subscription changed listener, since
+     * there are mo more active callbacks.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_onSubscriptionsChangedOneSub() throws Exception {
+        TestCallback testCallback1 = new TestCallback();
+        int testSub1 = 1;
+        int testSub2 = 2;
+        mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        // Simulate subscriptions changed, where testSub1 is no longer active
+        doReturn(createSubscriptionInfoList(new int[] {testSub2}))
+                .when(mSubscriptionManager).getActiveSubscriptionInfoList(anyBoolean());
+        mCallbackManagerUT.mSubChangedListener.onSubscriptionsChanged();
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        // verify that the subscription listener is removed, since the only active callback has been
+        // removed.
+        verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    /**
+     * The close() method has been called, so al callbacks should be cleaned up and notified
+     * that they have been removed. The subscriptions changed listener should also be removed.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_closeMultipleSubs() throws Exception {
+        TestCallback testCallback1 = new TestCallback();
+        TestCallback testCallback2 = new TestCallback();
+        int testSub1 = 1;
+        int testSub2 = 2;
+        mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        mCallbackManagerUT.addCallbackForSubscription(testCallback2, testSub2);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        // Close the manager, ensure all subscription callbacks are removed
+        mCallbackManagerUT.close();
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        // verify that the subscription changed listener is removed.
+        verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    /**
+     * The close() method has been called, so all callbacks should be cleaned up. Since they are
+     * not associated with any subscriptions, no subscription based logic should be called.
+     */
+    @Test
+    @SmallTest
+    public void testCallbackAdapter_closeSlotBasedCallbacks() throws Exception {
+        TestCallback testCallback1 = new TestCallback();
+        TestCallback testCallback2 = new TestCallback();
+        mCallbackManagerUT.addCallback(testCallback1);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        mCallbackManagerUT.addCallback(testCallback2);
+        assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        // verify that the subscription changed listener is never called for these callbacks
+        // because they are not associated with any subscriptions.
+        verify(mSubscriptionManager, never()).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+        // Close the manager, ensure all subscription callbacks are removed
+        mCallbackManagerUT.close();
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+        assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback2));
+        // verify that the subscription changed removed method is never called
+        verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+    }
+
+    private List<SubscriptionInfo> createSubscriptionInfoList(int[] subIds) {
+        List<SubscriptionInfo> infos = new ArrayList<>();
+        for (int i = 0; i < subIds.length; i++) {
+            SubscriptionInfo info = new SubscriptionInfo(subIds[i], null, -1, null, null, -1, -1,
+                    null, -1, null, null, null, null, false, null, null);
+            infos.add(info);
+        }
+        return infos;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/OWNERS b/tests/src/com/android/ims/rcs/uce/OWNERS
new file mode 100644
index 0000000..dff71c4
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/OWNERS
@@ -0,0 +1,3 @@
+jamescflin@google.com
+calvinpan@google.com
+allenwtsu@google.com
\ No newline at end of file
diff --git a/tests/src/com/android/ims/rcs/uce/UceControllerTest.java b/tests/src/com/android/ims/rcs/uce/UceControllerTest.java
new file mode 100644
index 0000000..69d5281
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/UceControllerTest.java
@@ -0,0 +1,283 @@
+/*
+ * 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.ims.rcs.uce;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+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.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.eab.EabController;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.presence.publish.PublishController;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequestManager;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class UceControllerTest extends ImsTestBase {
+
+    @Mock EabController mEabController;
+    @Mock PublishController mPublishController;
+    @Mock SubscribeController mSubscribeController;
+    @Mock OptionsController mOptionsController;
+    @Mock UceController.ControllerFactory mControllerFactory;
+
+    @Mock UceRequestManager mTaskManager;
+    @Mock UceController.RequestManagerFactory mTaskManagerFactory;
+
+    @Mock UceDeviceState mDeviceState;
+    @Mock DeviceStateResult mDeviceStateResult;
+    @Mock RcsFeatureManager mFeatureManager;
+    @Mock UceController.UceControllerCallback mCallback;
+    @Mock IRcsUceControllerCallback mCapabilitiesCallback;
+    @Mock IOptionsRequestCallback mOptionsRequestCallback;
+
+    private int mSubId = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mEabController).when(mControllerFactory).createEabController(any(), eq(mSubId),
+                any(), any());
+        doReturn(mPublishController).when(mControllerFactory).createPublishController(any(),
+                eq(mSubId), any(), any());
+        doReturn(mSubscribeController).when(mControllerFactory).createSubscribeController(any(),
+                eq(mSubId));
+        doReturn(mOptionsController).when(mControllerFactory).createOptionsController(any(),
+                eq(mSubId));
+        doReturn(mTaskManager).when(mTaskManagerFactory).createRequestManager(any(), eq(mSubId),
+                any(), any());
+        doReturn(mDeviceStateResult).when(mDeviceState).getCurrentState();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsConnected() throws Exception {
+        UceController uceController = createUceController();
+
+        uceController.onRcsConnected(mFeatureManager);
+
+        verify(mEabController).onRcsConnected(mFeatureManager);
+        verify(mPublishController).onRcsConnected(mFeatureManager);
+        verify(mSubscribeController).onRcsConnected(mFeatureManager);
+        verify(mOptionsController).onRcsConnected(mFeatureManager);
+        verify(mFeatureManager).addCapabilityEventCallback(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsDisconnected() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsConnected(mFeatureManager);
+
+        uceController.onRcsDisconnected();
+
+        verify(mFeatureManager).removeCapabilityEventCallback(any());
+        verify(mEabController).onRcsDisconnected();
+        verify(mPublishController).onRcsDisconnected();
+        verify(mSubscribeController).onRcsDisconnected();
+        verify(mOptionsController).onRcsDisconnected();
+    }
+
+    @Test
+    @SmallTest
+    public void testOnDestroyed() throws Exception {
+        UceController uceController = createUceController();
+
+        uceController.onDestroy();
+
+        verify(mTaskManager).onDestroy();
+        verify(mEabController).onDestroy();
+        verify(mPublishController).onDestroy();
+        verify(mSubscribeController).onDestroy();
+        verify(mOptionsController).onDestroy();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilitiesWithRcsDisconnected() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsDisconnected();
+
+        List<Uri> uriList = new ArrayList<>();
+        uceController.requestCapabilities(uriList, mCapabilitiesCallback);
+
+        verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+        verify(mTaskManager, never()).sendCapabilityRequest(any(), eq(false), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilitiesWithForbidden() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsConnected(mFeatureManager);
+        doReturn(true).when(mDeviceStateResult).isRequestForbidden();
+        doReturn(Optional.of(RcsUceAdapter.ERROR_FORBIDDEN)).when(mDeviceStateResult)
+                .getErrorCode();
+
+        List<Uri> uriList = new ArrayList<>();
+        uriList.add(Uri.fromParts("sip", "test", null));
+        uceController.requestCapabilities(uriList, mCapabilitiesCallback);
+
+        verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_FORBIDDEN, 0L);
+        verify(mTaskManager, never()).sendCapabilityRequest(any(), eq(false), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilitiesWithRcsConnected() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsConnected(mFeatureManager);
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+
+        List<Uri> uriList = new ArrayList<>();
+        uriList.add(Uri.fromParts("sip", "test", null));
+        uceController.requestCapabilities(uriList, mCapabilitiesCallback);
+
+        verify(mTaskManager).sendCapabilityRequest(uriList, false, mCapabilitiesCallback);
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestAvailabilityWithRcsDisconnected() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsDisconnected();
+
+        Uri contact = Uri.fromParts("sip", "test", null);
+        uceController.requestAvailability(contact, mCapabilitiesCallback);
+
+        verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+        verify(mTaskManager, never()).sendAvailabilityRequest(any(), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestAvailabilityWithForbidden() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsConnected(mFeatureManager);
+        doReturn(true).when(mDeviceStateResult).isRequestForbidden();
+        doReturn(Optional.of(RcsUceAdapter.ERROR_FORBIDDEN)).when(mDeviceStateResult)
+                .getErrorCode();
+
+        Uri contact = Uri.fromParts("sip", "test", null);
+        uceController.requestAvailability(contact, mCapabilitiesCallback);
+
+        verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_FORBIDDEN, 0L);
+        verify(mTaskManager, never()).sendCapabilityRequest(any(), eq(false), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestAvailabilityWithRcsConnected() throws Exception {
+        UceController uceController = createUceController();
+        uceController.onRcsConnected(mFeatureManager);
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+
+        Uri contact = Uri.fromParts("sip", "test", null);
+        uceController.requestAvailability(contact, mCapabilitiesCallback);
+
+        verify(mTaskManager).sendAvailabilityRequest(contact, mCapabilitiesCallback);
+    }
+
+    @Test
+    @SmallTest
+    public void TestRequestPublishCapabilitiesFromService() throws Exception {
+        UceController uceController = createUceController();
+
+        int triggerType = RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_WLAN;
+        uceController.onRequestPublishCapabilitiesFromService(triggerType);
+
+        verify(mPublishController).requestPublishCapabilitiesFromService(triggerType);
+    }
+
+    @Test
+    @SmallTest
+    public void testUnpublish() throws Exception {
+        UceController uceController = createUceController();
+
+        uceController.onUnpublish();
+
+        verify(mPublishController).onUnpublish();
+    }
+
+    @Test
+    @SmallTest
+    public void testRegisterPublishStateCallback() {
+        UceController uceController = createUceController();
+
+        uceController.registerPublishStateCallback(any());
+
+        verify(mPublishController).registerPublishStateCallback(any());
+    }
+
+    @Test
+    @SmallTest
+    public void unregisterPublishStateCallback() {
+        UceController uceController = createUceController();
+
+        uceController.unregisterPublishStateCallback(any());
+
+        verify(mPublishController).unregisterPublishStateCallback(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetUcePublishState() {
+        UceController uceController = createUceController();
+
+        uceController.getUcePublishState();
+
+        verify(mPublishController).getUcePublishState();
+    }
+
+    private UceController createUceController() {
+        UceController uceController = new UceController(mContext, mSubId, mDeviceState,
+                mControllerFactory, mTaskManagerFactory);
+        uceController.setUceControllerCallback(mCallback);
+        return uceController;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java
new file mode 100644
index 0000000..61b0431
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EabBulkCapabilityUpdaterTest extends ImsTestBase {
+
+    private final int mSubId = 1;
+
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+
+    @Mock
+    private UceController.UceControllerCallback mMockUceControllerCallback;
+    @Mock
+    private EabControllerImpl mMockEabControllerImpl;
+    @Mock
+    private ImsRcsManager mImsRcsManager;
+    @Mock
+    private RcsUceAdapter mRcsUceAdapter;
+    @Mock
+    private SharedPreferences mSharedPreferences;
+    @Mock
+    private SharedPreferences.Editor mSharedPreferencesEditor;
+    @Mock
+    private EabContactSyncController mEabContactSyncController;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mHandlerThread = new HandlerThread("TestThread");
+        mHandlerThread.start();
+        mHandler = mHandlerThread.getThreadHandler();
+
+        doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+        doReturn(0L).when(mSharedPreferences).getLong(anyString(), anyInt());
+        doReturn(mSharedPreferencesEditor).when(mSharedPreferences).edit();
+        doReturn(mSharedPreferencesEditor).when(mSharedPreferencesEditor).putLong(anyString(),
+                anyLong());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mHandlerThread.quit();
+    }
+
+    @Test
+    public void testRefreshCapabilities() throws Exception {
+        // mock user settings
+        mockUceUserSettings(true);
+        mockBulkCapabilityCarrierConfig(true);
+        // mock expired contact list
+        List<Uri> expiredContactList = new ArrayList<>();
+        expiredContactList.add(Uri.parse("test"));
+        doReturn(expiredContactList)
+                .when(mEabContactSyncController)
+                .syncContactToEabProvider(any());
+
+        new EabBulkCapabilityUpdater(
+                mContext,
+                mSubId,
+                mMockEabControllerImpl,
+                mEabContactSyncController,
+                mMockUceControllerCallback,
+                mHandler);
+
+        waitHandlerThreadFinish();
+
+        verify(mMockUceControllerCallback).refreshCapabilities(
+                anyList(),
+                any(IRcsUceControllerCallback.class));
+    }
+
+    @Test
+    public void testUceSettingsDisabled() throws Exception {
+        // mock user settings
+        mockUceUserSettings(false);
+        mockBulkCapabilityCarrierConfig(true);
+        // mock expired contact list
+        List<Uri> expiredContactList = new ArrayList<>();
+        expiredContactList.add(Uri.parse("test"));
+        doReturn(expiredContactList)
+                .when(mEabContactSyncController)
+                .syncContactToEabProvider(any());
+
+        new EabBulkCapabilityUpdater(
+                mContext,
+                mSubId,
+                mMockEabControllerImpl,
+                mEabContactSyncController,
+                mMockUceControllerCallback,
+                mHandler);
+
+        waitHandlerThreadFinish();
+
+        verify(mMockUceControllerCallback, never()).refreshCapabilities(
+                any(),
+                any(IRcsUceControllerCallback.class));
+    }
+
+    @Test
+    public void testCarrierConfigDisabled() throws Exception {
+        // mock user settings
+        mockUceUserSettings(true);
+        mockBulkCapabilityCarrierConfig(false);
+        // mock expired contact list
+        List<Uri> expiredContactList = new ArrayList<>();
+        expiredContactList.add(Uri.parse("test"));
+        doReturn(expiredContactList)
+                .when(mEabContactSyncController)
+                .syncContactToEabProvider(any());
+
+        new EabBulkCapabilityUpdater(
+                mContext,
+                mSubId,
+                mMockEabControllerImpl,
+                mEabContactSyncController,
+                mMockUceControllerCallback,
+                mHandler);
+
+        waitHandlerThreadFinish();
+
+        verify(mMockUceControllerCallback, never()).refreshCapabilities(
+                anyList(),
+                any(IRcsUceControllerCallback.class));
+    }
+
+    private void mockBulkCapabilityCarrierConfig(boolean isEnabled) {
+        PersistableBundle persistableBundle = new PersistableBundle();
+        persistableBundle.putBoolean(
+                CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, isEnabled);
+        CarrierConfigManager carrierConfigManager =
+                mContext.getSystemService(CarrierConfigManager.class);
+        doReturn(persistableBundle).when(carrierConfigManager).getConfigForSubId(anyInt());
+    }
+
+    private void mockUceUserSettings(boolean isEnabled) throws ImsException {
+        // mock uce user settings
+        ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+        doReturn(mImsRcsManager).when(imsManager).getImsRcsManager(eq(mSubId));
+        doReturn(mRcsUceAdapter).when(mImsRcsManager).getUceAdapter();
+        doReturn(isEnabled).when(mRcsUceAdapter).isUceSettingEnabled();
+    }
+
+    private void waitHandlerThreadFinish() throws Exception {
+        int retryTimes = 0;
+        do {
+            Thread.sleep(1000);
+            retryTimes++;
+        } while(mHandler.hasMessagesOrCallbacks() && retryTimes < 2);
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java
new file mode 100644
index 0000000..b500629
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.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.ims.rcs.uce.eab;
+
+import static android.provider.ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.SharedPreferences;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.test.mock.MockContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.rule.provider.ProviderTestRule;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class EabContactSyncControllerTest extends ImsTestBase {
+    private static final String TAG = "EabContactDataSyncServiceTest";
+
+    FakeContactProvider mFakeContactProvider = new FakeContactProvider();
+
+    @Rule
+    public ProviderTestRule mProviderTestRule = new ProviderTestRule.Builder(
+            EabProvider.class, EabProvider.AUTHORITY).build();
+
+    @Mock private SharedPreferences mSharedPreferences;
+    @Mock private SharedPreferences.Editor mSharedPreferencesEditor;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockContentResolver mockContentResolver =
+                (MockContentResolver) mProviderTestRule.getResolver();
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = ContactsContract.AUTHORITY;
+        mFakeContactProvider.attachInfo(mContext, providerInfo);
+        mockContentResolver.addProvider(providerInfo.authority, mFakeContactProvider);
+        doReturn("com.android.phone.tests").when(mContext).getPackageName();
+
+        doReturn(mProviderTestRule.getResolver()).when(mContext).getContentResolver();
+
+        doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+        doReturn(0L).when(mSharedPreferences).getLong(anyString(), anyInt());
+        doReturn(mSharedPreferencesEditor).when(mSharedPreferences).edit();
+        doReturn(mSharedPreferencesEditor).when(mSharedPreferencesEditor).putLong(anyString(),
+                anyLong());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mFakeContactProvider.clearData();
+        mContext.getContentResolver().delete(EabProvider.CONTACT_URI, null, null);
+    }
+
+    @Test
+    public void testContactDeletedCase() {
+        insertContactToEabProvider(1, 2, 3, "123456");
+        insertDeletedContactToContactProvider(1, 1);
+
+        new EabContactSyncController().syncContactToEabProvider(mContext);
+
+        Cursor result = mProviderTestRule.getResolver().query(
+                EabProvider.CONTACT_URI,
+                null,
+                null,
+                null);
+        assertEquals(0, result.getCount());
+    }
+
+    @Test
+    public void testMultipleContactsDeletedCase() {
+        // Insert 3 contacts in EabProvider
+        insertContactToEabProvider(1, 1, 1, "123456");
+        insertContactToEabProvider(2, 2, 2, "1234567");
+        insertContactToEabProvider(3, 3, 3, "12345678");
+        // Insert 2 deleted contacts
+        insertDeletedContactToContactProvider(1, 1);
+        insertDeletedContactToContactProvider(2, 1);
+
+        new EabContactSyncController().syncContactToEabProvider(mContext);
+
+        // Make sure only 1 contact in Eab DB
+        Cursor result = mProviderTestRule.getResolver().query(
+                EabProvider.CONTACT_URI,
+                null,
+                null,
+                null);
+        assertEquals(1, result.getCount());
+    }
+
+    @Test
+    public void testPhoneNumberDeletedCase() {
+        insertContactToEabProvider(1, 1, 2, "123456");
+        insertContactToEabProvider(1, 1, 3, "1234567");
+        insertContactToEabProvider(1, 1, 4, "12345678");
+        // Delete phone number 12345678
+        insertContactToContactProvider(1, 1, 2, "123456");
+        insertContactToContactProvider(1, 1, 3, "1234567");
+
+        new EabContactSyncController().syncContactToEabProvider(mContext);
+
+        Cursor result = mProviderTestRule.getResolver().query(
+                EabProvider.CONTACT_URI,
+                null,
+                null,
+                null);
+        assertEquals(2, result.getCount());
+    }
+
+    @Test
+    public void testPhoneNumberUpdatedCase() {
+        insertContactToEabProvider(1, 1, 2, "123456");
+        insertContactToEabProvider(1, 1, 3, "1234567");
+        insertContactToEabProvider(1, 1, 4, "12345678");
+        // Update phone number to 1,2,3
+        insertContactToContactProvider(1, 1, 2, "1");
+        insertContactToContactProvider(1, 1, 3, "2");
+        insertContactToContactProvider(1, 1, 4, "3");
+
+        new EabContactSyncController().syncContactToEabProvider(mContext);
+
+        Cursor result = mProviderTestRule.getResolver().query(
+                EabProvider.CONTACT_URI,
+                null,
+                null,
+                null,
+                EabProvider.ContactColumns.DATA_ID);
+        result.moveToFirst();
+        assertEquals(3, result.getCount());
+        assertEquals("1",
+                result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+        result.moveToNext();
+        assertEquals("2",
+                result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+        result.moveToNext();
+        assertEquals("3",
+                result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+    }
+
+    private void insertDeletedContactToContactProvider(int contactId, int timestamp) {
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.DeletedContacts.CONTACT_ID, contactId);
+        values.put(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, timestamp);
+        mContext.getContentResolver().insert(
+                ContactsContract.DeletedContacts.CONTENT_URI, values);
+    }
+
+    private void insertContactToContactProvider(
+            int contactId, int rawContactId, int dataId, String number) {
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.Data._ID, dataId);
+        values.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+        values.put(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, rawContactId);
+        values.put(ContactsContract.Data.MIMETYPE, CONTENT_ITEM_TYPE);
+        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, number);
+        values.put(ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP, 1);
+
+        mContext.getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
+    }
+
+    private void insertContactToEabProvider(int contactId,
+            int rawContactId, int dataId, String phoneNumber) {
+        ContentValues values = new ContentValues();
+        values.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+        values.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId);
+        values.put(EabProvider.ContactColumns.DATA_ID, dataId);
+        values.put(EabProvider.ContactColumns.PHONE_NUMBER, phoneNumber);
+        mContext.getContentResolver().insert(EabProvider.CONTACT_URI, values);
+    }
+
+    /**
+     * Create a fake contact provider that store ContentValues in hashmap when invoke insert()
+     * and convert to cursor when invoke query()
+     */
+    public static class FakeContactProvider extends ContentProvider {
+        private final HashMap<Uri, List<ContentValues>> mFakeProviderData = new HashMap<>();
+
+        public FakeContactProvider() {
+        }
+
+        public void clearData() {
+            mFakeProviderData.clear();
+        }
+
+        @Override
+        public boolean onCreate() {
+            return true;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return convertContentValuesToCursor(mFakeProviderData.get(uri));
+        }
+
+        @Nullable
+        @Override
+        public String getType(@NonNull Uri uri) {
+            return null;
+        }
+
+        @Nullable
+        @Override
+        public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+            List<ContentValues> allDataList =
+                    mFakeProviderData.computeIfAbsent(uri, k -> new ArrayList<>());
+            allDataList.add(new ContentValues(values));
+            return null;
+        }
+
+        @Override
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            return 0;
+        }
+
+        @Override
+        public int update(@NonNull Uri uri, @Nullable ContentValues values,
+                @Nullable String selection, @Nullable String[] selectionArgs) {
+            return 0;
+        }
+
+        private Cursor convertContentValuesToCursor(List<ContentValues> valuesList) {
+            if (valuesList != null) {
+                MatrixCursor result =
+                        new MatrixCursor(valuesList.get(0).keySet().toArray(new String[0]));
+                for (ContentValues contentValue : valuesList) {
+                    MatrixCursor.RowBuilder builder = result.newRow();
+                    for (String key : contentValue.keySet()) {
+                        builder.add(key, contentValue.get(key));
+                    }
+                }
+                return result;
+            } else {
+                return new MatrixCursor(new String[0]);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
new file mode 100644
index 0000000..af52217
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_NETWORK;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.COMMON_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.CONTACT_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.OPTIONS_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.PRESENCE_URI;
+
+import static org.junit.Assert.fail;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactUceCapability;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.temporal.ChronoUnit;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class EabControllerTest extends ImsTestBase {
+    EabProviderTestable mEabProviderTestable = new EabProviderTestable();
+    EabControllerImpl mEabController;
+    PersistableBundle mBundle;
+    ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+    private static final int TEST_SUB_ID = 1;
+    private static final String TEST_PHONE_NUMBER = "16661234567";
+    private static final String TEST_SERVICE_STATUS = "status";
+    private static final String TEST_SERVICE_SERVICE_ID = "serviceId";
+    private static final String TEST_SERVICE_VERSION = "version";
+    private static final String TEST_SERVICE_DESCRIPTION = "description";
+    private static final boolean TEST_AUDIO_CAPABLE = true;
+    private static final boolean TEST_VIDEO_CAPABLE = false;
+
+    private static final int TIME_OUT_IN_SEC = 5;
+    private static final Uri TEST_CONTACT_URI = Uri.parse(TEST_PHONE_NUMBER + "@android.test");
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockContentResolver mockContentResolver =
+                (MockContentResolver) mContext.getContentResolver();
+        mEabProviderTestable.initializeForTesting(mContext);
+        mockContentResolver.addProvider(EabProvider.AUTHORITY, mEabProviderTestable);
+
+        insertContactInfoToDB();
+        mEabController = new EabControllerImpl(
+                mContext, TEST_SUB_ID, null, Looper.getMainLooper());
+
+        mBundle = mContextFixture.getTestCarrierConfigBundle();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testGetAvailability() {
+        List<RcsContactUceCapability> contactList = new ArrayList<>();
+        contactList.add(createPresenceCapability(false));
+
+        mEabController.saveCapabilities(contactList);
+
+        EabCapabilityResult result = mEabController.getAvailability(TEST_CONTACT_URI);
+        Assert.assertEquals(EabCapabilityResult.EAB_QUERY_SUCCESSFUL, result.getStatus());
+        Assert.assertEquals(TEST_CONTACT_URI,
+                result.getContactCapabilities().getContactUri());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetCapability() {
+        List<RcsContactUceCapability> contactList = new ArrayList<>();
+        contactList.add(createPresenceCapability(false));
+
+        mEabController.saveCapabilities(contactList);
+
+        List<Uri> contactUriList = new ArrayList<>();
+        contactUriList.add(TEST_CONTACT_URI);
+        Assert.assertEquals(1,
+                mEabController.getCapabilities(contactUriList).size());
+        Assert.assertEquals(EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+                mEabController.getCapabilities(contactUriList).get(0).getStatus());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetExpiredCapability() {
+        List<RcsContactUceCapability> contactList = new ArrayList<>();
+        contactList.add(createPresenceCapability(true));
+
+        mEabController.saveCapabilities(contactList);
+
+        List<Uri> contactUriList = new ArrayList<>();
+        contactUriList.add(TEST_CONTACT_URI);
+        Assert.assertEquals(1,
+                mEabController.getCapabilities(contactUriList).size());
+        Assert.assertEquals(EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
+                mEabController.getCapabilities(contactUriList).get(0).getStatus());
+    }
+
+    @Test
+    @SmallTest
+    public void testNonRcsCapability() {
+        // Set non-rcs capabilities expiration to 121 days
+        mBundle.putInt(KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT, 121 * 24 * 60 * 60);
+        // Set timestamp to 120 days age
+        GregorianCalendar date = new GregorianCalendar();
+        date.setTimeZone(TimeZone.getTimeZone("UTC"));
+        date.add(Calendar.DATE, -120);
+
+        List<RcsContactUceCapability> contactList = new ArrayList<>();
+        contactList.add(createPresenceNonRcsCapability(Instant.now()));
+
+        mEabController.saveCapabilities(contactList);
+
+        List<Uri> contactUriList = new ArrayList<>();
+        contactUriList.add(TEST_CONTACT_URI);
+
+        // Verify result is not expired
+        Assert.assertEquals(1,
+                mEabController.getCapabilities(contactUriList).size());
+        Assert.assertEquals(EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+                mEabController.getCapabilities(contactUriList).get(0).getStatus());
+    }
+
+    @Test
+    @SmallTest
+    public void testNonRcsCapabilityExpired() {
+        // Set non-rcs capabilities expiration to 119 days
+        mBundle.putInt(KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT, 119 * 24 * 60 * 60);
+        // Set timestamp to 120 days age
+        Instant timestamp = Instant.now().minus(120, ChronoUnit.DAYS);
+
+        List<RcsContactUceCapability> contactList = new ArrayList<>();
+        contactList.add(createPresenceNonRcsCapability(timestamp));
+        mEabController.saveCapabilities(contactList);
+
+        // Verify result is expired
+        List<Uri> contactUriList = new ArrayList<>();
+        contactUriList.add(TEST_CONTACT_URI);
+        Assert.assertEquals(1,
+                mEabController.getCapabilities(contactUriList).size());
+        Assert.assertEquals(EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
+                mEabController.getCapabilities(contactUriList).get(0).getStatus());
+    }
+
+    @Test
+    @SmallTest
+    public void testCleanupInvalidDataInCommonTable() throws InterruptedException {
+        // Insert invalid data in common table
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, -1);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        data.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, -1);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        mExecutor.execute(mEabController.mCapabilityCleanupRunnable);
+        mExecutor.awaitTermination(TIME_OUT_IN_SEC, TimeUnit.SECONDS);
+
+        // Verify the entry that cannot map to presence/option table has been removed
+        Cursor cursor = mContext.getContentResolver().query(COMMON_URI, null, null, null, null);
+        while(cursor.moveToNext()) {
+            int contactId = cursor.getInt(
+                    cursor.getColumnIndex(EabProvider.EabCommonColumns.EAB_CONTACT_ID));
+            if (contactId == -1) {
+                fail("Invalid data didn't been cleared");
+            }
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testCleanupInvalidDataInPresenceTable() throws InterruptedException {
+        String expiredContact = "expiredContact";
+        GregorianCalendar expiredDate = new GregorianCalendar();
+        expiredDate.setTimeZone(TimeZone.getTimeZone("UTC"));
+        expiredDate.add(Calendar.DATE, -120);
+        // Insert invalid data in presence table
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        Uri commonUri = mContext.getContentResolver().insert(COMMON_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonUri.getLastPathSegment());
+        data.put(EabProvider.PresenceTupleColumns.CONTACT_URI, expiredContact);
+        data.put(EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP,
+                expiredDate.getTime().getTime() / 1000);
+        mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+        mExecutor.execute(mEabController.mCapabilityCleanupRunnable);
+        mExecutor.awaitTermination(TIME_OUT_IN_SEC, TimeUnit.SECONDS);
+
+        // Verify the invalid data has been removed after save capabilities
+        Cursor cursor = mContext.getContentResolver().query(PRESENCE_URI, null, null, null, null);
+        while(cursor.moveToNext()) {
+            String contactUri = cursor.getString(
+                    cursor.getColumnIndex(EabProvider.PresenceTupleColumns.CONTACT_URI));
+            if (contactUri.equals(expiredContact)) {
+                fail("Invalid data didn't been cleared");
+            }
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testCleanupInvalidDataInOptionTable() throws InterruptedException {
+        String expiredFeatureTag = "expiredFeatureTag";
+        GregorianCalendar expiredDate = new GregorianCalendar();
+        expiredDate.setTimeZone(TimeZone.getTimeZone("UTC"));
+        expiredDate.add(Calendar.DATE, -120);
+        // Insert invalid data in presence table
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_NOT_FOUND);
+        Uri commonUri = mContext.getContentResolver().insert(COMMON_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonUri.getLastPathSegment());
+        data.put(EabProvider.OptionsColumns.FEATURE_TAG, expiredFeatureTag);
+        data.put(EabProvider.OptionsColumns.REQUEST_TIMESTAMP,
+                expiredDate.getTime().getTime() / 1000);
+        mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+        mExecutor.execute(mEabController.mCapabilityCleanupRunnable);
+        mExecutor.awaitTermination(TIME_OUT_IN_SEC, TimeUnit.SECONDS);
+
+        // Verify the invalid data has been removed after save capabilities
+        Cursor cursor = mContext.getContentResolver().query(OPTIONS_URI, null, null, null, null);
+        while(cursor.moveToNext()) {
+            String featureTag = cursor.getString(
+                    cursor.getColumnIndex(EabProvider.OptionsColumns.FEATURE_TAG));
+            if (featureTag.equals(expiredFeatureTag)) {
+                fail("Invalid data didn't been cleared");
+            }
+        }
+    }
+
+    private RcsContactUceCapability createPresenceCapability(boolean isExpired) {
+        Instant timestamp;
+        if (isExpired) {
+            timestamp = Instant.now().minus(120, ChronoUnit.DAYS);
+        } else {
+            timestamp = Instant.now().plus(120, ChronoUnit.DAYS);
+        }
+
+        RcsContactPresenceTuple.ServiceCapabilities.Builder serviceCapabilitiesBuilder =
+                new RcsContactPresenceTuple.ServiceCapabilities.Builder(TEST_AUDIO_CAPABLE,
+                        TEST_VIDEO_CAPABLE);
+        RcsContactPresenceTuple tupleWithServiceCapabilities =
+                new RcsContactPresenceTuple.Builder(TEST_SERVICE_STATUS, TEST_SERVICE_SERVICE_ID,
+                        TEST_SERVICE_VERSION)
+                        .setServiceDescription(TEST_SERVICE_DESCRIPTION)
+                        .setContactUri(TEST_CONTACT_URI)
+                        .setServiceCapabilities(serviceCapabilitiesBuilder.build())
+                        .setTime(timestamp)
+                        .build();
+
+        RcsContactPresenceTuple tupleWithEmptyServiceCapabilities =
+                new RcsContactPresenceTuple.Builder(TEST_SERVICE_STATUS, TEST_SERVICE_SERVICE_ID,
+                        TEST_SERVICE_VERSION)
+                        .setServiceDescription(TEST_SERVICE_DESCRIPTION)
+                        .setContactUri(TEST_CONTACT_URI)
+                        .setTime(timestamp)
+                        .build();
+
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(
+                        TEST_CONTACT_URI, SOURCE_TYPE_NETWORK, REQUEST_RESULT_FOUND);
+        builder.addCapabilityTuple(tupleWithServiceCapabilities);
+        builder.addCapabilityTuple(tupleWithEmptyServiceCapabilities);
+        return builder.build();
+    }
+
+    private RcsContactUceCapability createPresenceNonRcsCapability(Instant timestamp) {
+        RcsContactPresenceTuple.ServiceCapabilities.Builder serviceCapabilitiesBuilder =
+                new RcsContactPresenceTuple.ServiceCapabilities.Builder(false, false);
+        RcsContactPresenceTuple tupleWithServiceCapabilities =
+                new RcsContactPresenceTuple.Builder(TEST_SERVICE_STATUS, TEST_SERVICE_SERVICE_ID,
+                        TEST_SERVICE_VERSION)
+                        .setServiceDescription(TEST_SERVICE_DESCRIPTION)
+                        .setContactUri(TEST_CONTACT_URI)
+                        .setServiceCapabilities(serviceCapabilitiesBuilder.build())
+                        .setTime(timestamp)
+                        .build();
+
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(
+                        TEST_CONTACT_URI, SOURCE_TYPE_NETWORK, REQUEST_RESULT_NOT_FOUND);
+        builder.addCapabilityTuple(tupleWithServiceCapabilities);
+        return builder.build();
+    }
+
+    private void insertContactInfoToDB() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, TEST_PHONE_NUMBER);
+        data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java
new file mode 100644
index 0000000..3c22e0e
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java
@@ -0,0 +1,338 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_FOUND;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.ALL_DATA_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.COMMON_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.CONTACT_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.OPTIONS_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.PRESENCE_URI;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class EabProviderTest extends ImsTestBase {
+    EabProviderTestable mEabProviderTestable = new EabProviderTestable();
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockContentResolver mockContentResolver =
+                (MockContentResolver) mContext.getContentResolver();
+        mEabProviderTestable.initializeForTesting(mContext);
+        mockContentResolver.addProvider(EabProvider.AUTHORITY, mEabProviderTestable);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testQueryContactInfo() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+        data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(CONTACT_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testContactIsUnique() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(CONTACT_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testQueryCommonInfo() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(COMMON_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testCommonIsUnique() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(COMMON_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testQueryPresentInfo() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+        data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+        mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(PRESENCE_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testPresentTupleIsNotUnique() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+        data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+        mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.PresenceTupleColumns.SERVICE_ID, "Android is the best.");
+        data.put(EabProvider.PresenceTupleColumns.SERVICE_VERSION, "Android is the best.");
+        mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(PRESENCE_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(2, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testQueryOptionInfo() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best.");
+        mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(OPTIONS_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testOptionIsNotUnique() {
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best.");
+        mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best!");
+        mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(OPTIONS_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(2, cursor.getCount());
+    }
+
+
+    @Test
+    @SmallTest
+    public void testQueryByAllDataURI() {
+
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.ContactColumns._ID, 1);
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+        data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+        data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+        mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(ALL_DATA_URI,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+    }
+
+    @Test
+    @SmallTest
+    public void testQueryBySubIdAndPhoneNumber() {
+        int subid = 1;
+        int incorrectSubid = 2;
+
+        // Insert a contact that request by presence
+        ContentValues data = new ContentValues();
+        data.put(EabProvider.ContactColumns._ID, 1);
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+        data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        data.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, subid);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+        data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+        data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+        mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+        // Insert a contact that request by option
+        data = new ContentValues();
+        data.put(EabProvider.ContactColumns._ID, 2);
+        data.put(EabProvider.ContactColumns.PHONE_NUMBER, "654321");
+        data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 2);
+        mContext.getContentResolver().insert(CONTACT_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 2);
+        data.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, incorrectSubid);
+        data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_OPTIONS);
+        data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+        mContext.getContentResolver().insert(COMMON_URI, data);
+
+        data = new ContentValues();
+        data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 2);
+        data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best.");
+        mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+        Uri testUri = Uri.withAppendedPath(
+                Uri.withAppendedPath(ALL_DATA_URI, String.valueOf(1)), "123456");
+        Cursor cursor = mContext.getContentResolver().query(testUri,
+                null,
+                null,
+                null,
+                null);
+
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(1, cursor.getInt(cursor.getColumnIndex(
+                EabProvider.PresenceTupleColumns.VIDEO_CAPABLE)));
+    }
+
+    @Test
+    @SmallTest
+    public void testBulkInsert() {
+        ContentValues[] data = new ContentValues[2];
+        ContentValues insertData = new ContentValues();
+        insertData.put(EabProvider.ContactColumns._ID, 1);
+        insertData.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+        insertData.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+        data[0] = insertData;
+        insertData = new ContentValues();
+        insertData.put(EabProvider.ContactColumns._ID, 2);
+        insertData.put(EabProvider.ContactColumns.PHONE_NUMBER, "1234567");
+        insertData.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 2);
+        data[1] = insertData;
+
+        mContext.getContentResolver().bulkInsert(CONTACT_URI, data);
+
+        Cursor cursor = mContext.getContentResolver().query(CONTACT_URI,
+                null,
+                null,
+                null,
+                null);
+        assertEquals(2, cursor.getCount());
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java
new file mode 100644
index 0000000..79ebd54
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java
@@ -0,0 +1,105 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_COMMON_TABLE;
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_CONTACT_TABLE;
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_OPTIONS_TABLE;
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_PRESENCE_TUPLE_TABLE;
+
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+public class EabProviderTestable extends EabProvider {
+    private static final String TAG = EabProviderTestable.class.getSimpleName();
+
+    private InMemoryEabProviderDbHelper mDbHelper;
+
+    @Override
+    public boolean onCreate() {
+        Log.d(TAG, "onCreate called");
+        mDbHelper = new InMemoryEabProviderDbHelper();
+        return true;
+    }
+
+    // close mDbHelper database object
+    protected void closeDatabase() {
+        mDbHelper.close();
+    }
+
+    void initializeForTesting(Context context) {
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = EabProvider.AUTHORITY;
+
+        attachInfoForTesting(context, providerInfo);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        Cursor cursor = super.query(uri, projection, selection, selectionArgs, sortOrder);
+        Log.d(TAG, "InMemoryEabProviderDbHelper query" + DatabaseUtils.dumpCursorToString(cursor));
+        return cursor;
+    }
+
+    @Override
+    public SQLiteDatabase getReadableDatabase() {
+        Log.d(TAG, "getReadableDatabase called" + mDbHelper.getReadableDatabase());
+        return mDbHelper.getReadableDatabase();
+    }
+
+    @Override
+    public SQLiteDatabase getWritableDatabase() {
+        Log.d(TAG, "getWritableDatabase called" + mDbHelper.getWritableDatabase());
+        return mDbHelper.getWritableDatabase();
+    }
+
+    /**
+     * An in memory DB for EabProviderTestable to use
+     */
+    public static class InMemoryEabProviderDbHelper extends SQLiteOpenHelper {
+        public InMemoryEabProviderDbHelper() {
+            super(null,      // no context is needed for in-memory db
+                    null,      // db file name is null for in-memory db
+                    null,      // CursorFactory is null by default
+                    1);        // db version is no-op for tests
+            Log.d(TAG, "InMemoryEabProviderDbHelper creating in-memory database");
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            //set up the EAB table
+            Log.d(TAG, "InMemoryEabProviderDbHelper onCreate");
+            db.execSQL(SQL_CREATE_CONTACT_TABLE);
+            db.execSQL(SQL_CREATE_COMMON_TABLE);
+            db.execSQL(SQL_CREATE_PRESENCE_TUPLE_TABLE);
+            db.execSQL(SQL_CREATE_OPTIONS_TABLE);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            Log.d(TAG, "InMemoryEabProviderDbHelper onUpgrade");
+            return;
+        }
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java
new file mode 100644
index 0000000..f8038be
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java
@@ -0,0 +1,500 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class PidfParserTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testConvertToPidf() throws Exception {
+        RcsContactUceCapability capability = getRcsContactUceCapability();
+
+        String pidfResult = PidfParser.convertToPidf(capability);
+
+        String contact = "<contact>sip:test</contact>";
+        String audioSupported = "<caps:audio>true</caps:audio>";
+        String videoSupported = "<caps:video>true</caps:video>";
+        String description = "<op:version>1.0</op:version>";
+        assertTrue(pidfResult.contains(contact));
+        assertTrue(pidfResult.contains(audioSupported));
+        assertTrue(pidfResult.contains(videoSupported));
+        assertTrue(pidfResult.contains(description));
+    }
+
+    @Test
+    @SmallTest
+    public void testConvertFromPidfToRcsContactUceCapability() throws Exception {
+        final String contact = "sip:+11234567890@test";
+        final String serviceId = "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel";
+        final String serviceDescription = "MMTEL feature service";
+        final boolean isAudioSupported = true;
+        final boolean isVideoSupported = false;
+
+        // Create the first PIDF data
+        String pidfData = getPidfData(contact, serviceId, serviceDescription, isAudioSupported,
+                isVideoSupported);
+
+        // Convert to the class RcsContactUceCapability
+        RcsContactUceCapability capabilities = PidfParser.getRcsContactUceCapability(pidfData);
+        assertNotNull(capabilities);
+        assertEquals(Uri.parse(contact), capabilities.getContactUri());
+        assertEquals(RcsContactUceCapability.SOURCE_TYPE_NETWORK, capabilities.getSourceType());
+        assertEquals(RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE,
+                capabilities.getCapabilityMechanism());
+
+        List<RcsContactPresenceTuple> presenceTupleList = capabilities.getCapabilityTuples();
+        assertNotNull(presenceTupleList);
+        assertEquals(1, presenceTupleList.size());
+
+        RcsContactPresenceTuple presenceTuple1 = presenceTupleList.get(0);
+        assertEquals(serviceId, presenceTuple1.getServiceId());
+        assertEquals("1.0", presenceTuple1.getServiceVersion());
+        assertEquals(serviceDescription, presenceTuple1.getServiceDescription());
+        assertEquals(Uri.parse(contact), presenceTuple1.getContactUri());
+        assertEquals("2001-01-01T01:00:00Z", presenceTuple1.getTime().toString());
+        assertTrue(presenceTuple1.getServiceCapabilities().isAudioCapable());
+        assertFalse(presenceTuple1.getServiceCapabilities().isVideoCapable());
+    }
+
+    @Test
+    @SmallTest
+    public void testConvertFromNewlineIncludedPidfToRcsContactUceCapability() throws Exception {
+        final String contact = "tel:+11234567890";
+
+        final RcsContactPresenceTuple.Builder tuple1Builder = new RcsContactPresenceTuple.Builder(
+                "open",
+                "org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp",
+                "1.0");
+        tuple1Builder.setServiceDescription("DiscoveryPresence")
+                .setContactUri(Uri.parse(contact));
+
+        final RcsContactPresenceTuple.Builder tuple2Builder = new RcsContactPresenceTuple.Builder(
+                "open",
+                "org.openmobilealliance:StandaloneMsg",
+                "2.0");
+        tuple2Builder.setServiceDescription("StandaloneMsg")
+                .setContactUri(Uri.parse(contact));
+
+        final RcsContactPresenceTuple.Builder tuple3Builder = new RcsContactPresenceTuple.Builder(
+                "open",
+                "org.openmobilealliance:ChatSession",
+                "2.0");
+        tuple3Builder.setServiceDescription("Session Mode Messaging")
+                .setContactUri(Uri.parse(contact));
+
+        final RcsContactPresenceTuple.Builder tuple4Builder = new RcsContactPresenceTuple.Builder(
+                "open",
+                "org.openmobilealliance:File-Transfer",
+                "1.0");
+        tuple4Builder.setServiceDescription("File Transfer")
+                .setContactUri(Uri.parse(contact));
+
+        final RcsContactPresenceTuple.Builder tuple5Builder = new RcsContactPresenceTuple.Builder(
+                "open",
+                "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel",
+                "1.0");
+        tuple5Builder.setServiceDescription("VoLTE service");
+        ServiceCapabilities.Builder capBuilder = new ServiceCapabilities.Builder(true, true);
+        tuple5Builder.setServiceCapabilities(capBuilder.build())
+                .setContactUri(Uri.parse(contact));
+
+        final List<RcsContactPresenceTuple> expectedTupleList = new ArrayList<>(5);
+        expectedTupleList.add(tuple1Builder.build());
+        expectedTupleList.add(tuple2Builder.build());
+        expectedTupleList.add(tuple3Builder.build());
+        expectedTupleList.add(tuple4Builder.build());
+        expectedTupleList.add(tuple5Builder.build());
+
+        // Create the newline included PIDF data
+        String pidfData = getPidfDataWithNewlineAndWhitespaceCharacters();
+
+        // Convert to the class RcsContactUceCapability
+        RcsContactUceCapability capabilities = PidfParser.getRcsContactUceCapability(pidfData);
+
+        assertNotNull(capabilities);
+        assertEquals(Uri.parse(contact), capabilities.getContactUri());
+        assertEquals(RcsContactUceCapability.SOURCE_TYPE_NETWORK, capabilities.getSourceType());
+        assertEquals(RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE,
+                capabilities.getCapabilityMechanism());
+
+        List<RcsContactPresenceTuple> presenceTupleList = capabilities.getCapabilityTuples();
+        assertNotNull(presenceTupleList);
+        assertEquals(expectedTupleList.size(), presenceTupleList.size());
+
+        for(RcsContactPresenceTuple tuple : presenceTupleList) {
+            String serviceId = tuple.getServiceId();
+            RcsContactPresenceTuple expectedTuple = findTuple(serviceId, expectedTupleList);
+            if (expectedTuple == null) {
+                fail("The service ID is invalid");
+            }
+
+            assertEquals(expectedTuple.getStatus(), tuple.getStatus());
+            assertEquals(expectedTuple.getServiceVersion(), tuple.getServiceVersion());
+            assertEquals(expectedTuple.getServiceDescription(), tuple.getServiceDescription());
+            assertEquals(expectedTuple.getTime(), tuple.getTime());
+            assertEquals(expectedTuple.getContactUri(), tuple.getContactUri());
+
+            ServiceCapabilities expectedCap = expectedTuple.getServiceCapabilities();
+            ServiceCapabilities resultCap = tuple.getServiceCapabilities();
+            if (expectedCap != null) {
+                assertNotNull(resultCap);
+                assertEquals(expectedCap.isAudioCapable(), resultCap.isAudioCapable());
+                assertEquals(expectedCap.isVideoCapable(), resultCap.isVideoCapable());
+            } else {
+                assertNull(resultCap);
+            }
+        }
+    }
+
+    private RcsContactPresenceTuple findTuple(String serviceId,
+            List<RcsContactPresenceTuple> expectedTupleList) {
+        if (serviceId == null) {
+            return null;
+        }
+        for (RcsContactPresenceTuple tuple : expectedTupleList) {
+            if (serviceId.equalsIgnoreCase(tuple.getServiceId())) {
+                return tuple;
+            }
+        }
+        return null;
+    }
+
+    @Test
+    @SmallTest
+    public void testConvertToRcsContactUceCapabilityForMultipleTuples() throws Exception {
+        final String contact = "sip:+11234567890@test";
+        final String serviceId1 = "org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp";
+        final String serviceDescription1 = "capabilities discovery";
+        final String serviceId2 = "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel";
+        final String serviceDescription2 = "MMTEL feature service";
+        final boolean isAudioSupported = true;
+        final boolean isVideoSupported = false;
+
+        // Create the PIDF data
+        String pidfData = getPidfDataWithMultiTuples(contact, serviceId1, serviceDescription1,
+                serviceId2, serviceDescription2, isAudioSupported, isVideoSupported);
+
+        // Convert to the class RcsContactUceCapability
+        RcsContactUceCapability capabilities = PidfParser.getRcsContactUceCapability(pidfData);
+
+        assertNotNull(capabilities);
+        assertEquals(Uri.parse(contact), capabilities.getContactUri());
+
+        List<RcsContactPresenceTuple> presenceTupleList = capabilities.getCapabilityTuples();
+        assertNotNull(presenceTupleList);
+        assertEquals(2, presenceTupleList.size());
+
+        // Verify the first tuple information
+        RcsContactPresenceTuple presenceTuple1 = presenceTupleList.get(0);
+        assertEquals(serviceId1, presenceTuple1.getServiceId());
+        assertEquals("1.0", presenceTuple1.getServiceVersion());
+        assertEquals(serviceDescription1, presenceTuple1.getServiceDescription());
+        assertEquals(Uri.parse(contact), presenceTuple1.getContactUri());
+        assertEquals("2001-01-01T01:00:00Z", presenceTuple1.getTime().toString());
+        assertNull(presenceTuple1.getServiceCapabilities());
+
+        // Verify the second tuple information
+        RcsContactPresenceTuple presenceTuple2 = presenceTupleList.get(1);
+        assertEquals(serviceId2, presenceTuple2.getServiceId());
+        assertEquals("1.0", presenceTuple2.getServiceVersion());
+        assertTrue(presenceTuple2.getServiceCapabilities().isAudioCapable());
+        assertFalse(presenceTuple2.getServiceCapabilities().isVideoCapable());
+        assertEquals(serviceDescription2, presenceTuple2.getServiceDescription());
+        assertEquals(Uri.parse(contact), presenceTuple2.getContactUri());
+        assertEquals("2001-02-02T01:00:00Z", presenceTuple2.getTime().toString());
+        assertNotNull(presenceTuple2.getServiceCapabilities());
+        assertEquals(isAudioSupported, presenceTuple2.getServiceCapabilities().isAudioCapable());
+        assertEquals(isVideoSupported, presenceTuple2.getServiceCapabilities().isVideoCapable());
+    }
+
+    @Test
+    @SmallTest
+    public void testConversionAndRestoration() throws Exception {
+        // Create the capability
+        final RcsContactUceCapability capability = getRcsContactUceCapability();
+
+        // Convert the capability to the pidf
+        final String pidf = PidfParser.convertToPidf(capability);
+
+        // Restore to the RcsContactUceCapability from the pidf
+        final RcsContactUceCapability restoredCapability =
+                PidfParser.getRcsContactUceCapability(pidf);
+
+        assertEquals(capability.getContactUri(), restoredCapability.getContactUri());
+        assertEquals(capability.getCapabilityMechanism(),
+                restoredCapability.getCapabilityMechanism());
+        assertEquals(capability.getSourceType(), restoredCapability.getSourceType());
+
+        // Assert all the tuples are equal
+        List<RcsContactPresenceTuple> originalTuples = capability.getCapabilityTuples();
+        List<RcsContactPresenceTuple> restoredTuples = restoredCapability.getCapabilityTuples();
+
+        assertNotNull(restoredTuples);
+        assertEquals(originalTuples.size(), restoredTuples.size());
+
+        for (int i = 0; i < originalTuples.size(); i++) {
+            RcsContactPresenceTuple tuple = originalTuples.get(i);
+            RcsContactPresenceTuple restoredTuple = restoredTuples.get(i);
+
+            assertEquals(tuple.getContactUri(), restoredTuple.getContactUri());
+            assertEquals(tuple.getStatus(), restoredTuple.getStatus());
+            assertEquals(tuple.getServiceId(), restoredTuple.getServiceId());
+            assertEquals(tuple.getServiceVersion(), restoredTuple.getServiceVersion());
+            assertEquals(tuple.getServiceDescription(), restoredTuple.getServiceDescription());
+
+            boolean isAudioCapable = false;
+            boolean isVideoCapable = false;
+            boolean isRestoredAudioCapable = false;
+            boolean isRestoredVideoCapable = false;
+
+            ServiceCapabilities servCaps = tuple.getServiceCapabilities();
+            if (servCaps != null) {
+                isAudioCapable = servCaps.isAudioCapable();
+                isVideoCapable = servCaps.isVideoCapable();
+            }
+
+            ServiceCapabilities restoredServCaps = restoredTuple.getServiceCapabilities();
+            if (restoredServCaps != null) {
+                isRestoredAudioCapable = restoredServCaps.isAudioCapable();
+                isRestoredVideoCapable = restoredServCaps.isVideoCapable();
+            }
+
+            assertEquals(isAudioCapable, isRestoredAudioCapable);
+            assertEquals(isVideoCapable, isRestoredVideoCapable);
+        }
+     }
+
+    private String getPidfData(String contact, String serviceId, String serviceDescription,
+            boolean isAudioSupported, boolean isVideoSupported) {
+        StringBuilder pidfBuilder = new StringBuilder();
+        pidfBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<presence entity=\"" + contact + "\"")
+                .append(" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                // tuple data
+                .append("<tuple id=\"tid0\">")
+                .append("<status><basic>open</basic></status>")
+                .append("<op:service-description>")
+                .append("<op:service-id>").append(serviceId).append("</op:service-id>")
+                .append("<op:version>1.0</op:version>")
+                .append("<op:description>").append(serviceDescription).append("</op:description>")
+                .append("</op:service-description>")
+                // is audio supported
+                .append("<caps:servcaps>")
+                .append("<caps:audio>").append(isAudioSupported).append("</caps:audio>")
+                // is video supported
+                .append("<caps:video>").append(isVideoSupported).append("</caps:video>")
+                .append("</caps:servcaps>")
+                .append("<contact>").append(contact).append("</contact>")
+                .append("<timestamp>2001-01-01T01:00:00.00Z</timestamp>")
+                .append("</tuple></presence>");
+        return pidfBuilder.toString();
+    }
+
+    private String getPidfDataWithNewlineAndWhitespaceCharacters() {
+        String pidf = "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\" "
+                        + "xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\" "
+                        + "xmlns:b=\"urn:ietf:params:xml:ns:pidf:caps\" "
+                        + "entity=\"tel:+11234567890\">\n"
+                // Tuple: Discovery
+                + "   <tuple id=\"DiscoveryPres\">\n\t"
+                + "     <status>\n\t"
+                + "       <basic>open</basic>\n\t"
+                + "     </status>\n\t"
+                + "     <op:service-description>\n\t"
+                + "       <op:service-id>org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp"
+                                + "</op:service-id>\n\t"
+                + "       <op:version>1.0</op:version>\n\t"
+                + "       <op:description>DiscoveryPresence</op:description>\n\t"
+                + "     </op:service-description>\n\t"
+                + "     <contact>tel:+11234567890</contact>\n\t"
+                + "   </tuple>\n\t"
+                // Tuple: VoLTE
+                + "   <tuple id=\"VoLTE\">\n"
+                + "     <status>\n"
+                + "       <basic>open</basic>\n"
+                + "     </status>\n"
+                + "     <b:servcaps>\n"
+                + "       <b:audio>true</b:audio>\n"
+                + "       <b:video>true</b:video>\n"
+                + "       <b:duplex>\n"
+                + "         <b:supported>\n"
+                + "           <b:full/>\n"
+                + "         </b:supported>\n"
+                + "       </b:duplex>\n"
+                + "     </b:servcaps>\n"
+                + "     <op:service-description>\n"
+                + "       <op:service-id>org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel"
+                                + "</op:service-id>\n"
+                + "       <op:version>1.0</op:version>\n"
+                + "       <op:description>VoLTE service</op:description>\n"
+                + "     </op:service-description>\n"
+                + "     <contact>tel:+11234567890</contact>\n"
+                + "   </tuple>\n"
+                // Tuple: Standalone Message
+                + "   <tuple id=\"StandaloneMsg\">\n"
+                + "     <status>\n"
+                + "       <basic>open</basic>\n"
+                + "     </status>\n"
+                + "     <op:service-description>\n"
+                + "       <op:service-id>org.openmobilealliance:StandaloneMsg</op:service-id>\n"
+                + "       <op:version>2.0</op:version>\n"
+                + "       <op:description>StandaloneMsg</op:description>\n"
+                + "     </op:service-description>\n"
+                + "     <contact>tel:+11234567890</contact>\n"
+                + "   </tuple>\n"
+                // Tuple: Session Mode Message
+                + "   <tuple id=\"SessModeMessa\">\n"
+                + "     <status>\n"
+                + "       <basic>open</basic>\n"
+                + "     </status>\n"
+                + "     <op:service-description>\n"
+                + "       <op:service-id>org.openmobilealliance:ChatSession</op:service-id>\n"
+                + "       <op:version>2.0</op:version>\n"
+                + "       <op:description>Session Mode Messaging</op:description>\n"
+                + "     </op:service-description>\n"
+                + "     <contact>tel:+11234567890</contact>\n"
+                + "   </tuple>\n"
+                // Tuple: File Transfer
+                + "   <tuple id=\"FileTransfer\">\n"
+                + "     <status>\n"
+                + "       <basic>open</basic>\n"
+                + "     </status>\n"
+                + "     <op:service-description>\n"
+                + "       <op:service-id>org.openmobilealliance:File-Transfer</op:service-id>\n"
+                + "       <op:version>1.0</op:version>\n"
+                + "       <op:description>File Transfer</op:description>\n"
+                + "     </op:service-description>\n"
+                + "     <contact>tel:+11234567890</contact>\n"
+                + "   </tuple>\n"
+                + " </presence>";
+
+        return pidf;
+    }
+
+    private String getPidfDataWithMultiTuples(String contact, String serviceId1,
+            String serviceDescription1, String serviceId2, String serviceDescription2,
+            boolean audioSupported, boolean videoSupported) {
+        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                + "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\""
+                + " xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\""
+                + " xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\""
+                + " entity=\"" + contact + "\">"
+                // tuple 1
+                + "<tuple id=\"a0\">"
+                + "<status><basic>open</basic></status>"
+                + "<op:service-description>"
+                + "<op:service-id>" + serviceId1 + "</op:service-id>"
+                + "<op:version>1.0</op:version>"
+                + "<op:description>" + serviceDescription1 + "</op:description>"
+                + "</op:service-description>"
+                + "<contact>" + contact + "</contact>"
+                + "<timestamp>2001-01-01T01:00:00.00Z</timestamp>"
+                + "</tuple>"
+                // tuple 2
+                + "<tuple id=\"a1\">"
+                + "<status><basic>open</basic></status>"
+                + "<op:service-description>"
+                + "<op:service-id>" + serviceId2 + "</op:service-id>"
+                + "<op:version>1.0</op:version>"
+                + "<op:description>" + serviceDescription2 + "</op:description>"
+                + "</op:service-description>"
+                + "<caps:servcaps>"
+                + "<caps:audio>" + audioSupported + "</caps:audio>"
+                + "<caps:duplex>"
+                + "<caps:supported><caps:full></caps:full></caps:supported>"
+                + "</caps:duplex>"
+                + "<caps:video>" + videoSupported + "</caps:video>"
+                + "</caps:servcaps>"
+                + "<contact>" + contact + "</contact>"
+                + "<timestamp>2001-02-02T01:00:00.00Z</timestamp>"
+                + "</tuple>"
+                + "</presence>";
+    }
+
+    private RcsContactUceCapability getRcsContactUceCapability() {
+        final Uri contact = Uri.fromParts("sip", "test", null);
+        final boolean isAudioCapable = true;
+        final boolean isVideoCapable = true;
+        final String duplexMode = ServiceCapabilities.DUPLEX_MODE_FULL;
+        final String basicStatus = RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN;
+        final String version = "1.0";
+        final String description = "description test";
+        final Instant nowTime = Instant.now();
+
+        // init the capabilities
+        ServiceCapabilities.Builder servCapsBuilder =
+                new ServiceCapabilities.Builder(isAudioCapable, isVideoCapable);
+        servCapsBuilder.addSupportedDuplexMode(duplexMode);
+
+        // init the presence tuple
+        RcsContactPresenceTuple.Builder tupleBuilder = new RcsContactPresenceTuple.Builder(
+                basicStatus, RcsContactPresenceTuple.SERVICE_ID_MMTEL, version);
+        tupleBuilder.setContactUri(contact)
+                .setServiceDescription(description)
+                .setTime(nowTime)
+                .setServiceCapabilities(servCapsBuilder.build());
+
+        PresenceBuilder presenceBuilder = new PresenceBuilder(contact,
+                RcsContactUceCapability.SOURCE_TYPE_NETWORK,
+                RcsContactUceCapability.REQUEST_RESULT_FOUND);
+        presenceBuilder.addCapabilityTuple(tupleBuilder.build());
+
+        return presenceBuilder.build();
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java
new file mode 100644
index 0000000..5bf9715
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Audio audio = new Audio();
+
+        assertEquals(CapsConstant.NAMESPACE, audio.getNamespace());
+        assertEquals(Audio.ELEMENT_NAME, audio.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        Audio audio = new Audio(true);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        audio.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<caps:audio")
+                .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+                .append(" xmlns:caps=\"").append(CapsConstant.NAMESPACE).append("\">")
+                .append("true")
+                .append("</caps:audio>");
+
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        StringBuilder audioExample = new StringBuilder();
+        audioExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<caps:audio xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                .append(true)
+                .append("</caps:audio>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(audioExample.toString());
+        parser.setInput(reader);
+
+        Audio audio = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Audio.ELEMENT_NAME.equals(parser.getName())) {
+                audio = new Audio();
+                audio.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(audio);
+        assertTrue(audio.isAudioSupported());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java
new file mode 100644
index 0000000..8852c3d
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class DuplexTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Duplex duplex = new Duplex();
+
+        assertEquals(CapsConstant.NAMESPACE, duplex.getNamespace());
+        assertEquals(Duplex.ELEMENT_NAME, duplex.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        Duplex duplex = new Duplex();
+        duplex.addSupportedType(Duplex.DUPLEX_FULL);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        duplex.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<caps:duplex")
+                .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+                .append(" xmlns:caps=\"").append(CapsConstant.NAMESPACE).append("\">")
+                .append("<caps:supported>")
+                .append("<caps:full />")
+                .append("</caps:supported>").append("</caps:duplex>");
+
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        StringBuilder duplexExample = new StringBuilder();
+        duplexExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<caps:duplex xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                .append("<caps:supported>")
+                .append("<caps:full />")
+                .append("</caps:supported>").append("</caps:duplex>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(duplexExample.toString());
+        parser.setInput(reader);
+
+        Duplex duplex = null;
+        int nextType = parser.next();
+
+        do {
+            // Find the start tag
+            if (nextType == XmlPullParser.START_TAG
+                    && Duplex.ELEMENT_NAME.equals(parser.getName())) {
+                duplex = new Duplex();
+                duplex.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(duplex);
+
+        List<String> supportedTypes = duplex.getSupportedTypes();
+        assertEquals(1, supportedTypes.size());
+        assertEquals(Duplex.DUPLEX_FULL, supportedTypes.get(0));
+
+        List<String> notSupportedTypes = duplex.getNotSupportedTypes();
+        assertTrue(notSupportedTypes.isEmpty());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java
new file mode 100644
index 0000000..d18a7eb
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceCapsTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        ServiceCaps serviceCaps = new ServiceCaps();
+
+        assertEquals(CapsConstant.NAMESPACE, serviceCaps.getNamespace());
+        assertEquals(ServiceCaps.ELEMENT_NAME, serviceCaps.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        Audio audio = new Audio(true);
+        Video video = new Video(true);
+        Duplex duplex = new Duplex();
+        duplex.addSupportedType(Duplex.DUPLEX_FULL);
+
+        ServiceCaps serviceCaps = new ServiceCaps();
+        serviceCaps.addElement(audio);
+        serviceCaps.addElement(video);
+        serviceCaps.addElement(duplex);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        serviceCaps.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        String verificationAudio = "<caps:audio>true</caps:audio>";
+        String verificationVideo = "<caps:video>true</caps:video>";
+        StringBuilder verificationDuplex = new StringBuilder();
+        verificationDuplex.append("<caps:duplex>")
+                .append("<caps:supported>")
+                .append("<caps:full />")
+                .append("</caps:supported>")
+                .append("</caps:duplex>");
+
+        assertTrue(result.contains(verificationAudio));
+        assertTrue(result.contains(verificationVideo));
+        assertTrue(result.contains(verificationDuplex.toString()));
+    }
+
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        StringBuilder serviceCapsExample = new StringBuilder();
+        serviceCapsExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<caps:servcaps xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                .append("<caps:audio>true</caps:audio>")
+                .append("<caps:video>true</caps:video>")
+                .append("<caps:duplex><caps:supported>")
+                .append("<caps:full />")
+                .append("</caps:supported></caps:duplex>")
+                .append("</caps:servcaps>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(serviceCapsExample.toString());
+        parser.setInput(reader);
+
+        ServiceCaps serviceCaps = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && ServiceCaps.ELEMENT_NAME.equals(parser.getName())) {
+                serviceCaps = new ServiceCaps();
+                serviceCaps.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(serviceCaps);
+
+        List<ElementBase> elements = serviceCaps.getElements();
+        Audio resultAudio = null;
+        Video resultVideo = null;
+        Duplex resultDuplex = null;
+        for (ElementBase element : elements) {
+            String elementName = element.getElementName();
+            if (Audio.ELEMENT_NAME.equals(elementName)) {
+                resultAudio = (Audio) element;
+            } else if (Video.ELEMENT_NAME.equals(elementName)) {
+                resultVideo = (Video) element;
+            } else if (Duplex.ELEMENT_NAME.equals(elementName)) {
+                resultDuplex = (Duplex) element;
+            }
+        }
+
+        assertNotNull(resultAudio);
+        assertTrue(resultAudio.isAudioSupported());
+
+        assertNotNull(resultVideo);
+        assertTrue(resultVideo.isVideoSupported());
+
+        assertNotNull(resultDuplex);
+
+        List<String> supportedTypes = resultDuplex.getSupportedTypes();
+        assertEquals(1, supportedTypes.size());
+        assertEquals(Duplex.DUPLEX_FULL, supportedTypes.get(0));
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java
new file mode 100644
index 0000000..c102176
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class VideoTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Video video = new Video();
+
+        assertEquals(CapsConstant.NAMESPACE, video.getNamespace());
+        assertEquals(Video.ELEMENT_NAME, video.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        Video video = new Video(true);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        video.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<caps:video")
+                .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+                .append(" xmlns:caps=\"").append(CapsConstant.NAMESPACE).append("\">")
+                .append("true")
+                .append("</caps:video>");
+
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        StringBuilder videoExample = new StringBuilder();
+        videoExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<caps:video xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                .append(true)
+                .append("</caps:video>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(videoExample.toString());
+        parser.setInput(reader);
+
+        Video video = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Video.ELEMENT_NAME.equals(parser.getName())) {
+                video = new Video();
+                video.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(video);
+        assertTrue(video.isVideoSupported());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java
new file mode 100644
index 0000000..d60b664
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class DescriptionTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Description description = new Description();
+
+        assertEquals(OmaPresConstant.NAMESPACE, description.getNamespace());
+        assertEquals(Description.ELEMENT_NAME, description.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        final String descriptionValue = "Description test";
+        Description description = new Description(descriptionValue);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        description.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<op:description")
+                .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+                .append(" xmlns:op=\"").append(OmaPresConstant.NAMESPACE).append("\">")
+                .append(descriptionValue)
+                .append("</op:description>");
+
+         assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String descriptionValue = "Description test";
+
+        StringBuilder descriptionExample = new StringBuilder();
+        descriptionExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<op:description xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+                .append(descriptionValue)
+                .append("</op:description>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(descriptionExample.toString());
+        parser.setInput(reader);
+
+        Description description = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Description.ELEMENT_NAME.equals(parser.getName())) {
+                description = new Description();
+                description.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(description);
+        assertEquals(descriptionValue, description.getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java
new file mode 100644
index 0000000..5e064cf
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceDescriptionTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        ServiceDescription serviceDescription = new ServiceDescription();
+
+        assertEquals(OmaPresConstant.NAMESPACE, serviceDescription.getNamespace());
+        assertEquals(ServiceDescription.ELEMENT_NAME, serviceDescription.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        final ServiceId serviceId = new ServiceId("service_id_001");
+        final Version version = new Version(1, 0);
+        final Description description = new Description("description_test");
+
+        ServiceDescription serviceDescription = new ServiceDescription();
+        serviceDescription.setServiceId(serviceId);
+        serviceDescription.setVersion(version);
+        serviceDescription.setDescription(description);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        serviceDescription.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        assertTrue(result.contains(serviceId.getValue()));
+        assertTrue(result.contains(version.getValue()));
+        assertTrue(result.contains(description.getValue()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String serviceIdValue = "service_id_001";
+        final String version = "1.0";
+        final String description = "description test";
+
+        StringBuilder serviceDescriptionExample = new StringBuilder();
+        serviceDescriptionExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<op:service-description xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+                .append("<op:service-id>").append(serviceIdValue).append("</op:service-id>")
+                .append("<op:version>").append(version).append("</op:version>")
+                .append("<op:description>").append(description).append("</op:description>")
+                .append("</op:service-description>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(serviceDescriptionExample.toString());
+        parser.setInput(reader);
+
+        ServiceDescription serviceDescription = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && ServiceDescription.ELEMENT_NAME.equals(parser.getName())) {
+                serviceDescription = new ServiceDescription();
+                serviceDescription.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(serviceDescription);
+        assertNotNull(serviceDescription.getServiceId());
+        assertNotNull(serviceDescription.getVersion());
+        assertNotNull(serviceDescription.getDescription());
+
+        assertEquals(serviceIdValue, serviceDescription.getServiceId().getValue());
+        assertEquals(version, serviceDescription.getVersion().getValue());
+        assertEquals(description, serviceDescription.getDescription().getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java
new file mode 100644
index 0000000..d648ccf
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceIdTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        ServiceId serviceId = new ServiceId();
+
+        assertEquals(OmaPresConstant.NAMESPACE, serviceId.getNamespace());
+        assertEquals(ServiceId.ELEMENT_NAME, serviceId.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        final String serviceIdValue = "service_id_001";
+        ServiceId serviceId = new ServiceId(serviceIdValue);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        serviceId.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<op:service-id")
+                .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+                .append(" xmlns:op=\"").append(OmaPresConstant.NAMESPACE).append("\">")
+                .append(serviceIdValue)
+                .append("</op:service-id>");
+
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String serviceIdValue = "service_id_001";
+
+        StringBuilder serviceIdExample = new StringBuilder();
+        serviceIdExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<op:service-id xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+                .append(serviceIdValue)
+                .append("</op:service-id>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(serviceIdExample.toString());
+        parser.setInput(reader);
+
+        ServiceId serviceId = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && ServiceId.ELEMENT_NAME.equals(parser.getName())) {
+                serviceId = new ServiceId();
+                serviceId.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(serviceId);
+        assertEquals(serviceIdValue, serviceId.getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java
new file mode 100644
index 0000000..ae7d0c4
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class VersionTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Version version = new Version();
+
+        assertEquals(OmaPresConstant.NAMESPACE, version.getNamespace());
+        assertEquals(Version.ELEMENT_NAME, version.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        final int majorVersion = 1;
+        final int minorVersion = 0;
+        Version version = new Version(majorVersion, minorVersion);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        version.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<op:version")
+                .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+                .append(" xmlns:op=\"").append(OmaPresConstant.NAMESPACE).append("\">")
+                .append(majorVersion + "." + minorVersion)
+                .append("</op:version>");
+
+         assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String versionValue = "1.0";
+
+        StringBuilder versionExample = new StringBuilder();
+        versionExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<op:version xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+                .append(versionValue)
+                .append("</op:version>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(versionExample.toString());
+        parser.setInput(reader);
+
+        Version version = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Version.ELEMENT_NAME.equals(parser.getName())) {
+                version = new Version();
+                version.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(version);
+        assertEquals(versionValue, version.getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java
new file mode 100644
index 0000000..cb0ec0c
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class BasicTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Basic basic = new Basic();
+
+        assertEquals(PidfConstant.NAMESPACE, basic.getNamespace());
+        assertEquals(Basic.ELEMENT_NAME, basic.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializingWithBasicOpen() throws Exception {
+        Basic basic = new Basic(Basic.OPEN);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        basic.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        final String result = writer.toString();
+        final String basicElementWithOpenValue =
+                "<basic xmlns=\"" + PidfConstant.NAMESPACE + "\">open</basic>";
+        assertTrue(result.contains(basicElementWithOpenValue));
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializingWithBasicClosed() throws Exception {
+        Basic basic = new Basic(Basic.CLOSED);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        basic.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        final String result = writer.toString();
+        final String basicElementWithClosedValue =
+                "<basic xmlns=\"" + PidfConstant.NAMESPACE + "\">closed</basic>";
+        assertTrue(result.contains(basicElementWithClosedValue));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsingWithBasicOpen() throws Exception {
+        StringBuilder basicExample = new StringBuilder();
+        basicExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<basic xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+                .append(Basic.OPEN)
+                .append("</basic>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(basicExample.toString());
+        parser.setInput(reader);
+
+        Basic basic = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Basic.ELEMENT_NAME.equals(parser.getName())) {
+                basic = new Basic();
+                basic.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(basic);
+        assertEquals(Basic.OPEN, basic.getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java
new file mode 100644
index 0000000..cfab77a
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ContactTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Contact contact = new Contact();
+
+        assertEquals(PidfConstant.NAMESPACE, contact.getNamespace());
+        assertEquals(Contact.ELEMENT_NAME, contact.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        String testedContact = Uri.fromParts("sip", "test", null).toString();
+
+        Contact contact = new Contact();
+        contact.setContact(testedContact);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        contact.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        final String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<contact xmlns=\"").append(PidfConstant.NAMESPACE).append("\">")
+                .append(testedContact).append("</contact>");
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String testedContact = Uri.fromParts("sip", "test", null).toString();
+
+        StringBuilder contactExample = new StringBuilder();
+        contactExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<contact xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+                .append(testedContact)
+                .append("</contact>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(contactExample.toString());
+        parser.setInput(reader);
+
+        Contact contact = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Contact.ELEMENT_NAME.equals(parser.getName())) {
+                contact = new Contact();
+                contact.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(contact);
+        assertEquals(testedContact, contact.getContact());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java
new file mode 100644
index 0000000..8ee5ce4
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class NoteTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Note note = new Note();
+
+        assertEquals(PidfConstant.NAMESPACE, note.getNamespace());
+        assertEquals(Note.ELEMENT_NAME, note.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        final String noteValue = "Note test";
+
+        Note note = new Note(noteValue);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        note.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        final String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<note xmlns=\"").append(PidfConstant.NAMESPACE).append("\">")
+                .append(noteValue).append("</note>");
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String noteValue = "Note test";
+
+        StringBuilder noteExample = new StringBuilder();
+        noteExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<note xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+                .append(noteValue)
+                .append("</note>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(noteExample.toString());
+        parser.setInput(reader);
+
+        Note note = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Note.ELEMENT_NAME.equals(parser.getName())) {
+                note = new Note();
+                note.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(note);
+        assertEquals(noteValue, note.getNote());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java
new file mode 100644
index 0000000..99606f9
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.CapsConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Description;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.OmaPresConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceId;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Version;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class PresenceTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Presence presence = new Presence();
+
+        assertEquals(PidfConstant.NAMESPACE, presence.getNamespace());
+        assertEquals(Presence.ELEMENT_NAME, presence.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        String contact = Uri.fromParts("sip", "test", null).toString();
+
+        String serviceId1 = "service_id_01";
+        String description1 = "description_test1";
+        Tuple tuple1 = getTuple(Basic.OPEN, serviceId1, description1, contact);
+
+        String serviceId2 = "service_id_02";
+        String description2 = "description_test2";
+        Tuple tuple2 = getTuple(Basic.OPEN, serviceId2, description2, contact);
+
+        Presence presence = new Presence();
+        presence.setEntity(contact);
+        presence.addTuple(tuple1);
+        presence.addTuple(tuple2);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        presence.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        String verificationServiceId1 = "<op:service-id>service_id_01</op:service-id>";
+        String verificationDescription1 = "<op:description>description_test1</op:description>";
+        String verificationServiceId2 = "<op:service-id>service_id_02</op:service-id>";
+        String verificationDescription2 = "<op:description>description_test2</op:description>";
+        String verificationContact = "<contact>sip:test</contact>";
+
+        assertTrue(result.contains(verificationServiceId1));
+        assertTrue(result.contains(verificationDescription1));
+        assertTrue(result.contains(verificationServiceId2));
+        assertTrue(result.contains(verificationDescription2));
+        assertTrue(result.contains(verificationContact));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String contact = Uri.fromParts("sip", "test", null).toString();
+        final String serviceId = "service_id_01";
+        final String version = "1.0";
+        final String description = "description_test";
+
+        StringBuilder presenceExample = new StringBuilder();
+        presenceExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<presence entity=\"").append(contact).append("\"")
+                .append(" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                .append("<tuple id=\"tid0\"><status><basic>open</basic></status>")
+                .append("<op:service-description>")
+                .append("<op:service-id>").append(serviceId).append("</op:service-id>")
+                .append("<op:version>").append(version).append("</op:version>")
+                .append("<op:description>").append(description).append("</op:description>")
+                .append("</op:service-description>")
+                .append("<contact>sip:test</contact></tuple></presence>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(presenceExample.toString());
+        parser.setInput(reader);
+
+        Presence presence = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Presence.ELEMENT_NAME.equals(parser.getName())) {
+                presence = new Presence();
+                presence.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(presence);
+        assertEquals(contact, presence.getEntity());
+
+        List<Tuple> tupleList = presence.getTupleList();
+        assertNotNull(tupleList);
+        assertEquals(1, tupleList.size());
+
+        Tuple tuple = tupleList.get(0);
+        assertEquals(serviceId, PidfParserUtils.getTupleServiceId(tuple));
+        assertEquals(version, PidfParserUtils.getTupleServiceVersion(tuple));
+        assertEquals(description, PidfParserUtils.getTupleServiceDescription(tuple));
+        assertEquals(contact, PidfParserUtils.getTupleContact(tuple));
+    }
+
+    private Tuple getTuple(String statusValue, String serviceIdValue, String descValue,
+            String contactValue) {
+        Basic basic = new Basic(statusValue);
+        Status status = new Status();
+        status.setBasic(basic);
+
+        ServiceId serviceId = new ServiceId(serviceIdValue);
+        Version version = new Version(1, 0);
+        Description description = new Description(descValue);
+        ServiceDescription serviceDescription = new ServiceDescription();
+        serviceDescription.setServiceId(serviceId);
+        serviceDescription.setVersion(version);
+        serviceDescription.setDescription(description);
+
+        Contact contact = new Contact();
+        contact.setContact(contactValue);
+
+        Tuple tuple = new Tuple();
+        tuple.setStatus(status);
+        tuple.setServiceDescription(serviceDescription);
+        tuple.setContact(contact);
+
+        return tuple;
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+        serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java
new file mode 100644
index 0000000..945cb09
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class StatusTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Status status = new Status();
+
+        assertEquals(PidfConstant.NAMESPACE, status.getNamespace());
+        assertEquals(Status.ELEMENT_NAME, status.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        Basic basic = new Basic(Basic.OPEN);
+        Status status = new Status();
+        status.setBasic(basic);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        status.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+        String verification =
+                "<status xmlns=\"" + PidfConstant.NAMESPACE + "\"><basic>open</basic></status>";
+        assertTrue(result.contains(verification));
+   }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        StringBuilder statusExample = new StringBuilder();
+        statusExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<status xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+                .append("<basic>").append(Basic.OPEN).append("</basic>")
+                .append("</status>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(statusExample.toString());
+        parser.setInput(reader);
+
+        Status status = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Status.ELEMENT_NAME.equals(parser.getName())) {
+                status = new Status();
+                status.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(status);
+        assertNotNull(status.getBasic());
+        assertEquals(Basic.OPEN, status.getBasic().getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java
new file mode 100644
index 0000000..1dc76e6
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import java.time.Instant;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class TimestampTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Timestamp timestamp = new Timestamp();
+
+        assertEquals(PidfConstant.NAMESPACE, timestamp.getNamespace());
+        assertEquals(Timestamp.ELEMENT_NAME, timestamp.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        final String timestamp = Instant.now().toString();
+
+        Timestamp timestampElement = new Timestamp(timestamp);
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        timestampElement.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        StringBuilder verificationBuilder = new StringBuilder();
+        verificationBuilder.append("<timestamp xmlns=\"").append(PidfConstant.NAMESPACE).append("\">")
+                .append(timestamp).append("</timestamp>");
+
+        assertTrue(result.contains(verificationBuilder.toString()));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String timestamp = Instant.now().toString();
+
+        StringBuilder timestampExample = new StringBuilder();
+        timestampExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<timestamp xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+                .append(timestamp)
+                .append("</timestamp>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(timestampExample.toString());
+        parser.setInput(reader);
+
+        Timestamp timestampElement = null;
+        int nextType = parser.next();
+
+        do {
+            // Find the start tag
+            if (nextType == XmlPullParser.START_TAG
+                    && Timestamp.ELEMENT_NAME.equals(parser.getName())) {
+                timestampElement = new Timestamp();
+                timestampElement.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(timestampElement);
+        assertEquals(timestamp, timestampElement.getValue());
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java
new file mode 100644
index 0000000..3c44bd2
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Audio;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.CapsConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Duplex;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Video;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Description;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.OmaPresConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceId;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Version;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class TupleTest extends ImsTestBase {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testElementName() throws Exception {
+        Tuple tuple = new Tuple();
+
+        assertEquals(PidfConstant.NAMESPACE, tuple.getNamespace());
+        assertEquals(Tuple.ELEMENT_NAME, tuple.getElementName());
+    }
+
+    @Test
+    @SmallTest
+    public void testSerializing() throws Exception {
+        Tuple tuple = new Tuple();
+
+        Basic basic = new Basic(Basic.OPEN);
+        Status status = new Status();
+        status.setBasic(basic);
+        tuple.setStatus(status);
+
+        ServiceId serviceId = new ServiceId("service_id_001");
+        Version version = new Version(1, 0);
+        Description description = new Description("description test");
+        ServiceDescription serviceDescription = new ServiceDescription();
+        serviceDescription.setServiceId(serviceId);
+        serviceDescription.setVersion(version);
+        serviceDescription.setDescription(description);
+        tuple.setServiceDescription(serviceDescription);
+
+        Audio audio = new Audio(true);
+        Video video = new Video(true);
+        Duplex duplex = new Duplex();
+        duplex.addSupportedType(Duplex.DUPLEX_FULL);
+
+        ServiceCaps serviceCaps = new ServiceCaps();
+        serviceCaps.addElement(audio);
+        serviceCaps.addElement(video);
+        serviceCaps.addElement(duplex);
+        tuple.setServiceCaps(serviceCaps);
+
+        Note note = new Note("Note test");
+        tuple.addNote(note);
+
+        String nowTime = Instant.now().toString();
+        Timestamp timestamp = new Timestamp(nowTime);
+        tuple.setTimestamp(timestamp);
+
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = getXmlSerializer(writer);
+
+        // Serializing
+        serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+        tuple.serialize(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        String result = writer.toString();
+
+        String verificationStatus = "<status><basic>open</basic></status>";
+        String verificationServiceId = "<op:service-id>service_id_001</op:service-id>";
+        String verificationVersion = "<op:version>1.0</op:version>";
+        String verificationDescription = "<op:description>description test</op:description>";
+        String verificationAudio = "<caps:audio>true</caps:audio>";
+        String verificationVideo = "<caps:video>true</caps:video>";
+        String verificationNote = "<note>Note test</note>";
+        String verificationTimestamp = "<timestamp>" + nowTime + "</timestamp>";
+
+        assertTrue(result.contains(verificationStatus));
+        assertTrue(result.contains(verificationServiceId));
+        assertTrue(result.contains(verificationVersion));
+        assertTrue(result.contains(verificationDescription));
+        assertTrue(result.contains(verificationAudio));
+        assertTrue(result.contains(verificationVideo));
+        assertTrue(result.contains(verificationNote));
+        assertTrue(result.contains(verificationTimestamp));
+    }
+
+    @Test
+    @SmallTest
+    public void testParsing() throws Exception {
+        final String status = Basic.OPEN;
+        final String serviceId = "service_id_001";
+        final String version = "1.0";
+        final String description = "description test";
+        final boolean audio = true;
+        final boolean video = true;
+        final String note = "note test";
+        final String nowTime = Instant.now().toString();
+
+        StringBuilder tupleExample = new StringBuilder();
+        tupleExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<tuple id=\"tid0\" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                .append("<status><basic>").append(status).append("</basic></status>")
+                .append("<op:service-description>")
+                .append("<op:service-id>").append(serviceId).append("</op:service-id>")
+                .append("<op:version>").append(version).append("</op:version>")
+                .append("<op:description>").append(description).append("</op:description>")
+                .append("</op:service-description>")
+                .append("<caps:servcaps><caps:audio>").append(audio).append("</caps:audio>")
+                .append("<caps:video>").append(video).append("</caps:video>")
+                .append("<caps:duplex><caps:supported>")
+                .append("<caps:full />")
+                .append("</caps:supported></caps:duplex>")
+                .append("</caps:servcaps>")
+                .append("<note>").append(note).append("</note>")
+                .append("<timestamp>").append(nowTime).append("</timestamp></tuple>");
+
+        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+        Reader reader = new StringReader(tupleExample.toString());
+        parser.setInput(reader);
+
+        Tuple tuple = null;
+        int nextType = parser.next();
+
+        // Find the start tag
+        do {
+            if (nextType == XmlPullParser.START_TAG
+                    && Tuple.ELEMENT_NAME.equals(parser.getName())) {
+                tuple = new Tuple();
+                tuple.parse(parser);
+                break;
+            }
+            nextType = parser.next();
+        } while(nextType != XmlPullParser.END_DOCUMENT);
+
+        reader.close();
+
+        assertNotNull(tuple);
+        assertEquals(Basic.OPEN, PidfParserUtils.getTupleStatus(tuple));
+        assertEquals(serviceId, PidfParserUtils.getTupleServiceId(tuple));
+        assertEquals(version, PidfParserUtils.getTupleServiceVersion(tuple));
+        assertEquals(description, PidfParserUtils.getTupleServiceDescription(tuple));
+
+        boolean resultAudio = false;
+        boolean resultVideo = false;
+        List<ElementBase> elements = tuple.getServiceCaps().getElements();
+        for (ElementBase element : elements) {
+            if (element instanceof Audio) {
+                resultAudio = ((Audio) element).isAudioSupported();
+            } else if (element instanceof Video) {
+                resultVideo = ((Video) element).isVideoSupported();
+            }
+        }
+        assertTrue(resultAudio);
+        assertTrue(resultVideo);
+
+        String resultNote = null;
+        List<Note> noteList = tuple.getNoteList();
+        if (noteList != null && !noteList.isEmpty()) {
+            resultNote = noteList.get(0).getNote();
+        }
+
+        assertTrue(note.equals(resultNote));
+        assertEquals(nowTime, PidfParserUtils.getTupleTimestamp(tuple));
+    }
+
+    private XmlSerializer getXmlSerializer(StringWriter writer)
+            throws XmlPullParserException, IOException {
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+        XmlSerializer serializer = factory.newSerializer();
+        serializer.setOutput(writer);
+        serializer.setPrefix("", PidfConstant.NAMESPACE);
+        serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+        serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+        return serializer;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java
new file mode 100644
index 0000000..bf33103
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.os.Handler;
+import android.telecom.TelecomManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RegistrationManager.RegistrationCallback;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class DeviceCapabilityListenerTest extends ImsTestBase {
+
+    private static final long HANDLER_WAIT_TIMEOUT_MS = 2000L;
+    private static final long HANDLER_SENT_DELAY_MS = 1000L;
+
+    @Mock DeviceCapabilityInfo mDeviceCapability;
+    @Mock PublishController.PublishControllerCallback mCallback;
+    @Mock ImsMmTelManager mImsMmTelManager;
+    @Mock ImsRcsManager mImsRcsManager;
+    @Mock ProvisioningManager mProvisioningManager;
+    @Mock DeviceCapabilityListener.ImsMmTelManagerFactory mImsMmTelMgrFactory;
+    @Mock DeviceCapabilityListener.ImsRcsManagerFactory mImsRcsMgrFactory;
+    @Mock DeviceCapabilityListener.ProvisioningManagerFactory mProvisioningMgrFactory;
+
+    int mSubId = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mImsMmTelManager).when(mImsMmTelMgrFactory).getImsMmTelManager(anyInt());
+        doReturn(mImsRcsManager).when(mImsRcsMgrFactory).getImsRcsManager(anyInt());
+        doReturn(mProvisioningManager).when(mProvisioningMgrFactory).
+                getProvisioningManager(anyInt());
+
+        doReturn(true).when(mDeviceCapability).updateTtyPreferredMode(anyInt());
+        doReturn(true).when(mDeviceCapability).updateAirplaneMode(anyBoolean());
+        doReturn(true).when(mDeviceCapability).updateMobileData(anyBoolean());
+        doReturn(true).when(mDeviceCapability).updateVtSetting(anyBoolean());
+        doReturn(true).when(mDeviceCapability).updateVtSetting(anyBoolean());
+        doReturn(true).when(mDeviceCapability).updateMmtelCapabilitiesChanged(any());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testTurnOnListener() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+
+        deviceCapListener.initialize();
+
+        verify(mContext).registerReceiver(any(), any());
+        verify(mProvisioningManager).registerProvisioningChangedCallback(any(), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testDestroy() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        deviceCapListener.initialize();
+
+        // The listener is destroyed.
+        deviceCapListener.onDestroy();
+
+        verify(mContext).unregisterReceiver(any());
+        verify(mProvisioningManager).unregisterProvisioningChangedCallback(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testTtyPreferredModeChange() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        final BroadcastReceiver receiver = deviceCapListener.mReceiver;
+
+        Intent intent = new Intent(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+        receiver.onReceive(mContext, intent);
+
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateTtyPreferredMode(anyInt());
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE);
+    }
+
+    @Test
+    @SmallTest
+    public void testAirplaneModeChange() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        final BroadcastReceiver receiver = deviceCapListener.mReceiver;
+
+        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        receiver.onReceive(mContext, intent);
+
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateAirplaneMode(anyBoolean());
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE);
+    }
+
+    @Test
+    @SmallTest
+    public void testMmtelRegistration() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        deviceCapListener.setImsCallbackRegistered(true);
+        RegistrationCallback registrationCallback = deviceCapListener.mMmtelRegistrationCallback;
+
+        registrationCallback.onRegistered(1);
+
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateImsMmtelRegistered(1);
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_MMTEL_REGISTERED);
+    }
+
+    @Test
+    @SmallTest
+    public void testMmTelUnregistration() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        deviceCapListener.setImsCallbackRegistered(true);
+        RegistrationCallback registrationCallback = deviceCapListener.mMmtelRegistrationCallback;
+
+        ImsReasonInfo info = new ImsReasonInfo(ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED, -1, "");
+        registrationCallback.onUnregistered(info);
+
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateImsMmtelUnregistered();
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_MMTEL_UNREGISTERED);
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsRegistration() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        deviceCapListener.setImsCallbackRegistered(true);
+        RegistrationCallback registrationCallback = deviceCapListener.mRcsRegistrationCallback;
+        ImsRegistrationAttributes attr = new ImsRegistrationAttributes.Builder(
+                ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build();
+        // Notify DeviceCapabilityListener that registered has caused a change and requires publish
+        doReturn(true).when(mDeviceCapability).updateImsRcsRegistered(attr);
+
+        registrationCallback.onRegistered(attr);
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateImsRcsRegistered(attr);
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_RCS_REGISTERED);
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsUnregistration() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        deviceCapListener.setImsCallbackRegistered(true);
+        RegistrationCallback registrationCallback = deviceCapListener.mRcsRegistrationCallback;
+        // Notify DeviceCapabilityListener that unregistered has caused a change and requires
+        // publish.
+        doReturn(true).when(mDeviceCapability).updateImsRcsUnregistered();
+
+        ImsReasonInfo info = new ImsReasonInfo(ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED, -1, "");
+        registrationCallback.onUnregistered(info);
+
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateImsRcsUnregistered();
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_RCS_UNREGISTERED);
+    }
+
+    @Test
+    @SmallTest
+    public void testMmtelCapabilityChange() throws Exception {
+        DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+        ImsMmTelManager.CapabilityCallback callback = deviceCapListener.mMmtelCapabilityCallback;
+
+        MmTelFeature.MmTelCapabilities capabilities = new MmTelFeature.MmTelCapabilities();
+        callback.onCapabilitiesStatusChanged(capabilities);
+
+        Handler handler = deviceCapListener.getHandler();
+        waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+        verify(mDeviceCapability).updateMmtelCapabilitiesChanged(capabilities);
+        verify(mCallback).requestPublishFromInternal(
+                PublishController.PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE);
+    }
+
+    private DeviceCapabilityListener createDeviceCapabilityListener() {
+        DeviceCapabilityListener deviceCapListener = new DeviceCapabilityListener(mContext,
+                mSubId, mDeviceCapability, mCallback);
+        deviceCapListener.setImsMmTelManagerFactory(mImsMmTelMgrFactory);
+        deviceCapListener.setImsRcsManagerFactory(mImsRcsMgrFactory);
+        deviceCapListener.setProvisioningMgrFactory(mProvisioningMgrFactory);
+        return deviceCapListener;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java
new file mode 100644
index 0000000..76934a4
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static com.android.ims.rcs.uce.presence.publish.PublishController.PUBLISH_TRIGGER_RETRY;
+import static com.android.ims.rcs.uce.presence.publish.PublishController.PUBLISH_TRIGGER_VT_SETTING_CHANGE;
+import static org.junit.Assert.assertEquals;
+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.verify;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteCallbackList;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl.DeviceCapListenerFactory;
+import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl.PublishProcessorFactory;
+import com.android.ims.ImsTestBase;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class PublishControllerImplTest extends ImsTestBase {
+
+    @Mock RcsFeatureManager mFeatureManager;
+    @Mock PublishProcessor mPublishProcessor;
+    @Mock PublishProcessorFactory mPublishProcessorFactory;
+    @Mock DeviceCapabilityListener mDeviceCapListener;
+    @Mock DeviceCapListenerFactory mDeviceCapListenerFactory;
+    @Mock UceController.UceControllerCallback mUceCtrlCallback;
+    @Mock RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
+    @Mock DeviceStateResult mDeviceStateResult;
+
+    private int mSubId = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mPublishProcessor).when(mPublishProcessorFactory).createPublishProcessor(any(),
+                eq(mSubId), any(), any());
+        doReturn(mDeviceCapListener).when(mDeviceCapListenerFactory).createDeviceCapListener(any(),
+                eq(mSubId), any(), any());
+        doReturn(mDeviceStateResult).when(mUceCtrlCallback).getDeviceState();
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsConnected() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        publishController.onRcsConnected(mFeatureManager);
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+
+        verify(mPublishProcessor).onRcsConnected(mFeatureManager);
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsDisconnected() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        publishController.onRcsDisconnected();
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+
+        verify(mPublishProcessor).onRcsDisconnected();
+    }
+
+    @Test
+    @SmallTest
+    public void testDestroyed() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        publishController.onDestroy();
+
+        verify(mPublishProcessor, never()).doPublish(anyInt());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetPublishState() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        int initState = publishController.getUcePublishState();
+        assertEquals(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED, initState);
+
+        publishController.getPublishControllerCallback().updatePublishRequestResult(
+                RcsUceAdapter.PUBLISH_STATE_OK, Instant.now(), null);
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+
+        int latestState = publishController.getUcePublishState();
+        assertEquals(RcsUceAdapter.PUBLISH_STATE_OK, latestState);
+    }
+
+    @Test
+    @SmallTest
+    public void testRegisterPublishStateCallback() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        publishController.registerPublishStateCallback(any());
+
+        verify(mPublishStateCallbacks).register(any());
+    }
+
+    @Test
+    @SmallTest
+    public void unregisterPublishStateCallback() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        publishController.unregisterPublishStateCallback(any());
+
+        verify(mPublishStateCallbacks).unregister(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testUnpublish() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        publishController.onUnpublish();
+
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+        int publishState = publishController.getUcePublishState();
+        assertEquals(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED, publishState);
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestPublishFromServiceWithoutRcsPresenceCapability() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+
+        // Trigger the PUBLISH request from the service
+        publishController.requestPublishCapabilitiesFromService(
+                RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+        verify(mPublishProcessor, never()).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+        IImsCapabilityCallback callback = publishController.getRcsCapabilitiesCallback();
+        callback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+        waitForHandlerAction(handler, 1000);
+
+        verify(mPublishProcessor).checkAndSendPendingRequest();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestPublishFromServiceWithRcsCapability() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+        doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+        // Set the PRESENCE is capable
+        IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+        RcsCapCallback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+
+        // Trigger the PUBLISH request from the service.
+        publishController.requestPublishCapabilitiesFromService(
+                RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+        verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+    }
+
+    @Test
+    @SmallTest
+    public void testFirstRequestPublishIsTriggeredFromService() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+        doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+        // Set the PRESENCE is capable
+        IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+        RcsCapCallback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+
+        // Trigger a publish request (VT changes)
+        PublishControllerCallback callback = publishController.getPublishControllerCallback();
+        callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+
+        // Verify it cannot be processed because the first request should triggred from service.
+        verify(mPublishProcessor, never()).doPublish(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+
+        // Trigger the PUBLISH request from the service.
+        publishController.requestPublishCapabilitiesFromService(
+                RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+        waitForHandlerAction(handler, 1000);
+
+        // Verify the request which is from the service can be processed
+        verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+        // Trigger the third publish request (VT changes)
+        callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+        waitForHandlerAction(handler, 1000);
+
+        // Verify the publish request can be processed this time.
+        verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestPublishWhenDeviceCapabilitiesChange() throws Exception {
+        PublishControllerImpl publishController = createPublishController();
+        doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+        // Set the PRESENCE is capable
+        IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+        RcsCapCallback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+
+        // Trigger the PUBLISH request from the service.
+        publishController.requestPublishCapabilitiesFromService(
+                RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+        Handler handler = publishController.getPublishHandler();
+        waitForHandlerAction(handler, 1000);
+
+        // Verify the request which is from the service can be processed
+        verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+        // Trigger the sedond publish (RETRY), it should be processed after 10 seconds.
+        PublishControllerCallback callback = publishController.getPublishControllerCallback();
+        callback.requestPublishFromInternal(PUBLISH_TRIGGER_RETRY);
+
+        // Trigger another publish request (VT changes)
+        callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+        waitForHandlerAction(handler, 1000);
+
+        // Verify the publish request can be processed immediately
+        verify(mPublishProcessor).doPublish(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+    }
+
+    private PublishControllerImpl createPublishController() {
+        PublishControllerImpl publishController = new PublishControllerImpl(mContext, mSubId,
+                mUceCtrlCallback, Looper.getMainLooper(), mDeviceCapListenerFactory,
+                mPublishProcessorFactory);
+        publishController.setPublishStateCallback(mPublishStateCallbacks);
+        return publishController;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java
new file mode 100644
index 0000000..d83158f
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static android.telephony.ims.RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactUceCapability;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class PublishProcessorTest extends ImsTestBase {
+
+    @Mock RcsFeatureManager mRcsFeatureManager;
+    @Mock DeviceCapabilityInfo mDeviceCapabilities;
+    @Mock PublishControllerCallback mPublishCtrlCallback;
+    @Mock PublishProcessorState mProcessorState;
+    @Mock PublishRequestResponse mResponseCallback;
+
+    private int mSub = 1;
+    private long mTaskId = 1L;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(true).when(mProcessorState).isPublishAllowedAtThisTime();
+        doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+
+        doReturn(true).when(mDeviceCapabilities).isImsRegistered();
+        RcsContactUceCapability capability = getRcsContactUceCapability();
+        doReturn(capability).when(mDeviceCapabilities).getDeviceCapabilities(anyInt(), any());
+
+        doReturn(mTaskId).when(mResponseCallback).getTaskId();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testDoPublish() throws Exception {
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+        verify(mDeviceCapabilities).getDeviceCapabilities(anyInt(), any());
+        verify(mProcessorState).setPublishingFlag(true);
+        verify(mRcsFeatureManager).requestPublication(any(), any());
+        verify(mPublishCtrlCallback).setupRequestCanceledTimer(anyLong(), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testPublishWithoutResetRetryCount() throws Exception {
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY);
+
+        verify(mProcessorState, never()).resetRetryCount();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotPublishWhenImsNotRegistered() throws Exception {
+        doReturn(false).when(mDeviceCapabilities).isImsRegistered();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY);
+
+        verify(mRcsFeatureManager, never()).requestPublication(any(), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testNotPublishWhenReachMaximumRetries() throws Exception {
+        doReturn(true).when(mProcessorState).isPublishingNow();
+        doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+        doReturn(mTaskId).when(mResponseCallback).getTaskId();
+        doReturn(true).when(mResponseCallback).needRetry();
+        doReturn(true).when(mProcessorState).isReachMaximumRetries();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.onNetworkResponse(mResponseCallback);
+
+        verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt(), any(), any());
+        verify(mResponseCallback).onDestroy();
+        verify(mProcessorState).setPublishingFlag(false);
+        verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotPublishWhenCurrentTimeNotAllowed() throws Exception {
+        doReturn(false).when(mProcessorState).isPublishAllowedAtThisTime();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY);
+
+        verify(mPublishCtrlCallback).requestPublishFromInternal(
+                eq(PublishController.PUBLISH_TRIGGER_RETRY));
+        verify(mRcsFeatureManager, never()).requestPublication(any(), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testCommandErrorWithRetry() throws Exception {
+        doReturn(true).when(mProcessorState).isPublishingNow();
+        doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+        doReturn(mTaskId).when(mResponseCallback).getTaskId();
+        doReturn(true).when(mResponseCallback).needRetry();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.onCommandError(mResponseCallback);
+
+        verify(mProcessorState).increaseRetryCount();
+        verify(mPublishCtrlCallback).requestPublishFromInternal(
+                eq(PublishController.PUBLISH_TRIGGER_RETRY));
+        verify(mResponseCallback).onDestroy();
+        verify(mProcessorState).setPublishingFlag(false);
+        verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+    }
+
+    @Test
+    @SmallTest
+    public void testCommandErrorWithoutRetry() throws Exception {
+        doReturn(true).when(mProcessorState).isPublishingNow();
+        doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+        doReturn(mTaskId).when(mResponseCallback).getTaskId();
+        doReturn(false).when(mResponseCallback).needRetry();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.onCommandError(mResponseCallback);
+
+        verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt(), any(), any());
+        verify(mResponseCallback).onDestroy();
+        verify(mProcessorState).setPublishingFlag(false);
+        verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+    }
+
+    @Test
+    @SmallTest
+    public void testNetworkResponseWithRetry() throws Exception {
+        doReturn(true).when(mProcessorState).isPublishingNow();
+        doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+        doReturn(mTaskId).when(mResponseCallback).getTaskId();
+        doReturn(true).when(mResponseCallback).needRetry();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.onNetworkResponse(mResponseCallback);
+
+        verify(mProcessorState).increaseRetryCount();
+        verify(mPublishCtrlCallback).requestPublishFromInternal(
+                eq(PublishController.PUBLISH_TRIGGER_RETRY));
+        verify(mResponseCallback).onDestroy();
+        verify(mProcessorState).setPublishingFlag(false);
+        verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+    }
+
+    @Test
+    @SmallTest
+    public void testNetworkResponseSuccessful() throws Exception {
+        doReturn(true).when(mProcessorState).isPublishingNow();
+        doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+        doReturn(mTaskId).when(mResponseCallback).getTaskId();
+        doReturn(false).when(mResponseCallback).needRetry();
+        doReturn(true).when(mResponseCallback).isRequestSuccess();
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.onNetworkResponse(mResponseCallback);
+
+        verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt(), any(), any());
+        verify(mResponseCallback).onDestroy();
+        verify(mProcessorState).setPublishingFlag(false);
+        verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+    }
+
+    @Test
+    @SmallTest
+    public void testCancelPublishRequest() throws Exception {
+        PublishProcessor publishProcessor = getPublishProcessor();
+
+        publishProcessor.cancelPublishRequest(mTaskId);
+
+        verify(mProcessorState).setPublishingFlag(false);
+        verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+    }
+
+    private PublishProcessor getPublishProcessor() {
+        PublishProcessor publishProcessor = new PublishProcessor(mContext, mSub,
+                mDeviceCapabilities, mPublishCtrlCallback);
+        publishProcessor.setProcessorState(mProcessorState);
+        publishProcessor.onRcsConnected(mRcsFeatureManager);
+        return publishProcessor;
+    }
+
+    private RcsContactUceCapability getRcsContactUceCapability() {
+        Uri contact = Uri.fromParts("sip", "test", null);
+
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(contact,
+                        RcsContactUceCapability.SOURCE_TYPE_CACHED,
+                        RcsContactUceCapability.REQUEST_RESULT_FOUND);
+
+        RcsContactPresenceTuple.Builder tupleBuilder = new RcsContactPresenceTuple.Builder(
+                TUPLE_BASIC_STATUS_OPEN, RcsContactPresenceTuple.SERVICE_ID_MMTEL, "1.0");
+
+        builder.addCapabilityTuple(tupleBuilder.build());
+        return builder.build();
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java
new file mode 100644
index 0000000..6d15946
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
+
+import static org.junit.Assert.assertEquals;
+
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.util.ArraySet;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class PublishServiceDescTrackerTest {
+
+    public static final ServiceDescription TEST_SERVICE_DESC_1 = new ServiceDescription(
+            "org.test.test1", "1.0", "ABC");
+    public static final ServiceDescription TEST_SERVICE_DESC_2 = new ServiceDescription(
+            "org.test.test1", "2.0", "DEF");
+    public static final ServiceDescription TEST_OVERRIDE_MMTEL_DESC = new ServiceDescription(
+            RcsContactPresenceTuple.SERVICE_ID_MMTEL, "1.0", "ABC");
+
+    public static final String TEST_FEATURE_TAG_1 = "+g.test.test1=\"testing\"";
+    public static final String TEST_FEATURE_TAG_2A = "+g.test.test2=\"testing\"";
+    public static final String TEST_FEATURE_TAG_2B = "+g.test.testAdd";
+
+    public static final String[] TEST_OVERRIDE_CONFIG = new String[] {
+            "org.test.test1|1.0|ABC|+g.test.test1=\"testing\"",
+            "org.test.test1|2.0|DEF|+g.test.test2=\"testing\";+g.test.testAdd",
+            "org.test.test3|1.0|ABC|+g.test.test3",
+            // Modify MMTEL+video to have a different description
+            "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel|1.0|ABC|+g.3gpp.icsi-ref=\""
+                    + "urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\";video"
+    };
+
+    public static final String TEST_FEATURE_TAG_FT_FORMAT =
+            " +g.3gpp.iari-ref =  \" urn%3AuRN-7%3A3gpp-applicatION.ims.iari.rcs.fthttp \"   ";
+
+    public static final String TEST_FEATURE_TAG_FT_SMS_FORMAT =
+            " +g.3gpp.iari-ref=\"    urn%3Aurn-7%3A3gpp-application.iMS.iari.rcs.ftsms    \"";
+
+    public static final String TEST_FEATURE_TAG_CHATBOT_FORMAT =
+            " +g.3gpp.iari-ref=  \" Urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot \"";
+
+    public static final String TEST_FEATURE_TAG_BOTVERSION_FORMAT =
+            "+g.gsma.rcs.botVersion =\" #=1  , #=2 \"    ";
+
+    public static final String TEST_FEATURE_TAG_MMTEL_FORMAT =
+            "  +g.3gpp.icsi-ref =    \"urn%3Aurn-7%3A3gpp-servIce.ims.icsi.mmtel \"   ";
+
+    public static final String TEST_FEATURE_TAG_VIDEO_FORMAT = " VIDEO  ";
+
+    public static final String[] TEST_OVERRIDE_CONFIG_FORMAT = new String[] {
+            " org.test.test1 | 1.0 | ABC | +g.test.tEST1= \" testing  \"         ",
+            "  org.test.test1 |2.0  |DEF | +g.teSt.test2 = \"testing\" ;  +g.test.testAdd  ",
+            " org.test.test3  | 1.0   |ABC| +g.TEst.test3   ",
+            // Modify MMTEL+video to have a different description
+            " org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel |   1.0 |  ABC |  +g.3gPp.icsi-ref=\""
+                    + "urn%3Aurn-7%3A3gpp-seRVice.ims.icsi.mmtel   \" ;  video   "
+    };
+
+    @SmallTest
+    @Test
+    public void testDefaultConfigMatch() {
+        PublishServiceDescTracker t1 =
+                PublishServiceDescTracker.fromCarrierConfig(new String[]{});
+
+        Set<ServiceDescription> expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_FT);
+        Set<String> imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_FT,
+                ServiceDescription.SERVICE_DESCRIPTION_FT_SMS));
+        imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Should see chatbot v1 and v2 pop up in this case (same FTs)
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION,
+                ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2));
+        imsReg = createImsRegistration(
+                FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
+                FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+        imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_MMTEL);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO);
+        imsReg = createImsRegistration(
+                FeatureTags.FEATURE_TAG_MMTEL,
+                FeatureTags.FEATURE_TAG_VIDEO);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+    }
+
+    @SmallTest
+    @Test
+    public void testOverrideCarrierConfigMatch() {
+        PublishServiceDescTracker t1 =
+                PublishServiceDescTracker.fromCarrierConfig(TEST_OVERRIDE_CONFIG);
+
+        Set<ServiceDescription> expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_FT);
+        Set<String> imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_FT, TEST_SERVICE_DESC_1));
+        imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                TEST_FEATURE_TAG_1);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Test overrides also allow for multiple FT specifications
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, TEST_SERVICE_DESC_2));
+        imsReg = createImsRegistration(
+                FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
+                TEST_FEATURE_TAG_2B, TEST_FEATURE_TAG_2A);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Test override does not affect mmtel voice
+        expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+        imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_MMTEL);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Test override description works for existing tags
+        expectedSet =  Collections.singleton(TEST_OVERRIDE_MMTEL_DESC);
+        imsReg = createImsRegistration(
+                FeatureTags.FEATURE_TAG_MMTEL,
+                FeatureTags.FEATURE_TAG_VIDEO);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+    }
+
+    @SmallTest
+    @Test
+    public void testNonstandardImsRegFormatMatch() {
+        PublishServiceDescTracker t1 =
+                PublishServiceDescTracker.fromCarrierConfig(new String[]{});
+
+        Set<ServiceDescription> expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_FT,
+                ServiceDescription.SERVICE_DESCRIPTION_FT_SMS));
+        Set<String> imsReg = createImsRegistration(TEST_FEATURE_TAG_FT_FORMAT,
+                TEST_FEATURE_TAG_FT_SMS_FORMAT);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Should see chatbot v1 and v2 pop up in this case (same FTs)
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION,
+                ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2));
+        imsReg = createImsRegistration(
+                TEST_FEATURE_TAG_CHATBOT_FORMAT,
+                TEST_FEATURE_TAG_BOTVERSION_FORMAT);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+        imsReg = createImsRegistration(TEST_FEATURE_TAG_MMTEL_FORMAT);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO);
+        imsReg = createImsRegistration(
+                TEST_FEATURE_TAG_MMTEL_FORMAT,
+                TEST_FEATURE_TAG_VIDEO_FORMAT);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+    }
+
+    @SmallTest
+    @Test
+    public void testOverrideCarrierConfigNonstandardFormatMatch() {
+        PublishServiceDescTracker t1 =
+                PublishServiceDescTracker.fromCarrierConfig(TEST_OVERRIDE_CONFIG_FORMAT);
+
+        Set<ServiceDescription> expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_FT);
+        Set<String> imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_FT, TEST_SERVICE_DESC_1));
+        imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                TEST_FEATURE_TAG_1);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Test overrides also allow for multiple FT specifications
+        expectedSet = new ArraySet<>(Arrays.asList(
+                ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, TEST_SERVICE_DESC_2));
+        imsReg = createImsRegistration(
+                FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
+                TEST_FEATURE_TAG_2B, TEST_FEATURE_TAG_2A);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Test override does not affect mmtel voice
+        expectedSet = Collections.singleton(
+                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+        imsReg = createImsRegistration(TEST_FEATURE_TAG_MMTEL_FORMAT);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+        // Test override description works for existing tags
+        expectedSet =  Collections.singleton(TEST_OVERRIDE_MMTEL_DESC);
+        imsReg = createImsRegistration(
+                FeatureTags.FEATURE_TAG_MMTEL,
+                FeatureTags.FEATURE_TAG_VIDEO);
+        t1.updateImsRegistration(imsReg);
+        assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+    }
+
+    private Set<String> createImsRegistration(String... imsReg) {
+        return new ArraySet<>(imsReg);
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java
new file mode 100644
index 0000000..4aef42e
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static com.android.ims.rcs.uce.eab.EabCapabilityResult.EAB_QUERY_SUCCESSFUL;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class CapabilityRequestTest extends ImsTestBase {
+
+    @Mock CapabilityRequestResponse mRequestResponse;
+    @Mock UceRequestManager.RequestManagerCallback mReqMgrCallback;
+    @Mock DeviceStateResult mDeviceStateResult;
+
+    private final int mSubId = 1;
+    private final long mCoordId = 1L;
+    private final Uri contact1 = Uri.fromParts("sip", "test1", null);
+    private final Uri contact2 = Uri.fromParts("sip", "test2", null);
+
+    private boolean mRequestExecuted;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mDeviceStateResult).when(mReqMgrCallback).getDeviceState();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testCachedCapabilityCallback() throws Exception {
+        CapabilityRequest request = getCapabilityRequest();
+
+        // Assume that all the requested capabilities can be retrieved from the cache.
+        PresenceBuilder builder1 = new PresenceBuilder(contact1, SOURCE_TYPE_CACHED,
+                REQUEST_RESULT_FOUND);
+        PresenceBuilder builder2 = new PresenceBuilder(contact2, SOURCE_TYPE_CACHED,
+                REQUEST_RESULT_FOUND);
+
+        EabCapabilityResult eabResult1 = new EabCapabilityResult(contact1, EAB_QUERY_SUCCESSFUL,
+                builder1.build());
+        EabCapabilityResult eabResult2 = new EabCapabilityResult(contact2, EAB_QUERY_SUCCESSFUL,
+                builder2.build());
+
+        List<EabCapabilityResult> eabResultList = new ArrayList<>();
+        eabResultList.add(eabResult1);
+        eabResultList.add(eabResult2);
+
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+        doReturn(eabResultList).when(mReqMgrCallback).getCapabilitiesFromCache(any());
+
+        // Execute the request.
+        request.executeRequest();
+
+        // Verify that it will notify the cached capabilities is updated.
+        verify(mReqMgrCallback).notifyCachedCapabilitiesUpdated(eq(mCoordId), anyLong());
+
+        // Verify that it does not need to request capabilities from network.
+        verify(mReqMgrCallback).notifyNoNeedRequestFromNetwork(eq(mCoordId), anyLong());
+        assertFalse(mRequestExecuted);
+    }
+
+    @Test
+    @SmallTest
+    public void testCachedCapabilityCallbackWithSkipGettingFromCache() throws Exception {
+        CapabilityRequest request = getCapabilityRequest();
+
+        // Assume that all the requested capabilities can be retrieved from the cache.
+        PresenceBuilder builder1 = new PresenceBuilder(contact1, SOURCE_TYPE_CACHED,
+                REQUEST_RESULT_FOUND);
+        PresenceBuilder builder2 = new PresenceBuilder(contact2, SOURCE_TYPE_CACHED,
+                REQUEST_RESULT_FOUND);
+
+        EabCapabilityResult eabResult1 = new EabCapabilityResult(contact1, EAB_QUERY_SUCCESSFUL,
+                builder1.build());
+        EabCapabilityResult eabResult2 = new EabCapabilityResult(contact2, EAB_QUERY_SUCCESSFUL,
+                builder2.build());
+
+        List<EabCapabilityResult> eabResultList = new ArrayList<>();
+        eabResultList.add(eabResult1);
+        eabResultList.add(eabResult2);
+
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+        doReturn(eabResultList).when(mReqMgrCallback).getCapabilitiesFromCache(any());
+
+        // Assume that skip getting capabilities from the cache.
+        request.setSkipGettingFromCache(true);
+
+        // Execute the request.
+        request.executeRequest();
+
+        // Verify that it will not notify the cached capabilities.
+        verify(mReqMgrCallback, never()).notifyCachedCapabilitiesUpdated(eq(mCoordId), anyLong());
+
+        // Verify that it will request capabilities from network.
+        verify(mReqMgrCallback, never()).notifyNoNeedRequestFromNetwork(eq(mCoordId), anyLong());
+        assertTrue(mRequestExecuted);
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilities() throws Exception {
+        CapabilityRequest request = getCapabilityRequest();
+
+        // Assume that only one requested capabilities can be retrieved from the cache.
+        PresenceBuilder builder = new PresenceBuilder(contact1, SOURCE_TYPE_CACHED,
+                REQUEST_RESULT_FOUND);
+
+        EabCapabilityResult eabResult = new EabCapabilityResult(contact1, EAB_QUERY_SUCCESSFUL,
+                builder.build());
+
+        List<EabCapabilityResult> eabResultList = new ArrayList<>();
+        eabResultList.add(eabResult);
+
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+        doReturn(eabResultList).when(mReqMgrCallback).getCapabilitiesFromCache(any());
+
+        // Execute the request.
+        request.executeRequest();
+
+        // Verify that it will notify the cached capabilities is updated.
+        verify(mReqMgrCallback).notifyCachedCapabilitiesUpdated(eq(mCoordId), anyLong());
+
+        // Verify that it will request capability from the network.
+        verify(mReqMgrCallback, never()).notifyNoNeedRequestFromNetwork(eq(mCoordId), anyLong());
+        assertTrue(mRequestExecuted);
+    }
+
+    private CapabilityRequest getCapabilityRequest() {
+        CapabilityRequest request = new CapabilityRequest(mSubId,
+                UceRequest.REQUEST_TYPE_CAPABILITY, mReqMgrCallback, mRequestResponse) {
+            @Override
+            protected void requestCapabilities(List<Uri> requestCapUris) {
+                mRequestExecuted = true;
+            }
+        };
+        // Set the request coordinator ID
+        request.setRequestCoordinatorId(mCoordId);
+
+        // Set two contacts
+        List<Uri> uriList = new ArrayList<>();
+        uriList.add(contact1);
+        uriList.add(contact2);
+        request.setContactUri(uriList);
+        return request;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java b/tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java
new file mode 100644
index 0000000..9c270fb
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.RequestResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class OptionsCoordinatorTest extends ImsTestBase {
+
+    @Mock OptionsRequest mRequest;
+    @Mock CapabilityRequestResponse mResponse;
+    @Mock RequestManagerCallback mRequestMgrCallback;
+    @Mock IRcsUceControllerCallback mUceCallback;
+
+    private int mSubId = 1;
+    private long mTaskId = 1L;
+    private Uri mContact = Uri.fromParts("sip", "test1", null);
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mTaskId).when(mRequest).getTaskId();
+        doReturn(mResponse).when(mRequest).getRequestResponse();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestUpdatedWithError() throws Exception {
+        OptionsRequestCoordinator coordinator = getOptionsCoordinator();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_ERROR);
+
+        verify(mRequest).onFinish();
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCommandError() throws Exception {
+        OptionsRequestCoordinator coordinator = getOptionsCoordinator();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_COMMAND_ERROR);
+
+        verify(mRequest).onFinish();
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestNetworkResponse() throws Exception {
+        OptionsRequestCoordinator coordinator = getOptionsCoordinator();
+        doReturn(true).when(mResponse).isNetworkResponseOK();
+
+        final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+        RcsContactUceCapability updatedCapability = getContactUceCapability();
+        updatedCapList.add(updatedCapability);
+        doReturn(updatedCapList).when(mResponse).getUpdatedContactCapability();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+        verify(mRequestMgrCallback).saveCapabilities(updatedCapList);
+        verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+        verify(mResponse).removeUpdatedCapabilities(updatedCapList);
+
+        verify(mRequest).onFinish();
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+    }
+
+    private OptionsRequestCoordinator getOptionsCoordinator() {
+        OptionsRequestCoordinator.Builder builder = new OptionsRequestCoordinator.Builder(
+                mSubId, Collections.singletonList(mRequest), mRequestMgrCallback);
+        builder.setCapabilitiesCallback(mUceCallback);
+        return builder.build();
+    }
+
+    private RcsContactUceCapability getContactUceCapability() {
+        int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(
+                        mContact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+        return builder.build();
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java
new file mode 100644
index 0000000..54a0252
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class OptionsRequestTest extends ImsTestBase {
+
+    private static final String FEATURE_TAG_CHAT =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+    private static final String FEATURE_TAG_FILE_TRANSFER =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+    private static final String FEATURE_TAG_MMTEL_AUDIO_CALL =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";
+    private static final String FEATURE_TAG_MMTEL_VIDEO_CALL =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\";video";
+
+    private int mSubId = 1;
+    private long mCoordId = 1L;
+    private int mRequestType = UceRequest.REQUEST_TYPE_CAPABILITY;
+    private Uri mTestContact;
+    private Set<String> mFeatureTags;
+    private RcsContactUceCapability mDeviceCapability;
+
+    @Mock OptionsController mOptionsController;
+    @Mock CapabilityRequestResponse mRequestResponse;
+    @Mock RequestManagerCallback mRequestManagerCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mTestContact = Uri.fromParts("sip", "test", null);
+
+        mFeatureTags = new HashSet<>();
+        mFeatureTags.add(FEATURE_TAG_CHAT);
+        mFeatureTags.add(FEATURE_TAG_FILE_TRANSFER);
+        mFeatureTags.add(FEATURE_TAG_MMTEL_AUDIO_CALL);
+        mFeatureTags.add(FEATURE_TAG_MMTEL_VIDEO_CALL);
+
+        OptionsBuilder builder = new OptionsBuilder(mTestContact, SOURCE_TYPE_CACHED);
+        builder.addFeatureTag(FEATURE_TAG_CHAT);
+        builder.addFeatureTag(FEATURE_TAG_FILE_TRANSFER);
+        builder.addFeatureTag(FEATURE_TAG_MMTEL_AUDIO_CALL);
+        builder.addFeatureTag(FEATURE_TAG_MMTEL_VIDEO_CALL);
+        mDeviceCapability = builder.build();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilities() throws Exception {
+        OptionsRequest request = getOptionsRequest();
+        doReturn(mDeviceCapability).when(mRequestManagerCallback).getDeviceCapabilities(anyInt());
+
+        List<Uri> uriList = Collections.singletonList(mTestContact);
+        request.requestCapabilities(uriList);
+
+        verify(mOptionsController).sendCapabilitiesRequest(eq(mTestContact),
+                eq(mFeatureTags), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testsendCapabilitiesRequestWhenDestroy() throws Exception {
+        OptionsRequest request = getOptionsRequest();
+        request.onFinish();
+
+        List<Uri> uriList = Collections.singletonList(mTestContact);
+        request.requestCapabilities(uriList);
+
+        verify(mRequestResponse).setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+        verify(mRequestManagerCallback).notifyRequestError(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testCommandErrorCallback() throws Exception {
+        OptionsRequest request = getOptionsRequest();
+        IOptionsResponseCallback callback = request.getResponseCallback();
+
+        int errorCode = RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED;
+        callback.onCommandError(errorCode);
+
+        verify(mRequestResponse).setCommandError(errorCode);
+        verify(mRequestManagerCallback).notifyCommandError(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testNetworkResponse() throws Exception {
+       OptionsRequest request = getOptionsRequest();
+        IOptionsResponseCallback callback = request.getResponseCallback();
+
+        int sipCode = NetworkSipCode.SIP_CODE_ACCEPTED;
+        String reason = NetworkSipCode.SIP_ACCEPTED;
+        callback.onNetworkResponse(sipCode, reason, new ArrayList<>(mFeatureTags));
+
+        verify(mRequestResponse).setNetworkResponseCode(sipCode, reason);
+        verify(mRequestResponse).setRemoteCapabilities(eq(mFeatureTags));
+        verify(mRequestManagerCallback).notifyNetworkResponse(eq(mCoordId), anyLong());
+    }
+
+    private OptionsRequest getOptionsRequest() {
+        OptionsRequest request = new OptionsRequest(mSubId, mRequestType, mRequestManagerCallback,
+                mOptionsController, mRequestResponse);
+        request.setRequestCoordinatorId(mCoordId);
+        return request;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java
new file mode 100644
index 0000000..1a6ed4a
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.request.RemoteOptionsRequest.RemoteOptResponse;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.RequestResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class RemoteOptionsCoordinatorTest extends ImsTestBase {
+
+    @Mock RemoteOptionsRequest mRequest;
+    @Mock RemoteOptResponse mResponse;
+    @Mock RequestManagerCallback mRequestMgrCallback;
+    @Mock IOptionsRequestCallback mOptRequestCallback;
+
+    private int mSubId = 1;
+    private long mTaskId = 1L;
+    private Uri mContact = Uri.fromParts("sip", "test1", null);
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mTaskId).when(mRequest).getTaskId();
+        doReturn(mResponse).when(mRequest).getRemoteOptResponse();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoteRequestFinished() throws Exception {
+        RemoteOptionsCoordinator coordinator = getRemoteOptCoordinator();
+        RcsContactUceCapability updatedCapability = getContactUceCapability();
+        doReturn(updatedCapability).when(mResponse).getRcsContactCapability();
+        doReturn(true).when(mResponse).isNumberBlocked();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_REMOTE_REQUEST_DONE);
+
+        verify(mOptRequestCallback).respondToCapabilityRequest(updatedCapability, true);
+
+        verify(mRequest).onFinish();
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+    }
+
+    private RemoteOptionsCoordinator getRemoteOptCoordinator() {
+        RemoteOptionsCoordinator.Builder builder = new RemoteOptionsCoordinator.Builder(
+                mSubId, Collections.singletonList(mRequest), mRequestMgrCallback);
+        builder.setOptionsRequestCallback(mOptRequestCallback);
+        return builder.build();
+    }
+
+    private RcsContactUceCapability getContactUceCapability() {
+        int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(
+                        mContact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+        return builder.build();
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java
new file mode 100644
index 0000000..13777f3
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.request.RemoteOptionsRequest.RemoteOptResponse;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class RemoteOptionsRequestTest extends ImsTestBase {
+
+    private static final String FEATURE_TAG_CHAT =
+            "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+    private static final String FEATURE_TAG_FILE_TRANSFER =
+            "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+
+    private int mSubId = 1;
+    private long mCoordId = 1L;
+    private Uri mTestContact;
+    private RcsContactUceCapability mDeviceCapability;
+
+    @Mock RequestManagerCallback mRequestManagerCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mTestContact = Uri.fromParts("sip", "test", null);
+
+        List<String> featureTags = new ArrayList<>();
+        featureTags.add(FEATURE_TAG_CHAT);
+        featureTags.add(FEATURE_TAG_FILE_TRANSFER);
+
+        OptionsBuilder builder = new OptionsBuilder(mTestContact, SOURCE_TYPE_CACHED);
+        builder.addFeatureTag(FEATURE_TAG_CHAT);
+        builder.addFeatureTag(FEATURE_TAG_FILE_TRANSFER);
+        mDeviceCapability = builder.build();
+
+        doReturn(mDeviceCapability).when(mRequestManagerCallback).getDeviceCapabilities(anyInt());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilities() throws Exception {
+        RemoteOptionsRequest request = getRequest();
+        List<String> featureTags = Arrays.asList(FEATURE_TAG_CHAT, FEATURE_TAG_FILE_TRANSFER);
+        request.setRemoteFeatureTags(featureTags);
+        request.setIsRemoteNumberBlocked(false);
+
+        request.executeRequest();
+
+        verify(mRequestManagerCallback).saveCapabilities(any());
+
+        RemoteOptResponse response = request.getRemoteOptResponse();
+        assertEquals(mDeviceCapability, response.getRcsContactCapability());
+        assertFalse(response.isNumberBlocked());
+
+        verify(mRequestManagerCallback).notifyRemoteRequestDone(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilitiesWhenBlocked() throws Exception {
+        RemoteOptionsRequest request = getRequest();
+        List<String> featureTags = Arrays.asList(FEATURE_TAG_CHAT, FEATURE_TAG_FILE_TRANSFER);
+        request.setRemoteFeatureTags(featureTags);
+        request.setIsRemoteNumberBlocked(true);
+
+        request.executeRequest();
+
+        verify(mRequestManagerCallback).saveCapabilities(any());
+
+        RemoteOptResponse response = request.getRemoteOptResponse();
+        assertEquals(mDeviceCapability, response.getRcsContactCapability());
+        assertTrue(response.isNumberBlocked());
+
+        verify(mRequestManagerCallback).notifyRemoteRequestDone(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testsendCapabilitiesRequestWhenDestroy() throws Exception {
+        RemoteOptionsRequest request = getRequest();
+        request.onFinish();
+
+        request.executeRequest();
+
+        RemoteOptResponse response = request.getRemoteOptResponse();
+        int errorSipCode = response.getErrorSipCode().orElse(-1);
+        String reason = response.getErrorReason().orElse("");
+        assertEquals(NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE, errorSipCode);
+        assertEquals(NetworkSipCode.SIP_SERVICE_UNAVAILABLE, reason);
+
+        verify(mRequestManagerCallback).notifyRemoteRequestDone(eq(mCoordId), anyLong());
+        verify(mRequestManagerCallback, never()).saveCapabilities(any());
+    }
+
+    private RemoteOptionsRequest getRequest() {
+        RemoteOptionsRequest request = new RemoteOptionsRequest(mSubId, mRequestManagerCallback);
+        request.setRequestCoordinatorId(mCoordId);
+        request.setContactUri(Collections.singletonList(mTestContact));
+        return request;
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java b/tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java
new file mode 100644
index 0000000..137b4ac
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_TERMINATED;
+
+import static java.lang.Boolean.TRUE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.RequestResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class SubscribeCoordinatorTest extends ImsTestBase {
+
+    @Mock SubscribeRequest mRequest;
+    @Mock CapabilityRequestResponse mResponse;
+    @Mock RequestManagerCallback mRequestMgrCallback;
+    @Mock IRcsUceControllerCallback mUceCallback;
+    @Mock DeviceStateResult mDeviceStateResult;
+
+    private int mSubId = 1;
+    private long mTaskId = 1L;
+    private Uri mContact = Uri.fromParts("sip", "test1", null);
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mTaskId).when(mRequest).getTaskId();
+        doReturn(mResponse).when(mRequest).getRequestResponse();
+        doReturn(Optional.empty()).when(mResponse).getReasonHeaderCause();
+        doReturn(mDeviceStateResult).when(mRequestMgrCallback).getDeviceState();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestUpdatedWithError() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_ERROR);
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequest).onFinish();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCommandError() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_COMMAND_ERROR);
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequest).onFinish();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestNetworkRespSuccess() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+        doReturn(true).when(mResponse).isNetworkResponseOK();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertEquals(1, requestList.size());
+        assertTrue(resultList.isEmpty());
+        verify(mRequest, never()).onFinish();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestNetworkRespError() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+        doReturn(false).when(mResponse).isNetworkResponseOK();
+        doReturn(true).when(mResponse).isRequestForbidden();
+        Optional<Integer> respSipCode = Optional.of(400);
+        Optional<String> respReason = Optional.of("Bad Request");
+        doReturn(respSipCode).when(mResponse).getResponseSipCode();
+        doReturn(respReason).when(mResponse).getResponseReason();
+        doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+        verify(mRequestMgrCallback).refreshDeviceState(respSipCode.get(), respReason.get());
+        verify(mRequest).onFinish();
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+        verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilityUpdated() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+        RcsContactUceCapability updatedCapability = getContactUceCapability();
+        updatedCapList.add(updatedCapability);
+        doReturn(updatedCapList).when(mResponse).getUpdatedContactCapability();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_CAPABILITY_UPDATE);
+
+        verify(mRequestMgrCallback).saveCapabilities(updatedCapList);
+        verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+        verify(mResponse).removeUpdatedCapabilities(updatedCapList);
+    }
+
+    @Test
+    @SmallTest
+    public void testResourceTerminated() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+        RcsContactUceCapability updatedCapability = getContactUceCapability();
+        updatedCapList.add(updatedCapability);
+        doReturn(updatedCapList).when(mResponse).getTerminatedResources();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_RESOURCE_TERMINATED);
+
+        verify(mRequestMgrCallback).saveCapabilities(updatedCapList);
+        verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+        verify(mResponse).removeTerminatedResources(updatedCapList);
+    }
+
+    @Test
+    @SmallTest
+    public void testCachedCapabilityUpdated() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+        RcsContactUceCapability updatedCapability = getContactUceCapability();
+        updatedCapList.add(updatedCapability);
+        doReturn(updatedCapList).when(mResponse).getCachedContactCapability();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
+
+        verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+        verify(mResponse).removeCachedContactCapabilities();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestTerminated() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_TERMINATED);
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+    }
+
+    @Test
+    @SmallTest
+    public void testNoNeedRequestFromNetwork() throws Exception {
+        SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+        coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
+
+        Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+        Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+        assertTrue(requestList.isEmpty());
+        assertEquals(1, resultList.size());
+    }
+
+    private SubscribeRequestCoordinator getSubscribeCoordinator() {
+        SubscribeRequestCoordinator.Builder builder = new SubscribeRequestCoordinator.Builder(
+                mSubId, Collections.singletonList(mRequest), mRequestMgrCallback);
+        builder.setCapabilitiesCallback(mUceCallback);
+        return builder.build();
+    }
+
+    private RcsContactUceCapability getContactUceCapability() {
+        int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+        RcsContactUceCapability.PresenceBuilder builder =
+                new RcsContactUceCapability.PresenceBuilder(
+                        mContact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+        return builder.build();
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java
new file mode 100644
index 0000000..b4f9cca
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.stub.RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactTerminatedReason;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class SubscribeRequestTest extends ImsTestBase {
+
+    @Mock SubscribeController mSubscribeController;
+    @Mock CapabilityRequestResponse mRequestResponse;
+    @Mock RequestManagerCallback mRequestManagerCallback;
+
+    private int mSubId = 1;
+    private long mCoordId = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilities() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+
+        List<Uri> uriList = new ArrayList<>();
+        subscribeRequest.requestCapabilities(uriList);
+
+        verify(mSubscribeController).requestCapabilities(eq(uriList), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestCapabilitiesWhenDestroy() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+        subscribeRequest.onFinish();
+
+        List<Uri> uriList = new ArrayList<>();
+        subscribeRequest.requestCapabilities(uriList);
+
+        verify(mRequestResponse).setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+        verify(mRequestManagerCallback).notifyRequestError(eq(mCoordId), anyLong());
+        verify(mSubscribeController, never()).requestCapabilities(any(), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testCommandErrorCallback() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+        ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+        callback.onCommandError(COMMAND_CODE_NOT_SUPPORTED);
+
+        verify(mRequestResponse).setCommandError(COMMAND_CODE_NOT_SUPPORTED);
+        verify(mRequestManagerCallback).notifyCommandError(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testNetworkResponse() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+
+        int sipCode = NetworkSipCode.SIP_CODE_FORBIDDEN;
+        String reason = "forbidden";
+        ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+        callback.onNetworkResponse(sipCode, reason);
+
+        verify(mRequestResponse).setNetworkResponseCode(sipCode, reason);
+        verify(mRequestManagerCallback).notifyNetworkResponse(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testResourceTerminated() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+        ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+        Uri contact = Uri.fromParts("sip", "test", null);
+        List<RcsContactTerminatedReason> list = new ArrayList<>();
+        list.add(new RcsContactTerminatedReason(contact, "terminated"));
+        callback.onResourceTerminated(list);
+
+        verify(mRequestResponse).addTerminatedResource(list);
+        verify(mRequestManagerCallback).notifyResourceTerminated(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testCapabilitiesUpdate() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+        ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+        List<String> pidfXml = new ArrayList<>();
+        pidfXml.add(getPidfData());
+        callback.onNotifyCapabilitiesUpdate(pidfXml);
+
+        verify(mRequestResponse).addUpdatedCapabilities(any());
+        verify(mRequestManagerCallback).notifyCapabilitiesUpdated(eq(mCoordId), anyLong());
+    }
+
+    @Test
+    @SmallTest
+    public void testTerminatedCallback() throws Exception {
+        SubscribeRequest subscribeRequest = getSubscribeRequest();
+        doReturn(true).when(mRequestResponse).isNetworkResponseOK();
+        ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+        String reason = "forbidden";
+        long retryAfterMillis = 10000L;
+        callback.onTerminated(reason, retryAfterMillis);
+
+        verify(mRequestResponse).setTerminated(reason, retryAfterMillis);
+        verify(mRequestManagerCallback).notifyTerminated(eq(mCoordId), anyLong());
+    }
+
+    private SubscribeRequest getSubscribeRequest() {
+        SubscribeRequest request = new SubscribeRequest(mSubId, UceRequest.REQUEST_TYPE_CAPABILITY,
+                mRequestManagerCallback, mSubscribeController, mRequestResponse);
+        request.setRequestCoordinatorId(mCoordId);
+        return request;
+    }
+
+    private String getPidfData() {
+        StringBuilder pidfBuilder = new StringBuilder();
+        pidfBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+                .append("<presence entity=\"sip:test\"")
+                .append(" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+                .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+                .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+                // tuple 1
+                .append("<tuple id=\"tid0\"><status><basic>open</basic></status>")
+                .append("<op:service-description>")
+                .append("<op:service-id>service_id_01</op:service-id>")
+                .append("<op:version>1.0</op:version>")
+                .append("<op:description>description_test1</op:description>")
+                .append("</op:service-description>")
+                // support audio
+                .append("<caps:servcaps>")
+                .append("<caps:audio>true</caps:audio>")
+                // support video
+                .append("<caps:video>true</caps:video>")
+                .append("</caps:servcaps>")
+                .append("<contact>sip:test</contact>")
+                .append("</tuple>");
+
+        return pidfBuilder.toString();
+    }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.java b/tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.java
new file mode 100644
index 0000000..607772e
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_TERMINATED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.request.UceRequestManager.UceUtilsProxy;
+import com.android.ims.rcs.uce.util.FeatureTags;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class UceRequestManagerTest extends ImsTestBase {
+
+    @Mock UceRequest mUceRequest;
+    @Mock UceRequestCoordinator mCoordinator;
+    @Mock UceControllerCallback mCallback;
+    @Mock UceRequestRepository mRequestRepository;
+    @Mock IRcsUceControllerCallback mCapabilitiesCallback;
+    @Mock IOptionsRequestCallback mOptionsReqCallback;
+
+    private int mSubId = 1;
+    private long mTaskId = 1L;
+    private long mCoordId = 1L;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        doReturn(mUceRequest).when(mRequestRepository).getUceRequest(anyLong());
+        doReturn(mCoordinator).when(mRequestRepository).getRequestCoordinator(anyLong());
+        doReturn(mCoordinator).when(mRequestRepository).removeRequestCoordinator(anyLong());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCapabilityRequest() throws Exception {
+        UceRequestManager requestManager = getUceRequestManager();
+        requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, false, false, true, 10));
+
+        List<Uri> uriList = new ArrayList<>();
+        uriList.add(Uri.fromParts("sip", "test", null));
+        requestManager.sendCapabilityRequest(uriList, false, mCapabilitiesCallback);
+
+        verify(mRequestRepository).addRequestCoordinator(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendAvailabilityRequest() throws Exception {
+        UceRequestManager requestManager = getUceRequestManager();
+        requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, false, false, true, 10));
+
+        Uri uri = Uri.fromParts("sip", "test", null);
+        requestManager.sendAvailabilityRequest(uri, mCapabilitiesCallback);
+
+        verify(mRequestRepository).addRequestCoordinator(any());
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestDestroyed() throws Exception {
+        UceRequestManager requestManager = getUceRequestManager();
+        requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, true, false, true, 10));
+
+        requestManager.onDestroy();
+
+        List<Uri> uriList = new ArrayList<>();
+        requestManager.sendCapabilityRequest(uriList, false, mCapabilitiesCallback);
+
+        Handler handler = requestManager.getUceRequestHandler();
+        waitForHandlerAction(handler, 500L);
+
+        verify(mUceRequest, never()).executeRequest();
+        verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+    }
+
+    @Test
+    @SmallTest
+    public void testRequestManagerCallback() throws Exception {
+        UceRequestManager requestManager = getUceRequestManager();
+        requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, true, false, true, 10));
+        RequestManagerCallback requestMgrCallback = requestManager.getRequestManagerCallback();
+        Handler handler = requestManager.getUceRequestHandler();
+
+        Uri contact = Uri.fromParts("sip", "test", null);
+        List<Uri> uriList = new ArrayList<>();
+        uriList.add(contact);
+
+        requestMgrCallback.notifySendingRequest(mCoordId, mTaskId, 0L);
+        waitForHandlerAction(handler, 400L);
+        verify(mUceRequest).executeRequest();
+
+        requestMgrCallback.getCapabilitiesFromCache(uriList);
+        verify(mCallback).getCapabilitiesFromCache(uriList);
+
+        requestMgrCallback.getAvailabilityFromCache(contact);
+        verify(mCallback).getAvailabilityFromCache(contact);
+
+        List<RcsContactUceCapability> capabilityList = new ArrayList<>();
+        requestMgrCallback.saveCapabilities(capabilityList);
+        verify(mCallback).saveCapabilities(capabilityList);
+
+        requestMgrCallback.getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE);
+        verify(mCallback).getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE);
+
+        requestMgrCallback.getDeviceState();
+        verify(mCallback).getDeviceState();
+
+        requestMgrCallback.refreshDeviceState(200, "OK");
+        verify(mCallback).refreshDeviceState(200, "OK");
+
+        requestMgrCallback.notifyRequestError(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_ERROR);
+
+        requestMgrCallback.notifyCommandError(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_COMMAND_ERROR);
+
+        requestMgrCallback.notifyNetworkResponse(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+        requestMgrCallback.notifyTerminated(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_TERMINATED);
+
+        requestMgrCallback.notifyResourceTerminated(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_RESOURCE_TERMINATED);
+
+        requestMgrCallback.notifyCapabilitiesUpdated(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_CAPABILITY_UPDATE);
+
+        requestMgrCallback.notifyCachedCapabilitiesUpdated(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
+
+        requestMgrCallback.notifyNoNeedRequestFromNetwork(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
+
+        requestMgrCallback.notifyRemoteRequestDone(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_REMOTE_REQUEST_DONE);
+
+        requestMgrCallback.notifyUceRequestFinished(mCoordId, mTaskId);
+        waitForHandlerAction(handler, 400L);
+        verify(mRequestRepository).notifyRequestFinished(mTaskId);
+
+        requestMgrCallback.notifyRequestCoordinatorFinished(mCoordId);
+        waitForHandlerAction(handler, 400L);
+        verify(mCoordinator).onFinish();
+    }
+
+    @Test
+    @SmallTest
+    public void testRetrieveCapForRemote() throws Exception {
+        UceRequestManager requestManager = getUceRequestManager();
+        requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, true, false, true, 10));
+
+        Uri contact = Uri.fromParts("sip", "test", null);
+        List<String> remoteCapList = new ArrayList<>();
+        remoteCapList.add(FeatureTags.FEATURE_TAG_CHAT_IM);
+        remoteCapList.add(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+        requestManager.retrieveCapabilitiesForRemote(contact, remoteCapList, mOptionsReqCallback);
+
+        verify(mRequestRepository).addRequestCoordinator(any());
+    }
+
+    private UceRequestManager getUceRequestManager() {
+        UceRequestManager manager = new UceRequestManager(mContext, mSubId, Looper.getMainLooper(),
+                mCallback, mRequestRepository);
+        return manager;
+    }
+
+    private UceUtilsProxy getUceUtilsProxy(boolean presenceCapEnabled, boolean supportPresence,
+            boolean supportOptions, boolean isBlocked, boolean groupSubscribe, int rclMaximum) {
+        return new UceUtilsProxy() {
+            @Override
+            public boolean isPresenceCapExchangeEnabled(Context context, int subId) {
+                return presenceCapEnabled;
+            }
+
+            @Override
+            public boolean isPresenceSupported(Context context, int subId) {
+                return supportPresence;
+            }
+
+            @Override
+            public boolean isSipOptionsSupported(Context context, int subId) {
+                return supportOptions;
+            }
+
+            @Override
+            public boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
+                return groupSubscribe;
+            }
+
+            @Override
+            public int getRclMaxNumberEntries(int subId) {
+                return rclMaximum;
+            }
+
+            @Override
+            public boolean isNumberBlocked(Context context, String phoneNumber) {
+                return isBlocked;
+            }
+        };
+    }
+}