Merge "Only config VoNR per carrier config if card is PRESENT" into sc-v2-dev
diff --git a/proto/src/persist_atoms.proto b/proto/src/persist_atoms.proto
index 1701318..bb7d8d3 100644
--- a/proto/src/persist_atoms.proto
+++ b/proto/src/persist_atoms.proto
@@ -23,7 +23,7 @@
 
 // Holds atoms to store on persist storage in case of power cycle or process crash.
 // NOTE: using int64 rather than google.protobuf.Timestamp for timestamps simplifies implementation.
-// Next id: 22
+// Next id: 50
 message PersistAtoms {
     /* Aggregated RAT usage during the call. */
     repeated VoiceCallRatUsage voice_call_rat_usage = 1;
@@ -93,6 +93,84 @@
 
     /* Timestamp of last network_requests pull. */
     optional int64 network_requests_pull_timestamp_millis = 23;
+
+    /* RCS single registrtions feature tag information. */
+    repeated ImsRegistrationFeatureTagStats ims_registration_feature_tag_stats = 24;
+
+    /* Timestamp of last ims_registration_feature_tag_stats pull. */
+    optional int64 ims_registration_feature_tag_stats_pull_timestamp_millis = 25;
+
+    /* RCS client provisioning statistics and information. */
+    repeated RcsClientProvisioningStats rcs_client_provisioning_stats = 26;
+
+    /* Timestamp of last rcs_client_provisioning_stats pull. */
+    optional int64 rcs_client_provisioning_stats_pull_timestamp_millis = 27;
+
+    /* RCS configuration statistics and information based ACS. */
+    repeated RcsAcsProvisioningStats rcs_acs_provisioning_stats = 28;
+
+    /* Timestamp of last rcs_acs_provisioning_stats pull. */
+    optional int64 rcs_acs_provisioning_stats_pull_timestamp_millis = 29;
+
+    /* SIP delegate statistics and information. */
+    repeated SipDelegateStats sip_delegate_stats = 30;
+
+    /* Timestamp of last sip_delegate_stats pull. */
+    optional int64 sip_delegate_stats_pull_timestamp_millis = 31;
+
+    /* SIP Transport featuere tag statistics and information. */
+    repeated SipTransportFeatureTagStats sip_transport_feature_tag_stats = 32;
+
+    /* Timestamp of last sip_transport_feature_tag_stats pull. */
+    optional int64 sip_transport_feature_tag_stats_pull_timestamp_millis = 33;
+
+    /* SIP Message response statistics and information. */
+    repeated SipMessageResponse sip_message_response = 34;
+
+    /* Timestamp of last sip_message_response pull. */
+    optional int64 sip_message_response_pull_timestamp_millis = 35;
+
+    /* SIP Transport session statistics and information. */
+    repeated SipTransportSession sip_transport_session = 36;
+
+    /* Timestamp of last sip_transport_session pull. */
+    optional int64 sip_transport_session_pull_timestamp_millis = 37;
+
+    /* Dedicated bearer listener statistics and information. */
+    repeated ImsDedicatedBearerListenerEvent ims_dedicated_bearer_listener_event = 38;
+
+    /* Timestamp of last ims_dedicated_bearer_listener_event pull. */
+    optional int64 ims_dedicated_bearer_listener_event_pull_timestamp_millis = 39;
+
+    /* Dedicated bearer event statistics and information. */
+    repeated ImsDedicatedBearerEvent ims_dedicated_bearer_event = 40;
+
+    /* Timestamp of last ims_dedicated_bearer_event pull. */
+    optional int64 ims_dedicated_bearer_event_pull_timestamp_millis = 41;
+
+    /* Publish featere tag statistics and information. */
+    repeated ImsRegistrationServiceDescStats ims_registration_service_desc_stats = 42;
+
+    /* Timestamp of last ims_registration_service_desc_stats pull. */
+    optional int64 ims_registration_service_desc_stats_pull_timestamp_millis = 43;
+
+    /* UCE event stats statistics and information. */
+    repeated UceEventStats uce_event_stats = 44;
+
+    /* Timestamp of last uce_event_stats pull. */
+    optional int64 uce_event_stats_pull_timestamp_millis = 45;
+
+    /* Presence notify event statistics and information. */
+    repeated PresenceNotifyEvent presence_notify_event = 46;
+
+    /* Timestamp of last presence_notify_event pull. */
+    optional int64 presence_notify_event_pull_timestamp_millis = 47;
+
+    /* GBA event statistics and information. */
+    repeated GbaEvent gba_event = 48;
+
+    /* Timestamp of last gba_event pull. */
+    optional int64 gba_event_pull_timestamp_millis = 49;
 }
 
 // The canonical versions of the following enums live in:
@@ -273,3 +351,127 @@
     optional int32 enterprise_request_count = 2;
     optional int32 enterprise_release_count = 3;
 }
+
+message ImsRegistrationFeatureTagStats {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 feature_tag_name = 3;
+    optional int32 registration_tech = 4;
+    optional int64 registered_millis = 5;
+}
+
+message RcsClientProvisioningStats {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 event = 3;
+    optional int32 count = 4;
+}
+
+message RcsAcsProvisioningStats {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 response_code = 3;
+    optional int32 response_type = 4;
+    optional bool is_single_registration_enabled = 5;
+    optional int32 count = 6;
+    optional int64 state_timer_millis = 7;
+}
+
+message SipDelegateStats {
+    optional int32 dimension = 1;
+    optional int32 carrier_id = 2;
+    optional int32 slot_id = 3;
+    optional int32 destroy_reason = 4;
+    optional int64 uptime_millis = 5;
+}
+
+message SipTransportFeatureTagStats {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 feature_tag_name = 3;
+    optional int32 sip_transport_denied_reason = 4;
+    optional int32 sip_transport_deregistered_reason = 5;
+    optional int64 associated_millis = 6;
+}
+
+message SipMessageResponse {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 sip_message_method = 3;
+    optional int32 sip_message_response = 4;
+    optional int32 sip_message_direction = 5;
+    optional int32 message_error = 6;
+    optional int32 count = 7;
+}
+
+message SipTransportSession {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 session_method = 3;
+    optional int32 sip_message_direction = 4;
+    optional int32 sip_response = 5;
+    optional int32 session_count = 6;
+    optional int32 ended_gracefully_count = 7;
+
+    // Internal use only
+    optional bool is_ended_gracefully = 10001;
+}
+
+message ImsDedicatedBearerListenerEvent {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 rat_at_end = 3;
+    optional int32 qci = 4;
+    optional bool dedicated_bearer_established = 5;
+    optional int32 event_count = 6;
+}
+
+message ImsDedicatedBearerEvent {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 rat_at_end = 3;
+    optional int32 qci = 4;
+    optional int32 bearer_state = 5;
+    optional bool local_connection_info_received = 6;
+    optional bool remote_connection_info_received = 7;
+    optional bool has_listeners = 8;
+    optional int32 count = 9;
+}
+
+message ImsRegistrationServiceDescStats {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 service_id_name = 3;
+    optional float service_id_version = 4;
+    optional int32 registration_tech = 5;
+    optional int64 published_millis = 6;
+}
+
+message UceEventStats {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 type = 3;
+    optional bool successful = 4;
+    optional int32 command_code = 5;
+    optional int32 network_response = 6;
+    optional int32 count = 7;
+}
+
+message PresenceNotifyEvent {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional int32 reason = 3;
+    optional bool content_body_received = 4;
+    optional int32 rcs_caps_count = 5;
+    optional int32 mmtel_caps_count = 6;
+    optional int32 no_caps_count = 7;
+    optional int32 count = 8;
+}
+
+message GbaEvent {
+    optional int32 carrier_id = 1;
+    optional int32 slot_id = 2;
+    optional bool successful = 3;
+    optional int32 failed_reason = 4;
+    optional int32 count = 5;
+}
\ No newline at end of file
diff --git a/src/java/com/android/internal/telephony/GbaManager.java b/src/java/com/android/internal/telephony/GbaManager.java
index d6c59ea..b1db1ac 100644
--- a/src/java/com/android/internal/telephony/GbaManager.java
+++ b/src/java/com/android/internal/telephony/GbaManager.java
@@ -37,6 +37,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.telephony.Rlog;
 
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -60,6 +61,7 @@
     public static final int MAX_RETRY = 5;
     @VisibleForTesting
     public static final int REQUEST_TIMEOUT_MS = 5000;
+    private final RcsStats mRcsStats;
 
     private final String mLogTag;
     private final Context mContext;
@@ -191,7 +193,8 @@
     }
 
     @VisibleForTesting
-    public GbaManager(Context context, int subId, String servicePackageName, int releaseTime) {
+    public GbaManager(Context context, int subId, String servicePackageName, int releaseTime,
+            RcsStats rcsStats) {
         mContext = context;
         mSubId = subId;
         mLogTag = "GbaManager[" + subId + "]";
@@ -206,6 +209,7 @@
         if (mReleaseTime < 0) {
             mHandler.sendEmptyMessage(EVENT_BIND_SERVICE);
         }
+        mRcsStats = rcsStats;
     }
 
     /**
@@ -213,7 +217,8 @@
      */
     public static GbaManager make(Context context, int subId,
             String servicePackageName, int releaseTime) {
-        GbaManager gm = new GbaManager(context, subId, servicePackageName, releaseTime);
+        GbaManager gm = new GbaManager(context, subId, servicePackageName, releaseTime,
+                RcsStats.getInstance());
         synchronized (sGbaManagers) {
             sGbaManagers.put(subId, gm);
         }
@@ -267,6 +272,7 @@
                     if (cb != null) {
                         try {
                             cb.onKeysAvailable(token, gbaKey, btId);
+                            mRcsStats.onGbaSuccessEvent(mSubId);
                         } catch (RemoteException exception) {
                             logd("RemoteException " + exception);
                         }
@@ -291,6 +297,7 @@
                     if (cb != null) {
                         try {
                             cb.onAuthenticationFailure(token, reason);
+                            mRcsStats.onGbaFailureEvent(mSubId, reason);
                         } catch (RemoteException exception) {
                             logd("RemoteException " + exception);
                         }
diff --git a/src/java/com/android/internal/telephony/NetworkTypeController.java b/src/java/com/android/internal/telephony/NetworkTypeController.java
index e8ad6ff..92f277c 100644
--- a/src/java/com/android/internal/telephony/NetworkTypeController.java
+++ b/src/java/com/android/internal/telephony/NetworkTypeController.java
@@ -1010,6 +1010,12 @@
     }
 
     private void updateTimers() {
+        if ((mPhone.getCachedAllowedNetworkTypesBitmask()
+                & TelephonyManager.NETWORK_TYPE_BITMASK_NR) == 0) {
+            resetAllTimers();
+            return;
+        }
+
         String currentState = getCurrentState().getName();
 
         if (mIsPrimaryTimerActive && getOverrideNetworkType() == getCurrentOverrideNetworkType()) {
diff --git a/src/java/com/android/internal/telephony/dataconnection/DcNetworkAgent.java b/src/java/com/android/internal/telephony/dataconnection/DcNetworkAgent.java
index 32b2f7b..7d7f065 100644
--- a/src/java/com/android/internal/telephony/dataconnection/DcNetworkAgent.java
+++ b/src/java/com/android/internal/telephony/dataconnection/DcNetworkAgent.java
@@ -118,7 +118,7 @@
         } else {
             loge("The connection does not have a valid link properties.");
         }
-        mQosCallbackTracker = new QosCallbackTracker(this);
+        mQosCallbackTracker = new QosCallbackTracker(this, mPhone.getPhoneId());
     }
 
     private @NetworkType int getNetworkType() {
diff --git a/src/java/com/android/internal/telephony/dataconnection/QosCallbackTracker.java b/src/java/com/android/internal/telephony/dataconnection/QosCallbackTracker.java
index 795ed14..e4b14e8 100644
--- a/src/java/com/android/internal/telephony/dataconnection/QosCallbackTracker.java
+++ b/src/java/com/android/internal/telephony/dataconnection/QosCallbackTracker.java
@@ -17,22 +17,25 @@
 package com.android.internal.telephony.dataconnection;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.net.LinkAddress;
 import android.net.QosSession;
+import android.telephony.TelephonyProtoEnums;
+import android.telephony.data.EpsBearerQosSessionAttributes;
 import android.telephony.data.EpsQos;
 import android.telephony.data.NrQos;
-import android.telephony.data.EpsBearerQosSessionAttributes;
 import android.telephony.data.NrQosSessionAttributes;
 import android.telephony.data.QosBearerFilter;
 import android.telephony.data.QosBearerSession;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.telephony.Rlog;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 
@@ -44,21 +47,48 @@
  * {@hide}
  */
 public class QosCallbackTracker {
+    private static final int DedicatedBearerEvent_STATE_NONE = 0;
+    private static final int DedicatedBearerEvent_STATE_ADDED = 1;
+    private static final int DedicatedBearerEvent_STATE_MODIFIED = 2;
+    private static final int DedicatedBearerEvent_STATE_DELETED = 3;
+
     @NonNull private final String mTag;
     @NonNull private final DcNetworkAgent mDcNetworkAgent;
     @NonNull private final Map<Integer, QosBearerSession> mQosBearerSessions;
+    @NonNull private final RcsStats mRcsStats;
 
     // We perform an exact match on the address
     @NonNull private final Map<Integer, IFilter> mCallbacksToFilter;
 
+    @NonNull private final int mPhoneID;
+
     /**
      * Construct a new tracker
      * @param dcNetworkAgent the network agent to send events to
      */
-    public QosCallbackTracker(@NonNull final DcNetworkAgent dcNetworkAgent) {
+    public QosCallbackTracker(@NonNull final DcNetworkAgent dcNetworkAgent,
+            @NonNull final int phoneID) {
         mQosBearerSessions = new HashMap<>();
         mCallbacksToFilter = new HashMap<>();
         mDcNetworkAgent = dcNetworkAgent;
+        mPhoneID = phoneID;
+        mRcsStats = RcsStats.getInstance();
+        mTag = "QosCallbackTracker" + "-" + mDcNetworkAgent.getNetwork().getNetId();
+    }
+
+    /**
+     * Construct a new tracker
+     * @param dcNetworkAgent the network agent to send events to
+     * @param rcsStats metrics package to store QoS event info
+     */
+    @VisibleForTesting
+    public QosCallbackTracker(@NonNull final DcNetworkAgent dcNetworkAgent,
+            @NonNull final int phoneID, @NonNull final RcsStats rcsStats) {
+        mQosBearerSessions = new HashMap<>();
+        mCallbacksToFilter = new HashMap<>();
+        mDcNetworkAgent = dcNetworkAgent;
+        mPhoneID = phoneID;
+        mRcsStats = rcsStats;
         mTag = "QosCallbackTracker" + "-" + mDcNetworkAgent.getNetwork().getNetId();
     }
 
@@ -77,6 +107,8 @@
         for (final QosBearerSession session : mQosBearerSessions.values()) {
             if (doFiltersMatch(session, filter)) {
                 sendSessionAvailable(callbackId, session, filter);
+
+                notifyMetricDedicatedBearerListenerAdded(callbackId, session);
             }
         }
     }
@@ -89,6 +121,7 @@
     public void removeFilter(final int callbackId) {
         logd("removeFilter: callbackId=" + callbackId);
         mCallbacksToFilter.remove(callbackId);
+        notifyMetricDedicatedBearerListenerRemoved(callbackId);
     }
 
     /**
@@ -98,13 +131,18 @@
      */
     public void updateSessions(@NonNull final List<QosBearerSession> sessions) {
         logd("updateSessions: sessions size=" + sessions.size());
+
+        int bearerState = DedicatedBearerEvent_STATE_NONE;
+
         final List<QosBearerSession> sessionsToAdd = new ArrayList<>();
         final Map<Integer, QosBearerSession> incomingSessions = new HashMap<>();
+        final HashSet<Integer> sessionsReportedToMetric = new HashSet<>();
         for (final QosBearerSession incomingSession : sessions) {
-            incomingSessions.put(incomingSession.getQosBearerSessionId(), incomingSession);
+            int sessionId = incomingSession.getQosBearerSessionId();
+            incomingSessions.put(sessionId, incomingSession);
 
             final QosBearerSession existingSession = mQosBearerSessions.get(
-                    incomingSession.getQosBearerSessionId());
+                    sessionId);
             for (final int callbackId : mCallbacksToFilter.keySet()) {
                 final IFilter filter = mCallbacksToFilter.get(callbackId);
 
@@ -115,6 +153,7 @@
                 if (!existingSessionMatch && incomingSessionMatch) {
                     // The filter matches now and didn't match earlier
                     sendSessionAvailable(callbackId, incomingSession, filter);
+                    bearerState = DedicatedBearerEvent_STATE_ADDED;
                 }
 
                 if (existingSessionMatch && incomingSessionMatch) {
@@ -122,24 +161,50 @@
                     // the callback still needs to be notified
                     if (!incomingSession.getQos().equals(existingSession.getQos())) {
                         sendSessionAvailable(callbackId, incomingSession, filter);
+                        bearerState = DedicatedBearerEvent_STATE_MODIFIED;
                     }
                 }
+
+                // this QosBearerSession has registered QosCallbackId
+                if (!sessionsReportedToMetric.contains(sessionId) && incomingSessionMatch) {
+                    // this session has listener
+                    notifyMetricDedicatedBearerEvent(incomingSession, bearerState, true);
+                    sessionsReportedToMetric.add(sessionId);
+                }
+            }
+
+            // this QosBearerSession does not have registered QosCallbackId
+            if (!sessionsReportedToMetric.contains(sessionId)) {
+                // no listener is registered to this session
+                bearerState = DedicatedBearerEvent_STATE_ADDED;
+                notifyMetricDedicatedBearerEvent(incomingSession, bearerState, false);
+                sessionsReportedToMetric.add(sessionId);
             }
             sessionsToAdd.add(incomingSession);
         }
 
         final List<Integer> sessionsToRemove = new ArrayList<>();
+        sessionsReportedToMetric.clear();
+        bearerState = DedicatedBearerEvent_STATE_DELETED;
+
         // Find sessions that no longer exist
         for (final QosBearerSession existingSession : mQosBearerSessions.values()) {
-            if (!incomingSessions.containsKey(existingSession.getQosBearerSessionId())) {
+            final int sessionId = existingSession.getQosBearerSessionId();
+            if (!incomingSessions.containsKey(sessionId)) {
                 for (final int callbackId : mCallbacksToFilter.keySet()) {
                     final IFilter filter = mCallbacksToFilter.get(callbackId);
                     // The filter matches which means it was previously available, and now is lost
                     if (doFiltersMatch(existingSession, filter)) {
                         sendSessionLost(callbackId, existingSession);
+                        notifyMetricDedicatedBearerEvent(existingSession, bearerState, true);
+                        sessionsReportedToMetric.add(sessionId);
                     }
                 }
                 sessionsToRemove.add(existingSession.getQosBearerSessionId());
+                if (!sessionsReportedToMetric.contains(sessionId)) {
+                    notifyMetricDedicatedBearerEvent(existingSession, bearerState, false);
+                    sessionsReportedToMetric.add(sessionId);
+                }
             }
         }
 
@@ -161,7 +226,8 @@
 
     private boolean matchesByLocalAddress(
         QosBearerFilter sessionFilter, final IFilter filter) {
-        for (final LinkAddress qosAddress : sessionFilter.getLocalAddresses()) {
+        if (sessionFilter.getLocalPortRange() == null) return false;
+        for (final LinkAddress qosAddress: sessionFilter.getLocalAddresses()) {
             return filter.matchesLocalAddress(qosAddress.getAddress(),
                   sessionFilter.getLocalPortRange().getStart(),
                   sessionFilter.getLocalPortRange().getEnd());
@@ -171,6 +237,7 @@
 
     private boolean matchesByRemoteAddress(
             QosBearerFilter sessionFilter, final IFilter filter) {
+        if (sessionFilter.getRemotePortRange() == null) return false;
         for (final LinkAddress qosAddress : sessionFilter.getRemoteAddresses()) {
             return filter.matchesRemoteAddress(qosAddress.getAddress(),
                   sessionFilter.getRemotePortRange().getStart(),
@@ -181,6 +248,8 @@
 
     private boolean matchesByRemoteAndLocalAddress(
             QosBearerFilter sessionFilter, final IFilter filter) {
+        if (sessionFilter.getLocalPortRange() == null
+                || sessionFilter.getRemotePortRange() == null) return false;
         for (final LinkAddress remoteAddress : sessionFilter.getRemoteAddresses()) {
             for (final LinkAddress localAddress : sessionFilter.getLocalAddresses()) {
                 return filter.matchesRemoteAddress(remoteAddress.getAddress(),
@@ -208,17 +277,21 @@
         for (final QosBearerFilter sessionFilter : qosBearerSession.getQosBearerFilterList()) {
            if (!sessionFilter.getLocalAddresses().isEmpty()
                    && !sessionFilter.getRemoteAddresses().isEmpty()
+                   && sessionFilter.getLocalPortRange() != null
                    && sessionFilter.getLocalPortRange().isValid()
+                   && sessionFilter.getRemotePortRange() != null
                    && sessionFilter.getRemotePortRange().isValid()) {
                if (matchesByRemoteAndLocalAddress(sessionFilter, filter)) {
                    qosFilter = getFilterByPrecedence(qosFilter, sessionFilter);
                }
            } else if (!sessionFilter.getRemoteAddresses().isEmpty()
+                   && sessionFilter.getRemotePortRange() != null
                    && sessionFilter.getRemotePortRange().isValid()) {
                if (matchesByRemoteAddress(sessionFilter, filter)) {
                    qosFilter = getFilterByPrecedence(qosFilter, sessionFilter);
                }
            } else if (!sessionFilter.getLocalAddresses().isEmpty()
+                   && sessionFilter.getLocalPortRange() != null
                    && sessionFilter.getLocalPortRange().isValid()) {
                if (matchesByLocalAddress(sessionFilter, filter)) {
                    qosFilter = getFilterByPrecedence(qosFilter, sessionFilter);
@@ -232,7 +305,8 @@
             @NonNull final QosBearerSession session, @NonNull IFilter filter) {
         QosBearerFilter qosBearerFilter = getMatchingQosBearerFilter(session, filter);
         List<InetSocketAddress> remoteAddresses = new ArrayList<>();
-        if(qosBearerFilter.getRemoteAddresses().size() > 0) {
+        if (qosBearerFilter.getRemoteAddresses().size() > 0
+                && qosBearerFilter.getRemotePortRange() != null) {
             remoteAddresses.add(
                   new InetSocketAddress(qosBearerFilter.getRemoteAddresses().get(0).getAddress(),
                   qosBearerFilter.getRemotePortRange().getStart()));
@@ -262,6 +336,9 @@
                     callbackId, session.getQosBearerSessionId(), nrQosAttr);
         }
 
+        /** added to notify to Metric for passing DedicatedBearerEstablished info */
+        notifyMetricDedicatedBearerListenerBearerUpdateSession(callbackId, session);
+
         logd("sendSessionAvailable, callbackId=" + callbackId);
     }
 
@@ -272,6 +349,85 @@
         logd("sendSessionLost, callbackId=" + callbackId);
     }
 
+    private void notifyMetricDedicatedBearerListenerAdded(
+            final int callbackId, final QosBearerSession session) {
+
+        final int slotId = mPhoneID;
+        final int rat = getRatInfoFromSessionInfo(session);
+        final int qci = getQCIFromSessionInfo(session);
+
+        mRcsStats.onImsDedicatedBearerListenerAdded(callbackId, slotId, rat, qci);
+    }
+
+    private void notifyMetricDedicatedBearerListenerBearerUpdateSession(
+            final int callbackId, final QosBearerSession session) {
+
+        final int slotId = mPhoneID;
+        final int rat = getRatInfoFromSessionInfo(session);
+        final int qci = getQCIFromSessionInfo(session);
+
+        mRcsStats.onImsDedicatedBearerListenerUpdateSession(callbackId, slotId, rat, qci, true);
+    }
+
+    private void notifyMetricDedicatedBearerListenerRemoved(final int callbackId) {
+        mRcsStats.onImsDedicatedBearerListenerRemoved(callbackId);
+    }
+
+    private int getQCIFromSessionInfo(final QosBearerSession session) {
+        if (session.getQos() instanceof EpsQos) {
+            return ((EpsQos) session.getQos()).getQci();
+        } else if (session.getQos() instanceof NrQos) {
+            return ((NrQos) session.getQos()).get5Qi();
+        }
+
+        return 0;
+    }
+
+    private int getRatInfoFromSessionInfo(final QosBearerSession session) {
+        if (session.getQos() instanceof EpsQos) {
+            return TelephonyProtoEnums.NETWORK_TYPE_LTE;
+        } else if (session.getQos() instanceof NrQos) {
+            return TelephonyProtoEnums.NETWORK_TYPE_NR;
+        }
+
+        return 0;
+    }
+
+    private boolean doesLocalConnectionInfoExist(final QosBearerSession qosBearerSession) {
+        for (final QosBearerFilter sessionFilter : qosBearerSession.getQosBearerFilterList()) {
+            if (!sessionFilter.getLocalAddresses().isEmpty()
+                    && sessionFilter.getLocalPortRange() != null
+                    && sessionFilter.getLocalPortRange().isValid()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean doesRemoteConnectionInfoExist(final QosBearerSession qosBearerSession) {
+        for (final QosBearerFilter sessionFilter : qosBearerSession.getQosBearerFilterList()) {
+            if (!sessionFilter.getRemoteAddresses().isEmpty()
+                    && sessionFilter.getRemotePortRange() != null
+                    && sessionFilter.getRemotePortRange().isValid()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void notifyMetricDedicatedBearerEvent(final QosBearerSession session,
+            final int bearerState, final boolean hasListener) {
+
+        final int slotId = mPhoneID;
+        int ratAtEnd = getRatInfoFromSessionInfo(session);
+        int qci = getQCIFromSessionInfo(session);
+        boolean localConnectionInfoReceived = doesLocalConnectionInfoExist(session);
+        boolean remoteConnectionInfoReceived = doesRemoteConnectionInfoExist(session);
+
+        mRcsStats.onImsDedicatedBearerEvent(slotId, ratAtEnd, qci, bearerState,
+                localConnectionInfoReceived, remoteConnectionInfoReceived, hasListener);
+    }
+
     public interface IFilter {
         public boolean matchesLocalAddress(InetAddress address, int startPort, int endPort);
         public boolean matchesRemoteAddress(InetAddress address, int startPort, int endPort);
diff --git a/src/java/com/android/internal/telephony/metrics/MetricsCollector.java b/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
index 6e54230..40e32e2 100644
--- a/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
+++ b/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
@@ -24,13 +24,26 @@
 import static com.android.internal.telephony.TelephonyStatsLog.CELLULAR_DATA_SERVICE_SWITCH;
 import static com.android.internal.telephony.TelephonyStatsLog.CELLULAR_SERVICE_STATE;
 import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION;
+import static com.android.internal.telephony.TelephonyStatsLog.GBA_EVENT;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_DEDICATED_BEARER_EVENT;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_DEDICATED_BEARER_LISTENER_EVENT;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_FEATURE_TAG_STATS;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS;
 import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_STATS;
 import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_TERMINATION;
 import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS;
 import static com.android.internal.telephony.TelephonyStatsLog.OUTGOING_SMS;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS;
 import static com.android.internal.telephony.TelephonyStatsLog.SIM_SLOT_STATE;
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_DELEGATE_STATS;
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_MESSAGE_RESPONSE;
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_FEATURE_TAG_STATS;
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_SESSION;
 import static com.android.internal.telephony.TelephonyStatsLog.SUPPORTED_RADIO_ACCESS_FAMILY;
 import static com.android.internal.telephony.TelephonyStatsLog.TELEPHONY_NETWORK_REQUESTS;
+import static com.android.internal.telephony.TelephonyStatsLog.UCE_EVENT_STATS;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_RAT_USAGE;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION;
 
@@ -47,11 +60,24 @@
 import com.android.internal.telephony.nano.PersistAtomsProto.CellularDataServiceSwitch;
 import com.android.internal.telephony.nano.PersistAtomsProto.CellularServiceState;
 import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.GbaEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerListenerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationServiceDescStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
 import com.android.internal.telephony.nano.PersistAtomsProto.IncomingSms;
 import com.android.internal.telephony.nano.PersistAtomsProto.NetworkRequests;
 import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms;
+import com.android.internal.telephony.nano.PersistAtomsProto.PresenceNotifyEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsAcsProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsClientProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipDelegateStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipMessageResponse;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.UceEventStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
 import com.android.internal.util.ConcurrentUtils;
@@ -124,6 +150,19 @@
             registerAtom(IMS_REGISTRATION_STATS, POLICY_PULL_DAILY);
             registerAtom(IMS_REGISTRATION_TERMINATION, POLICY_PULL_DAILY);
             registerAtom(TELEPHONY_NETWORK_REQUESTS, POLICY_PULL_DAILY);
+            registerAtom(IMS_REGISTRATION_FEATURE_TAG_STATS, POLICY_PULL_DAILY);
+            registerAtom(RCS_CLIENT_PROVISIONING_STATS, POLICY_PULL_DAILY);
+            registerAtom(RCS_ACS_PROVISIONING_STATS, POLICY_PULL_DAILY);
+            registerAtom(SIP_DELEGATE_STATS, POLICY_PULL_DAILY);
+            registerAtom(SIP_TRANSPORT_FEATURE_TAG_STATS, POLICY_PULL_DAILY);
+            registerAtom(SIP_MESSAGE_RESPONSE, POLICY_PULL_DAILY);
+            registerAtom(SIP_TRANSPORT_SESSION, POLICY_PULL_DAILY);
+            registerAtom(IMS_DEDICATED_BEARER_LISTENER_EVENT, POLICY_PULL_DAILY);
+            registerAtom(IMS_DEDICATED_BEARER_EVENT, POLICY_PULL_DAILY);
+            registerAtom(IMS_REGISTRATION_SERVICE_DESC_STATS, POLICY_PULL_DAILY);
+            registerAtom(UCE_EVENT_STATS, POLICY_PULL_DAILY);
+            registerAtom(PRESENCE_NOTIFY_EVENT, POLICY_PULL_DAILY);
+            registerAtom(GBA_EVENT, POLICY_PULL_DAILY);
 
             Rlog.d(TAG, "registered");
         } else {
@@ -175,6 +214,32 @@
                 return pullImsRegistrationTermination(data);
             case TELEPHONY_NETWORK_REQUESTS:
                 return pullTelephonyNetworkRequests(data);
+            case IMS_REGISTRATION_FEATURE_TAG_STATS:
+                return pullImsRegistrationFeatureTagStats(data);
+            case RCS_CLIENT_PROVISIONING_STATS:
+                return pullRcsClientProvisioningStats(data);
+            case RCS_ACS_PROVISIONING_STATS:
+                return pullRcsAcsProvisioningStats(data);
+            case SIP_DELEGATE_STATS:
+                return pullSipDelegateStats(data);
+            case SIP_TRANSPORT_FEATURE_TAG_STATS:
+                return pullSipTransportFeatureTagStats(data);
+            case SIP_MESSAGE_RESPONSE:
+                return pullSipMessageResponse(data);
+            case SIP_TRANSPORT_SESSION:
+                return pullSipTransportSession(data);
+            case IMS_DEDICATED_BEARER_LISTENER_EVENT:
+                return pullImsDedicatedBearerListenerEvent(data);
+            case IMS_DEDICATED_BEARER_EVENT:
+                return pullImsDedicatedBearerEvent(data);
+            case IMS_REGISTRATION_SERVICE_DESC_STATS:
+                return pullImsRegistrationServiceDescStats(data);
+            case UCE_EVENT_STATS:
+                return pullUceEventStats(data);
+            case PRESENCE_NOTIFY_EVENT:
+                return pullPresenceNotifyEvent(data);
+            case GBA_EVENT:
+                return pullGbaEvent(data);
             default:
                 Rlog.e(TAG, String.format("unexpected atom ID %d", atomTag));
                 return StatsManager.PULL_SKIP;
@@ -401,6 +466,179 @@
         }
     }
 
+    private int pullImsRegistrationFeatureTagStats(List<StatsEvent> data) {
+        RcsStats.getInstance().onFlushIncompleteImsRegistrationFeatureTagStats();
+
+        ImsRegistrationFeatureTagStats[] persistAtoms =
+                mStorage.getImsRegistrationFeatureTagStats(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "IMS_REGISTRATION_FEATURE_TAG_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullRcsClientProvisioningStats(List<StatsEvent> data) {
+        RcsClientProvisioningStats[] persistAtoms =
+                mStorage.getRcsClientProvisioningStats(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "RCS_CLIENT_PROVISIONING_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullRcsAcsProvisioningStats(List<StatsEvent> data) {
+        RcsStats.getInstance().onFlushIncompleteRcsAcsProvisioningStats();
+
+        RcsAcsProvisioningStats[] persistAtoms =
+                mStorage.getRcsAcsProvisioningStats(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "RCS_ACS_PROVISIONING_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullSipDelegateStats(List<StatsEvent> data) {
+        SipDelegateStats[] persisAtoms =
+                mStorage.getSipDelegateStats(MIN_COOLDOWN_MILLIS);
+        if (persisAtoms != null) {
+            Arrays.stream(persisAtoms)
+                    .forEach(persisAtom -> data.add(buildStatsEvent(persisAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "SIP_DELEGATE_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullSipTransportFeatureTagStats(List<StatsEvent> data) {
+        RcsStats.getInstance().concludeSipTransportFeatureTagsStat();
+
+        SipTransportFeatureTagStats[] persisAtoms =
+                mStorage.getSipTransportFeatureTagStats(MIN_COOLDOWN_MILLIS);
+        if (persisAtoms != null) {
+            Arrays.stream(persisAtoms)
+                    .forEach(persisAtom -> data.add(buildStatsEvent(persisAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "SIP_DELEGATE_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullSipMessageResponse(List<StatsEvent> data) {
+        SipMessageResponse[] persistAtoms =
+                mStorage.getSipMessageResponse(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "RCS_SIP_MESSAGE_RESPONSE pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullSipTransportSession(List<StatsEvent> data) {
+        SipTransportSession[] persistAtoms =
+                mStorage.getSipTransportSession(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "RCS_SIP_TRANSPORT_SESSION pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullImsDedicatedBearerListenerEvent(List<StatsEvent> data) {
+        ImsDedicatedBearerListenerEvent[] persistAtoms =
+            mStorage.getImsDedicatedBearerListenerEvent(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "IMS_DEDICATED_BEARER_LISTENER_EVENT pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullImsDedicatedBearerEvent(List<StatsEvent> data) {
+        ImsDedicatedBearerEvent[] persistAtoms =
+            mStorage.getImsDedicatedBearerEvent(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "IMS_DEDICATED_BEARER_EVENT pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullImsRegistrationServiceDescStats(List<StatsEvent> data) {
+        RcsStats.getInstance().onFlushIncompleteImsRegistrationServiceDescStats();
+        ImsRegistrationServiceDescStats[] persistAtoms =
+            mStorage.getImsRegistrationServiceDescStats(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "IMS_REGISTRATION_SERVICE_DESC_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullUceEventStats(List<StatsEvent> data) {
+        UceEventStats[] persistAtoms = mStorage.getUceEventStats(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "UCE_EVENT_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullPresenceNotifyEvent(List<StatsEvent> data) {
+        PresenceNotifyEvent[] persistAtoms = mStorage.getPresenceNotifyEvent(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "PRESENCE_NOTIFY_EVENT pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullGbaEvent(List<StatsEvent> data) {
+        GbaEvent[] persistAtoms = mStorage.getGbaEvent(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            Arrays.stream(persistAtoms)
+                .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "GBA_EVENT pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
     /** Registers a pulled atom ID {@code atomId} with optional {@code policy} for pulling. */
     private void registerAtom(int atomId, @Nullable StatsManager.PullAtomMetadata policy) {
         mStatsManager.setPullAtomCallback(atomId, policy, ConcurrentUtils.DIRECT_EXECUTOR, this);
@@ -582,6 +820,153 @@
                 networkRequests.enterpriseReleaseCount);
     }
 
+    private static StatsEvent buildStatsEvent(ImsRegistrationFeatureTagStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                IMS_REGISTRATION_FEATURE_TAG_STATS,
+                stats.carrierId,
+                stats.slotId,
+                stats.featureTagName,
+                stats.registrationTech,
+                (int) (round(stats.registeredMillis, DURATION_BUCKET_MILLIS) / SECOND_IN_MILLIS));
+    }
+
+    private static StatsEvent buildStatsEvent(RcsClientProvisioningStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                RCS_CLIENT_PROVISIONING_STATS,
+                stats.carrierId,
+                stats.slotId,
+                stats.event,
+                stats.count);
+    }
+
+    private static StatsEvent buildStatsEvent(RcsAcsProvisioningStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                RCS_ACS_PROVISIONING_STATS,
+                stats.carrierId,
+                stats.slotId,
+                stats.responseCode,
+                stats.responseType,
+                stats.isSingleRegistrationEnabled,
+                stats.count,
+                (int) (round(stats.stateTimerMillis, DURATION_BUCKET_MILLIS) / SECOND_IN_MILLIS));
+    }
+
+    private static StatsEvent buildStatsEvent(SipDelegateStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                SIP_DELEGATE_STATS,
+                stats.dimension,
+                stats.carrierId,
+                stats.slotId,
+                (int) (round(stats.uptimeMillis, DURATION_BUCKET_MILLIS) / SECOND_IN_MILLIS),
+                stats.destroyReason);
+    }
+
+    private static StatsEvent buildStatsEvent(SipTransportFeatureTagStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                SIP_TRANSPORT_FEATURE_TAG_STATS,
+                stats.carrierId,
+                stats.slotId,
+                stats.featureTagName,
+                stats.sipTransportDeniedReason,
+                stats.sipTransportDeregisteredReason,
+                (int) (round(stats.associatedMillis, DURATION_BUCKET_MILLIS) / SECOND_IN_MILLIS));
+    }
+
+    private static StatsEvent buildStatsEvent(SipMessageResponse stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                SIP_MESSAGE_RESPONSE,
+                stats.carrierId,
+                stats.slotId,
+                stats.sipMessageMethod,
+                stats.sipMessageResponse,
+                stats.sipMessageDirection,
+                stats.messageError,
+                stats.count);
+    }
+
+    private static StatsEvent buildStatsEvent(SipTransportSession stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                SIP_TRANSPORT_SESSION,
+                stats.carrierId,
+                stats.slotId,
+                stats.sessionMethod,
+                stats.sipMessageDirection,
+                stats.sipResponse,
+                stats.sessionCount,
+                stats.endedGracefullyCount);
+    }
+
+    private static StatsEvent buildStatsEvent(ImsDedicatedBearerListenerEvent stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                IMS_DEDICATED_BEARER_LISTENER_EVENT,
+                stats.carrierId,
+                stats.slotId,
+                stats.ratAtEnd,
+                stats.qci,
+                stats.dedicatedBearerEstablished,
+                stats.eventCount);
+    }
+
+    private static StatsEvent buildStatsEvent(ImsDedicatedBearerEvent stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                IMS_DEDICATED_BEARER_EVENT,
+                stats.carrierId,
+                stats.slotId,
+                stats.ratAtEnd,
+                stats.qci,
+                stats.bearerState,
+                stats.localConnectionInfoReceived,
+                stats.remoteConnectionInfoReceived,
+                stats.hasListeners,
+                stats.count);
+    }
+
+    private static StatsEvent buildStatsEvent(ImsRegistrationServiceDescStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                IMS_REGISTRATION_SERVICE_DESC_STATS,
+                stats.carrierId,
+                stats.slotId,
+                stats.serviceIdName,
+                stats.serviceIdVersion,
+                stats.registrationTech,
+                (int) (round(stats.publishedMillis, DURATION_BUCKET_MILLIS) / SECOND_IN_MILLIS));
+    }
+
+    private static StatsEvent buildStatsEvent(UceEventStats stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                UCE_EVENT_STATS,
+                stats.carrierId,
+                stats.slotId,
+                stats.type,
+                stats.successful,
+                stats.commandCode,
+                stats.networkResponse,
+                stats.count);
+    }
+
+    private static StatsEvent buildStatsEvent(PresenceNotifyEvent stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                PRESENCE_NOTIFY_EVENT,
+                stats.carrierId,
+                stats.slotId,
+                stats.reason,
+                stats.contentBodyReceived,
+                stats.rcsCapsCount,
+                stats.mmtelCapsCount,
+                stats.noCapsCount,
+                stats.count);
+    }
+
+    private static StatsEvent buildStatsEvent(GbaEvent stats) {
+        return TelephonyStatsLog.buildStatsEvent(
+                GBA_EVENT,
+                stats.carrierId,
+                stats.slotId,
+                stats.successful,
+                stats.failedReason,
+                stats.count);
+    }
+
     /** Returns all phones in {@link PhoneFactory}, or an empty array if phones not made yet. */
     private static Phone[] getPhonesIfAny() {
         try {
diff --git a/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java b/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
index 6bdceed..a93fb0e 100644
--- a/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
+++ b/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
@@ -28,12 +28,25 @@
 import com.android.internal.telephony.nano.PersistAtomsProto.CellularDataServiceSwitch;
 import com.android.internal.telephony.nano.PersistAtomsProto.CellularServiceState;
 import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.GbaEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerListenerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationServiceDescStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
 import com.android.internal.telephony.nano.PersistAtomsProto.IncomingSms;
 import com.android.internal.telephony.nano.PersistAtomsProto.NetworkRequests;
 import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms;
 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms;
+import com.android.internal.telephony.nano.PersistAtomsProto.PresenceNotifyEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsAcsProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsClientProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipDelegateStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipMessageResponse;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.UceEventStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
 import com.android.internal.util.ArrayUtils;
@@ -100,6 +113,45 @@
     /** Maximum number of IMS registration terminations to store between pulls. */
     private static final int MAX_NUM_IMS_REGISTRATION_TERMINATIONS = 10;
 
+    /** Maximum number of IMS Registration Feature Tags to store between pulls. */
+    private static final int MAX_NUM_IMS_REGISTRATION_FEATURE_STATS = 25;
+
+    /** Maximum number of RCS Client Provisioning to store between pulls. */
+    private static final int MAX_NUM_RCS_CLIENT_PROVISIONING_STATS = 10;
+
+    /** Maximum number of RCS Acs Provisioning to store between pulls. */
+    private static final int MAX_NUM_RCS_ACS_PROVISIONING_STATS = 10;
+
+    /** Maximum number of Sip Message Response to store between pulls. */
+    private static final int MAX_NUM_SIP_MESSAGE_RESPONSE_STATS = 25;
+
+    /** Maximum number of Sip Transport Session to store between pulls. */
+    private static final int MAX_NUM_SIP_TRANSPORT_SESSION_STATS = 25;
+
+    /** Maximum number of Sip Delegate to store between pulls. */
+    private static final int MAX_NUM_SIP_DELEGATE_STATS = 10;
+
+    /** Maximum number of Sip Transport Feature Tag to store between pulls. */
+    private static final int MAX_NUM_SIP_TRANSPORT_FEATURE_TAG_STATS = 25;
+
+    /** Maximum number of Dedicated Bearer Listener Event to store between pulls. */
+    private static final int MAX_NUM_DEDICATED_BEARER_LISTENER_EVENT_STATS = 10;
+
+    /** Maximum number of Dedicated Bearer Event to store between pulls. */
+    private static final int MAX_NUM_DEDICATED_BEARER_EVENT_STATS = 10;
+
+    /** Maximum number of IMS Registration Service Desc to store between pulls. */
+    private static final int MAX_NUM_IMS_REGISTRATION_SERVICE_DESC_STATS = 25;
+
+    /** Maximum number of UCE Event to store between pulls. */
+    private static final int MAX_NUM_UCE_EVENT_STATS = 25;
+
+    /** Maximum number of Presence Notify Event to store between pulls. */
+    private static final int MAX_NUM_PRESENCE_NOTIFY_EVENT_STATS = 50;
+
+    /** Maximum number of GBA Event to store between pulls. */
+    private static final int MAX_NUM_GBA_EVENT_STATS = 10;
+
     /** Stores persist atoms and persist states of the puller. */
     @VisibleForTesting protected final PersistAtoms mAtoms;
 
@@ -324,6 +376,178 @@
         saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
     }
 
+    /** Adds a new {@link ImsRegistrationFeatureTagStats} to the storage. */
+    public synchronized void addImsRegistrationFeatureTagStats(
+                ImsRegistrationFeatureTagStats stats) {
+        ImsRegistrationFeatureTagStats existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.registeredMillis += stats.registeredMillis;
+        } else {
+            mAtoms.imsRegistrationFeatureTagStats =
+                insertAtRandomPlace(mAtoms.imsRegistrationFeatureTagStats,
+                    stats, MAX_NUM_IMS_REGISTRATION_FEATURE_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link RcsClientProvisioningStats} to the storage. */
+    public synchronized void addRcsClientProvisioningStats(RcsClientProvisioningStats stats) {
+        RcsClientProvisioningStats existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.count += 1;
+        } else {
+            mAtoms.rcsClientProvisioningStats =
+                insertAtRandomPlace(mAtoms.rcsClientProvisioningStats, stats,
+                        MAX_NUM_RCS_CLIENT_PROVISIONING_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link RcsAcsProvisioningStats} to the storage. */
+    public synchronized void addRcsAcsProvisioningStats(RcsAcsProvisioningStats stats) {
+        RcsAcsProvisioningStats existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.count += 1;
+            existingStats.stateTimerMillis += stats.stateTimerMillis;
+        } else {
+            // prevent that wrong count from caller effects total count
+            stats.count = 1;
+            mAtoms.rcsAcsProvisioningStats =
+                insertAtRandomPlace(mAtoms.rcsAcsProvisioningStats, stats,
+                        MAX_NUM_RCS_ACS_PROVISIONING_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link SipDelegateStats} to the storage. */
+    public synchronized void addSipDelegateStats(SipDelegateStats stats) {
+        mAtoms.sipDelegateStats = insertAtRandomPlace(mAtoms.sipDelegateStats, stats,
+                MAX_NUM_SIP_DELEGATE_STATS);
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link SipTransportFeatureTagStats} to the storage. */
+    public synchronized void addSipTransportFeatureTagStats(SipTransportFeatureTagStats stats) {
+        SipTransportFeatureTagStats lastStat = find(stats);
+        if (lastStat != null) {
+            lastStat.associatedMillis += stats.associatedMillis;
+        } else {
+            mAtoms.sipTransportFeatureTagStats =
+                    insertAtRandomPlace(mAtoms.sipTransportFeatureTagStats, stats,
+                            MAX_NUM_SIP_TRANSPORT_FEATURE_TAG_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link SipMessageResponse} to the storage. */
+    public synchronized void addSipMessageResponse(SipMessageResponse stats) {
+        SipMessageResponse existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.count += 1;
+        } else {
+            mAtoms.sipMessageResponse = insertAtRandomPlace(mAtoms.sipMessageResponse, stats,
+                    MAX_NUM_SIP_MESSAGE_RESPONSE_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link SipTransportSession} to the storage. */
+    public synchronized void addCompleteSipTransportSession(SipTransportSession stats) {
+        SipTransportSession existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.sessionCount += 1;
+            if (stats.isEndedGracefully) {
+                existingStats.endedGracefullyCount += 1;
+            }
+        } else {
+            mAtoms.sipTransportSession =
+                    insertAtRandomPlace(mAtoms.sipTransportSession, stats,
+                            MAX_NUM_SIP_TRANSPORT_SESSION_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link ImsDedicatedBearerListenerEvent} to the storage. */
+    public synchronized void addImsDedicatedBearerListenerEvent(
+                ImsDedicatedBearerListenerEvent stats) {
+        ImsDedicatedBearerListenerEvent existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.eventCount += 1;
+        } else {
+            mAtoms.imsDedicatedBearerListenerEvent =
+                insertAtRandomPlace(mAtoms.imsDedicatedBearerListenerEvent,
+                    stats, MAX_NUM_DEDICATED_BEARER_LISTENER_EVENT_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link ImsDedicatedBearerEvent} to the storage. */
+    public synchronized void addImsDedicatedBearerEvent(ImsDedicatedBearerEvent stats) {
+        ImsDedicatedBearerEvent existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.count += 1;
+        } else {
+            mAtoms.imsDedicatedBearerEvent =
+                insertAtRandomPlace(mAtoms.imsDedicatedBearerEvent, stats,
+                        MAX_NUM_DEDICATED_BEARER_EVENT_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link ImsRegistrationServiceDescStats} to the storage. */
+    public synchronized void addImsRegistrationServiceDescStats(
+            ImsRegistrationServiceDescStats stats) {
+        ImsRegistrationServiceDescStats existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.publishedMillis += stats.publishedMillis;
+        } else {
+            mAtoms.imsRegistrationServiceDescStats =
+                insertAtRandomPlace(mAtoms.imsRegistrationServiceDescStats,
+                    stats, MAX_NUM_IMS_REGISTRATION_SERVICE_DESC_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link UceEventStats} to the storage. */
+    public synchronized void addUceEventStats(UceEventStats stats) {
+        UceEventStats existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.count += 1;
+        } else {
+            mAtoms.uceEventStats =
+                insertAtRandomPlace(mAtoms.uceEventStats, stats, MAX_NUM_UCE_EVENT_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link PresenceNotifyEvent} to the storage. */
+    public synchronized void addPresenceNotifyEvent(PresenceNotifyEvent stats) {
+        PresenceNotifyEvent existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.rcsCapsCount += stats.rcsCapsCount;
+            existingStats.mmtelCapsCount += stats.mmtelCapsCount;
+            existingStats.noCapsCount += stats.noCapsCount;
+            existingStats.count += stats.count;
+        } else {
+            mAtoms.presenceNotifyEvent =
+                insertAtRandomPlace(mAtoms.presenceNotifyEvent, stats,
+                        MAX_NUM_PRESENCE_NOTIFY_EVENT_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
+    /** Adds a new {@link GbaEvent} to the storage. */
+    public synchronized void addGbaEvent(GbaEvent stats) {
+        GbaEvent existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.count += 1;
+        } else {
+            mAtoms.gbaEvent =
+                insertAtRandomPlace(mAtoms.gbaEvent, stats, MAX_NUM_GBA_EVENT_STATS);
+        }
+        saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
+    }
+
     /**
      * Returns and clears the voice call sessions if last pulled longer than {@code
      * minIntervalMillis} ago, otherwise returns {@code null}.
@@ -507,6 +731,251 @@
         }
     }
 
+    /**
+     * Returns and clears the ImsRegistrationFeatureTagStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized ImsRegistrationFeatureTagStats[] getImsRegistrationFeatureTagStats(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.imsRegistrationFeatureTagStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.imsRegistrationFeatureTagStatsPullTimestampMillis = getWallTimeMillis();
+            ImsRegistrationFeatureTagStats[] previousStats =
+                    mAtoms.imsRegistrationFeatureTagStats;
+            mAtoms.imsRegistrationFeatureTagStats = new ImsRegistrationFeatureTagStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the RcsClientProvisioningStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized RcsClientProvisioningStats[] getRcsClientProvisioningStats(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.rcsClientProvisioningStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.rcsClientProvisioningStatsPullTimestampMillis = getWallTimeMillis();
+            RcsClientProvisioningStats[] previousStats = mAtoms.rcsClientProvisioningStats;
+            mAtoms.rcsClientProvisioningStats = new RcsClientProvisioningStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the RcsAcsProvisioningStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized RcsAcsProvisioningStats[] getRcsAcsProvisioningStats(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.rcsAcsProvisioningStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.rcsAcsProvisioningStatsPullTimestampMillis = getWallTimeMillis();
+            RcsAcsProvisioningStats[] previousStats = mAtoms.rcsAcsProvisioningStats;
+            mAtoms.rcsAcsProvisioningStats = new RcsAcsProvisioningStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the SipDelegateStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized SipDelegateStats[] getSipDelegateStats(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.sipDelegateStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.sipDelegateStatsPullTimestampMillis = getWallTimeMillis();
+            SipDelegateStats[] previousStats = mAtoms.sipDelegateStats;
+            mAtoms.sipDelegateStats = new SipDelegateStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the SipTransportFeatureTagStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized SipTransportFeatureTagStats[] getSipTransportFeatureTagStats(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.sipTransportFeatureTagStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.sipTransportFeatureTagStatsPullTimestampMillis = getWallTimeMillis();
+            SipTransportFeatureTagStats[] previousStats = mAtoms.sipTransportFeatureTagStats;
+            mAtoms.sipTransportFeatureTagStats = new SipTransportFeatureTagStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the SipMessageResponse if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized SipMessageResponse[] getSipMessageResponse(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.sipMessageResponsePullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.sipMessageResponsePullTimestampMillis = getWallTimeMillis();
+            SipMessageResponse[] previousStats =
+                    mAtoms.sipMessageResponse;
+            mAtoms.sipMessageResponse = new SipMessageResponse[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the SipTransportSession if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized SipTransportSession[] getSipTransportSession(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.sipTransportSessionPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.sipTransportSessionPullTimestampMillis = getWallTimeMillis();
+            SipTransportSession[] previousStats =
+                    mAtoms.sipTransportSession;
+            mAtoms.sipTransportSession = new SipTransportSession[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the ImsDedicatedBearerListenerEvent if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized ImsDedicatedBearerListenerEvent[] getImsDedicatedBearerListenerEvent(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.imsDedicatedBearerListenerEventPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.imsDedicatedBearerListenerEventPullTimestampMillis = getWallTimeMillis();
+            ImsDedicatedBearerListenerEvent[] previousStats =
+                mAtoms.imsDedicatedBearerListenerEvent;
+            mAtoms.imsDedicatedBearerListenerEvent = new ImsDedicatedBearerListenerEvent[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the ImsDedicatedBearerEvent if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized ImsDedicatedBearerEvent[] getImsDedicatedBearerEvent(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.imsDedicatedBearerEventPullTimestampMillis
+                  > minIntervalMillis) {
+            mAtoms.imsDedicatedBearerEventPullTimestampMillis = getWallTimeMillis();
+            ImsDedicatedBearerEvent[] previousStats =
+                mAtoms.imsDedicatedBearerEvent;
+            mAtoms.imsDedicatedBearerEvent = new ImsDedicatedBearerEvent[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the ImsRegistrationServiceDescStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized ImsRegistrationServiceDescStats[] getImsRegistrationServiceDescStats(long
+            minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.imsRegistrationServiceDescStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.imsRegistrationServiceDescStatsPullTimestampMillis = getWallTimeMillis();
+            ImsRegistrationServiceDescStats[] previousStats =
+                mAtoms.imsRegistrationServiceDescStats;
+            mAtoms.imsRegistrationServiceDescStats = new ImsRegistrationServiceDescStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the UceEventStats if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized UceEventStats[] getUceEventStats(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.uceEventStatsPullTimestampMillis > minIntervalMillis) {
+            mAtoms.uceEventStatsPullTimestampMillis = getWallTimeMillis();
+            UceEventStats[] previousStats = mAtoms.uceEventStats;
+            mAtoms.uceEventStats = new UceEventStats[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the PresenceNotifyEvent if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized PresenceNotifyEvent[] getPresenceNotifyEvent(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.presenceNotifyEventPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.presenceNotifyEventPullTimestampMillis = getWallTimeMillis();
+            PresenceNotifyEvent[] previousStats = mAtoms.presenceNotifyEvent;
+            mAtoms.presenceNotifyEvent = new PresenceNotifyEvent[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the GbaEvent if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized GbaEvent[] getGbaEvent(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.gbaEventPullTimestampMillis > minIntervalMillis) {
+            mAtoms.gbaEventPullTimestampMillis = getWallTimeMillis();
+            GbaEvent[] previousStats = mAtoms.gbaEvent;
+            mAtoms.gbaEvent = new GbaEvent[0];
+            saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
     /** Loads {@link PersistAtoms} from a file in private storage. */
     private PersistAtoms loadAtomsFromFile() {
         try {
@@ -558,6 +1027,72 @@
                             ImsRegistrationTermination.class,
                             MAX_NUM_IMS_REGISTRATION_TERMINATIONS);
             atoms.networkRequests = sanitizeAtoms(atoms.networkRequests, NetworkRequests.class);
+            atoms.imsRegistrationFeatureTagStats =
+                    sanitizeAtoms(
+                            atoms.imsRegistrationFeatureTagStats,
+                            ImsRegistrationFeatureTagStats.class,
+                            MAX_NUM_IMS_REGISTRATION_FEATURE_STATS);
+            atoms.rcsClientProvisioningStats =
+                    sanitizeAtoms(
+                            atoms.rcsClientProvisioningStats,
+                            RcsClientProvisioningStats.class,
+                            MAX_NUM_RCS_CLIENT_PROVISIONING_STATS);
+            atoms.rcsAcsProvisioningStats =
+                    sanitizeAtoms(
+                            atoms.rcsAcsProvisioningStats,
+                            RcsAcsProvisioningStats.class,
+                            MAX_NUM_RCS_ACS_PROVISIONING_STATS);
+            atoms.sipDelegateStats =
+                    sanitizeAtoms(
+                            atoms.sipDelegateStats,
+                            SipDelegateStats.class,
+                            MAX_NUM_SIP_DELEGATE_STATS);
+            atoms.sipTransportFeatureTagStats =
+                    sanitizeAtoms(
+                            atoms.sipTransportFeatureTagStats,
+                            SipTransportFeatureTagStats.class,
+                            MAX_NUM_SIP_TRANSPORT_FEATURE_TAG_STATS);
+            atoms.sipMessageResponse =
+                    sanitizeAtoms(
+                            atoms.sipMessageResponse,
+                            SipMessageResponse.class,
+                            MAX_NUM_SIP_MESSAGE_RESPONSE_STATS);
+            atoms.sipTransportSession =
+                    sanitizeAtoms(
+                            atoms.sipTransportSession,
+                            SipTransportSession.class,
+                            MAX_NUM_SIP_TRANSPORT_SESSION_STATS);
+            atoms.imsDedicatedBearerListenerEvent =
+                    sanitizeAtoms(
+                            atoms.imsDedicatedBearerListenerEvent,
+                            ImsDedicatedBearerListenerEvent.class,
+                            MAX_NUM_DEDICATED_BEARER_LISTENER_EVENT_STATS);
+            atoms.imsDedicatedBearerEvent =
+                    sanitizeAtoms(
+                            atoms.imsDedicatedBearerEvent,
+                            ImsDedicatedBearerEvent.class,
+                            MAX_NUM_DEDICATED_BEARER_EVENT_STATS);
+            atoms.imsRegistrationServiceDescStats =
+                    sanitizeAtoms(
+                            atoms.imsRegistrationServiceDescStats,
+                            ImsRegistrationServiceDescStats.class,
+                            MAX_NUM_IMS_REGISTRATION_SERVICE_DESC_STATS);
+            atoms.uceEventStats =
+                    sanitizeAtoms(
+                            atoms.uceEventStats,
+                            UceEventStats.class,
+                            MAX_NUM_UCE_EVENT_STATS);
+            atoms.presenceNotifyEvent =
+                    sanitizeAtoms(
+                            atoms.presenceNotifyEvent,
+                            PresenceNotifyEvent.class,
+                            MAX_NUM_PRESENCE_NOTIFY_EVENT_STATS);
+            atoms.gbaEvent =
+                    sanitizeAtoms(
+                            atoms.gbaEvent,
+                            GbaEvent.class,
+                            MAX_NUM_GBA_EVENT_STATS);
+
             // out of caution, sanitize also the timestamps
             atoms.voiceCallRatUsagePullTimestampMillis =
                     sanitizeTimestamp(atoms.voiceCallRatUsagePullTimestampMillis);
@@ -579,6 +1114,33 @@
                     sanitizeTimestamp(atoms.imsRegistrationTerminationPullTimestampMillis);
             atoms.networkRequestsPullTimestampMillis =
                     sanitizeTimestamp(atoms.networkRequestsPullTimestampMillis);
+            atoms.imsRegistrationFeatureTagStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.imsRegistrationFeatureTagStatsPullTimestampMillis);
+            atoms.rcsClientProvisioningStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.rcsClientProvisioningStatsPullTimestampMillis);
+            atoms.rcsAcsProvisioningStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.rcsAcsProvisioningStatsPullTimestampMillis);
+            atoms.sipDelegateStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.sipDelegateStatsPullTimestampMillis);
+            atoms.sipTransportFeatureTagStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.sipTransportFeatureTagStatsPullTimestampMillis);
+            atoms.sipMessageResponsePullTimestampMillis =
+                    sanitizeTimestamp(atoms.sipMessageResponsePullTimestampMillis);
+            atoms.sipTransportSessionPullTimestampMillis =
+                    sanitizeTimestamp(atoms.sipTransportSessionPullTimestampMillis);
+            atoms.imsDedicatedBearerListenerEventPullTimestampMillis =
+                    sanitizeTimestamp(atoms.imsDedicatedBearerListenerEventPullTimestampMillis);
+            atoms.imsDedicatedBearerEventPullTimestampMillis =
+                    sanitizeTimestamp(atoms.imsDedicatedBearerEventPullTimestampMillis);
+            atoms.imsRegistrationServiceDescStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.imsRegistrationServiceDescStatsPullTimestampMillis);
+            atoms.uceEventStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.uceEventStatsPullTimestampMillis);
+            atoms.presenceNotifyEventPullTimestampMillis =
+                    sanitizeTimestamp(atoms.presenceNotifyEventPullTimestampMillis);
+            atoms.gbaEventPullTimestampMillis =
+                    sanitizeTimestamp(atoms.gbaEventPullTimestampMillis);
+
             return atoms;
         } catch (NoSuchFileException e) {
             Rlog.d(TAG, "PersistAtoms file not found");
@@ -726,6 +1288,213 @@
         }
         return -1;
     }
+    /**
+     * Returns the Dedicated Bearer Listener event that has the same carrier id, slot id, rat, qci
+     * and established state as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable ImsDedicatedBearerListenerEvent find(ImsDedicatedBearerListenerEvent key) {
+        for (ImsDedicatedBearerListenerEvent stats : mAtoms.imsDedicatedBearerListenerEvent) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.ratAtEnd == key.ratAtEnd
+                    && stats.qci == key.qci
+                    && stats.dedicatedBearerEstablished == key.dedicatedBearerEstablished) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the Dedicated Bearer event that has the same carrier id, slot id, rat,
+     * qci, bearer state, local/remote connection and exsting listener as the given one,
+     * or {@code null} if it does not exist.
+     */
+    private @Nullable ImsDedicatedBearerEvent find(ImsDedicatedBearerEvent key) {
+        for (ImsDedicatedBearerEvent stats : mAtoms.imsDedicatedBearerEvent) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.ratAtEnd == key.ratAtEnd
+                    && stats.qci == key.qci
+                    && stats.bearerState == key.bearerState
+                    && stats.localConnectionInfoReceived == key.localConnectionInfoReceived
+                    && stats.remoteConnectionInfoReceived == key.remoteConnectionInfoReceived
+                    && stats.hasListeners == key.hasListeners) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the Registration Feature Tag that has the same carrier id, slot id,
+     * feature tag name or custom feature tag name and registration tech as the given one,
+     * or {@code null} if it does not exist.
+     */
+    private @Nullable ImsRegistrationFeatureTagStats find(ImsRegistrationFeatureTagStats key) {
+        for (ImsRegistrationFeatureTagStats stats : mAtoms.imsRegistrationFeatureTagStats) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.featureTagName == key.featureTagName
+                    && stats.registrationTech == key.registrationTech) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns Client Provisioning that has the same carrier id, slot id and event as the given
+     * one, or {@code null} if it does not exist.
+     */
+    private @Nullable RcsClientProvisioningStats find(RcsClientProvisioningStats key) {
+        for (RcsClientProvisioningStats stats : mAtoms.rcsClientProvisioningStats) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.event == key.event) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns ACS Provisioning that has the same carrier id, slot id, response code, response type
+     * and SR supported as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable RcsAcsProvisioningStats find(RcsAcsProvisioningStats key) {
+        for (RcsAcsProvisioningStats stats : mAtoms.rcsAcsProvisioningStats) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.responseCode == key.responseCode
+                    && stats.responseType == key.responseType
+                    && stats.isSingleRegistrationEnabled == key.isSingleRegistrationEnabled) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns Sip Message Response that has the same carrier id, slot id, method, response,
+     * direction and error as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable SipMessageResponse find(SipMessageResponse key) {
+        for (SipMessageResponse stats : mAtoms.sipMessageResponse) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.sipMessageMethod == key.sipMessageMethod
+                    && stats.sipMessageResponse == key.sipMessageResponse
+                    && stats.sipMessageDirection == key.sipMessageDirection
+                    && stats.messageError == key.messageError) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns Sip Transport Session that has the same carrier id, slot id, method, direction and
+     * response as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable SipTransportSession find(SipTransportSession key) {
+        for (SipTransportSession stats : mAtoms.sipTransportSession) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.sessionMethod == key.sessionMethod
+                    && stats.sipMessageDirection == key.sipMessageDirection
+                    && stats.sipResponse == key.sipResponse) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns Registration Service Desc Stats that has the same carrier id, slot id, service id or
+     * custom service id, service id version and registration tech as the given one,
+     * or {@code null} if it does not exist.
+     */
+    private @Nullable ImsRegistrationServiceDescStats find(ImsRegistrationServiceDescStats key) {
+        for (ImsRegistrationServiceDescStats stats : mAtoms.imsRegistrationServiceDescStats) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.serviceIdName == key.serviceIdName
+                    && stats.serviceIdVersion == key.serviceIdVersion
+                    && stats.registrationTech == key.registrationTech) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns UCE Event Stats that has the same carrier id, slot id, event result, command code and
+     * network response as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable UceEventStats find(UceEventStats key) {
+        for (UceEventStats stats : mAtoms.uceEventStats) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.type == key.type
+                    && stats.successful == key.successful
+                    && stats.commandCode == key.commandCode
+                    && stats.networkResponse == key.networkResponse) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns Presence Notify Event that has the same carrier id, slot id, reason and body in
+     * response as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable PresenceNotifyEvent find(PresenceNotifyEvent key) {
+        for (PresenceNotifyEvent stats : mAtoms.presenceNotifyEvent) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.reason == key.reason
+                    && stats.contentBodyReceived == key.contentBodyReceived) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns GBA Event that has the same carrier id, slot id, result of operation and fail reason
+     * as the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable GbaEvent find(GbaEvent key) {
+        for (GbaEvent stats : mAtoms.gbaEvent) {
+            if (stats.carrierId == key.carrierId
+                    && stats.slotId == key.slotId
+                    && stats.successful == key.successful
+                    && stats.failedReason == key.failedReason) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns Sip Transport Feature Tag Stats that has the same carrier id, slot id, feature tag
+     * name, deregister reason, denied reason and feature tag name or custom feature tag name as
+     * the given one, or {@code null} if it does not exist.
+     */
+    private @Nullable SipTransportFeatureTagStats find(SipTransportFeatureTagStats key) {
+        for (SipTransportFeatureTagStats stat: mAtoms.sipTransportFeatureTagStats) {
+            if (stat.carrierId == key.carrierId
+                    && stat.slotId == key.slotId
+                    && stat.featureTagName == key.featureTagName
+                    && stat.sipTransportDeregisteredReason == key.sipTransportDeregisteredReason
+                    && stat.sipTransportDeniedReason == key.sipTransportDeniedReason) {
+                return stat;
+            }
+        }
+        return null;
+    }
 
     /**
      * Inserts a new element in a random position in an array with a maximum size, replacing the
@@ -817,6 +1586,20 @@
         atoms.imsRegistrationStatsPullTimestampMillis = currentTime;
         atoms.imsRegistrationTerminationPullTimestampMillis = currentTime;
         atoms.networkRequestsPullTimestampMillis = currentTime;
+        atoms.imsRegistrationFeatureTagStatsPullTimestampMillis = currentTime;
+        atoms.rcsClientProvisioningStatsPullTimestampMillis = currentTime;
+        atoms.rcsAcsProvisioningStatsPullTimestampMillis = currentTime;
+        atoms.sipDelegateStatsPullTimestampMillis = currentTime;
+        atoms.sipTransportFeatureTagStatsPullTimestampMillis = currentTime;
+        atoms.sipMessageResponsePullTimestampMillis = currentTime;
+        atoms.sipTransportSessionPullTimestampMillis = currentTime;
+        atoms.imsDedicatedBearerListenerEventPullTimestampMillis = currentTime;
+        atoms.imsDedicatedBearerEventPullTimestampMillis = currentTime;
+        atoms.imsRegistrationServiceDescStatsPullTimestampMillis = currentTime;
+        atoms.uceEventStatsPullTimestampMillis = currentTime;
+        atoms.presenceNotifyEventPullTimestampMillis = currentTime;
+        atoms.gbaEventPullTimestampMillis = currentTime;
+
         Rlog.d(TAG, "created new PersistAtoms");
         return atoms;
     }
diff --git a/src/java/com/android/internal/telephony/metrics/RcsStats.java b/src/java/com/android/internal/telephony/metrics/RcsStats.java
new file mode 100644
index 0000000..de77b12
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/RcsStats.java
@@ -0,0 +1,1860 @@
+/*
+ * 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.internal.telephony.metrics;
+
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CALL_COMPOSER;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHATBOT;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHATBOT_ROLE;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHATBOT_STANDALONE;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHAT_V1;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHAT_V2;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CUSTOM;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_FT;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_FT_OVER_SMS;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_GEO_PUSH;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_GEO_PUSH_VIA_SMS;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_MMTEL;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_POST_CALL;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_SHARED_MAP;
+import static com.android.internal.telephony.TelephonyStatsLog.IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_SHARED_SKETCH;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_CUSTOM;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_DEACTIVATED;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_GIVEUP;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_NORESOURCE;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_PROBATION;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_REJECTED;
+import static com.android.internal.telephony.TelephonyStatsLog.PRESENCE_NOTIFY_EVENT__REASON__REASON_TIMEOUT;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PRE_PROVISIONING_XML;
+import static com.android.internal.telephony.TelephonyStatsLog.UCE_EVENT_STATS__TYPE__INCOMING_OPTION;
+import static com.android.internal.telephony.TelephonyStatsLog.UCE_EVENT_STATS__TYPE__OUTGOING_OPTION;
+import static com.android.internal.telephony.TelephonyStatsLog.UCE_EVENT_STATS__TYPE__PUBLISH;
+import static com.android.internal.telephony.TelephonyStatsLog.UCE_EVENT_STATS__TYPE__SUBSCRIBE;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.TelephonyProtoEnums;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsConfigCallback;
+import android.util.Base64;
+import android.util.IndentingPrintWriter;
+
+import com.android.ims.rcs.uce.UceStatsWriter;
+import com.android.ims.rcs.uce.UceStatsWriter.UceStatsCallback;
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.nano.PersistAtomsProto;
+import com.android.internal.telephony.nano.PersistAtomsProto.GbaEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerListenerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationServiceDescStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.PresenceNotifyEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsAcsProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsClientProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipDelegateStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipMessageResponse;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.UceEventStats;
+import com.android.telephony.Rlog;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+/** Tracks RCS provisioning, sip transport, UCE metrics for phone. */
+public class RcsStats {
+    private static final String TAG = RcsStats.class.getSimpleName();
+    private static final long MIN_DURATION_MILLIS = 1L * SECOND_IN_MILLIS;
+    private final PersistAtomsStorage mAtomsStorage =
+            PhoneFactory.getMetricsCollector().getAtomsStorage();
+    private static final Random RANDOM = new Random();
+
+    private UceStatsWriterCallback mCallback;
+    private static RcsStats sInstance;
+
+    public static final int NONE = -1;
+    public static final int STATE_REGISTERED = 0;
+    public static final int STATE_DEREGISTERED = 1;
+    public static final int STATE_DENIED = 2;
+
+    private static final String SIP_REQUEST_MESSAGE_TYPE_INVITE = "INVITE";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_ACK = "ACK";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_OPTIONS = "OPTIONS";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_BYE = "BYE";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_CANCEL = "CANCEL";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_REGISTER = "REGISTER";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_PRACK = "PRACK";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_SUBSCRIBE = "SUBSCRIBE";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_NOTIFY = "NOTIFY";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_PUBLISH = "PUBLISH";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_INFO = "INFO";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_REFER = "REFER";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_MESSAGE = "MESSAGE";
+    private static final String SIP_REQUEST_MESSAGE_TYPE_UPDATE = "UPDATE";
+
+    /**
+     * Describe Feature Tags
+     * See frameworks/opt/net/ims/src/java/com/android/ims/rcs/uce/util/FeatureTags.java
+     * and int value matching the Feature Tags
+     * See stats/enums/telephony/enums.proto
+     */
+    private static final Map<String, Integer> FEATURE_TAGS = new HashMap<>();
+
+    static {
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_STANDALONE_MSG.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_STANDALONE_MSG);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_CHAT_IM.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CHAT_IM);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_CHAT_SESSION.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CHAT_SESSION);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_FILE_TRANSFER.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_FILE_TRANSFER);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_FILE_TRANSFER_VIA_SMS);
+        FEATURE_TAGS.put(
+                FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING);
+        FEATURE_TAGS.put(
+                FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_POST_CALL.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_POST_CALL);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_SHARED_MAP.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_SHARED_MAP);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_SHARED_SKETCH.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_SHARED_SKETCH);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_GEO_PUSH.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_GEO_PUSH);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_GEO_PUSH_VIA_SMS.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_GEO_PUSH_VIA_SMS);
+        FEATURE_TAGS.put(
+                FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION);
+        String FeatureTag = FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG;
+        FEATURE_TAGS.put(FeatureTag.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG);
+        FEATURE_TAGS.put(
+                FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CHATBOT_VERSION_SUPPORTED);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_CHATBOT_ROLE.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CHATBOT_ROLE);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_MMTEL.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_MMTEL);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_VIDEO.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_VIDEO);
+        FEATURE_TAGS.put(FeatureTags.FEATURE_TAG_PRESENCE.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_PRESENCE);
+    }
+
+    /**
+     * Describe Service IDs
+     * See frameworks/base/telephony/java/android/telephony/ims/RcsContactPresenceTuple.java
+     * and int value matching the service IDs
+     * See frameworks/proto_logging/stats/atoms.proto
+     */
+    private static final Map<String, Integer> SERVICE_IDS = new HashMap<>();
+
+    static {
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_MMTEL.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_MMTEL);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_CHAT_V1.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHAT_V1);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_CHAT_V2.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHAT_V2);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_FT.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_FT);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_FT_OVER_SMS.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_FT_OVER_SMS);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_GEO_PUSH);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH_VIA_SMS.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_GEO_PUSH_VIA_SMS);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CALL_COMPOSER);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_POST_CALL.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_POST_CALL);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_SHARED_MAP.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_SHARED_MAP);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_SHARED_SKETCH.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_SHARED_SKETCH);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_CHATBOT.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHATBOT);
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_CHATBOT_STANDALONE.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHATBOT_STANDALONE
+        );
+        SERVICE_IDS.put(RcsContactPresenceTuple.SERVICE_ID_CHATBOT_ROLE.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CHATBOT_ROLE);
+    }
+
+    /**
+     * Describe Message Method Type
+     * See stats/enums/telephony/enums.proto
+     */
+    private static final Map<String, Integer> MESSAGE_TYPE = new HashMap<>();
+
+    static {
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_INVITE.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_INVITE);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_ACK.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_ACK);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_OPTIONS.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_OPTIONS);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_BYE.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_BYE);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_CANCEL.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_CANCEL);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_REGISTER.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_REGISTER);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_PRACK.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_PRACK);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_SUBSCRIBE.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_SUBSCRIBE);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_NOTIFY.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_NOTIFY);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_PUBLISH.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_PUBLISH);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_INFO.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_INFO);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_REFER.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_REFER);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_MESSAGE.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_MESSAGE);
+        MESSAGE_TYPE.put(SIP_REQUEST_MESSAGE_TYPE_UPDATE.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_UPDATE);
+    }
+
+    /**
+     * Describe Reasons
+     * See frameworks/opt/net/ims/src/java/com/android/ims/rcs/uce/request/
+     * SubscriptionTerminatedHelper.java
+     * and int value matching the Reasons
+     * See frameworks/proto_logging/stats/atoms.proto
+     */
+    private static final Map<String, Integer> NOTIFY_REASONS = new HashMap<>();
+
+    static {
+        NOTIFY_REASONS.put("deactivated", PRESENCE_NOTIFY_EVENT__REASON__REASON_DEACTIVATED);
+        NOTIFY_REASONS.put("probation", PRESENCE_NOTIFY_EVENT__REASON__REASON_PROBATION);
+        NOTIFY_REASONS.put("rejected", PRESENCE_NOTIFY_EVENT__REASON__REASON_REJECTED);
+        NOTIFY_REASONS.put("timeout", PRESENCE_NOTIFY_EVENT__REASON__REASON_TIMEOUT);
+        NOTIFY_REASONS.put("giveup", PRESENCE_NOTIFY_EVENT__REASON__REASON_GIVEUP);
+        NOTIFY_REASONS.put("noresource", PRESENCE_NOTIFY_EVENT__REASON__REASON_NORESOURCE);
+    }
+
+    /**
+     * Describe Rcs Capability set
+     * See frameworks/base/telephony/java/android/telephony/ims/RcsContactPresenceTuple.java
+     */
+    private static final HashSet<String> RCS_SERVICE_ID_SET = new HashSet<>();
+    static {
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_CHAT_V1);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_CHAT_V2);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_FT);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_FT_OVER_SMS);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH_VIA_SMS);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_SHARED_MAP);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_SHARED_SKETCH);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_CHATBOT);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_CHATBOT_STANDALONE);
+        RCS_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_CHATBOT_ROLE);
+    }
+
+    /**
+     * Describe Mmtel Capability set
+     * See frameworks/base/telephony/java/android/telephony/ims/RcsContactPresenceTuple.java
+     */
+    private static final HashSet<String> MMTEL_SERVICE_ID_SET = new HashSet<>();
+    static {
+        MMTEL_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_MMTEL);
+        MMTEL_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER);
+        MMTEL_SERVICE_ID_SET.add(RcsContactPresenceTuple.SERVICE_ID_POST_CALL);
+    }
+
+    private static final Map<Long, Integer> sSubscribeTaskIds = new HashMap<>();
+    private static final int SUBSCRIBE_SUCCESS = 1;
+    private static final int SUBSCRIBE_NOTIFY = 2;
+
+    @VisibleForTesting
+    protected final Map<Integer, ImsDedicatedBearerListenerEvent> mDedicatedBearerListenerEventMap =
+            new HashMap<>();
+    @VisibleForTesting
+    protected final List<RcsAcsProvisioningStats> mRcsAcsProvisioningStatsList =
+            new ArrayList<RcsAcsProvisioningStats>();
+    @VisibleForTesting
+    protected final HashMap<Integer, RcsProvisioningCallback> mRcsProvisioningCallbackMap =
+            new HashMap<>();
+
+    // Maps feature tag name -> ImsRegistrationFeatureTagStats.
+    private final List<ImsRegistrationFeatureTagStats> mImsRegistrationFeatureTagStatsList =
+            new ArrayList<>();
+
+    // Maps service id -> ImsRegistrationServiceDescStats.
+    @VisibleForTesting
+    protected final List<ImsRegistrationServiceDescStats> mImsRegistrationServiceDescStatsList =
+            new ArrayList<>();
+
+    private List<LastSipDelegateStat> mLastSipDelegateStatList = new ArrayList<>();
+    private HashMap<Integer, SipTransportFeatureTags> mLastFeatureTagStatMap = new HashMap<>();
+    private ArrayList<SipMessageArray> mSipMessageArray = new ArrayList<>();
+    private ArrayList<SipTransportSessionArray> mSipTransportSessionArray = new ArrayList<>();
+    private SipTransportSessionArray mSipTransportSession;
+    private SipMessageArray mSipMessage;
+
+    private class LastSipDelegateStat {
+        public int mSubId;
+        public SipDelegateStats mLastStat;
+        private Set<String> mSupportedTags;
+
+        LastSipDelegateStat(int subId, Set<String> supportedTags) {
+            mSubId = subId;
+            mSupportedTags = supportedTags;
+        }
+
+        public void createSipDelegateStat(int subId) {
+            mLastStat = getDefaultSipDelegateStat(subId);
+            mLastStat.uptimeMillis = getWallTimeMillis();
+            mLastStat.destroyReason = NONE;
+        }
+
+        public void setSipDelegateDestroyReason(int destroyReason) {
+            mLastStat.destroyReason = destroyReason;
+        }
+
+        public boolean isDestroyed() {
+            return mLastStat.destroyReason > NONE;
+        }
+
+        public void conclude(long now) {
+            long duration = now - mLastStat.uptimeMillis;
+            if (duration < MIN_DURATION_MILLIS) {
+                logd("concludeSipDelegateStat: discarding transient stats,"
+                        + " duration= " + duration);
+            } else {
+                mLastStat.uptimeMillis = duration;
+                mAtomsStorage.addSipDelegateStats(copyOf(mLastStat));
+            }
+            mLastStat.uptimeMillis = now;
+        }
+
+        public boolean compare(int subId, Set<String> supportedTags) {
+            if (subId != mSubId || supportedTags == null || supportedTags.isEmpty()) {
+                return false;
+            }
+            for (String tag : supportedTags) {
+                if (!mSupportedTags.contains(tag)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private SipDelegateStats getDefaultSipDelegateStat(int subId) {
+            SipDelegateStats stat = new SipDelegateStats();
+            stat.dimension = RANDOM.nextInt();
+            stat.carrierId = getCarrierId(subId);
+            stat.slotId = getSlotId(subId);
+            return stat;
+        }
+    }
+
+    private static SipDelegateStats copyOf(@NonNull SipDelegateStats source) {
+        SipDelegateStats newStat = new SipDelegateStats();
+
+        newStat.dimension = source.dimension;
+        newStat.slotId = source.slotId;
+        newStat.carrierId = source.carrierId;
+        newStat.destroyReason = source.destroyReason;
+        newStat.uptimeMillis = source.uptimeMillis;
+
+        return newStat;
+    }
+
+    private class SipTransportFeatureTags {
+        private HashMap<String, LastFeatureTagState> mFeatureTagMap;
+        private int mSubId;
+
+        private class LastFeatureTagState {
+            public long timeStamp;
+            public int carrierId;
+            public int slotId;
+            public int state;
+            public int reason;
+
+            LastFeatureTagState(int carrierId, int slotId, int state, int reason, long timeStamp) {
+                this.carrierId = carrierId;
+                this.slotId = slotId;
+                this.state = state;
+                this.reason = reason;
+                this.timeStamp = timeStamp;
+            }
+
+            public void update(int state, int reason, long timeStamp) {
+                this.state = state;
+                this.reason = reason;
+                this.timeStamp = timeStamp;
+            }
+
+            public void update(long timeStamp) {
+                this.timeStamp = timeStamp;
+            }
+        }
+
+        SipTransportFeatureTags(int subId) {
+            mFeatureTagMap = new HashMap<>();
+            mSubId = subId;
+        }
+
+        public HashMap<String, LastFeatureTagState> getLastTagStates() {
+            return mFeatureTagMap;
+        }
+
+        /*** Create or update featureTags whenever feature Tag states are changed */
+        public synchronized void updateLastFeatureTagState(String tagName, int state, int reason,
+                long timeStamp) {
+            int carrierId = getCarrierId(mSubId);
+            int slotId = getSlotId(mSubId);
+            if (mFeatureTagMap.containsKey(tagName)) {
+                LastFeatureTagState lastFeatureTagState = mFeatureTagMap.get(tagName);
+                if (lastFeatureTagState != null) {
+                    addFeatureTagStat(tagName, lastFeatureTagState, timeStamp);
+                    lastFeatureTagState.update(state, reason, timeStamp);
+                } else {
+                    create(tagName, carrierId, slotId, state, reason, timeStamp);
+                }
+
+            } else {
+                create(tagName, carrierId, slotId, state, reason, timeStamp);
+            }
+        }
+
+        /** Update current featureTags associated to active SipDelegates when metrics is pulled */
+        public synchronized void conclude(long timeStamp) {
+            HashMap<String, LastFeatureTagState> featureTagsCopy = new HashMap<>();
+            featureTagsCopy.putAll(mFeatureTagMap);
+            for (Map.Entry<String, LastFeatureTagState> last : featureTagsCopy.entrySet()) {
+                String tagName = last.getKey();
+                LastFeatureTagState lastFeatureTagState = last.getValue();
+                addFeatureTagStat(tagName, lastFeatureTagState, timeStamp);
+                updateTimeStamp(mSubId, tagName, timeStamp);
+            }
+        }
+
+        /** Finalizes the durations of the current featureTags associated to active SipDelegates */
+        private synchronized boolean addFeatureTagStat(@NonNull String tagName,
+                @NonNull LastFeatureTagState lastFeatureTagState, long now) {
+            long duration = now - lastFeatureTagState.timeStamp;
+            if (duration < MIN_DURATION_MILLIS
+                    || !isValidCarrierId(lastFeatureTagState.carrierId)) {
+                logd("conclude: discarding transient stats, duration= " + duration
+                        + ", carrierId = " + lastFeatureTagState.carrierId);
+            } else {
+                SipTransportFeatureTagStats sipFeatureTagStat = new SipTransportFeatureTagStats();
+                switch (lastFeatureTagState.state) {
+                    case STATE_DENIED:
+                        sipFeatureTagStat.sipTransportDeniedReason = lastFeatureTagState.reason;
+                        sipFeatureTagStat.sipTransportDeregisteredReason = NONE;
+                        break;
+                    case STATE_DEREGISTERED:
+                        sipFeatureTagStat.sipTransportDeniedReason = NONE;
+                        sipFeatureTagStat.sipTransportDeregisteredReason =
+                                lastFeatureTagState.reason;
+                        break;
+                    default:
+                        sipFeatureTagStat.sipTransportDeniedReason = NONE;
+                        sipFeatureTagStat.sipTransportDeregisteredReason = NONE;
+                        break;
+                }
+
+                sipFeatureTagStat.carrierId = lastFeatureTagState.carrierId;
+                sipFeatureTagStat.slotId = lastFeatureTagState.slotId;
+                sipFeatureTagStat.associatedMillis = duration;
+                sipFeatureTagStat.featureTagName = convertTagNameToValue(tagName);
+                mAtomsStorage.addSipTransportFeatureTagStats(sipFeatureTagStat);
+                return true;
+            }
+            return false;
+        }
+
+        private void updateTimeStamp(int subId, String tagName, long timeStamp) {
+            SipTransportFeatureTags sipTransportFeatureTags = mLastFeatureTagStatMap.get(subId);
+            if (sipTransportFeatureTags != null) {
+                HashMap<String, LastFeatureTagState> lastTagStates =
+                        sipTransportFeatureTags.getLastTagStates();
+                if (lastTagStates != null && lastTagStates.containsKey(tagName)) {
+                    LastFeatureTagState lastFeatureTagState = lastTagStates.get(tagName);
+                    if (lastFeatureTagState != null) {
+                        lastFeatureTagState.update(timeStamp);
+                    }
+                }
+            }
+        }
+
+        private LastFeatureTagState create(String tagName, int carrierId, int slotId, int state,
+                int reason, long timeStamp) {
+            LastFeatureTagState lastFeatureTagState = new LastFeatureTagState(carrierId, slotId,
+                    state, reason, timeStamp);
+            mFeatureTagMap.put(tagName, lastFeatureTagState);
+            return lastFeatureTagState;
+        }
+    }
+
+    class UceStatsWriterCallback implements UceStatsCallback {
+        private RcsStats mRcsStats;
+
+        UceStatsWriterCallback(RcsStats rcsStats) {
+            logd("created Callback");
+            mRcsStats = rcsStats;
+        }
+
+        public void onImsRegistrationFeatureTagStats(int subId, List<String> featureTagList,
+                int registrationTech) {
+            mRcsStats.onImsRegistrationFeatureTagStats(subId, featureTagList, registrationTech);
+        }
+
+        public void onStoreCompleteImsRegistrationFeatureTagStats(int subId) {
+            mRcsStats.onStoreCompleteImsRegistrationFeatureTagStats(subId);
+        }
+
+        public void onImsRegistrationServiceDescStats(int subId, List<String> serviceIdList,
+                List<String> serviceIdVersionList, int registrationTech) {
+            mRcsStats.onImsRegistrationServiceDescStats(subId, serviceIdList, serviceIdVersionList,
+                    registrationTech);
+        }
+
+        public void onSubscribeResponse(int subId, long taskId, int networkResponse) {
+            if (networkResponse >= 200 && networkResponse <= 299) {
+                if (!sSubscribeTaskIds.containsKey(taskId)) {
+                    sSubscribeTaskIds.put(taskId, SUBSCRIBE_SUCCESS);
+                }
+            }
+            mRcsStats.onUceEventStats(subId, UCE_EVENT_STATS__TYPE__SUBSCRIBE,
+                    true, 0, networkResponse);
+        }
+
+        public void onUceEvent(int subId, int type, boolean successful, int commandCode,
+                int networkResponse) {
+            int eventType = 0;
+            switch (type) {
+                case UceStatsWriter.PUBLISH_EVENT:
+                    eventType = UCE_EVENT_STATS__TYPE__PUBLISH;
+                    break;
+                case UceStatsWriter.SUBSCRIBE_EVENT:
+                    eventType = UCE_EVENT_STATS__TYPE__SUBSCRIBE;
+                    break;
+                case UceStatsWriter.INCOMING_OPTION_EVENT:
+                    eventType = UCE_EVENT_STATS__TYPE__INCOMING_OPTION;
+                    break;
+                case UceStatsWriter.OUTGOING_OPTION_EVENT:
+                    eventType = UCE_EVENT_STATS__TYPE__OUTGOING_OPTION;
+                    break;
+                default:
+                    return;
+            }
+            mRcsStats.onUceEventStats(subId, eventType, successful, commandCode, networkResponse);
+        }
+
+        public void onSubscribeTerminated(int subId, long taskId, String reason) {
+            if (sSubscribeTaskIds.containsKey(taskId)) {
+                int previousSubscribeStatus = sSubscribeTaskIds.get(taskId);
+                sSubscribeTaskIds.remove(taskId);
+                // The device received a success response related to the subscription request.
+                // However, PIDF was not received due to reason value.
+                if (previousSubscribeStatus == SUBSCRIBE_SUCCESS) {
+                    mRcsStats.onPresenceNotifyEvent(subId, reason, false,
+                            false, false, false);
+                }
+            }
+        }
+
+        public void onPresenceNotifyEvent(int subId, long taskId,
+                List<RcsContactUceCapability> updatedCapList) {
+            if (updatedCapList == null || updatedCapList.isEmpty()) {
+                return;
+            }
+            if (sSubscribeTaskIds.containsKey(taskId)) {
+                sSubscribeTaskIds.replace(taskId, SUBSCRIBE_NOTIFY);
+            }
+            for (RcsContactUceCapability capability : updatedCapList) {
+                boolean rcsCap = false;
+                boolean mmtelCap = false;
+                boolean noCap = true;
+                List<RcsContactPresenceTuple> tupleList = capability.getCapabilityTuples();
+                if (tupleList.isEmpty()) {
+                    noCap = true;
+                    mRcsStats.onPresenceNotifyEvent(subId, "", true,
+                            rcsCap, mmtelCap, noCap);
+                    continue;
+                }
+                for (RcsContactPresenceTuple tuple : tupleList) {
+                    String serviceId = tuple.getServiceId();
+                    if (RCS_SERVICE_ID_SET.contains(serviceId)) {
+                        rcsCap = true;
+                        noCap = false;
+                    } else if (MMTEL_SERVICE_ID_SET.contains(serviceId)) {
+                        if (serviceId.equals(RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER)) {
+                            if ("1.0".equals(tuple.getServiceVersion())) {
+                                rcsCap = true;
+                                noCap = false;
+                                continue;
+                            }
+                        }
+                        mmtelCap = true;
+                        noCap = false;
+                    }
+                }
+                mRcsStats.onPresenceNotifyEvent(subId, "", true, rcsCap,
+                        mmtelCap, noCap);
+            }
+        }
+
+        public void onStoreCompleteImsRegistrationServiceDescStats(int subId) {
+            mRcsStats.onStoreCompleteImsRegistrationServiceDescStats(subId);
+        }
+    }
+
+    /** Callback class to receive RCS ACS result and to store metrics. */
+    public class RcsProvisioningCallback extends IRcsConfigCallback.Stub {
+        private RcsStats mRcsStats;
+        private int mSubId;
+        private boolean mEnableSingleRegistration;
+        private boolean mRegistered;
+
+        RcsProvisioningCallback(RcsStats rcsStats, int subId, boolean enableSingleRegistration) {
+            logd("created RcsProvisioningCallback");
+            mRcsStats = rcsStats;
+            mSubId = subId;
+            mEnableSingleRegistration = enableSingleRegistration;
+            mRegistered = false;
+        }
+
+        public synchronized void setEnableSingleRegistration(boolean enableSingleRegistration) {
+            mEnableSingleRegistration = enableSingleRegistration;
+        }
+
+        public boolean getRegistered() {
+            return mRegistered;
+        }
+
+        public void setRegistered(boolean registered) {
+            mRegistered = registered;
+        }
+
+        @Override
+        public void onConfigurationChanged(byte[] config) {
+            // this callback will not be handled.
+        }
+
+        @Override
+        public void onAutoConfigurationErrorReceived(int errorCode, String errorString) {
+            final long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                mRcsStats.onRcsAcsProvisioningStats(mSubId, errorCode,
+                        RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR,
+                        mEnableSingleRegistration);
+            } finally {
+                restoreCallingIdentity(callingIdentity);
+            }
+        }
+
+        @Override
+        public void onConfigurationReset() {
+            // this callback will not be handled.
+        }
+
+        @Override
+        public void onRemoved() {
+            final long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                // store cached metrics
+                mRcsStats.onStoreCompleteRcsAcsProvisioningStats(mSubId);
+               // remove this obj from Map
+                mRcsStats.removeRcsProvisioningCallback(mSubId);
+            } finally {
+                restoreCallingIdentity(callingIdentity);
+            }
+        }
+
+        @Override
+        public void onPreProvisioningReceived(byte[] config) {
+            final long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                // Receiving pre provisioning means http 200 OK with body.
+                mRcsStats.onRcsAcsProvisioningStats(mSubId, 200,
+                        RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PRE_PROVISIONING_XML,
+                        mEnableSingleRegistration);
+            } finally {
+                restoreCallingIdentity(callingIdentity);
+            }
+        }
+    };
+
+    private class SipMessageArray {
+        private String mMethod;
+        private String mCallId;
+        private int mDirection;
+
+        SipMessageArray(String method, int direction, String callId) {
+            this.mMethod = method;
+            this.mCallId = callId;
+            this.mDirection = direction;
+        }
+
+        private synchronized void addSipMessageStat(
+                @NonNull int subId, @NonNull String sipMessageMethod,
+                int sipMessageResponse, int sipMessageDirection, int messageError) {
+            int carrierId = getCarrierId(subId);
+            if (!isValidCarrierId(carrierId)) {
+                return;
+            }
+            SipMessageResponse proto = new SipMessageResponse();
+            proto.carrierId = carrierId;
+            proto.slotId = getSlotId(subId);
+            proto.sipMessageMethod = convertMessageTypeToValue(sipMessageMethod);
+            proto.sipMessageResponse = sipMessageResponse;
+            proto.sipMessageDirection = sipMessageDirection;
+            proto.messageError = messageError;
+            proto.count = 1;
+            mAtomsStorage.addSipMessageResponse(proto);
+        }
+    }
+
+    private class SipTransportSessionArray {
+        private String mMethod;
+        private String mCallId;
+        private int mDirection;
+        private int mSipResponse;
+
+        SipTransportSessionArray(String method, int direction, String callId) {
+            this.mMethod = method;
+            this.mCallId = callId;
+            this.mDirection = direction;
+            this.mSipResponse = 0;
+        }
+
+        private synchronized void addSipTransportSessionStat(
+                @NonNull int subId, @NonNull String sessionMethod, int sipMessageDirection,
+                int sipResponse, boolean isEndedGracefully) {
+            int carrierId = getCarrierId(subId);
+            if (!isValidCarrierId(carrierId)) {
+                return;
+            }
+            SipTransportSession proto = new SipTransportSession();
+            proto.carrierId = carrierId;
+            proto.slotId = getSlotId(subId);
+            proto.sessionMethod = convertMessageTypeToValue(sessionMethod);
+            proto.sipMessageDirection = sipMessageDirection;
+            proto.sipResponse = sipResponse;
+            proto.sessionCount = 1;
+            proto.endedGracefullyCount = 1;
+            proto.isEndedGracefully = isEndedGracefully;
+            mAtomsStorage.addCompleteSipTransportSession(proto);
+        }
+    }
+
+    @VisibleForTesting
+    protected RcsStats() {
+        mCallback = null;
+    }
+
+    /** Gets a RcsStats instance. */
+    public static RcsStats getInstance() {
+        synchronized (RcsStats.class) {
+            if (sInstance == null) {
+                Rlog.d(TAG, "RcsStats created.");
+                sInstance = new RcsStats();
+            }
+            return sInstance;
+        }
+    }
+
+    /** register callback to UceStatsWriter. */
+    public void registerUceCallback() {
+        if (mCallback == null) {
+            mCallback = new UceStatsWriterCallback(sInstance);
+            Rlog.d(TAG, "UceStatsWriterCallback created.");
+            UceStatsWriter.init(mCallback);
+        }
+    }
+
+    /** Update or create new atom when RCS service registered. */
+    public void onImsRegistrationFeatureTagStats(int subId, List<String> featureTagList,
+            int registrationTech) {
+        synchronized (mImsRegistrationFeatureTagStatsList) {
+            int carrierId = getCarrierId(subId);
+            if (!isValidCarrierId(carrierId)) {
+                flushImsRegistrationFeatureTagStatsInvalid();
+                return;
+            }
+
+            // update cached atom if exists
+            onStoreCompleteImsRegistrationFeatureTagStats(subId);
+
+            if (featureTagList == null) {
+                Rlog.d(TAG, "featureTagNames is null or empty");
+                return;
+            }
+
+            for (String featureTag : featureTagList) {
+                ImsRegistrationFeatureTagStats proto = new ImsRegistrationFeatureTagStats();
+                proto.carrierId = carrierId;
+                proto.slotId = getSlotId(subId);
+                proto.featureTagName = convertTagNameToValue(featureTag);
+                proto.registrationTech = registrationTech;
+                proto.registeredMillis = getWallTimeMillis();
+                mImsRegistrationFeatureTagStatsList.add(proto);
+            }
+        }
+    }
+
+    /** Update duration, store and delete cached ImsRegistrationFeatureTagStats list to storage. */
+    public void onStoreCompleteImsRegistrationFeatureTagStats(int subId) {
+        synchronized (mImsRegistrationFeatureTagStatsList) {
+            int carrierId = getCarrierId(subId);
+            List<ImsRegistrationFeatureTagStats> deleteList = new ArrayList<>();
+            long now = getWallTimeMillis();
+            for (ImsRegistrationFeatureTagStats proto : mImsRegistrationFeatureTagStatsList) {
+                if (proto.carrierId == carrierId) {
+                    proto.registeredMillis = now - proto.registeredMillis;
+                    mAtomsStorage.addImsRegistrationFeatureTagStats(proto);
+                    deleteList.add(proto);
+                }
+            }
+            for (ImsRegistrationFeatureTagStats proto : deleteList) {
+                mImsRegistrationFeatureTagStatsList.remove(proto);
+            }
+        }
+    }
+
+    /** Update duration and store cached ImsRegistrationFeatureTagStats when metrics are pulled */
+    public void onFlushIncompleteImsRegistrationFeatureTagStats() {
+        synchronized (mImsRegistrationFeatureTagStatsList) {
+            long now = getWallTimeMillis();
+            for (ImsRegistrationFeatureTagStats proto : mImsRegistrationFeatureTagStatsList) {
+                ImsRegistrationFeatureTagStats newProto = copyImsRegistrationFeatureTagStats(proto);
+                // the current time is a placeholder and total registered time will be
+                // calculated when generating final atoms
+                newProto.registeredMillis = now - proto.registeredMillis;
+                mAtomsStorage.addImsRegistrationFeatureTagStats(newProto);
+                proto.registeredMillis = now;
+            }
+        }
+    }
+
+    /** Create a new atom when RCS client stat changed. */
+    public synchronized void onRcsClientProvisioningStats(int subId, int event) {
+        int carrierId = getCarrierId(subId);
+
+        if (!isValidCarrierId(carrierId)) {
+            return;
+        }
+
+        RcsClientProvisioningStats proto = new RcsClientProvisioningStats();
+        proto.carrierId = carrierId;
+        proto.slotId = getSlotId(subId);
+        proto.event = event;
+        proto.count = 1;
+        mAtomsStorage.addRcsClientProvisioningStats(proto);
+    }
+
+    /** Update or create new atom when RCS ACS stat changed. */
+    public void onRcsAcsProvisioningStats(int subId, int responseCode, int responseType,
+            boolean enableSingleRegistration) {
+
+        synchronized (mRcsAcsProvisioningStatsList) {
+            int carrierId = getCarrierId(subId);
+            if (!isValidCarrierId(carrierId)) {
+                flushRcsAcsProvisioningStatsInvalid();
+                return;
+            }
+
+            // update cached atom if exists
+            onStoreCompleteRcsAcsProvisioningStats(subId);
+
+            // create new stats to cache
+            RcsAcsProvisioningStats newStats = new RcsAcsProvisioningStats();
+            newStats.carrierId = carrierId;
+            newStats.slotId = getSlotId(subId);
+            newStats.responseCode = responseCode;
+            newStats.responseType = responseType;
+            newStats.isSingleRegistrationEnabled = enableSingleRegistration;
+            newStats.count = 1;
+            newStats.stateTimerMillis = getWallTimeMillis();
+
+            // add new stats in list
+            mRcsAcsProvisioningStatsList.add(newStats);
+        }
+    }
+
+    /** Update duration, store and delete cached RcsAcsProvisioningStats */
+    public void onStoreCompleteRcsAcsProvisioningStats(int subId) {
+        synchronized (mRcsAcsProvisioningStatsList) {
+            // find cached RcsAcsProvisioningStats based sub ID
+            RcsAcsProvisioningStats existingStats = getRcsAcsProvisioningStats(subId);
+            if (existingStats != null) {
+                existingStats.stateTimerMillis =
+                        getWallTimeMillis() - existingStats.stateTimerMillis;
+                mAtomsStorage.addRcsAcsProvisioningStats(existingStats);
+                // remove cached atom from list
+                mRcsAcsProvisioningStatsList.remove(existingStats);
+            }
+        }
+    }
+
+    /** Update duration and store cached RcsAcsProvisioningStats when metrics are pulled */
+    public void onFlushIncompleteRcsAcsProvisioningStats() {
+        synchronized (mRcsAcsProvisioningStatsList) {
+            long now = getWallTimeMillis();
+            for (RcsAcsProvisioningStats stats : mRcsAcsProvisioningStatsList) {
+                // we store a copy into atoms storage
+                // so that we can continue using the original object.
+                RcsAcsProvisioningStats proto = copyRcsAcsProvisioningStats(stats);
+                // the current time is a placeholder and total registered time will be
+                // calculated when generating final atoms
+                proto.stateTimerMillis = now - proto.stateTimerMillis;
+                mAtomsStorage.addRcsAcsProvisioningStats(proto);
+                // update cached atom's time
+                stats.stateTimerMillis = now;
+            }
+        }
+    }
+
+    /** Create SipDelegateStat when SipDelegate is created */
+    public synchronized void createSipDelegateStats(int subId, Set<String> supportedTags) {
+        if (supportedTags != null && !supportedTags.isEmpty()) {
+            LastSipDelegateStat lastState = getLastSipDelegateStat(subId, supportedTags);
+            lastState.createSipDelegateStat(subId);
+        }
+    }
+
+    /** Update destroyReason and duration of SipDelegateStat when SipDelegate is destroyed */
+    public synchronized void onSipDelegateStats(int subId, Set<String> supportedTags,
+            int destroyReason) {
+        if (supportedTags != null && !supportedTags.isEmpty()) {
+            LastSipDelegateStat lastState = getLastSipDelegateStat(subId, supportedTags);
+            lastState.setSipDelegateDestroyReason(destroyReason);
+            concludeSipDelegateStat();
+        }
+    }
+
+    /** Create/Update atoms when states of sipTransportFeatureTags are changed */
+    public synchronized void onSipTransportFeatureTagStats(
+            int subId,
+            Set<FeatureTagState> deniedTags,
+            Set<FeatureTagState> deRegiTags,
+            Set<String> regiTags) {
+        long now = getWallTimeMillis();
+        SipTransportFeatureTags sipTransportFeatureTags = getLastFeatureTags(subId);
+        if (regiTags != null && !regiTags.isEmpty()) {
+            for (String tag : regiTags) {
+                sipTransportFeatureTags.updateLastFeatureTagState(tag, STATE_REGISTERED,
+                        NONE, now);
+            }
+        }
+        if (deniedTags != null && !deniedTags.isEmpty()) {
+            for (FeatureTagState tag : deniedTags) {
+                sipTransportFeatureTags.updateLastFeatureTagState(tag.getFeatureTag(), STATE_DENIED,
+                        tag.getState(), now);
+            }
+        }
+        if (deRegiTags != null && !deRegiTags.isEmpty()) {
+            for (FeatureTagState tag : deRegiTags) {
+                sipTransportFeatureTags.updateLastFeatureTagState(
+                        tag.getFeatureTag(), STATE_DEREGISTERED, tag.getState(), now);
+            }
+        }
+    }
+
+    /** Update duration of  sipTransportFeatureTags when metrics are pulled */
+    public synchronized void concludeSipTransportFeatureTagsStat() {
+        if (mLastFeatureTagStatMap.isEmpty()) {
+            return;
+        }
+
+        long now = getWallTimeMillis();
+        HashMap<Integer, SipTransportFeatureTags> lastFeatureTagStatsCopy = new HashMap<>();
+        lastFeatureTagStatsCopy.putAll(mLastFeatureTagStatMap);
+        for (SipTransportFeatureTags sipTransportFeatureTags : lastFeatureTagStatsCopy.values()) {
+            if (sipTransportFeatureTags != null) {
+                sipTransportFeatureTags.conclude(now);
+            }
+        }
+    }
+
+    /** Request Message */
+    public synchronized void onSipMessageRequest(String callId, String sipMessageMethod,
+            int sipMessageDirection) {
+        mSipMessage = new SipMessageArray(sipMessageMethod, sipMessageDirection, callId);
+        mSipMessageArray.add(mSipMessage);
+    }
+
+    /** invalidated result when Request message is sent */
+    public synchronized void invalidatedMessageResult(int subId, String sipMessageMethod,
+            int sipMessageDirection, int messageError) {
+        mSipMessage.addSipMessageStat(subId, sipMessageMethod, 0,
+                sipMessageDirection, messageError);
+    }
+
+    /** Create a new atom when RCS SIP Message Response changed. */
+    public synchronized void onSipMessageResponse(int subId, String callId,
+            int sipMessageResponse, int messageError) {
+        SipMessageArray match = mSipMessageArray.stream()
+                .filter(d -> d.mCallId.equals(callId)).findFirst().orElse(null);
+        if (match != null) {
+            mSipMessage.addSipMessageStat(subId, match.mMethod, sipMessageResponse,
+                    match.mDirection, messageError);
+            mSipMessageArray.removeIf(d -> d.mCallId.equals(callId));
+        }
+    }
+
+    /** Request SIP Method Message */
+    public synchronized void earlySipTransportSession(String sessionMethod, String callId,
+            int sipMessageDirection) {
+        mSipTransportSession = new SipTransportSessionArray(sessionMethod,
+                sipMessageDirection, callId);
+        mSipTransportSessionArray.add(mSipTransportSession);
+    }
+
+    /** Response Message */
+    public synchronized void confirmedSipTransportSession(String callId,
+            int sipResponse) {
+        SipTransportSessionArray match = mSipTransportSessionArray.stream()
+                .filter(d -> d.mCallId.equals(callId)).findFirst().orElse(null);
+        if (match != null) {
+            match.mSipResponse = sipResponse;
+        }
+    }
+
+    /** Create a new atom when RCS SIP Transport Session changed. */
+    public synchronized void onSipTransportSessionClosed(int subId, String callId,
+            int sipResponse, boolean isEndedGracefully) {
+        SipTransportSessionArray match = mSipTransportSessionArray.stream()
+                .filter(d -> d.mCallId.equals(callId)).findFirst().orElse(null);
+        if (match != null) {
+            if (sipResponse != 0) {
+                match.mSipResponse = sipResponse;
+            }
+            mSipTransportSession.addSipTransportSessionStat(subId, match.mMethod, match.mDirection,
+                    sipResponse, isEndedGracefully);
+            mSipTransportSessionArray.removeIf(d -> d.mCallId.equals(callId));
+        }
+    }
+
+    /** Add a listener to the hashmap for waiting upcoming DedicatedBearer established event */
+    public synchronized void onImsDedicatedBearerListenerAdded(@NonNull final int listenerId,
+            @NonNull final int slotId, @NonNull final int ratAtEnd, @NonNull final int qci) {
+        int subId = getSubId(slotId);
+        int carrierId = getCarrierId(subId);
+
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                || !isValidCarrierId(carrierId)) {
+            return;
+        }
+
+        if (mDedicatedBearerListenerEventMap.containsKey(listenerId)) {
+            return;
+        }
+
+        ImsDedicatedBearerListenerEvent preProto = new ImsDedicatedBearerListenerEvent();
+        preProto.carrierId = carrierId;
+        preProto.slotId = slotId;
+        preProto.ratAtEnd = ratAtEnd;
+        preProto.qci = qci;
+        preProto.dedicatedBearerEstablished = false;
+        preProto.eventCount = 1;
+
+        mDedicatedBearerListenerEventMap.put(listenerId, preProto);
+    }
+
+    /** update previously added atom with dedicatedBearerEstablished = true when
+     *  DedicatedBearerListener Event changed. */
+    public synchronized void onImsDedicatedBearerListenerUpdateSession(final int listenerId,
+            final int slotId, final int rat, final int qci,
+            @NonNull final boolean dedicatedBearerEstablished) {
+        int subId = getSubId(slotId);
+        int carrierId = getCarrierId(subId);
+
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                || !isValidCarrierId(carrierId)) {
+            return;
+        }
+
+        if (mDedicatedBearerListenerEventMap.containsKey(listenerId)) {
+            ImsDedicatedBearerListenerEvent preProto =
+                    mDedicatedBearerListenerEventMap.get(listenerId);
+
+            preProto.ratAtEnd = rat;
+            preProto.qci = qci;
+            preProto.dedicatedBearerEstablished = dedicatedBearerEstablished;
+
+            mDedicatedBearerListenerEventMap.replace(listenerId, preProto);
+        } else {
+            ImsDedicatedBearerListenerEvent preProto = new ImsDedicatedBearerListenerEvent();
+            preProto.carrierId = carrierId;
+            preProto.slotId = slotId;
+            preProto.ratAtEnd = rat;
+            preProto.qci = qci;
+            preProto.dedicatedBearerEstablished = dedicatedBearerEstablished;
+            preProto.eventCount = 1;
+
+            mDedicatedBearerListenerEventMap.put(listenerId, preProto);
+        }
+    }
+
+    /** add proto to atom when listener is removed, so that I can save the status of dedicatedbearer
+     *  establishment per listener id */
+    public synchronized void onImsDedicatedBearerListenerRemoved(@NonNull final int listenerId) {
+        if (mDedicatedBearerListenerEventMap.containsKey(listenerId)) {
+
+            ImsDedicatedBearerListenerEvent newProto =
+                    mDedicatedBearerListenerEventMap.get(listenerId);
+
+            mAtomsStorage.addImsDedicatedBearerListenerEvent(newProto);
+            mDedicatedBearerListenerEventMap.remove(listenerId);
+        }
+    }
+
+    /** Create a new atom when DedicatedBearer Event changed. */
+    public synchronized void onImsDedicatedBearerEvent(int slotId, int ratAtEnd, int qci,
+            int bearerState, boolean localConnectionInfoReceived,
+            boolean remoteConnectionInfoReceived, boolean hasListeners) {
+        int subId = getSubId(slotId);
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            return;
+        }
+
+        ImsDedicatedBearerEvent proto = new ImsDedicatedBearerEvent();
+        proto.carrierId = getCarrierId(subId);
+        proto.slotId = getSlotId(subId);
+        proto.ratAtEnd = ratAtEnd;
+        proto.qci = qci;
+        proto.bearerState = bearerState;
+        proto.localConnectionInfoReceived = localConnectionInfoReceived;
+        proto.remoteConnectionInfoReceived = remoteConnectionInfoReceived;
+        proto.hasListeners = hasListeners;
+        proto.count = 1;
+        mAtomsStorage.addImsDedicatedBearerEvent(proto);
+    }
+
+    /**
+     * Update or Create a new atom when Ims Registration Service Desc state changed.
+     * Use-related parts are already converted from UseStatsWriter based on RcsContactPresenceTuple.
+     */
+    public void onImsRegistrationServiceDescStats(int subId, List<String> serviceIdList,
+            List<String> serviceIdVersionList, int registrationTech) {
+        synchronized (mImsRegistrationServiceDescStatsList) {
+            int carrierId = getCarrierId(subId);
+            if (!isValidCarrierId(carrierId)) {
+                handleImsRegistrationServiceDescStats();
+                return;
+            }
+            // update cached atom if exists
+            onStoreCompleteImsRegistrationServiceDescStats(subId);
+
+            if (serviceIdList == null) {
+                Rlog.d(TAG, "serviceIds is null or empty");
+                return;
+            }
+
+            int index = 0;
+            for (String serviceId : serviceIdList) {
+                ImsRegistrationServiceDescStats mImsRegistrationServiceDescStats =
+                        new ImsRegistrationServiceDescStats();
+
+                mImsRegistrationServiceDescStats.carrierId = carrierId;
+                mImsRegistrationServiceDescStats.slotId = getSlotId(subId);
+                mImsRegistrationServiceDescStats.serviceIdName = convertServiceIdToValue(serviceId);
+                mImsRegistrationServiceDescStats.serviceIdVersion =
+                        Float.parseFloat(serviceIdVersionList.get(index++));
+                mImsRegistrationServiceDescStats.registrationTech = registrationTech;
+                mImsRegistrationServiceDescStatsList.add(mImsRegistrationServiceDescStats);
+            }
+        }
+    }
+
+    /** Update duration and cached of ImsRegistrationServiceDescStats when metrics are pulled */
+    public void onFlushIncompleteImsRegistrationServiceDescStats() {
+        synchronized (mImsRegistrationServiceDescStatsList) {
+            for (ImsRegistrationServiceDescStats proto : mImsRegistrationServiceDescStatsList) {
+                ImsRegistrationServiceDescStats newProto =
+                        copyImsRegistrationServiceDescStats(proto);
+                long now = getWallTimeMillis();
+                // the current time is a placeholder and total registered time will be
+                // calculated when generating final atoms
+                newProto.publishedMillis = now - proto.publishedMillis;
+                mAtomsStorage.addImsRegistrationServiceDescStats(newProto);
+                proto.publishedMillis = now;
+            }
+        }
+    }
+
+    /** Create a new atom when Uce Event Stats changed. */
+    public synchronized void onUceEventStats(int subId, int type, boolean successful,
+            int commandCode, int networkResponse) {
+        UceEventStats proto = new UceEventStats();
+
+        int carrierId = getCarrierId(subId);
+        if (!isValidCarrierId(carrierId)) {
+            handleImsRegistrationServiceDescStats();
+            return;
+        }
+        proto.carrierId = carrierId;
+        proto.slotId = getSlotId(subId);
+        proto.type = type;
+        proto.successful = successful;
+        proto.commandCode = commandCode;
+        proto.networkResponse = networkResponse;
+        proto.count = 1;
+        mAtomsStorage.addUceEventStats(proto);
+
+        /**
+         * The publishedMillis of ImsRegistrationServiceDescStat is the time gap between
+         * Publish success and Un publish.
+         * So, when the publish operation is successful, the corresponding time gap is set,
+         * and in case of failure, the cached stat is deleted.
+         */
+        if (type == UCE_EVENT_STATS__TYPE__PUBLISH) {
+            if (successful) {
+                setImsRegistrationServiceDescStatsTime(proto.carrierId);
+            } else {
+                deleteImsRegistrationServiceDescStats(proto.carrierId);
+            }
+        }
+    }
+
+    /** Create a new atom when Presence Notify Event changed. */
+    public synchronized void onPresenceNotifyEvent(int subId, String reason,
+            boolean contentBodyReceived, boolean rcsCaps, boolean mmtelCaps, boolean noCaps) {
+        PresenceNotifyEvent proto = new PresenceNotifyEvent();
+
+        int carrierId = getCarrierId(subId);
+        if (!isValidCarrierId(carrierId)) {
+            handleImsRegistrationServiceDescStats();
+            return;
+        }
+
+        proto.carrierId = carrierId;
+        proto.slotId = getSlotId(subId);
+        proto.reason = convertPresenceNotifyReason(reason);
+        proto.contentBodyReceived = contentBodyReceived;
+        proto.rcsCapsCount = rcsCaps ? 1 : 0;
+        proto.mmtelCapsCount = mmtelCaps ? 1 : 0;
+        proto.noCapsCount = noCaps ? 1 : 0;
+        proto.count = 1;
+        mAtomsStorage.addPresenceNotifyEvent(proto);
+    }
+
+    /** Update duration a created Ims Registration Desc Stat atom when Un publish event happened. */
+    public void onStoreCompleteImsRegistrationServiceDescStats(int subId) {
+        synchronized (mImsRegistrationServiceDescStatsList) {
+            int carrierId = getCarrierId(subId);
+            List<ImsRegistrationServiceDescStats> deleteList = new ArrayList<>();
+            for (ImsRegistrationServiceDescStats proto : mImsRegistrationServiceDescStatsList) {
+                if (proto.carrierId == carrierId) {
+                    proto.publishedMillis = getWallTimeMillis() - proto.publishedMillis;
+                    mAtomsStorage.addImsRegistrationServiceDescStats(proto);
+                    deleteList.add(proto);
+                }
+            }
+            for (ImsRegistrationServiceDescStats proto : deleteList) {
+                mImsRegistrationServiceDescStatsList.remove(proto);
+            }
+        }
+    }
+
+    /** Create a new atom when GBA Success Event changed. */
+    public synchronized void onGbaSuccessEvent(int subId) {
+        int carrierId = getCarrierId(subId);
+        if (!isValidCarrierId(carrierId)) {
+            return;
+        }
+
+        GbaEvent proto = new GbaEvent();
+        proto.carrierId = carrierId;
+        proto.slotId = getSlotId(subId);
+        proto.successful = true;
+        proto.failedReason = -1;
+        proto.count = 1;
+        mAtomsStorage.addGbaEvent(proto);
+    }
+
+    /** Create a new atom when GBA Failure Event changed. */
+    public synchronized void onGbaFailureEvent(int subId, int reason) {
+        int carrierId = getCarrierId(subId);
+        if (!isValidCarrierId(carrierId)) {
+            return;
+        }
+
+        GbaEvent proto = new GbaEvent();
+        proto.carrierId = carrierId;
+        proto.slotId = getSlotId(subId);
+        proto.successful = false;
+        proto.failedReason = reason;
+        proto.count = 1;
+        mAtomsStorage.addGbaEvent(proto);
+    }
+
+    /** Create or return exist RcsProvisioningCallback based on subId. */
+    public synchronized RcsProvisioningCallback getRcsProvisioningCallback(int subId,
+            boolean enableSingleRegistration) {
+        // find exist obj in Map
+        RcsProvisioningCallback rcsProvisioningCallback = mRcsProvisioningCallbackMap.get(subId);
+        if (rcsProvisioningCallback != null) {
+            return rcsProvisioningCallback;
+        }
+
+        // create new, add Map and return
+        rcsProvisioningCallback = new RcsProvisioningCallback(this, subId,
+                enableSingleRegistration);
+        mRcsProvisioningCallbackMap.put(subId, rcsProvisioningCallback);
+        return rcsProvisioningCallback;
+    }
+
+    /** Set whether single registration is supported. */
+    public synchronized void setEnableSingleRegistration(int subId,
+            boolean enableSingleRegistration) {
+        // find exist obj and set
+        RcsProvisioningCallback callbackBinder = mRcsProvisioningCallbackMap.get(subId);
+        if (callbackBinder != null) {
+            callbackBinder.setEnableSingleRegistration(enableSingleRegistration);
+        }
+    }
+
+    private synchronized void removeRcsProvisioningCallback(int subId) {
+        // remove obj from Map based on subId
+        mRcsProvisioningCallbackMap.remove(subId);
+    }
+
+    private ImsRegistrationFeatureTagStats copyImsRegistrationFeatureTagStats(
+            ImsRegistrationFeatureTagStats proto) {
+        ImsRegistrationFeatureTagStats newProto = new ImsRegistrationFeatureTagStats();
+        newProto.carrierId = proto.carrierId;
+        newProto.slotId = proto.slotId;
+        newProto.featureTagName = proto.featureTagName;
+        newProto.registrationTech = proto.registrationTech;
+        newProto.registeredMillis = proto.registeredMillis;
+
+        return newProto;
+    }
+
+    private RcsAcsProvisioningStats copyRcsAcsProvisioningStats(RcsAcsProvisioningStats proto) {
+        RcsAcsProvisioningStats newProto = new RcsAcsProvisioningStats();
+        newProto.carrierId = proto.carrierId;
+        newProto.slotId = proto.slotId;
+        newProto.responseCode = proto.responseCode;
+        newProto.responseType = proto.responseType;
+        newProto.isSingleRegistrationEnabled = proto.isSingleRegistrationEnabled;
+        newProto.count = proto.count;
+        newProto.stateTimerMillis = proto.stateTimerMillis;
+
+        return newProto;
+    }
+
+    private ImsRegistrationServiceDescStats copyImsRegistrationServiceDescStats(
+            ImsRegistrationServiceDescStats proto) {
+        ImsRegistrationServiceDescStats newProto = new ImsRegistrationServiceDescStats();
+        newProto.carrierId = proto.carrierId;
+        newProto.slotId = proto.slotId;
+        newProto.serviceIdName = proto.serviceIdName;
+        newProto.serviceIdVersion = proto.serviceIdVersion;
+        newProto.registrationTech = proto.registrationTech;
+        return newProto;
+    }
+
+    private void setImsRegistrationServiceDescStatsTime(int carrierId) {
+        synchronized (mImsRegistrationServiceDescStatsList) {
+            for (ImsRegistrationServiceDescStats descStats : mImsRegistrationServiceDescStatsList) {
+                if (descStats.carrierId == carrierId) {
+                    descStats.publishedMillis = getWallTimeMillis();
+                }
+            }
+        }
+    }
+
+    private void deleteImsRegistrationServiceDescStats(int carrierId) {
+        synchronized (mImsRegistrationServiceDescStatsList) {
+            List<ImsRegistrationServiceDescStats> deleteList = new ArrayList<>();
+            for (ImsRegistrationServiceDescStats proto : mImsRegistrationServiceDescStatsList) {
+                if (proto.carrierId == carrierId) {
+                    deleteList.add(proto);
+                }
+            }
+            for (ImsRegistrationServiceDescStats stats : deleteList) {
+                mImsRegistrationServiceDescStatsList.remove(stats);
+            }
+        }
+    }
+
+    private void handleImsRegistrationServiceDescStats() {
+        synchronized (mImsRegistrationServiceDescStatsList) {
+            List<ImsRegistrationServiceDescStats> deleteList = new ArrayList<>();
+            for (ImsRegistrationServiceDescStats proto : mImsRegistrationServiceDescStatsList) {
+                int subId = getSubId(proto.slotId);
+                int newCarrierId = getCarrierId(subId);
+                if (proto.carrierId != newCarrierId) {
+                    deleteList.add(proto);
+                    if (proto.publishedMillis != 0) {
+                        proto.publishedMillis = getWallTimeMillis() - proto.publishedMillis;
+                        mAtomsStorage.addImsRegistrationServiceDescStats(proto);
+                    }
+                }
+            }
+            for (ImsRegistrationServiceDescStats stats : deleteList) {
+                mImsRegistrationServiceDescStatsList.remove(stats);
+            }
+        }
+    }
+
+    private RcsAcsProvisioningStats getRcsAcsProvisioningStats(int subId) {
+        int carrierId = getCarrierId(subId);
+        int slotId = getSlotId(subId);
+
+        for (RcsAcsProvisioningStats stats : mRcsAcsProvisioningStatsList) {
+            if (stats == null) {
+                continue;
+            }
+            if (stats.carrierId == carrierId && stats.slotId == slotId) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    private void flushRcsAcsProvisioningStatsInvalid() {
+        List<RcsAcsProvisioningStats> inValidList = new ArrayList<RcsAcsProvisioningStats>();
+
+        int subId;
+        int newCarrierId;
+
+        for (RcsAcsProvisioningStats stats : mRcsAcsProvisioningStatsList) {
+            subId = getSubId(stats.slotId);
+            newCarrierId = getCarrierId(subId);
+            if (stats.carrierId != newCarrierId) {
+                inValidList.add(stats);
+            }
+        }
+
+        for (RcsAcsProvisioningStats inValid : inValidList) {
+            inValid.stateTimerMillis = getWallTimeMillis() - inValid.stateTimerMillis;
+            mAtomsStorage.addRcsAcsProvisioningStats(inValid);
+            mRcsAcsProvisioningStatsList.remove(inValid);
+        }
+        inValidList.clear();
+    }
+
+    private void flushImsRegistrationFeatureTagStatsInvalid() {
+        List<ImsRegistrationFeatureTagStats> inValidList =
+                new ArrayList<ImsRegistrationFeatureTagStats>();
+
+        int subId;
+        int newCarrierId;
+
+        for (ImsRegistrationFeatureTagStats stats : mImsRegistrationFeatureTagStatsList) {
+            subId = getSubId(stats.slotId);
+            newCarrierId = getCarrierId(subId);
+            if (stats.carrierId != newCarrierId) {
+                inValidList.add(stats);
+            }
+        }
+
+        for (ImsRegistrationFeatureTagStats inValid : inValidList) {
+            inValid.registeredMillis = getWallTimeMillis() - inValid.registeredMillis;
+            mAtomsStorage.addImsRegistrationFeatureTagStats(inValid);
+            mImsRegistrationFeatureTagStatsList.remove(inValid);
+        }
+        inValidList.clear();
+    }
+
+    private LastSipDelegateStat getLastSipDelegateStat(int subId, Set<String> supportedTags) {
+        LastSipDelegateStat stat = null;
+        for (LastSipDelegateStat lastStat : mLastSipDelegateStatList) {
+            if (lastStat.compare(subId, supportedTags)) {
+                stat = lastStat;
+                break;
+            }
+        }
+
+        if (stat == null) {
+            stat = new LastSipDelegateStat(subId, supportedTags);
+            mLastSipDelegateStatList.add(stat);
+        }
+
+        return stat;
+    }
+
+    private void concludeSipDelegateStat() {
+        if (mLastSipDelegateStatList.isEmpty()) {
+            return;
+        }
+        long now = getWallTimeMillis();
+        List<LastSipDelegateStat> sipDelegateStatsCopy = new ArrayList<>(mLastSipDelegateStatList);
+        for (LastSipDelegateStat stat : sipDelegateStatsCopy) {
+            if (stat.isDestroyed()) {
+                stat.conclude(now);
+                mLastSipDelegateStatList.remove(stat);
+            }
+        }
+    }
+
+    private SipTransportFeatureTags getLastFeatureTags(int subId) {
+        SipTransportFeatureTags sipTransportFeatureTags;
+        if (mLastFeatureTagStatMap.containsKey(subId)) {
+            sipTransportFeatureTags = mLastFeatureTagStatMap.get(subId);
+        } else {
+            sipTransportFeatureTags = new SipTransportFeatureTags(subId);
+            mLastFeatureTagStatMap.put(subId, sipTransportFeatureTags);
+        }
+        return sipTransportFeatureTags;
+    }
+    @VisibleForTesting
+    protected boolean isValidCarrierId(int carrierId) {
+        return carrierId > TelephonyManager.UNKNOWN_CARRIER_ID;
+    }
+
+    @VisibleForTesting
+    protected int getSlotId(int subId) {
+        return SubscriptionManager.getPhoneId(subId);
+    }
+
+    @VisibleForTesting
+    protected int getCarrierId(int subId) {
+        int phoneId = SubscriptionManager.getPhoneId(subId);
+        Phone phone = PhoneFactory.getPhone(phoneId);
+        return phone != null ? phone.getCarrierId() : TelephonyManager.UNKNOWN_CARRIER_ID;
+    }
+
+    @VisibleForTesting
+    protected long getWallTimeMillis() {
+        //time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP
+        return System.currentTimeMillis();
+    }
+
+    @VisibleForTesting
+    protected void logd(String msg) {
+        Rlog.d(TAG, msg);
+    }
+
+    @VisibleForTesting
+    protected int getSubId(int slotId) {
+        final int[] subIds = SubscriptionManager.getSubId(slotId);
+        int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        if (subIds != null && subIds.length >= 1) {
+            subId = subIds[0];
+        }
+        return subId;
+    }
+
+    /** Get a enum value from pre-defined feature tag name list */
+    @VisibleForTesting
+    public int convertTagNameToValue(@NonNull String tagName) {
+        return FEATURE_TAGS.getOrDefault(tagName.trim().toLowerCase(),
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CUSTOM);
+    }
+
+    /** Get a enum value from pre-defined service id list */
+    @VisibleForTesting
+    public int convertServiceIdToValue(@NonNull String serviceId) {
+        return SERVICE_IDS.getOrDefault(serviceId.trim().toLowerCase(),
+                IMS_REGISTRATION_SERVICE_DESC_STATS__SERVICE_ID_NAME__SERVICE_ID_CUSTOM);
+    }
+
+    /** Get a enum value from pre-defined message type list */
+    @VisibleForTesting
+    public int convertMessageTypeToValue(@NonNull String messageType) {
+        return MESSAGE_TYPE.getOrDefault(messageType.trim().toLowerCase(),
+                TelephonyProtoEnums.SIP_REQUEST_CUSTOM);
+    }
+
+    /** Get a enum value from pre-defined reason list */
+    @VisibleForTesting
+    public int convertPresenceNotifyReason(@NonNull String reason) {
+        return NOTIFY_REASONS.getOrDefault(reason.trim().toLowerCase(),
+                PRESENCE_NOTIFY_EVENT__REASON__REASON_CUSTOM);
+    }
+
+    /**
+     * Print all metrics data for debugging purposes
+     *
+     * @param rawWriter Print writer
+     */
+    public synchronized void printAllMetrics(PrintWriter rawWriter) {
+        if (mAtomsStorage == null || mAtomsStorage.mAtoms == null) {
+            return;
+        }
+
+        final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, "  ");
+        PersistAtomsProto.PersistAtoms metricAtoms = mAtomsStorage.mAtoms;
+
+        pw.println("RcsStats Metrics Proto: ");
+        pw.println("------------------------------------------");
+        pw.println("ImsRegistrationFeatureTagStats:");
+        pw.increaseIndent();
+        for (ImsRegistrationFeatureTagStats stat : metricAtoms.imsRegistrationFeatureTagStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Feature Tag Name = " + stat.featureTagName
+                    + ", Registration Tech = " + stat.registrationTech
+                    + ", Registered Duration (ms) = " + stat.registeredMillis);
+        }
+        pw.decreaseIndent();
+
+        pw.println("RcsClientProvisioningStats:");
+        pw.increaseIndent();
+        for (RcsClientProvisioningStats stat : metricAtoms.rcsClientProvisioningStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Event = " + stat.event
+                    + ", Count = " + stat.count);
+        }
+        pw.decreaseIndent();
+
+        pw.println("RcsAcsProvisioningStats:");
+        pw.increaseIndent();
+        for (RcsAcsProvisioningStats stat : metricAtoms.rcsAcsProvisioningStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Response Code = " + stat.responseCode
+                    + ", Response Type = " + stat.responseType
+                    + ", Single Registration Enabled = " + stat.isSingleRegistrationEnabled
+                    + ", Count = " + stat.count
+                    + ", State Timer (ms) = " + stat.stateTimerMillis);
+        }
+        pw.decreaseIndent();
+
+        pw.println("SipDelegateStats:");
+        pw.increaseIndent();
+        for (SipDelegateStats stat : metricAtoms.sipDelegateStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " [" + stat.dimension + "]"
+                    + " Destroy Reason = " + stat.destroyReason
+                    + ", Uptime (ms) = " + stat.uptimeMillis);
+        }
+        pw.decreaseIndent();
+
+        pw.println("SipTransportFeatureTagStats:");
+        pw.increaseIndent();
+        for (SipTransportFeatureTagStats stat : metricAtoms.sipTransportFeatureTagStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Feature Tag Name = " + stat.featureTagName
+                    + ", Denied Reason = " + stat.sipTransportDeniedReason
+                    + ", Deregistered Reason = " + stat.sipTransportDeregisteredReason
+                    + ", Associated Time (ms) = " + stat.associatedMillis);
+        }
+        pw.decreaseIndent();
+
+        pw.println("SipMessageResponse:");
+        pw.increaseIndent();
+        for (SipMessageResponse stat : metricAtoms.sipMessageResponse) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Message Method = " + stat.sipMessageMethod
+                    + ", Response = " + stat.sipMessageResponse
+                    + ", Direction = " + stat.sipMessageDirection
+                    + ", Error = " + stat.messageError
+                    + ", Count = " + stat.count);
+        }
+        pw.decreaseIndent();
+
+        pw.println("SipTransportSession:");
+        pw.increaseIndent();
+        for (SipTransportSession stat : metricAtoms.sipTransportSession) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Session Method = " + stat.sessionMethod
+                    + ", Direction = " + stat.sipMessageDirection
+                    + ", Response = " + stat.sipResponse
+                    + ", Count = " + stat.sessionCount
+                    + ", GraceFully Count = " + stat.endedGracefullyCount);
+        }
+        pw.decreaseIndent();
+
+        pw.println("ImsDedicatedBearerListenerEvent:");
+        pw.increaseIndent();
+        for (ImsDedicatedBearerListenerEvent stat : metricAtoms.imsDedicatedBearerListenerEvent) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " RAT = " + stat.ratAtEnd
+                    + ", QCI = " + stat.qci
+                    + ", Dedicated Bearer Established = " + stat.dedicatedBearerEstablished
+                    + ", Count = " + stat.eventCount);
+        }
+        pw.decreaseIndent();
+
+        pw.println("ImsDedicatedBearerEvent:");
+        pw.increaseIndent();
+        for (ImsDedicatedBearerEvent stat : metricAtoms.imsDedicatedBearerEvent) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " RAT = " + stat.ratAtEnd
+                    + ", QCI = " + stat.qci
+                    + ", Bearer State = " + stat.bearerState
+                    + ", Local Connection Info = " + stat.localConnectionInfoReceived
+                    + ", Remote Connection Info = " + stat.remoteConnectionInfoReceived
+                    + ", Listener Existence = " + stat.hasListeners
+                    + ", Count = " + stat.count);
+        }
+        pw.decreaseIndent();
+
+        pw.println("ImsRegistrationServiceDescStats:");
+        pw.increaseIndent();
+        for (ImsRegistrationServiceDescStats stat : metricAtoms.imsRegistrationServiceDescStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Name = " + stat.serviceIdName
+                    + ", Version = " + stat.serviceIdVersion
+                    + ", Registration Tech = " + stat.registrationTech
+                    + ", Published Time (ms) = " + stat.publishedMillis);
+        }
+        pw.decreaseIndent();
+
+        pw.println("UceEventStats:");
+        pw.increaseIndent();
+        for (UceEventStats stat : metricAtoms.uceEventStats) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Type = " + stat.type
+                    + ", Successful = " + stat.successful
+                    + ", Code = " + stat.commandCode
+                    + ", Response = " + stat.networkResponse
+                    + ", Count = " + stat.count);
+        }
+        pw.decreaseIndent();
+
+        pw.println("PresenceNotifyEvent:");
+        pw.increaseIndent();
+        for (PresenceNotifyEvent stat : metricAtoms.presenceNotifyEvent) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Reason = " + stat.reason
+                    + ", Body = " + stat.contentBodyReceived
+                    + ", RCS Count = " + stat.rcsCapsCount
+                    + ", MMTEL Count = " + stat.mmtelCapsCount
+                    + ", NoCaps Count = " + stat.noCapsCount
+                    + ", Count = " + stat.count);
+        }
+        pw.decreaseIndent();
+
+        pw.println("GbaEvent:");
+        pw.increaseIndent();
+        for (GbaEvent stat : metricAtoms.gbaEvent) {
+            pw.println("[" + stat.carrierId + "]"
+                    + " [" + stat.slotId + "]"
+                    + " Successful = "  + stat.successful
+                    + ", Fail Reason = " + stat.failedReason
+                    + ", Count = " + stat.count);
+        }
+        pw.decreaseIndent();
+    }
+
+    /**
+     * Reset all events
+     */
+    public synchronized void reset() {
+        if (mAtomsStorage == null || mAtomsStorage.mAtoms == null) {
+            return;
+        }
+
+        PersistAtomsProto.PersistAtoms metricAtoms = mAtomsStorage.mAtoms;
+
+        metricAtoms.imsRegistrationFeatureTagStats =
+                PersistAtomsProto.ImsRegistrationFeatureTagStats.emptyArray();
+        metricAtoms.rcsClientProvisioningStats =
+                PersistAtomsProto.RcsClientProvisioningStats.emptyArray();
+        metricAtoms.rcsAcsProvisioningStats =
+                PersistAtomsProto.RcsAcsProvisioningStats.emptyArray();
+        metricAtoms.sipDelegateStats = PersistAtomsProto.SipDelegateStats.emptyArray();
+        metricAtoms.sipTransportFeatureTagStats =
+                PersistAtomsProto.SipTransportFeatureTagStats.emptyArray();
+        metricAtoms.sipMessageResponse = PersistAtomsProto.SipMessageResponse.emptyArray();
+        metricAtoms.sipTransportSession = PersistAtomsProto.SipTransportSession.emptyArray();
+        metricAtoms.imsDedicatedBearerListenerEvent =
+                PersistAtomsProto.ImsDedicatedBearerListenerEvent.emptyArray();
+        metricAtoms.imsDedicatedBearerEvent =
+                PersistAtomsProto.ImsDedicatedBearerEvent.emptyArray();
+        metricAtoms.imsRegistrationServiceDescStats =
+                PersistAtomsProto.ImsRegistrationServiceDescStats.emptyArray();
+        metricAtoms.uceEventStats = PersistAtomsProto.UceEventStats.emptyArray();
+        metricAtoms.presenceNotifyEvent = PersistAtomsProto.PresenceNotifyEvent.emptyArray();
+        metricAtoms.gbaEvent = PersistAtomsProto.GbaEvent.emptyArray();
+    }
+
+    /**
+     * Convert the PersistAtomsProto into Base-64 encoded string
+     *
+     * @return Encoded string
+     */
+    public String buildLog() {
+        PersistAtomsProto.PersistAtoms log = buildProto();
+        return Base64.encodeToString(
+                PersistAtomsProto.PersistAtoms.toByteArray(log), Base64.DEFAULT);
+    }
+
+    /**
+     * Build the PersistAtomsProto
+     *
+     * @return PersistAtomsProto.PersistAtoms
+     */
+    public PersistAtomsProto.PersistAtoms buildProto() {
+        PersistAtomsProto.PersistAtoms log = new PersistAtomsProto.PersistAtoms();
+
+        PersistAtomsProto.PersistAtoms atoms = mAtomsStorage.mAtoms;
+        log.imsRegistrationFeatureTagStats = Arrays.copyOf(atoms.imsRegistrationFeatureTagStats,
+                atoms.imsRegistrationFeatureTagStats.length);
+        log.rcsClientProvisioningStats = Arrays.copyOf(atoms.rcsClientProvisioningStats,
+                atoms.rcsClientProvisioningStats.length);
+        log.rcsAcsProvisioningStats = Arrays.copyOf(atoms.rcsAcsProvisioningStats,
+                atoms.rcsAcsProvisioningStats.length);
+        log.sipDelegateStats = Arrays.copyOf(atoms.sipDelegateStats, atoms.sipDelegateStats.length);
+        log.sipTransportFeatureTagStats = Arrays.copyOf(atoms.sipTransportFeatureTagStats,
+                atoms.sipTransportFeatureTagStats.length);
+        log.sipMessageResponse = Arrays.copyOf(atoms.sipMessageResponse,
+                atoms.sipMessageResponse.length);
+        log.sipTransportSession = Arrays.copyOf(atoms.sipTransportSession,
+                atoms.sipTransportSession.length);
+        log.imsDedicatedBearerListenerEvent = Arrays.copyOf(atoms.imsDedicatedBearerListenerEvent,
+                atoms.imsDedicatedBearerListenerEvent.length);
+        log.imsDedicatedBearerEvent = Arrays.copyOf(atoms.imsDedicatedBearerEvent,
+                atoms.imsDedicatedBearerEvent.length);
+        log.imsRegistrationServiceDescStats = Arrays.copyOf(atoms.imsRegistrationServiceDescStats,
+                atoms.imsRegistrationServiceDescStats.length);
+        log.uceEventStats = Arrays.copyOf(atoms.uceEventStats, atoms.uceEventStats.length);
+        log.presenceNotifyEvent = Arrays.copyOf(atoms.presenceNotifyEvent,
+                atoms.presenceNotifyEvent.length);
+        log.gbaEvent = Arrays.copyOf(atoms.gbaEvent, atoms.gbaEvent.length);
+
+        return log;
+    }
+
+}
diff --git a/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java b/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
index 5e43876..7028d95 100644
--- a/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -290,12 +290,14 @@
                     break;
                 case "--metricsproto":
                     pw.println(convertProtoToBase64String(buildProto()));
+                    pw.println(RcsStats.getInstance().buildLog());
                     if (reset) {
                         reset();
                     }
                     break;
                 case "--metricsprototext":
                     pw.println(buildProto().toString());
+                    pw.println(RcsStats.getInstance().buildProto().toString());
                     break;
             }
         }
@@ -644,6 +646,8 @@
         for (BwEstimationStats stats : mBwEstStatsMapList.get(1).values()) {
             pw.println(stats.toString());
         }
+
+        RcsStats.getInstance().printAllMetrics(rawWriter);
     }
 
     /**
@@ -745,6 +749,8 @@
                     .setRadioState(mLastRadioState.get(key)).build();
             addTelephonyEvent(event);
         }
+
+        RcsStats.getInstance().reset();
     }
 
     /**
diff --git a/tests/telephonytests/src/com/android/internal/telephony/GbaManagerTest.java b/tests/telephonytests/src/com/android/internal/telephony/GbaManagerTest.java
index 8939977..8803984 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/GbaManagerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/GbaManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.telephony;
 
+import static com.android.internal.telephony.TelephonyStatsLog.GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
@@ -50,6 +52,8 @@
 import android.testing.TestableLooper;
 import android.util.Log;
 
+import com.android.internal.telephony.metrics.RcsStats;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -79,6 +83,7 @@
     @Mock IBinder mMockBinder;
     @Mock IGbaService mMockGbaServiceBinder;
     @Mock IBootstrapAuthenticationCallback mMockCallback;
+    @Mock RcsStats mMockRcsStats;
     private GbaManager mTestGbaManager;
     private Handler mHandler;
     private TestableLooper mLooper;
@@ -92,7 +97,7 @@
         }
         when(mMockContext.bindService(any(), any(), anyInt())).thenReturn(true);
         when(mMockGbaServiceBinder.asBinder()).thenReturn(mMockBinder);
-        mTestGbaManager = new GbaManager(mMockContext, TEST_SUB_ID, null, 0);
+        mTestGbaManager = new GbaManager(mMockContext, TEST_SUB_ID, null, 0, mMockRcsStats);
         mHandler = mTestGbaManager.getHandler();
         try {
             mLooper = new TestableLooper(mHandler.getLooper());
@@ -216,6 +221,45 @@
         assertTrue(!mTestGbaManager.isServiceConnected());
     }
 
+    @Test
+    @SmallTest
+    public void testMetricsGbaEvent() throws Exception {
+        mTestGbaManager.overrideServicePackage(TEST_DEFAULT_SERVICE_NAME.getPackageName());
+        mTestGbaManager.overrideReleaseTime(RELEASE_NEVER);
+
+        mLooper.processAllMessages();
+        bindAndConnectService(TEST_DEFAULT_SERVICE_NAME);
+        GbaAuthRequest request = createDefaultRequest();
+
+        // Failure case
+        mTestGbaManager.bootstrapAuthenticationRequest(request);
+        mLooper.processAllMessages();
+
+        ArgumentCaptor<GbaAuthRequest> captor = ArgumentCaptor.forClass(GbaAuthRequest.class);
+        verify(mMockGbaServiceBinder, times(1)).authenticationRequest(captor.capture());
+
+        GbaAuthRequest capturedRequest = captor.getValue();
+        IBootstrapAuthenticationCallback callback = capturedRequest.getCallback();
+        callback.onAuthenticationFailure(capturedRequest.getToken(),
+                GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY);
+
+        verify(mMockRcsStats).onGbaFailureEvent(anyInt(),
+                eq(GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY));
+
+        // Success case
+        mTestGbaManager.bootstrapAuthenticationRequest(request);
+        mLooper.processAllMessages();
+
+        ArgumentCaptor<GbaAuthRequest> captor2 = ArgumentCaptor.forClass(GbaAuthRequest.class);
+        verify(mMockGbaServiceBinder, times(2)).authenticationRequest(captor2.capture());
+
+        GbaAuthRequest capturedRequest2 = captor2.getValue();
+        IBootstrapAuthenticationCallback callback2 = capturedRequest2.getCallback();
+        callback2.onKeysAvailable(capturedRequest2.getToken(), "".getBytes(), "");
+
+        verify(mMockRcsStats).onGbaSuccessEvent(anyInt());
+    }
+
     private ServiceConnection bindAndConnectService(ComponentName component) {
         ServiceConnection connection = bindService(component);
         IGbaService.Stub serviceStub = mock(IGbaService.Stub.class);
diff --git a/tests/telephonytests/src/com/android/internal/telephony/NetworkTypeControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/NetworkTypeControllerTest.java
index cbb3ba0..bdd35f1 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/NetworkTypeControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/NetworkTypeControllerTest.java
@@ -919,6 +919,36 @@
     }
 
     @Test
+    public void testPrimaryTimerReset_theNetworkModeWithoutNr() throws Exception {
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType();
+        doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
+        mBundle.putString(CarrierConfigManager.KEY_5G_ICON_DISPLAY_GRACE_PERIOD_STRING,
+                "connected_mmwave,any,10;connected,any,10;not_restricted_rrc_con,any,10");
+        broadcastCarrierConfigs();
+
+        assertEquals("connected", getCurrentState().getName());
+        assertEquals(TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA,
+                mNetworkTypeController.getOverrideNetworkType());
+
+        // remove NR from preferred network types
+        doReturn(RadioAccessFamily.getRafFromNetworkType(
+                TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA)).when(
+                mPhone).getCachedAllowedNetworkTypesBitmask();
+
+        // trigger 10 second timer after disconnecting from NR, and then it does the timer reset
+        // since the network mode without the NR capability.
+        doReturn(NetworkRegistrationInfo.NR_STATE_NONE).when(mServiceState).getNrState();
+        mNetworkTypeController.sendMessage(EVENT_NR_STATE_CHANGED);
+        processAllMessages();
+
+        // timer should be reset.
+        assertEquals("legacy", getCurrentState().getName());
+        assertEquals(TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE,
+                mNetworkTypeController.getOverrideNetworkType());
+        assertFalse(mNetworkTypeController.is5GHysteresisActive());
+    }
+
+    @Test
     public void testPrimaryTimerExpireMmwave() throws Exception {
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType();
         doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
diff --git a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosBearerSessionTest.java b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosBearerSessionTest.java
new file mode 100644
index 0000000..2e15d3b
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosBearerSessionTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.dataconnection;
+
+import static com.android.internal.telephony.dataconnection.QosCallbackTrackerTest.createEpsQos;
+import static com.android.internal.telephony.dataconnection.QosCallbackTrackerTest.createIpv4QosFilter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.telephony.data.QosBearerFilter;
+import android.telephony.data.QosBearerSession;
+
+import com.android.internal.telephony.TelephonyTest;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+
+public class QosBearerSessionTest extends TelephonyTest {
+
+    @Test
+    public void testParcel() {
+        ArrayList<QosBearerFilter> qosFilters = new ArrayList<>();
+        qosFilters.add(createIpv4QosFilter("122.22.22.22",
+                new QosBearerFilter.PortRange(2222, 2222), 45));
+        QosBearerSession qosBearerSession = new QosBearerSession(1235,
+                createEpsQos(5, 6, 7, 8), qosFilters);
+
+        Parcel p = Parcel.obtain();
+        qosBearerSession.writeToParcel(p, 0);
+        p.setDataPosition(0);
+
+        QosBearerSession qosBearerSession2 = QosBearerSession.CREATOR.createFromParcel(p);
+        assertThat(qosBearerSession).isEqualTo(qosBearerSession2);
+    }
+
+    @Test
+    public void testEquals() {
+        ArrayList<QosBearerFilter> qosFilters = new ArrayList<>();
+        qosFilters.add(createIpv4QosFilter("122.22.22.22",
+                new QosBearerFilter.PortRange(2222, 2222), 45));
+        QosBearerSession qosBearerSession = new QosBearerSession(1235,
+                createEpsQos(5, 6, 7, 8), qosFilters);
+
+        ArrayList<QosBearerFilter> qosFilters2 = new ArrayList<>();
+        qosFilters2.add(createIpv4QosFilter("122.22.22.22",
+                new QosBearerFilter.PortRange(2222, 2222), 45));
+        QosBearerSession qosBearerSession2 = new QosBearerSession(1235,
+                createEpsQos(5, 6, 7, 8), qosFilters2);
+
+        assertThat(qosBearerSession).isEqualTo(qosBearerSession2);
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosCallbackTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosCallbackTrackerTest.java
index 5a8b540..62bf6e5 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosCallbackTrackerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/QosCallbackTrackerTest.java
@@ -16,13 +16,10 @@
 
 package com.android.internal.telephony.dataconnection;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -32,31 +29,28 @@
 import android.net.InetAddresses;
 import android.net.LinkAddress;
 import android.net.Network;
-import android.net.NetworkAgent;
 import android.telephony.data.EpsBearerQosSessionAttributes;
 import android.telephony.data.EpsQos;
 import android.telephony.data.QosBearerFilter;
 import android.telephony.data.QosBearerSession;
-
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.TelephonyTest;
+import com.android.internal.telephony.metrics.RcsStats;
 
 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.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -94,9 +88,13 @@
     }
 
     @Mock
+    private Phone mPhone;
+    @Mock
     private DcNetworkAgent mDcNetworkAgent;
     @Mock
     private Network mNetwork;
+    @Mock
+    private RcsStats mRcsStats;
 
     private QosCallbackTracker mQosCallbackTracker;
 
@@ -105,7 +103,9 @@
         super.setUp(getClass().getSimpleName());
         doReturn(mNetwork).when(mDcNetworkAgent).getNetwork();
         doReturn(100).when(mNetwork).getNetId();
-        mQosCallbackTracker = new QosCallbackTracker(mDcNetworkAgent);
+        doReturn(0).when(mPhone).getPhoneId();
+        mQosCallbackTracker = new QosCallbackTracker(mDcNetworkAgent, mPhone.getPhoneId(),
+                mRcsStats);
         processAllMessages();
     }
 
@@ -114,7 +114,7 @@
         super.tearDown();
     }
 
-    private EpsQos createEpsQos(int dlMbr, int ulMbr, int dlGbr, int ulGbr) {
+    public static EpsQos createEpsQos(int dlMbr, int ulMbr, int dlGbr, int ulGbr) {
         // Build android.hardware.radio.V1_6.EpsQos
         android.hardware.radio.V1_6.EpsQos halEpsQos = new android.hardware.radio.V1_6.EpsQos();
         halEpsQos.qci = 4;
@@ -126,7 +126,7 @@
         return new EpsQos(halEpsQos);
     }
 
-    private QosBearerFilter createIpv4QosFilter(String localAddress,
+    public static QosBearerFilter createIpv4QosFilter(String localAddress,
             QosBearerFilter.PortRange localPort, int precedence) {
         return new QosBearerFilter(
                 Arrays.asList(
@@ -135,7 +135,7 @@
                 7, 987, 678, QosBearerFilter.QOS_FILTER_DIRECTION_BIDIRECTIONAL, precedence);
     }
 
-    private QosBearerFilter createIpv4QosFilter(String localAddress, String remoteAddress,
+    public static QosBearerFilter createIpv4QosFilter(String localAddress, String remoteAddress,
             QosBearerFilter.PortRange localPort, QosBearerFilter.PortRange remotePort,
             int precedence) {
         return new QosBearerFilter(
@@ -486,5 +486,42 @@
                 eq(1235), any(EpsBearerQosSessionAttributes.class));
 
     }
+
+    @Test
+    @SmallTest
+    public void testQosMetrics() throws Exception {
+        final int callbackId = 1;
+        final int slotId = mPhone.getPhoneId();
+        // Add filter before update session
+        Filter filter1 = new Filter(new InetSocketAddress(
+                InetAddresses.parseNumericAddress("155.55.55.55"), 2222),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("144.44.44.44"), 2223));
+
+        mQosCallbackTracker.addFilter(callbackId, filter1);
+        verify(mRcsStats, never()).onImsDedicatedBearerListenerAdded(eq(callbackId), eq(slotId),
+                anyInt(), anyInt());
+
+        // QosBearerFilter
+        ArrayList<QosBearerFilter> qosFilters1 = new ArrayList<>();
+        qosFilters1.add(createIpv4QosFilter("155.55.55.55", "144.44.44.44",
+                new QosBearerFilter.PortRange(2222, 2222),
+                new QosBearerFilter.PortRange(2223, 2223), 45));
+
+        ArrayList<QosBearerSession> qosSessions = new ArrayList<>();
+        qosSessions.add(new QosBearerSession(1234, createEpsQos(5, 6, 7, 8), qosFilters1));
+        mQosCallbackTracker.updateSessions(qosSessions);
+
+        verify(mRcsStats, times(1))
+                .onImsDedicatedBearerListenerUpdateSession(
+                        eq(callbackId), eq(slotId), anyInt(), anyInt(), eq(true));
+
+        mQosCallbackTracker.addFilter(callbackId, filter1);
+        verify(mRcsStats, times(1)).onImsDedicatedBearerListenerAdded(
+                anyInt(), anyInt(), anyInt(), anyInt());
+
+        mQosCallbackTracker.removeFilter(callbackId);
+        verify(mRcsStats, times(1))
+                .onImsDedicatedBearerListenerRemoved(callbackId);
+    }
 }
 
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/PersistAtomsStorageTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/PersistAtomsStorageTest.java
index f200a0f..a9b056f 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/metrics/PersistAtomsStorageTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/PersistAtomsStorageTest.java
@@ -16,6 +16,12 @@
 
 package com.android.internal.telephony.metrics;
 
+import static com.android.internal.telephony.TelephonyStatsLog.GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY;
+import static com.android.internal.telephony.TelephonyStatsLog.GBA_EVENT__FAILED_REASON__UNKNOWN;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__CLIENT_PARAMS_SENT;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MO;
@@ -41,16 +47,32 @@
 import android.telephony.DisconnectCause;
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
+import android.telephony.TelephonyProtoEnums;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.SipDelegateManager;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.internal.telephony.TelephonyStatsLog;
 import com.android.internal.telephony.TelephonyTest;
 import com.android.internal.telephony.nano.PersistAtomsProto.CellularDataServiceSwitch;
 import com.android.internal.telephony.nano.PersistAtomsProto.CellularServiceState;
 import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.GbaEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerListenerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationServiceDescStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms;
+import com.android.internal.telephony.nano.PersistAtomsProto.PresenceNotifyEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsAcsProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsClientProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipDelegateStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipMessageResponse;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.UceEventStats;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
 import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.AudioCodec;
@@ -80,6 +102,10 @@
     private static final int CARRIER1_ID = 1;
     private static final int CARRIER2_ID = 1187;
     private static final int CARRIER3_ID = 1435;
+    private static final int SLOT_ID1 = 1;
+    private static final int SLOT_ID2 = 2;
+    private static final int REGISTRATION1_TECH = 1;
+    private static final int REGISTRATION2_TECH = 2;
 
     @Mock private FileOutputStream mTestFileOutputStream;
 
@@ -135,6 +161,64 @@
     private DataCallSession mDataCallSession0;
     private DataCallSession mDataCallSession1;
 
+    // RCS registration feature tags for slot 0 and 1
+    private ImsRegistrationFeatureTagStats mImsRegistrationFeatureTagStats1Proto;
+    private ImsRegistrationFeatureTagStats mImsRegistrationFeatureTagStats2Proto;
+    private ImsRegistrationFeatureTagStats[] mImsRegistrationFeatureTagStatses;
+
+    // RCS provisioning client stats for slot 0 and 1
+    private RcsClientProvisioningStats mRcsClientProvisioningStats1Proto;
+    private RcsClientProvisioningStats mRcsClientProvisioningStats2Proto;
+    private RcsClientProvisioningStats[] mRcsClientProvisioningStatses;
+
+    // RCS provisioning ACS stats for slot 0 and 1
+    private RcsAcsProvisioningStats mRcsAcsProvisioningStats1Proto;
+    private RcsAcsProvisioningStats mRcsAcsProvisioningStats2Proto;
+    private RcsAcsProvisioningStats[] mRcsAcsProvisioningStatses;
+
+    private ImsRegistrationServiceDescStats mImsRegistrationServiceIm;
+    private ImsRegistrationServiceDescStats mImsRegistrationServiceFt;
+    private ImsRegistrationServiceDescStats[] mImsRegistrationServiceDescStats;
+
+    // IMS dedicated bearer listener event stats for slot 0 and 1
+    private ImsDedicatedBearerListenerEvent mImsDedicatedBearerListenerEvent1;
+    private ImsDedicatedBearerListenerEvent mImsDedicatedBearerListenerEvent2;
+    private ImsDedicatedBearerListenerEvent[] mImsDedicatedBearerListenerEvents;
+
+    // IMS dedicated bearer event stats for slot 0 and 1
+    private ImsDedicatedBearerEvent mImsDedicatedBearerEvent1;
+    private ImsDedicatedBearerEvent mImsDedicatedBearerEvent2;
+    private ImsDedicatedBearerEvent[] mImsDedicatedBearerEvents;
+
+    private UceEventStats mUceEventStats1;
+    private UceEventStats mUceEventStats2;
+    private UceEventStats[] mUceEventStatses;
+
+    private PresenceNotifyEvent mPresenceNotifyEvent1;
+    private PresenceNotifyEvent mPresenceNotifyEvent2;
+    private PresenceNotifyEvent[] mPresenceNotifyEvents;
+
+    private SipTransportFeatureTagStats mSipTransportFeatureTagStats1;
+    private SipTransportFeatureTagStats mSipTransportFeatureTagStats2;
+    private SipTransportFeatureTagStats[] mSipTransportFeatureTagStatsArray;
+
+    private SipDelegateStats mSipDelegateStats1;
+    private SipDelegateStats mSipDelegateStats2;
+    private SipDelegateStats mSipDelegateStats3;
+    private SipDelegateStats[] mSipDelegateStatsArray;
+
+    private GbaEvent mGbaEvent1;
+    private GbaEvent mGbaEvent2;
+    private GbaEvent[] mGbaEvent;
+
+    private SipMessageResponse mSipMessageResponse1;
+    private SipMessageResponse mSipMessageResponse2;
+    private SipMessageResponse[] mSipMessageResponse;
+
+    private SipTransportSession mSipTransportSession1;
+    private SipTransportSession mSipTransportSession2;
+    private SipTransportSession[] mSipTransportSession;
+
     private void makeTestData() {
         // MO call with SRVCC (LTE to UMTS)
         mCall1Proto = new VoiceCallSession();
@@ -455,6 +539,309 @@
         mDataCallSession1.setupFailed = false;
         mDataCallSession1.durationMinutes = 5;
         mDataCallSession1.ongoing = false;
+
+        // RCS registrtion feature tag slot 0
+        mImsRegistrationFeatureTagStats1Proto = new ImsRegistrationFeatureTagStats();
+        mImsRegistrationFeatureTagStats1Proto.carrierId = CARRIER1_ID;
+        mImsRegistrationFeatureTagStats1Proto.slotId = 0;
+        mImsRegistrationFeatureTagStats1Proto.featureTagName = 1;
+        mImsRegistrationFeatureTagStats1Proto.registrationTech = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsRegistrationFeatureTagStats1Proto.registeredMillis = 3600L;
+
+        // RCS registrtion feature tag slot 1
+        mImsRegistrationFeatureTagStats2Proto = new ImsRegistrationFeatureTagStats();
+        mImsRegistrationFeatureTagStats2Proto.carrierId = CARRIER2_ID;
+        mImsRegistrationFeatureTagStats2Proto.slotId = 1;
+        mImsRegistrationFeatureTagStats2Proto.featureTagName = 0;
+        mImsRegistrationFeatureTagStats2Proto.registrationTech = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsRegistrationFeatureTagStats2Proto.registeredMillis = 3600L;
+
+        mImsRegistrationFeatureTagStatses =
+                new ImsRegistrationFeatureTagStats[] {
+                        mImsRegistrationFeatureTagStats1Proto,
+                        mImsRegistrationFeatureTagStats2Proto
+                };
+
+        // RCS client provisioning stats slot 0
+        mRcsClientProvisioningStats1Proto = new RcsClientProvisioningStats();
+        mRcsClientProvisioningStats1Proto.carrierId = CARRIER1_ID;
+        mRcsClientProvisioningStats1Proto.slotId = 0;
+        mRcsClientProvisioningStats1Proto.event =
+                RCS_CLIENT_PROVISIONING_STATS__EVENT__CLIENT_PARAMS_SENT;
+        mRcsClientProvisioningStats1Proto.count = 1;
+
+        // RCS client provisioning stats slot 1
+        mRcsClientProvisioningStats2Proto = new RcsClientProvisioningStats();
+        mRcsClientProvisioningStats2Proto.carrierId = CARRIER2_ID;
+        mRcsClientProvisioningStats2Proto.slotId = 1;
+        mRcsClientProvisioningStats2Proto.event =
+                RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION;
+        mRcsClientProvisioningStats2Proto.count = 1;
+
+        mRcsClientProvisioningStatses =
+                new RcsClientProvisioningStats[] {
+                        mRcsClientProvisioningStats1Proto,
+                        mRcsClientProvisioningStats2Proto
+                };
+
+        // RCS ACS provisioning stats : error response
+        mRcsAcsProvisioningStats1Proto = new RcsAcsProvisioningStats();
+        mRcsAcsProvisioningStats1Proto.carrierId = CARRIER1_ID;
+        mRcsAcsProvisioningStats1Proto.slotId = 0;
+        mRcsAcsProvisioningStats1Proto.responseCode = 401;
+        mRcsAcsProvisioningStats1Proto.responseType =
+                RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR;
+        mRcsAcsProvisioningStats1Proto.isSingleRegistrationEnabled = true;
+        mRcsAcsProvisioningStats1Proto.count = 1;
+        mRcsAcsProvisioningStats1Proto.stateTimerMillis = START_TIME_MILLIS;
+
+        // RCS ACS provisioning stats : xml
+        mRcsAcsProvisioningStats2Proto = new RcsAcsProvisioningStats();
+        mRcsAcsProvisioningStats2Proto.carrierId = CARRIER1_ID;
+        mRcsAcsProvisioningStats2Proto.slotId = 0;
+        mRcsAcsProvisioningStats2Proto.responseCode = 200;
+        mRcsAcsProvisioningStats2Proto.responseType =
+                RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML;
+        mRcsAcsProvisioningStats2Proto.isSingleRegistrationEnabled = true;
+        mRcsAcsProvisioningStats2Proto.count = 1;
+        mRcsAcsProvisioningStats2Proto.stateTimerMillis = START_TIME_MILLIS;
+
+        mRcsAcsProvisioningStatses =
+                new RcsAcsProvisioningStats[] {
+                        mRcsAcsProvisioningStats1Proto,
+                        mRcsAcsProvisioningStats2Proto
+                };
+
+        mImsRegistrationServiceIm = new ImsRegistrationServiceDescStats();
+        mImsRegistrationServiceIm.carrierId = CARRIER1_ID;
+        mImsRegistrationServiceIm.slotId = SLOT_ID1;
+        mImsRegistrationServiceIm.serviceIdName = 0;
+        mImsRegistrationServiceIm.serviceIdVersion = 1.0f;
+        mImsRegistrationServiceIm.registrationTech = REGISTRATION1_TECH;
+        mImsRegistrationServiceIm.publishedMillis = START_TIME_MILLIS;
+
+        mImsRegistrationServiceFt = new ImsRegistrationServiceDescStats();
+        mImsRegistrationServiceFt.carrierId = CARRIER2_ID;
+        mImsRegistrationServiceFt.slotId = SLOT_ID2;
+        mImsRegistrationServiceFt.serviceIdName = 1;
+        mImsRegistrationServiceFt.serviceIdVersion = 2.0f;
+        mImsRegistrationServiceFt.registrationTech = REGISTRATION2_TECH;
+        mImsRegistrationServiceIm.publishedMillis = START_TIME_MILLIS;
+
+        mImsRegistrationServiceDescStats =
+            new ImsRegistrationServiceDescStats[] {
+                mImsRegistrationServiceIm, mImsRegistrationServiceFt
+            };
+
+
+        mImsDedicatedBearerListenerEvent1 = new ImsDedicatedBearerListenerEvent();
+        mImsDedicatedBearerListenerEvent1.carrierId = CARRIER1_ID;
+        mImsDedicatedBearerListenerEvent1.slotId = SLOT_ID1;
+        mImsDedicatedBearerListenerEvent1.ratAtEnd = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsDedicatedBearerListenerEvent1.qci = 5;
+        mImsDedicatedBearerListenerEvent1.dedicatedBearerEstablished = true;
+        mImsDedicatedBearerListenerEvent1.eventCount = 1;
+
+        mImsDedicatedBearerListenerEvent2 = new ImsDedicatedBearerListenerEvent();
+        mImsDedicatedBearerListenerEvent2.carrierId = CARRIER2_ID;
+        mImsDedicatedBearerListenerEvent2.slotId = SLOT_ID1;
+        mImsDedicatedBearerListenerEvent2.ratAtEnd = TelephonyManager.NETWORK_TYPE_NR;
+        mImsDedicatedBearerListenerEvent2.qci = 6;
+        mImsDedicatedBearerListenerEvent2.dedicatedBearerEstablished = true;
+        mImsDedicatedBearerListenerEvent2.eventCount = 1;
+
+        mImsDedicatedBearerListenerEvents =
+                new ImsDedicatedBearerListenerEvent[] {
+                    mImsDedicatedBearerListenerEvent1, mImsDedicatedBearerListenerEvent2
+                };
+
+
+        mImsDedicatedBearerEvent1 = new ImsDedicatedBearerEvent();
+        mImsDedicatedBearerEvent1.carrierId = CARRIER1_ID;
+        mImsDedicatedBearerEvent1.slotId = SLOT_ID1;
+        mImsDedicatedBearerEvent1.ratAtEnd = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsDedicatedBearerEvent1.qci = 5;
+        mImsDedicatedBearerEvent1.bearerState =
+                TelephonyStatsLog.IMS_DEDICATED_BEARER_EVENT__BEARER_STATE__STATE_ADDED;
+        mImsDedicatedBearerEvent1.localConnectionInfoReceived = true;
+        mImsDedicatedBearerEvent1.remoteConnectionInfoReceived = true;
+        mImsDedicatedBearerEvent1.hasListeners = true;
+        mImsDedicatedBearerEvent1.count = 1;
+
+        mImsDedicatedBearerEvent2 = new ImsDedicatedBearerEvent();
+        mImsDedicatedBearerEvent2.carrierId = CARRIER1_ID;
+        mImsDedicatedBearerEvent2.slotId = SLOT_ID1;
+        mImsDedicatedBearerEvent2.ratAtEnd = TelephonyManager.NETWORK_TYPE_NR;
+        mImsDedicatedBearerEvent2.qci = 6;
+        mImsDedicatedBearerEvent2.bearerState =
+                TelephonyStatsLog.IMS_DEDICATED_BEARER_EVENT__BEARER_STATE__STATE_MODIFIED;
+        mImsDedicatedBearerEvent2.localConnectionInfoReceived = true;
+        mImsDedicatedBearerEvent2.remoteConnectionInfoReceived = true;
+        mImsDedicatedBearerEvent2.hasListeners = true;
+        mImsDedicatedBearerEvent2.count = 1;
+
+        mImsDedicatedBearerEvents =
+                new ImsDedicatedBearerEvent[] {
+                    mImsDedicatedBearerEvent1, mImsDedicatedBearerEvent2
+                };
+
+
+        mUceEventStats1 = new UceEventStats();
+        mUceEventStats1.carrierId = CARRIER1_ID;
+        mUceEventStats1.slotId = SLOT_ID1;
+        mUceEventStats1.type = 1;
+        mUceEventStats1.successful = true;
+        mUceEventStats1.commandCode = 0;
+        mUceEventStats1.networkResponse = 200;
+        mUceEventStats1.count = 1;
+
+        mUceEventStats2 = new UceEventStats();
+        mUceEventStats2.carrierId = CARRIER2_ID;
+        mUceEventStats2.slotId = SLOT_ID2;
+        mUceEventStats2.type = 2;
+        mUceEventStats2.successful = false;
+        mUceEventStats2.commandCode = 2;
+        mUceEventStats2.networkResponse = 0;
+        mUceEventStats2.count = 1;
+        mUceEventStatses = new UceEventStats[] {mUceEventStats1, mUceEventStats2};
+
+        mPresenceNotifyEvent1 = new PresenceNotifyEvent();
+        mPresenceNotifyEvent1.carrierId = CARRIER1_ID;
+        mPresenceNotifyEvent1.slotId = SLOT_ID1;
+        mPresenceNotifyEvent1.reason = 1;
+        mPresenceNotifyEvent1.contentBodyReceived = true;
+        mPresenceNotifyEvent1.rcsCapsCount = 1;
+        mPresenceNotifyEvent1.mmtelCapsCount = 1;
+        mPresenceNotifyEvent1.noCapsCount = 0;
+        mPresenceNotifyEvent1.count = 1;
+
+        mPresenceNotifyEvent2 = new PresenceNotifyEvent();
+        mPresenceNotifyEvent2.carrierId = CARRIER2_ID;
+        mPresenceNotifyEvent2.slotId = SLOT_ID2;
+        mPresenceNotifyEvent2.reason = 1;
+        mPresenceNotifyEvent2.contentBodyReceived = false;
+        mPresenceNotifyEvent2.rcsCapsCount = 0;
+        mPresenceNotifyEvent2.mmtelCapsCount = 0;
+        mPresenceNotifyEvent2.noCapsCount = 1;
+        mPresenceNotifyEvent2.count = 1;
+        mPresenceNotifyEvents = new PresenceNotifyEvent[] {mPresenceNotifyEvent1,
+                mPresenceNotifyEvent2};
+
+        //A destroyed SipDelegate
+        mSipDelegateStats1 = new SipDelegateStats();
+        mSipDelegateStats1.carrierId = CARRIER1_ID;
+        mSipDelegateStats1.slotId = SLOT_ID1;
+        mSipDelegateStats1.uptimeMillis = 1000L;
+        mSipDelegateStats1.destroyReason =
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP;
+
+        //An active SipDelegate
+        mSipDelegateStats2 = new SipDelegateStats();
+        mSipDelegateStats2.carrierId = CARRIER1_ID;
+        mSipDelegateStats2.slotId = SLOT_ID1;
+        mSipDelegateStats2.uptimeMillis = 1000L;
+        mSipDelegateStats2.destroyReason =
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD;
+
+        //An active SipDelegate
+        mSipDelegateStats3 = new SipDelegateStats();
+        mSipDelegateStats3.carrierId = CARRIER2_ID;
+        mSipDelegateStats3.slotId = SLOT_ID2;
+        mSipDelegateStats3.uptimeMillis = 3000L;
+        mSipDelegateStats3.destroyReason =
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN;
+
+        //A registered SipTransportFeatureTag
+        mSipTransportFeatureTagStats1 = new SipTransportFeatureTagStats();
+        mSipTransportFeatureTagStats1.carrierId = CARRIER1_ID;
+        mSipTransportFeatureTagStats1.slotId = SLOT_ID1;
+        mSipTransportFeatureTagStats1.featureTagName = TelephonyProtoEnums.IMS_FEATURE_TAG_CHAT_IM;
+        mSipTransportFeatureTagStats1.sipTransportDeniedReason =  RcsStats.NONE;
+        mSipTransportFeatureTagStats1.sipTransportDeregisteredReason = RcsStats.NONE;
+        mSipTransportFeatureTagStats1.associatedMillis = 1000L;
+
+        //A denied SipTransportFeatureTag
+        mSipTransportFeatureTagStats2 = new SipTransportFeatureTagStats();
+        mSipTransportFeatureTagStats2.carrierId = CARRIER1_ID;
+        mSipTransportFeatureTagStats2.slotId = SLOT_ID1;
+        mSipTransportFeatureTagStats2.featureTagName = 1;
+        mSipTransportFeatureTagStats2.sipTransportDeniedReason =
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE;
+        mSipTransportFeatureTagStats2.sipTransportDeregisteredReason = RcsStats.NONE;
+        mSipTransportFeatureTagStats2.associatedMillis = 1000L;
+
+        mSipDelegateStatsArray = new SipDelegateStats[]{mSipDelegateStats2, mSipDelegateStats3};
+
+        mSipTransportFeatureTagStatsArray = new SipTransportFeatureTagStats[]
+                {mSipTransportFeatureTagStats1, mSipTransportFeatureTagStats2};
+
+        mGbaEvent1 = new GbaEvent();
+        mGbaEvent1.carrierId = CARRIER1_ID;
+        mGbaEvent1.slotId = SLOT_ID1;
+        mGbaEvent1.successful = true;
+        mGbaEvent1.failedReason = GBA_EVENT__FAILED_REASON__UNKNOWN;
+        mGbaEvent1.count = 1;
+
+        mGbaEvent2 = new GbaEvent();
+        mGbaEvent2.carrierId = CARRIER2_ID;
+        mGbaEvent2.slotId = SLOT_ID2;
+        mGbaEvent2.successful = false;
+        mGbaEvent2.failedReason = GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY;
+        mGbaEvent2.count = 1;
+
+        mGbaEvent = new GbaEvent[] {mGbaEvent1, mGbaEvent2};
+
+        //stats slot 0
+        mSipMessageResponse1 = new SipMessageResponse();
+        mSipMessageResponse1.carrierId = CARRIER1_ID;
+        mSipMessageResponse1.slotId = SLOT_ID1;
+        //"INVITE"
+        mSipMessageResponse1.sipMessageMethod = 2;
+        mSipMessageResponse1.sipMessageResponse = 200;
+        mSipMessageResponse1.sipMessageDirection = 1;
+        mSipMessageResponse1.messageError = 0;
+        mSipMessageResponse1.count = 1;
+
+        //stats slot 1
+        mSipMessageResponse2 = new SipMessageResponse();
+        mSipMessageResponse2.carrierId = CARRIER2_ID;
+        mSipMessageResponse2.slotId = SLOT_ID2;
+        //"INVITE"
+        mSipMessageResponse2.sipMessageMethod = 2;
+        mSipMessageResponse2.sipMessageResponse = 200;
+        mSipMessageResponse2.sipMessageDirection = 0;
+        mSipMessageResponse2.messageError = 0;
+        mSipMessageResponse2.count = 1;
+
+        mSipMessageResponse =
+                new SipMessageResponse[] {mSipMessageResponse1, mSipMessageResponse2};
+
+        // stats slot 0
+        mSipTransportSession1 = new SipTransportSession();
+        mSipTransportSession1.carrierId = CARRIER1_ID;
+        mSipTransportSession1.slotId = SLOT_ID1;
+        //"INVITE"
+        mSipTransportSession1.sessionMethod = 2;
+        mSipTransportSession1.sipMessageDirection = 1;
+        mSipTransportSession1.sipResponse = 200;
+        mSipTransportSession1.sessionCount = 1;
+        mSipTransportSession1.endedGracefullyCount = 1;
+        mSipTransportSession1.isEndedGracefully = true;
+
+        // stats slot 1
+        mSipTransportSession2 = new SipTransportSession();
+        mSipTransportSession2.carrierId = CARRIER2_ID;
+        mSipTransportSession2.slotId = SLOT_ID2;
+        //"INVITE"
+        mSipTransportSession2.sessionMethod = 2;
+        mSipTransportSession2.sipMessageDirection = 0;
+        mSipTransportSession2.sipResponse = 200;
+        mSipTransportSession2.sessionCount = 1;
+        mSipTransportSession2.endedGracefullyCount = 1;
+        mSipTransportSession2.isEndedGracefully = true;
+
+        mSipTransportSession =
+                new SipTransportSession[] {mSipTransportSession1, mSipTransportSession2};
     }
 
     private static class TestablePersistAtomsStorage extends PersistAtomsStorage {
@@ -1311,12 +1698,32 @@
         verifyCurrentStateSavedToFileOnce();
         DataCallSession[] dataCalls = mPersistAtomsStorage.getDataCallSessions(0L);
         assertProtoArrayEqualsIgnoringOrder(
-                new DataCallSession[] {mDataCallSession0, mDataCallSession1},
+                new DataCallSession[]{mDataCallSession0, mDataCallSession1},
                 dataCalls);
     }
 
     @Test
     @SmallTest
+    public void addImsRegistrationFeatureTagStats_emptyProto() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage
+                .addImsRegistrationFeatureTagStats(mImsRegistrationFeatureTagStats1Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+
+        ImsRegistrationFeatureTagStats[] expected =
+                new ImsRegistrationFeatureTagStats[] {
+                        mImsRegistrationFeatureTagStats1Proto
+                };
+        assertProtoArrayEquals(
+                expected, mPersistAtomsStorage.getImsRegistrationFeatureTagStats(0L));
+    }
+
+    @Test
+    @SmallTest
     public void addDataCallSession_existingEntry()
             throws Exception {
         createEmptyTestFile();
@@ -1340,7 +1747,1227 @@
         verifyCurrentStateSavedToFileOnce();
         DataCallSession[] dataCalls = mPersistAtomsStorage.getDataCallSessions(0L);
         assertProtoArrayEqualsIgnoringOrder(
-                new DataCallSession[] {totalDataCallSession0}, dataCalls);
+                new DataCallSession[]{totalDataCallSession0}, dataCalls);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationFeatureTagStats_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage
+                .addImsRegistrationFeatureTagStats(mImsRegistrationFeatureTagStats1Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        mPersistAtomsStorage
+                .addImsRegistrationFeatureTagStats(mImsRegistrationFeatureTagStats2Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+
+        ImsRegistrationFeatureTagStats[] expected =
+                new ImsRegistrationFeatureTagStats[] {
+                        mImsRegistrationFeatureTagStats1Proto,
+                        mImsRegistrationFeatureTagStats2Proto
+                };
+        // 2 atoms stored on initially and when try to add 2 same atoms, should be increased.
+        assertProtoArrayEqualsIgnoringOrder(
+                expected, mPersistAtomsStorage.getImsRegistrationFeatureTagStats(0L));
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationFeatureTagStats_tooManyEntries() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        int maxCount = 10;
+        for (int i = 0; i < maxCount; i++) {
+            mPersistAtomsStorage
+                    .addImsRegistrationFeatureTagStats(
+                            copyOf(mImsRegistrationFeatureTagStats1Proto));
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+
+        mPersistAtomsStorage
+                .addImsRegistrationFeatureTagStats(copyOf(mImsRegistrationFeatureTagStats2Proto));
+
+        verifyCurrentStateSavedToFileOnce();
+
+        ImsRegistrationFeatureTagStats[] result =
+                mPersistAtomsStorage.getImsRegistrationFeatureTagStats(0L);
+
+        // tried store 26 statses, but only 2 statses stored
+        // total time 3600L * maxCount
+        assertHasStatsCountTime(result, mImsRegistrationFeatureTagStats1Proto, 1,
+                maxCount * 3600L);
+        // total time 3600L * 1
+        assertHasStatsCountTime(result, mImsRegistrationFeatureTagStats2Proto, 1,
+                1 * 3600L);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationFeatureTagStats_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        ImsRegistrationFeatureTagStats[] result =
+                mPersistAtomsStorage.getImsRegistrationFeatureTagStats(100L);
+
+        // should be denied
+        assertNull(result);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationFeatureTagStats_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationFeatureTagStats[] statses1 =
+                mPersistAtomsStorage.getImsRegistrationFeatureTagStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationFeatureTagStats[] statses2 =
+                mPersistAtomsStorage.getImsRegistrationFeatureTagStats(50L);
+
+        // first results of get should have two atoms, second should be empty
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(mImsRegistrationFeatureTagStatses, statses1);
+        assertProtoArrayEquals(new ImsRegistrationFeatureTagStats[0], statses2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto()
+                        .imsRegistrationFeatureTagStatsPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).imsRegistrationFeatureTagStatsPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).imsRegistrationFeatureTagStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addRcsClientProvisioningStats_emptyProto() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage
+                .addRcsClientProvisioningStats(mRcsClientProvisioningStats1Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        mPersistAtomsStorage
+                .addRcsClientProvisioningStats(mRcsClientProvisioningStats2Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+
+        RcsClientProvisioningStats[] expected =
+                new RcsClientProvisioningStats[] {
+                        mRcsClientProvisioningStats1Proto,
+                        mRcsClientProvisioningStats2Proto
+                };
+
+        assertProtoArrayEqualsIgnoringOrder(
+                expected, mPersistAtomsStorage.getRcsClientProvisioningStats(0L));
+    }
+
+    @Test
+    @SmallTest
+    public void addRcsClientProvisioningStats_tooManyEntries() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // store 11 same atoms, but only 1 atoms stored with count 11
+        for (int i = 0; i < 11; i++) {
+            mPersistAtomsStorage
+                    .addRcsClientProvisioningStats(mRcsClientProvisioningStats1Proto);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        // store 1 different atom and count 1
+        mPersistAtomsStorage
+                .addRcsClientProvisioningStats(mRcsClientProvisioningStats2Proto);
+
+        verifyCurrentStateSavedToFileOnce();
+
+        RcsClientProvisioningStats[] result =
+                mPersistAtomsStorage.getRcsClientProvisioningStats(0L);
+
+        // first atom has count 11, the other has 1
+        assertHasStatsAndCount(result, mRcsClientProvisioningStats1Proto, 11);
+        assertHasStatsAndCount(result, mRcsClientProvisioningStats2Proto, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void getRcsClientProvisioningStats_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        RcsClientProvisioningStats[] result =
+                mPersistAtomsStorage.getRcsClientProvisioningStats(100L);
+
+        // should be denied
+        assertNull(result);
+    }
+
+    @Test
+    @SmallTest
+    public void getRcsClientProvisioningStats_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        RcsClientProvisioningStats[] statses1 =
+                mPersistAtomsStorage.getRcsClientProvisioningStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        RcsClientProvisioningStats[] statses2 =
+                mPersistAtomsStorage.getRcsClientProvisioningStats(50L);
+
+        // first results of get should have two atoms, second should be empty
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(mRcsClientProvisioningStatses, statses1);
+        assertProtoArrayEquals(new RcsClientProvisioningStats[0], statses2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto()
+                        .rcsClientProvisioningStatsPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).rcsClientProvisioningStatsPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).rcsClientProvisioningStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addRcsAcsProvisioningStats_emptyProto() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage
+                .addRcsAcsProvisioningStats(mRcsAcsProvisioningStats1Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        mPersistAtomsStorage
+                .addRcsAcsProvisioningStats(mRcsAcsProvisioningStats2Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+
+        RcsAcsProvisioningStats[] expected =
+                new RcsAcsProvisioningStats[] {
+                        mRcsAcsProvisioningStats1Proto,
+                        mRcsAcsProvisioningStats2Proto
+                };
+
+        assertProtoArrayEqualsIgnoringOrder(
+                expected, mPersistAtomsStorage.getRcsAcsProvisioningStats(0L));
+    }
+
+    @Test
+    @SmallTest
+    public void addRcsAcsProvisioningStats_updateExistingEntries() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // store 5 same atoms (1Proto), but only 1 atoms stored with count 5, total time 2000L * 5
+        // store 5 same atoms (2Proto), but only 1 atoms stored with count 5, total time 2000L * 5
+        for (int i = 0; i < 5; i++) {
+            mPersistAtomsStorage
+                    .addRcsAcsProvisioningStats(copyOf(mRcsAcsProvisioningStats1Proto));
+            mPersistAtomsStorage.incTimeMillis(100L);
+            mPersistAtomsStorage
+                    .addRcsAcsProvisioningStats(copyOf(mRcsAcsProvisioningStats2Proto));
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        // add one more atoms (2Proto), count 6, total time 2000L * 6
+        mPersistAtomsStorage
+                .addRcsAcsProvisioningStats(copyOf(mRcsAcsProvisioningStats2Proto));
+
+        verifyCurrentStateSavedToFileOnce();
+
+        RcsAcsProvisioningStats[] result =
+                mPersistAtomsStorage.getRcsAcsProvisioningStats(0L);
+
+        // atom (1Proto) : count = 5, time = 2000L * 5
+        assertHasStatsAndCountDuration(result, mRcsAcsProvisioningStats1Proto, 5, 2000L * 5);
+        // atom (2Proto) : count = 6, time = 2000L * 6
+        assertHasStatsAndCountDuration(result, mRcsAcsProvisioningStats2Proto, 6, 2000L * 6);
+    }
+
+    @Test
+    @SmallTest
+    public void getRcsAcsProvisioningStats_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        RcsAcsProvisioningStats[] result =
+                mPersistAtomsStorage.getRcsAcsProvisioningStats(100L);
+
+        // should be denied
+        assertNull(result);
+    }
+
+    @Test
+    @SmallTest
+    public void getRcsAcstProvisioningStats_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        RcsAcsProvisioningStats[] statses1 =
+                mPersistAtomsStorage.getRcsAcsProvisioningStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        RcsAcsProvisioningStats[] statses2 =
+                mPersistAtomsStorage.getRcsAcsProvisioningStats(50L);
+
+        // first results of get should have two atoms, second should be empty
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(mRcsAcsProvisioningStatses, statses1);
+        assertProtoArrayEquals(new RcsAcsProvisioningStats[0], statses2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto()
+                        .rcsAcsProvisioningStatsPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).rcsAcsProvisioningStatsPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).rcsAcsProvisioningStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationServiceDescStats_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsRegistrationServiceDescStats(mImsRegistrationServiceIm);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationServiceDescStats[] outputs =
+            mPersistAtomsStorage.getImsRegistrationServiceDescStats(0L);
+        assertProtoArrayEquals(
+                new ImsRegistrationServiceDescStats[] {mImsRegistrationServiceIm}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationServiceDescStats_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsRegistrationServiceDescStats(mImsRegistrationServiceIm);
+
+        mPersistAtomsStorage.addImsRegistrationServiceDescStats(mImsRegistrationServiceFt);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationServiceDescStats[] output =
+            mPersistAtomsStorage.getImsRegistrationServiceDescStats(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationServiceDescStats[] {
+                    mImsRegistrationServiceIm,
+                    mImsRegistrationServiceFt
+                },
+                output);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationServiceDescStats_tooManyServiceDesc() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        Queue<ImsRegistrationServiceDescStats> expectedOutput = new LinkedList<>();
+        // Add 101 registration terminations
+        for (int i = 0; i < 26 + 1; i++) {
+            ImsRegistrationServiceDescStats stats = copyOf(mImsRegistrationServiceIm);
+            stats.registrationTech = i;
+            expectedOutput.add(stats);
+            mPersistAtomsStorage.addImsRegistrationServiceDescStats(stats);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // The least recent (the first) registration termination should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationServiceDescStats[] output =
+            mPersistAtomsStorage.getImsRegistrationServiceDescStats(0L);
+        expectedOutput.remove();
+        assertEquals(expectedOutput.size() - 1, output.length);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationServiceDescStats_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        ImsRegistrationServiceDescStats[] output =
+            mPersistAtomsStorage.getImsRegistrationServiceDescStats(100L);
+
+        // should be denied
+        assertNull(output);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationServiceDescStats_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationServiceDescStats[] output1 =
+            mPersistAtomsStorage.getImsRegistrationServiceDescStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationServiceDescStats[] output2 =
+            mPersistAtomsStorage.getImsRegistrationServiceDescStats(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationServiceDescStats[] {
+                    mImsRegistrationServiceIm,
+                    mImsRegistrationServiceFt
+                },
+                output1);
+        assertProtoArrayEquals(new ImsRegistrationServiceDescStats[0], output2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto()
+                    .imsRegistrationServiceDescStatsPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).imsRegistrationServiceDescStatsPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).imsRegistrationServiceDescStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerListenerEvent_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsDedicatedBearerListenerEvent(mImsDedicatedBearerListenerEvent1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerListenerEvent[] outputs =
+                mPersistAtomsStorage.getImsDedicatedBearerListenerEvent(0L);
+        assertProtoArrayEquals(
+                new ImsDedicatedBearerListenerEvent[] {mImsDedicatedBearerListenerEvent1}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerListenerEvent_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsDedicatedBearerListenerEvent(mImsDedicatedBearerListenerEvent1);
+
+        mPersistAtomsStorage.addImsDedicatedBearerListenerEvent(mImsDedicatedBearerListenerEvent2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerListenerEvent[] output =
+                mPersistAtomsStorage.getImsDedicatedBearerListenerEvent(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsDedicatedBearerListenerEvent[] {
+                    mImsDedicatedBearerListenerEvent1, mImsDedicatedBearerListenerEvent2}, output);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerListenerEvent_withSameProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addImsDedicatedBearerListenerEvent(mImsDedicatedBearerListenerEvent1);
+        mPersistAtomsStorage.addImsDedicatedBearerListenerEvent(mImsDedicatedBearerListenerEvent1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // The least recent (the first) registration termination should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerListenerEvent[] output =
+                mPersistAtomsStorage.getImsDedicatedBearerListenerEvent(0L);
+        assertEquals(mImsDedicatedBearerListenerEvent1.carrierId, output[0].carrierId);
+        assertEquals(mImsDedicatedBearerListenerEvent1.slotId, output[0].slotId);
+        assertEquals(mImsDedicatedBearerListenerEvent1.ratAtEnd, output[0].ratAtEnd);
+        assertEquals(mImsDedicatedBearerListenerEvent1.qci, output[0].qci);
+        assertEquals(mImsDedicatedBearerListenerEvent1.dedicatedBearerEstablished,
+                output[0].dedicatedBearerEstablished);
+        assertEquals(mImsDedicatedBearerListenerEvent1.eventCount,
+                output[0].eventCount);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerEvent_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerEvent[] outputs =
+            mPersistAtomsStorage.getImsDedicatedBearerEvent(0L);
+        assertProtoArrayEquals(
+                new ImsDedicatedBearerEvent[] {mImsDedicatedBearerEvent1}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerEvent_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent1);
+
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerEvent[] output =
+            mPersistAtomsStorage.getImsDedicatedBearerEvent(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                    new ImsDedicatedBearerEvent[] {
+                        mImsDedicatedBearerEvent1, mImsDedicatedBearerEvent2}, output);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerEvent_withSameProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent1);
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // The least recent (the first) registration termination should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerEvent[] output =
+            mPersistAtomsStorage.getImsDedicatedBearerEvent(0L);
+        assertEquals(mImsDedicatedBearerEvent1.carrierId, output[0].carrierId);
+        assertEquals(mImsDedicatedBearerEvent1.slotId, output[0].slotId);
+        assertEquals(mImsDedicatedBearerEvent1.ratAtEnd, output[0].ratAtEnd);
+        assertEquals(mImsDedicatedBearerEvent1.qci, output[0].qci);
+        assertEquals(mImsDedicatedBearerEvent1.localConnectionInfoReceived,
+                output[0].localConnectionInfoReceived);
+        assertEquals(mImsDedicatedBearerEvent1.remoteConnectionInfoReceived,
+                output[0].remoteConnectionInfoReceived);
+        assertEquals(mImsDedicatedBearerEvent1.hasListeners,
+                output[0].hasListeners);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerEvent_tooManyEntries() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // Add 11 stats, but max is 10
+        for (int i = 0; i < 11; i++) {
+            mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent1);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(mImsDedicatedBearerEvent2);
+
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerEvent[] stats =
+            mPersistAtomsStorage.getImsDedicatedBearerEvent(0L);
+        assertHasStatsAndCount(stats, mImsDedicatedBearerEvent1, 11);
+        assertHasStatsAndCount(stats, mImsDedicatedBearerEvent2, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsDedicatedBearerEvent_updateExistingEntries() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        ImsDedicatedBearerEvent newStats = copyOf(mImsDedicatedBearerEvent1);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addImsDedicatedBearerEvent(copyOf(mImsDedicatedBearerEvent1));
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // mImsDedicatedBearerEvent1's count should be doubled
+        verifyCurrentStateSavedToFileOnce();
+        ImsDedicatedBearerEvent[] stats =
+            mPersistAtomsStorage.getImsDedicatedBearerEvent(0L);
+        newStats.count *= 2;
+        assertProtoArrayEqualsIgnoringOrder(new ImsDedicatedBearerEvent[] {
+                mImsDedicatedBearerEvent2, newStats}, stats);
+    }
+
+
+    @Test
+    @SmallTest
+    public void addUceEventStats_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addUceEventStats(mUceEventStats1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        UceEventStats[] outputs = mPersistAtomsStorage.getUceEventStats(0L);
+        assertProtoArrayEquals(
+                new UceEventStats[] {mUceEventStats1}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addUceEventStats_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addUceEventStats(mUceEventStats1);
+
+        mPersistAtomsStorage.addUceEventStats(mUceEventStats2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        UceEventStats[] output = mPersistAtomsStorage.getUceEventStats(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new UceEventStats[] {mUceEventStats1, mUceEventStats2}, output);
+    }
+
+    @Test
+    @SmallTest
+    public void addUceEventStats_withSameProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addUceEventStats(mUceEventStats1);
+        mPersistAtomsStorage.addUceEventStats(mUceEventStats1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // The least recent (the first) registration termination should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        UceEventStats[] output = mPersistAtomsStorage.getUceEventStats(0L);
+        assertEquals(mUceEventStats1.carrierId, output[0].carrierId);
+        assertEquals(mUceEventStats1.slotId, output[0].slotId);
+        assertEquals(mUceEventStats1.type, output[0].type);
+        assertEquals(mUceEventStats1.successful, output[0].successful);
+        assertEquals(mUceEventStats1.commandCode, output[0].commandCode);
+        assertEquals(mUceEventStats1.networkResponse, output[0].networkResponse);
+        assertEquals(2, output[0].count);
+    }
+
+    @Test
+    @SmallTest
+    public void addPresenceNotifyEvent_withSameProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        PresenceNotifyEvent event1 = new PresenceNotifyEvent();
+        event1.carrierId = CARRIER1_ID;
+        event1.slotId = SLOT_ID1;
+        event1.reason = 1;
+        event1.contentBodyReceived = true;
+        event1.rcsCapsCount = 1;
+        event1.mmtelCapsCount = 1;
+        event1.noCapsCount = 0;
+        event1.count = 1;
+
+        PresenceNotifyEvent event2 = copyOf(event1);
+        event2.rcsCapsCount = 0;
+
+        mPersistAtomsStorage.addPresenceNotifyEvent(event1);
+        mPersistAtomsStorage.addPresenceNotifyEvent(event2);
+
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // The least recent (the first) registration termination should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        PresenceNotifyEvent[] output = mPersistAtomsStorage.getPresenceNotifyEvent(0L);
+
+        assertEquals(event1.carrierId, output[0].carrierId);
+        assertEquals(event1.slotId, output[0].slotId);
+        assertEquals(event1.contentBodyReceived, output[0].contentBodyReceived);
+        assertEquals(1, output[0].rcsCapsCount);
+        assertEquals(2, output[0].mmtelCapsCount);
+        assertEquals(2, output[0].count);
+
+    }
+    @Test
+    @SmallTest
+    public void addPresenceNotifyEvent_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addPresenceNotifyEvent(mPresenceNotifyEvent1);
+        mPersistAtomsStorage.addPresenceNotifyEvent(mPresenceNotifyEvent2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        PresenceNotifyEvent[] output = mPersistAtomsStorage.getPresenceNotifyEvent(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new PresenceNotifyEvent[] {mPresenceNotifyEvent1, mPresenceNotifyEvent2}, output);
+    }
+
+    @Test
+    @SmallTest
+    public void getPresenceNotifyEvent_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        PresenceNotifyEvent[] output = mPersistAtomsStorage.getPresenceNotifyEvent(100L);
+
+        // should be denied
+        assertNull(output);
+    }
+
+    @Test
+    @SmallTest
+    public void getPresenceNotifyEvent_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        PresenceNotifyEvent[] output1 = mPersistAtomsStorage.getPresenceNotifyEvent(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        PresenceNotifyEvent[] output2 = mPersistAtomsStorage.getPresenceNotifyEvent(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new PresenceNotifyEvent[] {mPresenceNotifyEvent1, mPresenceNotifyEvent2}, output1);
+        assertProtoArrayEquals(new PresenceNotifyEvent[0], output2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto().presenceNotifyEventPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).presenceNotifyEventPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).presenceNotifyEventPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addSipTransportFeatureTag_emptyProto() throws Exception {
+        // verify add atom into new file
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipTransportFeatureTagStats(mSipTransportFeatureTagStats1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipTransportFeatureTagStats[] outputs =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(0L);
+        assertProtoArrayEquals(
+                new SipTransportFeatureTagStats[] {mSipTransportFeatureTagStats1}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipTransportFeatureTagStats_withExistingEntries() throws Exception {
+        // verify add atom on existing atom already stored
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        //Add two different SipTransportFeatureTagStats.
+        mPersistAtomsStorage.addSipTransportFeatureTagStats(mSipTransportFeatureTagStats1);
+        mPersistAtomsStorage.addSipTransportFeatureTagStats(mSipTransportFeatureTagStats2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // SipTransportFeatureTagStats should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        SipTransportFeatureTagStats[] outputs =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(0L);
+
+        assertProtoArrayEqualsIgnoringOrder(new SipTransportFeatureTagStats[]
+                {mSipTransportFeatureTagStats1, mSipTransportFeatureTagStats2}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipTransportFeatureTagStats_tooManyEntries() throws Exception {
+        // verify add atom excess MAX count (100)
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // Try to add 26 stats where MAX is 25
+        int max = 26;
+        SipTransportFeatureTagStats[] overMaxSipTransportFeatureTagStats =
+                new SipTransportFeatureTagStats[max];
+
+        for (int i = 0; i < max; i++) {
+            overMaxSipTransportFeatureTagStats[i] = copyOf(mSipTransportFeatureTagStats1);
+            overMaxSipTransportFeatureTagStats[i].sipTransportDeniedReason = i;
+            mPersistAtomsStorage
+                    .addSipTransportFeatureTagStats(overMaxSipTransportFeatureTagStats[i]);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+
+        mPersistAtomsStorage
+                .addSipTransportFeatureTagStats(mSipTransportFeatureTagStats2);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipTransportFeatureTagStats[] outputs =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(0L);
+
+        // The last added SipTransportFeatureTagStat remains
+        // and two old stats should be removed
+        assertHasStats(outputs, overMaxSipTransportFeatureTagStats, max - 2);
+        assertHasStats(outputs, mSipTransportFeatureTagStats2, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipTransportFeatureTagStats_updateExistingEntries() throws Exception {
+        // verify count
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipTransportFeatureTagStats(copyOf(mSipTransportFeatureTagStats1));
+        mPersistAtomsStorage.incTimeMillis(100L);
+        verifyCurrentStateSavedToFileOnce();
+
+        // SipTransportFeatureTag's durations should be doubled
+        SipTransportFeatureTagStats newSipTransportFeatureTagStats1 =
+                copyOf(mSipTransportFeatureTagStats1);
+        newSipTransportFeatureTagStats1.associatedMillis *= 2;
+
+        SipTransportFeatureTagStats[] outputs =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(0L);
+
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipTransportFeatureTagStats[] {
+                        newSipTransportFeatureTagStats1,
+                        mSipTransportFeatureTagStats2
+                }, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void getSipTransportFeatureTagStats_tooFrequent() throws Exception {
+        // verify get frequently
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+
+        SipTransportFeatureTagStats[] outputs =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(100L);
+
+        // should be denied
+        assertNull(outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void getSipTransportFeatureTagStats_withSavedAtoms() throws Exception {
+        // verify last get time after get atoms
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        SipTransportFeatureTagStats[] output1 =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        SipTransportFeatureTagStats[] output2 =
+                mPersistAtomsStorage.getSipTransportFeatureTagStats(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipTransportFeatureTagStats[] {
+                        mSipTransportFeatureTagStats1,
+                        mSipTransportFeatureTagStats2
+                }, output1);
+        assertProtoArrayEquals(new SipTransportFeatureTagStats[0], output2);
+        assertEquals(START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto()
+                        .sipTransportFeatureTagStatsPullTimestampMillis);
+
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).sipTransportFeatureTagStatsPullTimestampMillis);
+        assertEquals(START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).sipTransportFeatureTagStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addSipDelegateStats_emptyProto() throws Exception {
+        // verify add atom into new file
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipDelegateStats(mSipDelegateStats1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipDelegateStats[] outputs = mPersistAtomsStorage.getSipDelegateStats(0L);
+        assertProtoArrayEquals(new SipDelegateStats[] {mSipDelegateStats1}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipDelegateStats_withExistingEntries() throws Exception {
+        // verify add atom on existing atom already stored
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipDelegateStats(copyOf(mSipDelegateStats1));
+        mPersistAtomsStorage.addSipDelegateStats(copyOf(mSipDelegateStats2));
+        mPersistAtomsStorage.addSipDelegateStats(copyOf(mSipDelegateStats3));
+        mPersistAtomsStorage.incTimeMillis(100L);
+        // Three SipDelegateStats should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+
+        SipDelegateStats[] outputs =
+                mPersistAtomsStorage.getSipDelegateStats(0L);
+
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipDelegateStats[] {mSipDelegateStats1, mSipDelegateStats2, mSipDelegateStats3},
+                outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipDelegateStats_tooManyEntries() throws Exception {
+        // verify add atom excess MAX count
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // Try to add 11 stats where MAX is 10
+        int max = 11;
+        SipDelegateStats[] overMaxSipDelegateStats = new SipDelegateStats[max];
+        for (int i = 0; i < max; i++) {
+            overMaxSipDelegateStats[i] = copyOf(mSipDelegateStats1);
+            mPersistAtomsStorage
+                    .addSipDelegateStats(overMaxSipDelegateStats[i]);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        mPersistAtomsStorage.addSipDelegateStats(mSipDelegateStats3);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipDelegateStats[] outputs =
+                mPersistAtomsStorage.getSipDelegateStats(0L);
+
+        // The last added SipDelegate remains
+        // and two old stats should be removed
+        assertHasStats(outputs, overMaxSipDelegateStats, max - 2);
+        assertHasStats(outputs, mSipDelegateStats3, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipDelegateStats_updateExistingEntries() throws Exception {
+        // verify count
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        SipDelegateStats newSipDelegateStats3 = copyOf(mSipDelegateStats3);
+        newSipDelegateStats3.destroyReason =
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD;
+        mPersistAtomsStorage.addSipDelegateStats(newSipDelegateStats3);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        SipDelegateStats newSipDelegateStats1 = copyOf(mSipDelegateStats1);
+        newSipDelegateStats1.destroyReason =
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP;
+        mPersistAtomsStorage.addSipDelegateStats(newSipDelegateStats1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipDelegateStats[] outputs = mPersistAtomsStorage.getSipDelegateStats(0L);
+
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipDelegateStats[] {mSipDelegateStats2, mSipDelegateStats3,
+                        newSipDelegateStats3, newSipDelegateStats1}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void getSipDelegateStats_tooFrequent() throws Exception {
+        // verify get frequently
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+
+        SipDelegateStats[] outputs = mPersistAtomsStorage.getSipDelegateStats(100L);
+        // should be denied
+        assertNull(outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void getSipDelegateStats_withSavedAtoms() throws Exception {
+        // verify last get time after get atoms
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        SipDelegateStats[] output1 = mPersistAtomsStorage.getSipDelegateStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        SipDelegateStats[] output2 = mPersistAtomsStorage.getSipDelegateStats(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipDelegateStats[] {
+                        mSipDelegateStats2,
+                        mSipDelegateStats3}, output1);
+        assertProtoArrayEquals(new SipDelegateStats[0], output2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto().sipDelegateStatsPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).sipDelegateStatsPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).sipDelegateStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addGbaEvent_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addGbaEvent(mGbaEvent1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // gba event should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        GbaEvent[] stats = mPersistAtomsStorage.getGbaEvent(0L);
+        assertProtoArrayEquals(new GbaEvent[] {mGbaEvent1}, stats);
+    }
+
+    @Test
+    @SmallTest
+    public void addGbaEvent_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addGbaEvent(mGbaEvent1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        mPersistAtomsStorage.addGbaEvent(mGbaEvent2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // gba event1, gba event2 should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        GbaEvent[] stats = mPersistAtomsStorage.getGbaEvent(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new GbaEvent[] {mGbaEvent1, mGbaEvent2}, stats);
+    }
+
+    @Test
+    @SmallTest
+    public void addGbaEvent_tooManyEntries() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // Add 11 stats, but max is 10
+        for (int i = 0; i < 11; i++) {
+            mPersistAtomsStorage.addGbaEvent(mGbaEvent1);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        mPersistAtomsStorage.addGbaEvent(mGbaEvent2);
+
+        verifyCurrentStateSavedToFileOnce();
+        GbaEvent[] stats = mPersistAtomsStorage.getGbaEvent(0L);
+        assertHasStatsAndCount(stats, mGbaEvent1, 11);
+        assertHasStatsAndCount(stats, mGbaEvent2, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void addGbaEvent_updateExistingEntries() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        GbaEvent newStats = copyOf(mGbaEvent1);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addGbaEvent(copyOf(mGbaEvent1));
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // mGbaEvent1's count should be doubled
+        verifyCurrentStateSavedToFileOnce();
+        GbaEvent[] stats =
+                mPersistAtomsStorage.getGbaEvent(0L);
+        newStats.count *= 2;
+        assertProtoArrayEqualsIgnoringOrder(new GbaEvent[] {mGbaEvent2, newStats}, stats);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipMessageResponse_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipMessageResponse(mSipMessageResponse1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+        SipMessageResponse[] expected = mPersistAtomsStorage.getSipMessageResponse(0L);
+        assertProtoArrayEquals(new SipMessageResponse[] {mSipMessageResponse1}, expected);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipMessageResponse_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipMessageResponse(mSipMessageResponse1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        mPersistAtomsStorage.addSipMessageResponse(mSipMessageResponse2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+        SipMessageResponse[] expected =
+                new SipMessageResponse[] {mSipMessageResponse1, mSipMessageResponse2};
+
+        assertProtoArrayEqualsIgnoringOrder(
+                expected, mPersistAtomsStorage.getSipMessageResponse(0L));
+    }
+
+    @Test
+    @SmallTest
+    public void addSipMessageResponse_tooManyEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // store 11 same atoms, but only 1 atoms stored with count 11
+        for (int i = 0; i < 11; i++) {
+            mPersistAtomsStorage.addSipMessageResponse(mSipMessageResponse1);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        // store 1 different atom and count 1
+        mPersistAtomsStorage.addSipMessageResponse(mSipMessageResponse2);
+
+        verifyCurrentStateSavedToFileOnce();
+        SipMessageResponse[] result = mPersistAtomsStorage.getSipMessageResponse(0L);
+
+        // first atom has count 11, the other has 1
+        assertHasStats(result, mSipMessageResponse1, 11);
+        assertHasStats(result, mSipMessageResponse2, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void addSipMessageResponse_updateExistingEntries() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addSipMessageResponse(copyOf(mSipMessageResponse2));
+        mPersistAtomsStorage.incTimeMillis(100L);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipMessageResponse[] outputs = mPersistAtomsStorage.getSipMessageResponse(0L);
+        SipMessageResponse newSipMessageResponse = copyOf(mSipMessageResponse2);
+        newSipMessageResponse.count *= 2;
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipMessageResponse[] {mSipMessageResponse1, newSipMessageResponse}, outputs);
+    }
+
+    @Test
+    @SmallTest
+    public void addCompleteSipTransportSession_emptyProto() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addCompleteSipTransportSession(mSipTransportSession1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+        SipTransportSession[] expected = mPersistAtomsStorage.getSipTransportSession(0L);
+        assertProtoArrayEquals(new SipTransportSession[] {mSipTransportSession1}, expected);
+    }
+
+    @Test
+    @SmallTest
+    public void addCompleteSipTransportSession_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addCompleteSipTransportSession(mSipTransportSession1);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        mPersistAtomsStorage.addCompleteSipTransportSession(mSipTransportSession2);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        verifyCurrentStateSavedToFileOnce();
+        SipTransportSession[] expected =
+                new SipTransportSession[] {mSipTransportSession1, mSipTransportSession2};
+
+        assertProtoArrayEqualsIgnoringOrder(
+                expected, mPersistAtomsStorage.getSipTransportSession(0L));
+    }
+
+    @Test
+    @SmallTest
+    public void addCompleteSipTransportSession_tooManyEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        // store 11 same atoms, but only 1 atoms stored with count 11
+        for (int i = 0; i < 11; i++) {
+            mPersistAtomsStorage.addCompleteSipTransportSession(mSipTransportSession1);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+        // store 1 different atom and count 1
+        mPersistAtomsStorage.addCompleteSipTransportSession(mSipTransportSession2);
+
+        verifyCurrentStateSavedToFileOnce();
+        SipTransportSession[] result = mPersistAtomsStorage.getSipTransportSession(0L);
+
+        // first atom has count 11, the other has 1
+        assertHasStats(result, mSipTransportSession1, 11);
+        assertHasStats(result, mSipTransportSession2, 1);
+    }
+
+    @Test
+    @SmallTest
+    public void addCompleteSipTransportSession_updateExistingEntries() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addCompleteSipTransportSession(copyOf(mSipTransportSession2));
+        mPersistAtomsStorage.incTimeMillis(100L);
+        verifyCurrentStateSavedToFileOnce();
+
+        SipTransportSession[] outputs = mPersistAtomsStorage.getSipTransportSession(0L);
+        SipTransportSession newSipTransportSession = copyOf(mSipTransportSession2);
+        newSipTransportSession.sessionCount *= 2;
+        newSipTransportSession.endedGracefullyCount *= 2;
+        assertProtoArrayEqualsIgnoringOrder(
+                new SipTransportSession[] {mSipTransportSession1,
+                        newSipTransportSession}, outputs);
     }
 
     /* Utilities */
@@ -1367,6 +2994,32 @@
         atoms.imsRegistrationStats = mImsRegistrationStats;
         atoms.imsRegistrationTerminationPullTimestampMillis = lastPullTimeMillis;
         atoms.imsRegistrationTermination = mImsRegistrationTerminations;
+        atoms.imsRegistrationFeatureTagStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.imsRegistrationFeatureTagStats = mImsRegistrationFeatureTagStatses;
+        atoms.rcsClientProvisioningStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.rcsClientProvisioningStats = mRcsClientProvisioningStatses;
+        atoms.rcsAcsProvisioningStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.rcsAcsProvisioningStats = mRcsAcsProvisioningStatses;
+        atoms.imsRegistrationServiceDescStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.imsRegistrationServiceDescStats = mImsRegistrationServiceDescStats;
+        atoms.imsDedicatedBearerListenerEventPullTimestampMillis = lastPullTimeMillis;
+        atoms.imsDedicatedBearerListenerEvent = mImsDedicatedBearerListenerEvents;
+        atoms.imsDedicatedBearerEventPullTimestampMillis = lastPullTimeMillis;
+        atoms.imsDedicatedBearerEvent = mImsDedicatedBearerEvents;
+        atoms.uceEventStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.uceEventStats = mUceEventStatses;
+        atoms.presenceNotifyEventPullTimestampMillis = lastPullTimeMillis;
+        atoms.presenceNotifyEvent = mPresenceNotifyEvents;
+        atoms.sipTransportFeatureTagStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.sipTransportFeatureTagStats = mSipTransportFeatureTagStatsArray;
+        atoms.sipDelegateStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.sipDelegateStats = mSipDelegateStatsArray;
+        atoms.gbaEventPullTimestampMillis = lastPullTimeMillis;
+        atoms.gbaEvent = mGbaEvent;
+        atoms.sipMessageResponsePullTimestampMillis = lastPullTimeMillis;
+        atoms.sipMessageResponse = mSipMessageResponse;
+        atoms.sipTransportSessionPullTimestampMillis = lastPullTimeMillis;
+        atoms.sipTransportSession = mSipTransportSession;
         FileOutputStream stream = new FileOutputStream(mTestFile);
         stream.write(PersistAtoms.toByteArray(atoms));
         stream.close();
@@ -1426,6 +3079,65 @@
         return DataCallSession.parseFrom(MessageNano.toByteArray(source));
     }
 
+    private static ImsRegistrationFeatureTagStats copyOf(ImsRegistrationFeatureTagStats source)
+            throws Exception {
+        return ImsRegistrationFeatureTagStats.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static RcsAcsProvisioningStats copyOf(RcsAcsProvisioningStats source)
+            throws Exception {
+        return RcsAcsProvisioningStats.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static ImsRegistrationServiceDescStats copyOf(ImsRegistrationServiceDescStats source)
+            throws Exception {
+        return ImsRegistrationServiceDescStats.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static ImsDedicatedBearerListenerEvent copyOf(ImsDedicatedBearerListenerEvent source)
+            throws Exception {
+        return ImsDedicatedBearerListenerEvent.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static ImsDedicatedBearerEvent copyOf(ImsDedicatedBearerEvent source)
+            throws Exception {
+        return ImsDedicatedBearerEvent.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static UceEventStats copyOf(UceEventStats source)
+            throws Exception {
+        return UceEventStats.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static PresenceNotifyEvent copyOf(PresenceNotifyEvent source)
+            throws Exception {
+        return PresenceNotifyEvent.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static SipDelegateStats copyOf(SipDelegateStats source)
+            throws Exception {
+        return SipDelegateStats.parseFrom(MessageNano.toByteArray(source));
+    }
+    private static SipTransportFeatureTagStats copyOf(SipTransportFeatureTagStats source)
+            throws Exception {
+        return SipTransportFeatureTagStats.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static GbaEvent copyOf(GbaEvent source)
+            throws Exception {
+        return GbaEvent.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static SipMessageResponse copyOf(SipMessageResponse source)
+            throws Exception {
+        return SipMessageResponse.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static SipTransportSession copyOf(SipTransportSession source)
+            throws Exception {
+        return SipTransportSession.parseFrom(MessageNano.toByteArray(source));
+    }
+
     private void assertAllPullTimestampEquals(long timestamp) {
         assertEquals(
                 timestamp,
@@ -1496,4 +3208,213 @@
         inOrder.verify(mTestFileOutputStream, times(1)).close();
         inOrder.verifyNoMoreInteractions();
     }
+
+    private static void assertHasStatsCountTime(
+            ImsRegistrationFeatureTagStats[] statses,
+            @Nullable ImsRegistrationFeatureTagStats expectedStats,
+            int expectedCount, long expectedTime) {
+        assertNotNull(statses);
+        int actualCount = 0;
+        long actualTime = 0;
+        for (ImsRegistrationFeatureTagStats stats : statses) {
+            if (stats != null && expectedStats != null) {
+                if (stats.carrierId == expectedStats.carrierId
+                        && stats.slotId == expectedStats.slotId
+                        && stats.featureTagName == expectedStats.featureTagName
+                        && stats.registrationTech == expectedStats.registrationTech) {
+                    actualCount++;
+                    actualTime += stats.registeredMillis;
+                }
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+        assertEquals(expectedTime, actualTime);
+    }
+
+    private static void assertHasStatsAndCount(
+            RcsClientProvisioningStats[] statses,
+            @Nullable RcsClientProvisioningStats expectedStats, int expectedCount) {
+        assertNotNull(statses);
+        int actualCount = -1;
+        for (RcsClientProvisioningStats stats : statses) {
+            if (stats.carrierId == expectedStats.carrierId
+                    && stats.slotId == expectedStats.slotId
+                    && stats.event == expectedStats.event) {
+                actualCount = stats.count;
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+    }
+
+    private static void assertHasStats(
+            ImsDedicatedBearerListenerEvent[] statses,
+            @Nullable ImsDedicatedBearerListenerEvent expectedStats, int expectedCount) {
+        assertNotNull(statses);
+        int actualCount = 0;
+        for (ImsDedicatedBearerListenerEvent stats : statses) {
+            if (stats != null && expectedStats != null) {
+                if (MessageNano.messageNanoEquals(stats, expectedStats)) {
+                    actualCount++;
+                }
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+    }
+
+    private static void assertHasStatsAndCount(
+            ImsDedicatedBearerEvent[] statses,
+            @Nullable ImsDedicatedBearerEvent expectedStats, int expectedCount) {
+        assertNotNull(statses);
+        int actualCount = -1;
+        for (ImsDedicatedBearerEvent stats : statses) {
+            if (stats.carrierId == expectedStats.carrierId
+                    && stats.slotId == expectedStats.slotId
+                    && stats.ratAtEnd == expectedStats.ratAtEnd
+                    && stats.qci == expectedStats.qci
+                    && stats.bearerState == expectedStats.bearerState
+                    && stats.localConnectionInfoReceived
+                            == expectedStats.localConnectionInfoReceived
+                    && stats.remoteConnectionInfoReceived
+                            == expectedStats.remoteConnectionInfoReceived
+                    && stats.hasListeners == expectedStats.hasListeners) {
+                actualCount = stats.count;
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+    }
+
+    private static void assertHasStatsAndCountDuration(
+            RcsAcsProvisioningStats[] statses,
+            @Nullable RcsAcsProvisioningStats expectedStats, int count, long duration) {
+        assertNotNull(statses);
+        int actualCount = -1;
+        long actualDuration = -1;
+        for (RcsAcsProvisioningStats stats : statses) {
+            if (stats.carrierId == expectedStats.carrierId
+                    && stats.slotId == expectedStats.slotId
+                    && stats.responseCode == expectedStats.responseCode
+                    && stats.responseType == expectedStats.responseType
+                    && stats.isSingleRegistrationEnabled
+                            == expectedStats.isSingleRegistrationEnabled) {
+                actualCount = stats.count;
+                actualDuration = stats.stateTimerMillis;
+            }
+        }
+        assertEquals(count, actualCount);
+        assertEquals(duration, actualDuration);
+    }
+
+    private static void assertHasStats(SipDelegateStats[] results,
+            Object expectedStats, int expectedCount) {
+        assertNotNull(results);
+        assertNotNull(expectedStats);
+
+        int realCount = 0;
+        if (expectedStats instanceof SipDelegateStats[]) {
+            SipDelegateStats[] expectedResults = (SipDelegateStats[]) expectedStats;
+            for (SipDelegateStats stat: results) {
+                for (SipDelegateStats estat : expectedResults) {
+                    if (stat != null && estat != null) {
+                        if (MessageNano.messageNanoEquals(stat, estat)) {
+                            realCount++;
+                            break;
+                        }
+                    }
+                }
+            }
+        } else {
+            SipDelegateStats expectedResult = (SipDelegateStats) expectedStats;
+            for (SipDelegateStats stat : results) {
+                if (stat != null && expectedStats != null) {
+                    if (MessageNano.messageNanoEquals(stat, expectedResult)) {
+                        realCount++;
+                    }
+                }
+            }
+        }
+        assertEquals(expectedCount, realCount);
+    }
+
+    private static void assertHasStats(SipTransportFeatureTagStats[] results,
+            Object expectedStats, int expectedCount) {
+        assertNotNull(results);
+        assertNotNull(expectedStats);
+
+        int realCount = 0;
+        if (expectedStats instanceof SipTransportFeatureTagStats[]) {
+            SipTransportFeatureTagStats[] expectedResults =
+                    (SipTransportFeatureTagStats[]) expectedStats;
+            for (SipTransportFeatureTagStats stat: results) {
+                for (SipTransportFeatureTagStats estat : expectedResults) {
+                    if (stat != null && estat != null) {
+                        if (MessageNano.messageNanoEquals(stat, estat)) {
+                            realCount++;
+                            break;
+                        }
+                    }
+                }
+            }
+        } else {
+            SipTransportFeatureTagStats expectedResult =
+                    (SipTransportFeatureTagStats) expectedStats;
+            for (SipTransportFeatureTagStats stat : results) {
+                if (stat != null && expectedStats != null) {
+                    if (MessageNano.messageNanoEquals(stat, expectedResult)) {
+                        realCount++;
+                    }
+                }
+            }
+        }
+        assertEquals(expectedCount, realCount);
+    }
+
+    private static void assertHasStatsAndCount(
+            GbaEvent[] statses,
+            @Nullable GbaEvent expectedStats, int expectedCount) {
+        assertNotNull(statses);
+        int actualCount = -1;
+        for (GbaEvent stats : statses) {
+            if (stats.carrierId == expectedStats.carrierId
+                    && stats.slotId == expectedStats.slotId
+                    && stats.successful == expectedStats.successful
+                    && stats.failedReason == expectedStats.failedReason) {
+                actualCount = stats.count;
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+    }
+
+    private static void assertHasStats(
+            SipMessageResponse[] statses,
+            @Nullable SipMessageResponse expectedStats, int expectedCount) {
+        assertNotNull(statses);
+        int actualCount = -1;
+        for (SipMessageResponse stats : statses) {
+            if (stats.carrierId == expectedStats.carrierId
+                    && stats.slotId == expectedStats.slotId
+                    && stats.sipMessageMethod == expectedStats.sipMessageMethod
+                    && stats.sipMessageResponse == expectedStats.sipMessageResponse
+                    && stats.sipMessageDirection == expectedStats.sipMessageDirection) {
+                actualCount = stats.count;
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+    }
+
+    private static void assertHasStats(
+            SipTransportSession[] statses,
+            @Nullable SipTransportSession expectedStats, int expectedCount) {
+        assertNotNull(statses);
+        int actualCount = -1;
+        for (SipTransportSession stats : statses) {
+            if (stats.carrierId == expectedStats.carrierId
+                    && stats.slotId == expectedStats.slotId
+                    && stats.sessionMethod == expectedStats.sessionMethod
+                    && stats.sipMessageDirection == expectedStats.sipMessageDirection
+                    && stats.sipResponse == expectedStats.sipResponse) {
+                actualCount = stats.sessionCount;
+            }
+        }
+        assertEquals(expectedCount, actualCount);
+    }
 }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/RcsStatsTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/RcsStatsTest.java
new file mode 100644
index 0000000..68fd017
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/RcsStatsTest.java
@@ -0,0 +1,1253 @@
+/*
+ * 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.internal.telephony.metrics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.telephony.TelephonyProtoEnums;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.internal.telephony.TelephonyStatsLog;
+import com.android.internal.telephony.TelephonyTest;
+import com.android.internal.telephony.nano.PersistAtomsProto.GbaEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsDedicatedBearerListenerEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationServiceDescStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.PresenceNotifyEvent;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsAcsProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.RcsClientProvisioningStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipDelegateStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipMessageResponse;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportFeatureTagStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.SipTransportSession;
+import com.android.internal.telephony.nano.PersistAtomsProto.UceEventStats;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+public class RcsStatsTest extends TelephonyTest {
+    private static final String TAG = RcsStatsTest.class.getSimpleName();
+
+    private static final long START_TIME_MILLIS = 2000L;
+    private static final int SLOT_ID = 0;
+    private static final int SLOT2_ID = 1;
+    private static final int INVALID_SLOT_ID = -1;
+    private static final int CARRIER_ID = 100;
+    private static final int CARRIER2_ID = 200;
+    private static final int INVALID_CARRIER_ID = -1;
+    private static final int INVALID_SUB_ID = Integer.MIN_VALUE;
+
+    private class TestResult {
+        public String tagName;
+        public int tagValue;
+        public long duration;
+        public int deniedReason;
+        public int deregiReason;
+        TestResult(String tagName, int tagValue, long duration,
+                int deniedReason, int deregiReason) {
+            this.tagName = tagName;
+            this.tagValue = tagValue;
+            this.duration = duration;
+            this.deniedReason = deniedReason;
+            this.deregiReason = deregiReason;
+        }
+    }
+
+    private int mSubId = 10;
+    private int mSubId2 = 20;
+
+    private TestableRcsStats mRcsStats;
+
+    private class TestableRcsStats extends RcsStats {
+        private long mTimeMillis = START_TIME_MILLIS;
+        private boolean mEnabledInvalidSubId = false;
+
+        TestableRcsStats() {
+            super();
+        }
+
+        @Override
+        protected int getSlotId(int subId) {
+            if (mEnabledInvalidSubId) {
+                return INVALID_SLOT_ID;
+            }
+
+            if (subId == mSubId) {
+                return SLOT_ID;
+            } else if (subId == mSubId2) {
+                return SLOT2_ID;
+            }
+            return SLOT2_ID;
+        }
+
+        @Override
+        protected int getCarrierId(int subId) {
+            if (mEnabledInvalidSubId) {
+                return INVALID_CARRIER_ID;
+            }
+
+            if (subId == mSubId) {
+                return CARRIER_ID;
+            } else if (subId == mSubId2) {
+                return CARRIER2_ID;
+            }
+            return INVALID_CARRIER_ID;
+        }
+
+        @Override
+        protected boolean isValidCarrierId(int carrierId) {
+            if (carrierId == INVALID_CARRIER_ID) {
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        protected long getWallTimeMillis() {
+            // NOTE: super class constructor will be executed before private field is set, which
+            // gives the wrong start time (mTimeMillis will have its default value of 0L)
+            Log.d(TAG, "getWallTimeMillis return value : " + mTimeMillis);
+            return mTimeMillis == 0L ? START_TIME_MILLIS : mTimeMillis;
+        }
+
+        @Override
+        protected void logd(String msg) {
+            Log.w(TAG, msg);
+        }
+
+        @Override
+        protected int getSubId(int slotId) {
+            if (mEnabledInvalidSubId) {
+                return INVALID_SUB_ID;
+            }
+
+            if (slotId == SLOT_ID) {
+                return mSubId;
+            } else if (slotId == SLOT2_ID) {
+                return mSubId2;
+            }
+            return INVALID_SUB_ID;
+        }
+
+        public void setEnableInvalidSubId() {
+            mEnabledInvalidSubId = true;
+        }
+        private void setTimeMillis(long timeMillis) {
+            mTimeMillis = timeMillis;
+        }
+
+        private void incTimeMillis(long timeMillis) {
+            mTimeMillis += timeMillis;
+            Log.d(TAG, "incTimeMillis   mTimeMillis : " + mTimeMillis);
+        }
+
+        public int getRcsAcsProvisioningCachedSize() {
+            return mRcsAcsProvisioningStatsList.size();
+        }
+
+        public int getImsRegistrationServiceDescCachedSize() {
+            return mImsRegistrationServiceDescStatsList.size();
+        }
+
+        public long getRcsAcsProvisioningCachedTime(int carreirId, int slotId) {
+            for (RcsAcsProvisioningStats stats : mRcsAcsProvisioningStatsList) {
+                if (stats.carrierId == carreirId && stats.slotId == slotId) {
+                    return stats.stateTimerMillis;
+                }
+            }
+            return 0L;
+        }
+
+        public int getRcsProvisioningCallbackMapSize() {
+            return mRcsProvisioningCallbackMap.size();
+        }
+
+        public ImsDedicatedBearerListenerEvent dedicatedBearerListenerEventMap_get(
+                final int listenerId) {
+            return mDedicatedBearerListenerEventMap.get(listenerId);
+        }
+
+        public boolean dedicatedBearerListenerEventMap_containsKey(final int listenerId) {
+            return mDedicatedBearerListenerEventMap.containsKey(listenerId);
+        }
+
+        public void dedicatedBearerListenerEventMap_remove(final int listenerId) {
+            mDedicatedBearerListenerEventMap.remove(listenerId);
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp(getClass().getSimpleName());
+
+        mRcsStats = new TestableRcsStats();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void onImsRegistrationFeatureTagStats_withAtoms() throws Exception {
+        int slotId = SLOT_ID;
+        int carrierId = CARRIER_ID;
+        List<String> featureTagList = Arrays.asList(
+                "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcse.im\"",
+                "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"",
+                "+g.3gpp.icsi-ref=\"hh%3Ashin%3A-b.a.b.o\"",
+                "+g.gsma.rcs.isbot"
+        );
+
+        int registrationTech  = 0;
+
+        mRcsStats.onImsRegistrationFeatureTagStats(
+                mSubId, featureTagList, registrationTech);
+
+        mRcsStats.onStoreCompleteImsRegistrationFeatureTagStats(mSubId);
+
+        ArgumentCaptor<ImsRegistrationFeatureTagStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationFeatureTagStats.class);
+        verify(mPersistAtomsStorage, times(featureTagList.size()))
+                .addImsRegistrationFeatureTagStats(captor.capture());
+        List<ImsRegistrationFeatureTagStats> captorValues = captor.getAllValues();
+
+        assertEquals(captorValues.size(), featureTagList.size());
+        for (int index = 0; index < captorValues.size(); index++) {
+            ImsRegistrationFeatureTagStats stats = captorValues.get(index);
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            assertEquals(mRcsStats.convertTagNameToValue(featureTagList.get(index)),
+                    stats.featureTagName);
+            assertEquals(registrationTech, stats.registrationTech);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void onRcsClientProvisioningStats_withAtoms() throws Exception {
+        /*
+         * RCS_CLIENT_PROVISIONING_STATS__EVENT__CLIENT_PARAMS_SENT
+         * RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION
+         * RCS_CLIENT_PROVISIONING_STATS__EVENT__DMA_CHANGED
+         */
+        int event =
+                TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__CLIENT_PARAMS_SENT;
+
+        mRcsStats.onRcsClientProvisioningStats(mSubId, event);
+
+        ArgumentCaptor<RcsClientProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsClientProvisioningStats.class);
+        verify(mPersistAtomsStorage).addRcsClientProvisioningStats(captor.capture());
+        RcsClientProvisioningStats stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(event, stats.event);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onRcsAcsProvisioningStats_withAtoms() throws Exception {
+        boolean isSingleRegistrationEnabled = true;
+        int[] responseCode = {200, 401};
+        /*
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PRE_PROVISIONING_XML
+         */
+        int[] responseType = {
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML,
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR};
+        int[] slotIds = {SLOT_ID, SLOT_ID};
+        int[] carrierIds = {CARRIER_ID, CARRIER_ID};
+
+        // this will be cached
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId, responseCode[0], responseType[0], isSingleRegistrationEnabled);
+
+        long timeGap = 6000L;
+        mRcsStats.incTimeMillis(timeGap);
+
+        // this will be cached, previous will be stored
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId, responseCode[1], responseType[1], isSingleRegistrationEnabled);
+
+        ArgumentCaptor<RcsAcsProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsAcsProvisioningStats.class);
+        verify(mPersistAtomsStorage).addRcsAcsProvisioningStats(captor.capture());
+        RcsAcsProvisioningStats stats = captor.getValue();
+        assertEquals(carrierIds[0], stats.carrierId);
+        assertEquals(slotIds[0], stats.slotId);
+        assertEquals(responseCode[0], stats.responseCode);
+        assertEquals(responseType[0], stats.responseType);
+        assertEquals(isSingleRegistrationEnabled, stats.isSingleRegistrationEnabled);
+        assertEquals(timeGap, stats.stateTimerMillis);
+
+        // the last atoms will be cached
+        assertEquals(1, mRcsStats.getRcsAcsProvisioningCachedSize());
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onRcsAcsProvisioningStats_withAtomsInvalidSubId() throws Exception {
+        boolean isSingleRegistrationEnabled = true;
+        int[] responseCode = {200, 401};
+        int[] responseType = {
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML,
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR};
+        int[] slotIds = {SLOT_ID, SLOT_ID};
+        int[] carrierIds = {CARRIER_ID, CARRIER_ID};
+
+        // this will be cached
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId, responseCode[0], responseType[0], isSingleRegistrationEnabled);
+
+        long timeGap = 6000L;
+        mRcsStats.incTimeMillis(timeGap);
+
+        // slotId and carrierId are invalid based on subId
+        mRcsStats.setEnableInvalidSubId();
+
+        // this will not be cached, previous will be stored
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId, responseCode[1], responseType[1], isSingleRegistrationEnabled);
+
+        ArgumentCaptor<RcsAcsProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsAcsProvisioningStats.class);
+        verify(mPersistAtomsStorage).addRcsAcsProvisioningStats(captor.capture());
+        RcsAcsProvisioningStats stats = captor.getValue();
+        assertEquals(carrierIds[0], stats.carrierId);
+        assertEquals(slotIds[0], stats.slotId);
+        assertEquals(responseCode[0], stats.responseCode);
+        assertEquals(responseType[0], stats.responseType);
+        assertEquals(isSingleRegistrationEnabled, stats.isSingleRegistrationEnabled);
+        assertEquals(timeGap, stats.stateTimerMillis);
+        // the last atoms will not be cached
+        assertEquals(0, mRcsStats.getRcsAcsProvisioningCachedSize());
+
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onRcsAcsProvisioningStats_byCallBack() throws Exception {
+        long timeGap = 6000L;
+        boolean isSingleRegistrationEnabled = true;
+        int responseCode = 200;
+        int responseType =
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PRE_PROVISIONING_XML;
+        byte[] config = new byte[0];
+
+        RcsStats.RcsProvisioningCallback rcsProvisioningCallback =
+                mRcsStats.getRcsProvisioningCallback(mSubId, isSingleRegistrationEnabled);
+        // has one callback obj
+        assertEquals(mRcsStats.getRcsProvisioningCallbackMapSize(), 1);
+
+        rcsProvisioningCallback.onPreProvisioningReceived(config);
+        mRcsStats.incTimeMillis(timeGap);
+        rcsProvisioningCallback.onRemoved();
+        // callback will be removed, Map is empty.
+        assertEquals(mRcsStats.getRcsProvisioningCallbackMapSize(), 0);
+
+        ArgumentCaptor<RcsAcsProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsAcsProvisioningStats.class);
+        verify(mPersistAtomsStorage).addRcsAcsProvisioningStats(captor.capture());
+        RcsAcsProvisioningStats stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(responseCode, stats.responseCode);
+        assertEquals(responseType, stats.responseType);
+        assertEquals(isSingleRegistrationEnabled, stats.isSingleRegistrationEnabled);
+        assertEquals(timeGap, stats.stateTimerMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onRcsAcsProvisioningStats_byErrorCallBack() throws Exception {
+        long timeGap = 6000L;
+        boolean isSingleRegistrationEnabled = true;
+        int responseCode = 401;
+        int responseType =
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR;
+
+        RcsStats.RcsProvisioningCallback rcsProvisioningCallback =
+                mRcsStats.getRcsProvisioningCallback(mSubId, false);
+        rcsProvisioningCallback =
+                mRcsStats.getRcsProvisioningCallback(mSubId2, isSingleRegistrationEnabled);
+        // has two callback obj, subId, subId2
+        assertEquals(mRcsStats.getRcsProvisioningCallbackMapSize(), 2);
+
+        rcsProvisioningCallback.onAutoConfigurationErrorReceived(responseCode, "responseCode");
+        mRcsStats.incTimeMillis(timeGap);
+        mRcsStats.onStoreCompleteRcsAcsProvisioningStats(mSubId2);
+        rcsProvisioningCallback.onRemoved();
+        // subId2's callback will be removed, Map has only one callback for subId.
+        assertEquals(mRcsStats.getRcsProvisioningCallbackMapSize(), 1);
+
+        // addRcsAcsProvisioningStats is called once.
+        ArgumentCaptor<RcsAcsProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsAcsProvisioningStats.class);
+        verify(mPersistAtomsStorage).addRcsAcsProvisioningStats(captor.capture());
+        RcsAcsProvisioningStats stats = captor.getValue();
+        assertEquals(CARRIER2_ID, stats.carrierId);
+        assertEquals(SLOT2_ID, stats.slotId);
+        assertEquals(responseCode, stats.responseCode);
+        assertEquals(responseType, stats.responseType);
+        assertEquals(isSingleRegistrationEnabled, stats.isSingleRegistrationEnabled);
+        assertEquals(timeGap, stats.stateTimerMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onStoreCompleteRcsAcsProvisioningStats_withSubId() throws Exception {
+        boolean isSingleRegistrationEnabled = true;
+        int[] responseCode = {401, 200};
+        /*
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PRE_PROVISIONING_XML
+         */
+        int[] responseType = {TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR,
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML};
+        int[] slotIds = {SLOT_ID, SLOT2_ID};
+        int[] carrierIds = {CARRIER_ID, CARRIER2_ID};
+
+        // this will be cached
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId, responseCode[0], responseType[0], isSingleRegistrationEnabled);
+        // this will be cached
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId2, responseCode[1], responseType[1], isSingleRegistrationEnabled);
+
+        long timeGap = 6000L;
+        mRcsStats.incTimeMillis(timeGap);
+
+        // cached atoms will be stored and removed
+        mRcsStats.onStoreCompleteRcsAcsProvisioningStats(mSubId);
+        mRcsStats.onStoreCompleteRcsAcsProvisioningStats(mSubId2);
+
+        ArgumentCaptor<RcsAcsProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsAcsProvisioningStats.class);
+        verify(mPersistAtomsStorage, times(slotIds.length))
+                .addRcsAcsProvisioningStats(captor.capture());
+        List<RcsAcsProvisioningStats> statsList = captor.getAllValues();
+        assertEquals(slotIds.length, statsList.size());
+        for (int i = 0; i < statsList.size(); i++) {
+            RcsAcsProvisioningStats stats = statsList.get(i);
+            assertEquals(carrierIds[i], stats.carrierId);
+            assertEquals(slotIds[i], stats.slotId);
+            assertEquals(responseCode[i], stats.responseCode);
+            assertEquals(responseType[i], stats.responseType);
+            assertEquals(isSingleRegistrationEnabled, stats.isSingleRegistrationEnabled);
+            assertEquals(timeGap, stats.stateTimerMillis);
+        }
+        // cached data should be empty
+        assertEquals(0, mRcsStats.getRcsAcsProvisioningCachedSize());
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onFlushIncompleteRcsAcsProvisioningStats_withoutSubId() throws Exception {
+        boolean isSingleRegistrationEnabled = true;
+        int[] responseCode = {401, 200};
+        /*
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML
+         * RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PRE_PROVISIONING_XML
+         */
+        int[] responseType = {TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__ERROR,
+                TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML};
+        int[] slotIds = {SLOT_ID, SLOT2_ID};
+        int[] carrierIds = {CARRIER_ID, CARRIER2_ID};
+
+        // this will be cached
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId, responseCode[0], responseType[0], isSingleRegistrationEnabled);
+        // this will be cached
+        mRcsStats.onRcsAcsProvisioningStats(
+                mSubId2, responseCode[1], responseType[1], isSingleRegistrationEnabled);
+
+        long timeGap = 6000L;
+        mRcsStats.incTimeMillis(timeGap);
+
+        // cached atoms will be stored, but atoms are keeped
+        mRcsStats.onFlushIncompleteRcsAcsProvisioningStats();
+
+        ArgumentCaptor<RcsAcsProvisioningStats> captor =
+                ArgumentCaptor.forClass(RcsAcsProvisioningStats.class);
+        verify(mPersistAtomsStorage, times(slotIds.length))
+                .addRcsAcsProvisioningStats(captor.capture());
+        List<RcsAcsProvisioningStats> statsList = captor.getAllValues();
+        assertEquals(slotIds.length, statsList.size());
+        for (int i = 0; i < statsList.size(); i++) {
+            RcsAcsProvisioningStats stats = statsList.get(i);
+            assertEquals(carrierIds[i], stats.carrierId);
+            assertEquals(slotIds[i], stats.slotId);
+            assertEquals(responseCode[i], stats.responseCode);
+            assertEquals(responseType[i], stats.responseType);
+            assertEquals(isSingleRegistrationEnabled, stats.isSingleRegistrationEnabled);
+            assertEquals(timeGap, stats.stateTimerMillis);
+
+            // check cached atom's time should be updated
+            assertEquals(mRcsStats.getWallTimeMillis(),
+                    mRcsStats.getRcsAcsProvisioningCachedTime(carrierIds[i], slotIds[i]));
+        }
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onSipDelegateStats_addStats() throws Exception {
+        final int destroyReason = SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD;
+        final long timeGap = 6000L;
+        List<Set<String>> supportedTagsList = getSupportedTagsList();
+        Set<String> registeredTags = supportedTagsList.get(0);
+        // create and destroy a sipDelegate..
+        mRcsStats.createSipDelegateStats(mSubId, registeredTags);
+        mRcsStats.incTimeMillis(timeGap);
+        mRcsStats.onSipDelegateStats(mSubId, registeredTags,
+                SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+
+        ArgumentCaptor<SipDelegateStats> captor =
+                ArgumentCaptor.forClass(SipDelegateStats.class);
+        verify(mPersistAtomsStorage).addSipDelegateStats(captor.capture());
+        SipDelegateStats stats = captor.getValue();
+        assertTrue(stats.dimension != 0);
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(timeGap, stats.uptimeMillis);
+        assertEquals(destroyReason, stats.destroyReason);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    private List<Set<String>> getSupportedTagsList() {
+        List<Set<String>> registeredTagsList = new ArrayList<>();
+        Set<String> supportedTags1 = new ArraySet<>();
+        supportedTags1.add(FeatureTags.FEATURE_TAG_STANDALONE_MSG);
+        supportedTags1.add(FeatureTags.FEATURE_TAG_CHAT_SESSION);
+        registeredTagsList.add(supportedTags1);
+
+        Set<String> supportedTags2 = new ArraySet<>();
+        supportedTags2.add(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+        supportedTags2.add(FeatureTags.FEATURE_TAG_CHAT_IM);
+        supportedTags2.add(FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING);
+        registeredTagsList.add(supportedTags2);
+
+        Set<String> supportedTags3 = new ArraySet<>();
+        supportedTags3.add(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION);
+        supportedTags3.add(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG);
+        supportedTags3.add(FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED);
+        registeredTagsList.add(supportedTags3);
+
+        return registeredTagsList;
+    }
+
+    @Test
+    @SmallTest
+    public void onSipDelegateStats_addMultipleEntries() throws Exception {
+        final long timeGap = 6000L;
+        List<Integer> destroyReasonList = new ArrayList<>();
+        destroyReasonList.add(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_UNKNOWN);
+        destroyReasonList.add(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+        destroyReasonList.add(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        final int testSize = destroyReasonList.size();
+        List<Set<String>> supportedTagsList = getSupportedTagsList();
+
+        // create and destroy a sipDelegate multiple times
+        for (int i = 0; i < testSize; i++) {
+            mRcsStats.createSipDelegateStats(mSubId, supportedTagsList.get(i));
+        }
+
+        for (int i = 0; i < testSize; i++) {
+            mRcsStats.incTimeMillis(timeGap);
+            mRcsStats.onSipDelegateStats(mSubId, supportedTagsList.get(i),
+                    destroyReasonList.get(i));
+        }
+
+        List<ExpectedSipDelegateResult> expectedSipDelegateResults =
+                getExpectedResult(destroyReasonList);
+        final int expectedResultSize = expectedSipDelegateResults.size();
+        ArgumentCaptor<SipDelegateStats> captor =
+                ArgumentCaptor.forClass(SipDelegateStats.class);
+        verify(mPersistAtomsStorage, times(expectedResultSize))
+                .addSipDelegateStats(captor.capture());
+
+        List<SipDelegateStats> captorValues = captor.getAllValues();
+        assertEquals(captorValues.size(), expectedResultSize);
+        for (int i = 0; i < expectedResultSize; i++) {
+            SipDelegateStats stats = captorValues.get(i);
+            ExpectedSipDelegateResult expectedResult = expectedSipDelegateResults.get(i);
+            assertTrue(stats.dimension != 0);
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            assertEquals(timeGap * (i + 1), stats.uptimeMillis);
+            assertEquals(expectedResult.destroyReason, stats.destroyReason);
+        }
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    private class ExpectedSipDelegateResult {
+        public int id;
+        public int destroyReason;
+        ExpectedSipDelegateResult(int id, int destroyReason) {
+            this.id = id;
+            this.destroyReason = destroyReason;
+        }
+    }
+
+    private List<ExpectedSipDelegateResult> getExpectedResult(List<Integer> destroyReasonList) {
+        List<ExpectedSipDelegateResult> results = new ArrayList<>();
+        int size = destroyReasonList.size();
+
+        for (int i = 0; i < size; i++) {
+            results.add(new ExpectedSipDelegateResult(i, destroyReasonList.get(i)));
+        }
+
+        return results;
+    }
+
+    @Test
+    @SmallTest
+    public void onSipTransportFeatureTagStats_addMultipleEntries() throws Exception {
+        final long timeGap = 6000L;
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        Set<FeatureTagState> deRegiTags = new ArraySet<>();
+        Set<String> regiTags = new ArraySet<>();
+
+        // create new featureTags
+        regiTags.add(FeatureTags.FEATURE_TAG_STANDALONE_MSG);
+        deniedTags.add(new FeatureTagState(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+        mRcsStats.incTimeMillis(timeGap);
+
+        // change status of featureTags
+        regiTags.clear();
+        deRegiTags.add(new FeatureTagState(FeatureTags.FEATURE_TAG_STANDALONE_MSG,
+                DelegateRegistrationState.DEREGISTERED_REASON_NOT_REGISTERED));
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+        mRcsStats.incTimeMillis(timeGap);
+
+        List<TestResult> expectedResults = getTestResult(timeGap, false);
+
+        int expectedResultSize = expectedResults.size();
+        ArgumentCaptor<SipTransportFeatureTagStats> captor =
+                ArgumentCaptor.forClass(SipTransportFeatureTagStats.class);
+        verify(mPersistAtomsStorage, times(expectedResultSize))
+                .addSipTransportFeatureTagStats(captor.capture());
+
+        List<SipTransportFeatureTagStats> captorValues = captor.getAllValues();
+
+        assertEquals(captorValues.size(), expectedResultSize);
+        for (int i = 0; i < captorValues.size(); i++) {
+            SipTransportFeatureTagStats stats = captorValues.get(i);
+            TestResult expectedResult = expectedResults.get(i);
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            assertEquals(expectedResult.tagValue, stats.featureTagName);
+            assertEquals(expectedResult.duration, stats.associatedMillis);
+            assertEquals(expectedResult.deniedReason, stats.sipTransportDeniedReason);
+            assertEquals(expectedResult.deregiReason, stats.sipTransportDeregisteredReason);
+        }
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onSipTransportFeatureTagStats_addInvalidEntries() throws Exception {
+        final long timeGap = 6000L;
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        Set<FeatureTagState> deRegiTags = new ArraySet<>();
+        Set<String> regiTags = new ArraySet<>();
+
+        final int invalidSubId = INVALID_SUB_ID;
+
+        // create new featureTags with an invalidId
+        regiTags.add(FeatureTags.FEATURE_TAG_STANDALONE_MSG);
+        deniedTags.add(new FeatureTagState(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+        mRcsStats.onSipTransportFeatureTagStats(invalidSubId, deniedTags, deRegiTags, regiTags);
+        mRcsStats.incTimeMillis(timeGap);
+
+        // change status of featureTags with an invalidId
+        regiTags.clear();
+        deRegiTags.add(new FeatureTagState(FeatureTags.FEATURE_TAG_STANDALONE_MSG,
+                DelegateRegistrationState.DEREGISTERED_REASON_NOT_REGISTERED));
+        mRcsStats.onSipTransportFeatureTagStats(invalidSubId, deniedTags, deRegiTags, regiTags);
+        mRcsStats.incTimeMillis(timeGap);
+
+        verify(mPersistAtomsStorage, never()).addSipTransportFeatureTagStats(any());
+    }
+
+
+    @Test
+    @SmallTest
+    public void onSipTransportFeatureTagStats_addCustomTag() throws Exception {
+        final long timeGap = 6000L;
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        Set<FeatureTagState> deRegiTags = new ArraySet<>();
+        Set<String> regiTags = new ArraySet<>();
+
+        // create new featureTags
+        String customTag = "custom@tag";
+        regiTags.add(customTag);
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+        mRcsStats.incTimeMillis(timeGap);
+
+        // change status of featureTags
+        regiTags.clear();
+        deRegiTags.add(new FeatureTagState(customTag,
+                DelegateRegistrationState.DEREGISTERED_REASON_NOT_REGISTERED));
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+        mRcsStats.incTimeMillis(timeGap);
+
+        TestResult expectedResult = new TestResult(customTag,
+                TelephonyProtoEnums.IMS_FEATURE_TAG_CUSTOM, timeGap, RcsStats.NONE, RcsStats.NONE);
+
+        ArgumentCaptor<SipTransportFeatureTagStats> captor =
+                ArgumentCaptor.forClass(SipTransportFeatureTagStats.class);
+
+        verify(mPersistAtomsStorage).addSipTransportFeatureTagStats(captor.capture());
+        SipTransportFeatureTagStats stats = captor.getValue();
+
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(expectedResult.tagValue, stats.featureTagName);
+        assertEquals(expectedResult.duration, stats.associatedMillis);
+        assertEquals(expectedResult.deniedReason, stats.sipTransportDeniedReason);
+        assertEquals(expectedResult.deregiReason, stats.sipTransportDeregisteredReason);
+
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void concludeSipTransportFeatureTagsStat_addMultipleEntries() throws Exception {
+        final long timeGap = 6000L;
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+        Set<FeatureTagState> deRegiTags = new ArraySet<>();
+        Set<String> regiTags = new ArraySet<>();
+        // create new featureTags
+        regiTags.add(FeatureTags.FEATURE_TAG_STANDALONE_MSG);
+        deniedTags.add(new FeatureTagState(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+        mRcsStats.incTimeMillis(timeGap);
+
+        // change status of featureTags
+        regiTags.clear();
+        deRegiTags.add(new FeatureTagState(FeatureTags.FEATURE_TAG_STANDALONE_MSG,
+                DelegateRegistrationState.DEREGISTERED_REASON_NOT_REGISTERED));
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+
+        mRcsStats.incTimeMillis(timeGap);
+
+        // change status of featureTags and metrics are pulled.
+        deRegiTags.clear();
+        regiTags.add(FeatureTags.FEATURE_TAG_STANDALONE_MSG);
+        mRcsStats.onSipTransportFeatureTagStats(mSubId, deniedTags, deRegiTags, regiTags);
+
+        mRcsStats.incTimeMillis(timeGap);
+        mRcsStats.concludeSipTransportFeatureTagsStat();
+
+        List<TestResult> expectedResults = getTestResult(timeGap, true);
+
+        int expectedResultSize = expectedResults.size();
+        ArgumentCaptor<SipTransportFeatureTagStats> captor =
+                ArgumentCaptor.forClass(SipTransportFeatureTagStats.class);
+        verify(mPersistAtomsStorage, times(expectedResultSize))
+                .addSipTransportFeatureTagStats(captor.capture());
+
+        List<SipTransportFeatureTagStats> captorValues = captor.getAllValues();
+
+        assertEquals(captorValues.size(), expectedResultSize);
+        for (int i = 0; i < captorValues.size(); i++) {
+            SipTransportFeatureTagStats stats = captorValues.get(i);
+            TestResult expectedResult = expectedResults.get(i);
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            assertEquals(expectedResult.tagValue, stats.featureTagName);
+            assertEquals(expectedResult.duration, stats.associatedMillis);
+            assertEquals(expectedResult.deniedReason, stats.sipTransportDeniedReason);
+            assertEquals(expectedResult.deregiReason, stats.sipTransportDeregisteredReason);
+        }
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+
+    }
+
+    private List<TestResult> getTestResult(long timeGap, boolean concludeTest) {
+        List<TestResult> results = new ArrayList<>();
+        results.add(new TestResult(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                TelephonyProtoEnums.IMS_FEATURE_TAG_FILE_TRANSFER,
+                timeGap,
+                SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, RcsStats.NONE));
+        results.add(new TestResult(FeatureTags.FEATURE_TAG_STANDALONE_MSG,
+                TelephonyProtoEnums.IMS_FEATURE_TAG_STANDALONE_MSG,
+                timeGap, RcsStats.NONE, RcsStats.NONE));
+        if (concludeTest) {
+            results.add(new TestResult(FeatureTags.FEATURE_TAG_STANDALONE_MSG,
+                    TelephonyProtoEnums.IMS_FEATURE_TAG_STANDALONE_MSG,
+                    timeGap, RcsStats.NONE,
+                    DelegateRegistrationState.DEREGISTERED_REASON_NOT_REGISTERED));
+            results.add(new TestResult(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                    TelephonyProtoEnums.IMS_FEATURE_TAG_FILE_TRANSFER,
+                    timeGap,
+                    SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, RcsStats.NONE));
+            results.add(new TestResult(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+                    TelephonyProtoEnums.IMS_FEATURE_TAG_FILE_TRANSFER,
+                    timeGap,
+                    SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, RcsStats.NONE));
+            results.add(new TestResult(FeatureTags.FEATURE_TAG_STANDALONE_MSG,
+                    TelephonyProtoEnums.IMS_FEATURE_TAG_STANDALONE_MSG,
+                    timeGap, RcsStats.NONE, RcsStats.NONE));
+
+        }
+        return results;
+    }
+
+    @Test
+    @SmallTest
+    public void onSipMessageResponse_withAtoms() throws Exception {
+        String testSipMessageMethod = "MESSAGE";
+        int testSipRequestMessageDirection = 1; //INCOMING: 0, OUTGOING: 1
+        int testSipMessageResponse = 200;
+        int testMessageError = 0;
+        String testCallId = "testId";
+        // Request message
+        mRcsStats.onSipMessageRequest(testCallId, testSipMessageMethod,
+                testSipRequestMessageDirection);
+        // Response message
+        mRcsStats.onSipMessageResponse(mSubId, testCallId, testSipMessageResponse,
+                testMessageError);
+        ArgumentCaptor<SipMessageResponse> captor =
+                ArgumentCaptor.forClass(SipMessageResponse.class);
+        verify(mPersistAtomsStorage).addSipMessageResponse(captor.capture());
+        SipMessageResponse stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(TelephonyProtoEnums.SIP_REQUEST_MESSAGE, stats.sipMessageMethod);
+        assertEquals(testSipRequestMessageDirection, stats.sipMessageDirection);
+        assertEquals(testSipMessageResponse, stats.sipMessageResponse);
+        assertEquals(testMessageError, stats.messageError);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onSipTransportSession_withAtoms() throws Exception {
+        String testInviteSipMethod = "INVITE";
+        String testCallId = "testId";
+        int testSipResponse = 0;
+        int testSipRequestMessageDirection = 1; //INCOMING: 0, OUTGOING: 1
+        // Request Message
+        mRcsStats.earlySipTransportSession(
+                testInviteSipMethod, testCallId, testSipRequestMessageDirection);
+        // gracefully close
+        mRcsStats.onSipTransportSessionClosed(mSubId, testCallId, testSipResponse, true);
+        ArgumentCaptor<SipTransportSession> captor =
+                ArgumentCaptor.forClass(SipTransportSession.class);
+        verify(mPersistAtomsStorage).addCompleteSipTransportSession(captor.capture());
+        SipTransportSession stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(TelephonyProtoEnums.SIP_REQUEST_INVITE, stats.sessionMethod);
+        assertEquals(testSipRequestMessageDirection, stats.sipMessageDirection);
+        assertEquals(testSipResponse, stats.sipResponse);
+        assertEquals(true/*isEndedGracefully*/, stats.isEndedGracefully);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsDedicatedBearerListenerEvent_Added() throws Exception {
+        final int listenerId = 1;
+        int ratAtEnd = TelephonyProtoEnums.NETWORK_TYPE_LTE;
+        final int qci = 5;
+
+        mRcsStats.dedicatedBearerListenerEventMap_remove(listenerId);
+        mRcsStats.onImsDedicatedBearerListenerAdded(listenerId, SLOT_ID, ratAtEnd, qci);
+        assertTrue(mRcsStats.dedicatedBearerListenerEventMap_containsKey(listenerId));
+        ImsDedicatedBearerListenerEvent testProto =
+                mRcsStats.dedicatedBearerListenerEventMap_get(listenerId);
+        assertEquals(SLOT_ID, testProto.slotId);
+        assertEquals(ratAtEnd, testProto.ratAtEnd);
+        assertEquals(qci, testProto.qci);
+        assertFalse(testProto.dedicatedBearerEstablished);
+        verify(mPersistAtomsStorage, never()).addImsDedicatedBearerListenerEvent(any());
+
+        // same listenerId, different contents. should be ignored
+        ratAtEnd = TelephonyProtoEnums.NETWORK_TYPE_NR;
+        mRcsStats.onImsDedicatedBearerListenerAdded(listenerId, SLOT_ID + 1, ratAtEnd + 1, qci + 1);
+        testProto = mRcsStats.dedicatedBearerListenerEventMap_get(listenerId);
+        assertEquals(SLOT_ID, testProto.slotId);
+        assertNotEquals(ratAtEnd, testProto.ratAtEnd);
+        assertEquals(qci, testProto.qci);
+        verify(mPersistAtomsStorage, never()).addImsDedicatedBearerListenerEvent(any());
+
+        mRcsStats.dedicatedBearerListenerEventMap_remove(listenerId);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsDedicatedBearerListenerEvent_bearerEstablished() throws Exception {
+        final int listenerId = 2;
+        final int rat = TelephonyProtoEnums.NETWORK_TYPE_LTE;
+        final int qci = 6;
+
+        mRcsStats.dedicatedBearerListenerEventMap_remove(listenerId);
+        mRcsStats.onImsDedicatedBearerListenerUpdateSession(listenerId, SLOT_ID, rat, qci, true);
+        verify(mPersistAtomsStorage, never()).addImsDedicatedBearerListenerEvent(any());
+
+        mRcsStats.dedicatedBearerListenerEventMap_remove(listenerId);
+        mRcsStats.onImsDedicatedBearerListenerAdded(listenerId, SLOT_ID, rat, qci);
+        assertTrue(mRcsStats.dedicatedBearerListenerEventMap_containsKey(listenerId));
+        mRcsStats.onImsDedicatedBearerListenerUpdateSession(listenerId, SLOT_ID, rat, qci, true);
+        ImsDedicatedBearerListenerEvent testProto =
+                mRcsStats.dedicatedBearerListenerEventMap_get(listenerId);
+        assertEquals(qci, testProto.qci);
+        assertTrue(testProto.dedicatedBearerEstablished);
+
+        verify(mPersistAtomsStorage, never()).addImsDedicatedBearerListenerEvent(any());
+    }
+
+    @Test
+    @SmallTest
+    public void onImsDedicatedBearerListenerEvent_Removed() throws Exception {
+        final int listenerId = 3;
+        final int rat = TelephonyProtoEnums.NETWORK_TYPE_LTE;
+        final int qci = 7;
+
+        mRcsStats.dedicatedBearerListenerEventMap_remove(listenerId);
+        mRcsStats.onImsDedicatedBearerListenerRemoved(listenerId);
+        verify(mPersistAtomsStorage, never()).addImsDedicatedBearerListenerEvent(any());
+
+        mRcsStats.onImsDedicatedBearerListenerAdded(listenerId, SLOT_ID, rat, qci);
+        mRcsStats.onImsDedicatedBearerListenerUpdateSession(listenerId, SLOT_ID, rat, qci, true);
+        mRcsStats.onImsDedicatedBearerListenerRemoved(listenerId);
+        verify(mPersistAtomsStorage, times(1)).addImsDedicatedBearerListenerEvent(any());
+
+        // and values should be same
+        ArgumentCaptor<ImsDedicatedBearerListenerEvent> captor =
+                ArgumentCaptor.forClass(ImsDedicatedBearerListenerEvent.class);
+        verify(mPersistAtomsStorage).addImsDedicatedBearerListenerEvent(captor.capture());
+        ImsDedicatedBearerListenerEvent stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(rat, stats.ratAtEnd);
+        assertEquals(qci, stats.qci);
+        assertEquals(true, stats.dedicatedBearerEstablished);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+
+        assertFalse(mRcsStats.dedicatedBearerListenerEventMap_containsKey(listenerId));
+    }
+
+    @Test
+    @SmallTest
+    public void onImsDedicatedBearerEvent_withAtoms() throws Exception {
+        // reference comments in test_imsDedicatedBearerListenerEvent for canditate value
+        int ratAtEnd = TelephonyStatsLog
+                .IMS_DEDICATED_BEARER_LISTENER_EVENT__RAT_AT_END__NETWORK_TYPE_LTE_CA;
+        int qci = 6;
+        /*
+         * IMS_DEDICATED_BEARER_EVENT__BEARER_STATE__STATE_ADDED = 1;
+         * IMS_DEDICATED_BEARER_EVENT__BEARER_STATE__STATE_MODIFIED = 2;
+         * IMS_DEDICATED_BEARER_EVENT__BEARER_STATE__STATE_DELETED = 3;
+         */
+        int bearerState = TelephonyStatsLog.IMS_DEDICATED_BEARER_EVENT__BEARER_STATE__STATE_ADDED;
+        boolean localConnectionInfoReceived = false;
+        boolean remoteConnectionInfoReceived = true;
+        boolean hasListeners = true;
+
+        mRcsStats.onImsDedicatedBearerEvent(SLOT_ID, ratAtEnd, qci, bearerState,
+                localConnectionInfoReceived, remoteConnectionInfoReceived, hasListeners);
+
+        ArgumentCaptor<ImsDedicatedBearerEvent> captor =
+                ArgumentCaptor.forClass(ImsDedicatedBearerEvent.class);
+        verify(mPersistAtomsStorage).addImsDedicatedBearerEvent(captor.capture());
+        ImsDedicatedBearerEvent stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(ratAtEnd, stats.ratAtEnd);
+        assertEquals(qci, stats.qci);
+        assertEquals(bearerState, stats.bearerState);
+        assertEquals(localConnectionInfoReceived, stats.localConnectionInfoReceived);
+        assertEquals(remoteConnectionInfoReceived, stats.remoteConnectionInfoReceived);
+        assertEquals(hasListeners, stats.hasListeners);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsRegistrationServiceDescStats_withAtoms() throws Exception {
+        int registrationTech  = 0; //ImsRegistrationImplBase.REGISTRATION_TECH_LTE
+        ArrayList<String> serviceIdList = new ArrayList<>();
+        serviceIdList.add("org.openmobilealliance:File-Transfer-HTTP");
+        serviceIdList.add("org.openmobilealliance:IM-session");
+        serviceIdList.add("Unknown1");
+        ArrayList<String> serviceIdVersionList = new ArrayList<>();
+        serviceIdVersionList.add("1.0");
+        serviceIdVersionList.add("1.0");
+        serviceIdVersionList.add("3.0");
+
+        mRcsStats.onImsRegistrationServiceDescStats(mSubId, serviceIdList, serviceIdVersionList,
+                registrationTech);
+
+        // getWallTimeMillis
+        /*
+         * UCE_EVENT__TYPE__PUBLISH = 0;
+         * UCE_EVENT__TYPE__SUBSCRIBE = 1;
+         * UCE_EVENT__TYPE__INCOMING_OPTION = 2;
+         * UCE_EVENT__TYPE__OUTGOING_OPTION = 3;
+         */
+        int type = TelephonyStatsLog.UCE_EVENT_STATS__TYPE__PUBLISH;
+        boolean successful = true;
+        /*
+         * UCE_EVENT__COMMAND_CODE__SERVICE_UNKNOWN = 0;
+         * UCE_EVENT__COMMAND_CODE__GENERIC_FAILURE = 1;
+         * UCE_EVENT__COMMAND_CODE__INVALID_PARAM = 2;
+         * UCE_EVENT__COMMAND_CODE__FETCH_ERROR = 3;
+         * UCE_EVENT__COMMAND_CODE__REQUEST_TIMEOUT = 4;
+         * UCE_EVENT__COMMAND_CODE__INSUFFICIENT_MEMORY = 5;
+         * UCE_EVENT__COMMAND_CODE__LOST_NETWORK_CONNECTION = 6;
+         * UCE_EVENT__COMMAND_CODE__NOT_SUPPORTED = 7;
+         * UCE_EVENT__COMMAND_CODE__NOT_FOUND = 8;
+         * UCE_EVENT__COMMAND_CODE__SERVICE_UNAVAILABLE = 9;
+         * UCE_EVENT__COMMAND_CODE__NO_CHANGE = 10;
+         */
+        int commandCode = TelephonyStatsLog.UCE_EVENT_STATS__COMMAND_CODE__SERVICE_UNAVAILABLE;
+        int networkResponse = 200;
+
+        mRcsStats.onUceEventStats(mSubId, type, successful, commandCode, networkResponse);
+
+        {
+            ArgumentCaptor<UceEventStats> captor = ArgumentCaptor.forClass(UceEventStats.class);
+            verify(mPersistAtomsStorage).addUceEventStats(captor.capture());
+            UceEventStats stats = captor.getValue();
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            assertEquals(successful, stats.successful);
+            assertEquals(commandCode, stats.commandCode);
+            assertEquals(networkResponse, stats.networkResponse);
+            verifyNoMoreInteractions(mPersistAtomsStorage);
+        }
+
+        long timeGap = 6000L;
+        mRcsStats.incTimeMillis(timeGap);
+
+        mRcsStats.onStoreCompleteImsRegistrationServiceDescStats(mSubId);
+
+        ArgumentCaptor<ImsRegistrationServiceDescStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationServiceDescStats.class);
+        verify(mPersistAtomsStorage, times(3))
+                .addImsRegistrationServiceDescStats(captor.capture());
+        List<ImsRegistrationServiceDescStats> captorValues = captor.getAllValues();
+
+        assertEquals(captorValues.size(), serviceIdList.size());
+
+        for (int index = 0; index < captorValues.size(); index++) {
+            ImsRegistrationServiceDescStats stats = captorValues.get(index);
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            int serviceId = mRcsStats.convertServiceIdToValue(serviceIdList.get(index));
+            assertEquals(serviceId, stats.serviceIdName);
+            float serviceVersionFloat = Float.parseFloat(serviceIdVersionList.get(index));
+            assertEquals(serviceVersionFloat, stats.serviceIdVersion, 0.1f);
+            assertEquals(registrationTech, stats.registrationTech);
+            assertEquals(timeGap, stats.publishedMillis);
+        }
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsRegistrationServiceDescStats_withAtomsInvalidSubId() throws Exception {
+        int registrationTech  = 0; //ImsRegistrationImplBase.REGISTRATION_TECH_LTE
+        ArrayList<String> serviceIdList = new ArrayList<>();
+        serviceIdList.add("org.openmobilealliance:File-Transfer-HTTP");
+        serviceIdList.add("org.openmobilealliance:IM-session");
+        serviceIdList.add("Unknown1");
+        ArrayList<String> serviceIdVersionList = new ArrayList<>();
+        serviceIdVersionList.add("1.0");
+        serviceIdVersionList.add("1.0");
+        serviceIdVersionList.add("3.0");
+
+        mRcsStats.onImsRegistrationServiceDescStats(mSubId, serviceIdList, serviceIdVersionList,
+                registrationTech);
+
+        // getWallTimeMillis
+        /*
+         * UCE_EVENT__TYPE__PUBLISH = 0;
+         * UCE_EVENT__TYPE__SUBSCRIBE = 1;
+         * UCE_EVENT__TYPE__INCOMING_OPTION = 2;
+         * UCE_EVENT__TYPE__OUTGOING_OPTION = 3;
+         */
+        int type = TelephonyStatsLog.UCE_EVENT_STATS__TYPE__PUBLISH;
+        boolean successful = true;
+        /*
+         * UCE_EVENT__COMMAND_CODE__SERVICE_UNKNOWN = 0;
+         * UCE_EVENT__COMMAND_CODE__GENERIC_FAILURE = 1;
+         * UCE_EVENT__COMMAND_CODE__INVALID_PARAM = 2;
+         * UCE_EVENT__COMMAND_CODE__FETCH_ERROR = 3;
+         * UCE_EVENT__COMMAND_CODE__REQUEST_TIMEOUT = 4;
+         * UCE_EVENT__COMMAND_CODE__INSUFFICIENT_MEMORY = 5;
+         * UCE_EVENT__COMMAND_CODE__LOST_NETWORK_CONNECTION = 6;
+         * UCE_EVENT__COMMAND_CODE__NOT_SUPPORTED = 7;
+         * UCE_EVENT__COMMAND_CODE__NOT_FOUND = 8;
+         * UCE_EVENT__COMMAND_CODE__SERVICE_UNAVAILABLE = 9;
+         * UCE_EVENT__COMMAND_CODE__NO_CHANGE = 10;
+         */
+        int commandCode = TelephonyStatsLog.UCE_EVENT_STATS__COMMAND_CODE__SERVICE_UNAVAILABLE;
+        int networkResponse = 200;
+        mRcsStats.onUceEventStats(mSubId, type, successful, commandCode, networkResponse);
+
+        // slotId and carrierId are invalid based on subId
+        mRcsStats.setEnableInvalidSubId();
+        long timeGap = 6000L;
+        mRcsStats.incTimeMillis(timeGap);
+        mRcsStats.onUceEventStats(mSubId, type, successful, commandCode, networkResponse);
+
+        ArgumentCaptor<ImsRegistrationServiceDescStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationServiceDescStats.class);
+        verify(mPersistAtomsStorage, times(3))
+                .addImsRegistrationServiceDescStats(captor.capture());
+        List<ImsRegistrationServiceDescStats> captorValues = captor.getAllValues();
+
+        assertEquals(captorValues.size(), serviceIdList.size());
+
+        for (int index = 0; index < captorValues.size(); index++) {
+            ImsRegistrationServiceDescStats stats = captorValues.get(index);
+            assertEquals(CARRIER_ID, stats.carrierId);
+            assertEquals(SLOT_ID, stats.slotId);
+            int serviceId = mRcsStats.convertServiceIdToValue(serviceIdList.get(index));
+            assertEquals(serviceId, stats.serviceIdName);
+            float serviceVersionFloat = Float.parseFloat(serviceIdVersionList.get(index));
+            assertEquals(serviceVersionFloat, stats.serviceIdVersion, 0.1f);
+            assertEquals(registrationTech, stats.registrationTech);
+            assertEquals(timeGap, stats.publishedMillis);
+        }
+        assertEquals(0, mRcsStats.getImsRegistrationServiceDescCachedSize());
+    }
+
+    @Test
+    @SmallTest
+    public void onUceEventStats_withAtoms() throws Exception {
+        int messageType = TelephonyStatsLog.UCE_EVENT_STATS__TYPE__PUBLISH;
+        boolean successful = true;
+        int commandCode = TelephonyStatsLog.UCE_EVENT_STATS__COMMAND_CODE__REQUEST_TIMEOUT;
+        int networkResponse = 408;
+
+        mRcsStats.onUceEventStats(mSubId, messageType, successful, commandCode, networkResponse);
+
+        ArgumentCaptor<UceEventStats> captor = ArgumentCaptor.forClass(UceEventStats.class);
+        verify(mPersistAtomsStorage).addUceEventStats(captor.capture());
+        UceEventStats stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(successful, stats.successful);
+        assertEquals(commandCode, stats.commandCode);
+        assertEquals(networkResponse, stats.networkResponse);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onPresenceNotifyEvent_withAtoms() throws Exception {
+        String reason = "deactivated";
+        boolean contentBodyReceived = true;
+        boolean rcsCaps = true;
+        boolean mmtelCaps = false;
+        boolean noCaps = false;
+
+        mRcsStats.onPresenceNotifyEvent(mSubId, reason, contentBodyReceived,
+                rcsCaps, mmtelCaps, noCaps);
+
+        ArgumentCaptor<PresenceNotifyEvent> captor =
+                ArgumentCaptor.forClass(PresenceNotifyEvent.class);
+        verify(mPersistAtomsStorage).addPresenceNotifyEvent(captor.capture());
+        PresenceNotifyEvent stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        int reasonInt = mRcsStats.convertPresenceNotifyReason(reason);
+        assertEquals(reasonInt, stats.reason);
+        assertEquals(contentBodyReceived, stats.contentBodyReceived);
+        assertEquals(1, stats.rcsCapsCount);
+        assertEquals(0, stats.mmtelCapsCount);
+        assertEquals(0, stats.noCapsCount);
+        assertEquals(1, stats.rcsCapsCount);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onGbaEvent_withAtoms() throws Exception {
+        boolean successful = false;
+        /*
+         * GBA_EVENT__FAILED_REASON__UNKNOWN
+         * GBA_EVENT__FAILED_REASON__FEATURE_NOT_SUPPORTED
+         * GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY
+         * GBA_EVENT__FAILED_REASON__NETWORK_FAILURE
+         * GBA_EVENT__FAILED_REASON__INCORRECT_NAF_ID
+         * GBA_EVENT__FAILED_REASON__SECURITY_PROTOCOL_NOT_SUPPORTED
+         */
+        int failedReason = TelephonyStatsLog.GBA_EVENT__FAILED_REASON__FEATURE_NOT_READY;
+
+        mRcsStats.onGbaFailureEvent(mSubId, failedReason);
+
+        ArgumentCaptor<GbaEvent> captor = ArgumentCaptor.forClass(GbaEvent.class);
+        verify(mPersistAtomsStorage).addGbaEvent(captor.capture());
+        GbaEvent stats = captor.getValue();
+        assertEquals(CARRIER_ID, stats.carrierId);
+        assertEquals(SLOT_ID, stats.slotId);
+        assertEquals(successful, stats.successful);
+        assertEquals(failedReason, stats.failedReason);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+}