Merge "[NS01.tl] Remove an unused argument"
diff --git a/proto/src/persist_atoms.proto b/proto/src/persist_atoms.proto
index 2f84639..d7cc58f 100644
--- a/proto/src/persist_atoms.proto
+++ b/proto/src/persist_atoms.proto
@@ -23,19 +23,67 @@
 
 // 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: 5
+// Next id: 21
 message PersistAtoms {
     /* Aggregated RAT usage during the call. */
-    repeated RawVoiceCallRatUsage raw_voice_call_rat_usage = 1;
+    repeated VoiceCallRatUsage voice_call_rat_usage = 1;
 
     /* Timestamp of last voice_call_rat_usages pull. */
-    optional int64 raw_voice_call_rat_usage_pull_timestamp_millis = 2;
+    optional int64 voice_call_rat_usage_pull_timestamp_millis = 2;
 
     /* Per call statistics and information. */
     repeated VoiceCallSession voice_call_session = 3;
 
     /* Timestamp of last voice_call_sessions pull. */
     optional int64 voice_call_session_pull_timestamp_millis = 4;
+
+    /* Incoming SMS statistics and information. */
+    repeated IncomingSms incoming_sms = 5;
+
+    /* Timestamp of last incoming_sms pull. */
+    optional int64 incoming_sms_pull_timestamp_millis = 6;
+
+    /* Outgoing SMS statistics and information. */
+    repeated OutgoingSms outgoing_sms = 7;
+
+    /* Timestamp of last incoming_sms pull. */
+    optional int64 outgoing_sms_pull_timestamp_millis = 8;
+
+    /* List of carrier ID mismatch events already sent. */
+    repeated CarrierIdMismatch carrier_id_mismatch = 9;
+
+    /* Last version of carrier ID table sent. */
+    optional int32 carrier_id_table_version = 10;
+
+    /* Data Call session statistics and information. */
+    repeated DataCallSession data_call_session = 11;
+
+    /* Timestamp of last data_call_session pull. */
+    optional int64 data_call_session_pull_timestamp_millis = 12;
+
+    /* Duration spent in each possible service state. */
+    repeated CellularServiceState cellular_service_state = 13;
+
+    /* Timestamp of last cellular_service_state pull. */
+    optional int64 cellular_service_state_pull_timestamp_millis = 14;
+
+    /* Switch count between data RATs. */
+    repeated CellularDataServiceSwitch cellular_data_service_switch = 15;
+
+    /* Timestamp of last cellular_data_service_switch pull. */
+    optional int64 cellular_data_service_switch_pull_timestamp_millis = 16;
+
+    /* List of IMS registration terminations. */
+    repeated ImsRegistrationTermination ims_registration_termination = 17;
+
+    /* Timestamp of last ims_registration_termination pull. */
+    optional int64 ims_registration_termination_pull_timestamp_millis = 18;
+
+    /* Durations of IMS registrations and capabilities. */
+    repeated ImsRegistrationStats ims_registration_stats = 19;
+
+    /* Timestamp of last ims_registration_stats pull. */
+    optional int64 ims_registration_stats_pull_timestamp_millis = 20;
 }
 
 // The canonical versions of the following enums live in:
@@ -70,15 +118,142 @@
     optional bool rtt_enabled = 22;
     optional bool is_emergency = 23;
     optional bool is_roaming = 24;
+    optional int32 signal_strength_at_end = 25;
+    optional int32 band_at_end = 26;
+    optional int32 setup_duration_millis = 27;
+    optional int32 main_codec_quality = 28;
+    optional bool video_enabled = 29;
+    optional int32 rat_at_connected = 30;
+    optional bool is_multiparty = 31;
 
     // Internal use only
     optional int64 setup_begin_millis = 10001;
 }
 
-// Internal use only
-message RawVoiceCallRatUsage {
+message VoiceCallRatUsage {
     optional int32 carrier_id = 1;
     optional int32 rat = 2;
-    optional int64 total_duration_millis = 3;
+    optional int64 total_duration_millis = 3; // Duration needs to be rounded when pulled
     optional int64 call_count = 4;
 }
+
+message IncomingSms {
+    optional int32 sms_format = 1;
+    optional int32 sms_tech = 2;
+    optional int32 rat = 3;
+    optional int32 sms_type = 4;
+    optional int32 total_parts = 5;
+    optional int32 received_parts = 6;
+    optional bool blocked = 7;
+    optional int32 error = 8;
+    optional bool is_roaming = 9;
+    optional int32 sim_slot_index = 10;
+    optional bool is_multi_sim = 11;
+    optional bool is_esim = 12;
+    optional int32 carrier_id = 13;
+    optional int64 message_id = 14;
+}
+
+message OutgoingSms {
+    optional int32 sms_format = 1;
+    optional int32 sms_tech = 2;
+    optional int32 rat = 3;
+    optional int32 send_result = 4;
+    optional int32 error_code = 5;
+    optional bool is_roaming = 6;
+    optional bool is_from_default_app = 7;
+    optional int32 sim_slot_index = 8;
+    optional bool is_multi_sim = 9;
+    optional bool is_esim = 10;
+    optional int32 carrier_id = 11;
+    optional int64 message_id = 12;
+    optional int32 retry_id = 13;
+}
+
+message CarrierIdMismatch {
+    optional string mcc_mnc = 1;
+    optional string gid1 = 2;
+    optional string spn = 3;
+    optional string pnn = 4;
+}
+
+message DataCallSession {
+    reserved 4;
+    optional int32 dimension = 1;
+    optional bool is_multi_sim = 2;
+    optional bool is_esim = 3;
+    optional int32 apn_type_bitmask = 5;
+    optional int32 carrier_id = 6;
+    optional bool is_roaming = 7;
+    optional int32 rat_at_end = 8;
+    optional bool oos_at_end = 9;
+    optional int64 rat_switch_count = 10;
+    optional bool is_opportunistic = 11;
+    optional int32 ip_type = 12;
+    optional bool setup_failed = 13;
+    optional int32 failure_cause = 14;
+    optional int32 suggested_retry_millis = 15;
+    optional int32 deactivate_reason = 16;
+    optional int64 duration_minutes = 17;
+    optional bool ongoing = 18;
+}
+
+message CellularServiceState {
+    optional int32 voice_rat = 1;
+    optional int32 data_rat = 2;
+    optional int32 voice_roaming_type = 3;
+    optional int32 data_roaming_type = 4;
+    optional bool is_endc = 5;
+    optional int32 sim_slot_index = 6;
+    optional bool is_multi_sim = 7;
+    optional int32 carrier_id = 8;
+    optional int64 total_time_millis = 9; // Duration needs to be rounded when pulled
+
+    // Internal use only
+    optional int64 last_used_millis = 10001;
+}
+
+message CellularDataServiceSwitch {
+    optional int32 rat_from = 1;
+    optional int32 rat_to = 2;
+    optional int32 sim_slot_index = 3;
+    optional bool is_multi_sim = 4;
+    optional int32 carrier_id = 5;
+    optional int32 switch_count = 6;
+
+    // Internal use only
+    optional int64 last_used_millis = 10001;
+}
+
+message ImsRegistrationTermination {
+    optional int32 carrier_id = 1;
+    optional bool is_multi_sim = 2;
+    optional int32 rat_at_end = 3;
+    optional bool setup_failed = 4;
+    optional int32 reason_code = 5;
+    optional int32 extra_code = 6;
+    optional string extra_message = 7;
+    optional int32 count = 8;
+
+    // Internal use only
+    optional int64 last_used_millis = 10001;
+}
+
+message ImsRegistrationStats {
+    optional int32 carrier_id = 1;
+    optional int32 sim_slot_index = 2;
+    optional int32 rat = 3;
+    // Durations need to be rounded when pulled
+    optional int64 registered_millis = 4;
+    optional int64 voice_capable_millis = 5;
+    optional int64 voice_available_millis = 6;
+    optional int64 sms_capable_millis = 7;
+    optional int64 sms_available_millis = 8;
+    optional int64 video_capable_millis = 9;
+    optional int64 video_available_millis = 10;
+    optional int64 ut_capable_millis = 11;
+    optional int64 ut_available_millis = 12;
+
+    // Internal use only
+    optional int64 last_used_millis = 10001;
+}
diff --git a/src/java/com/android/internal/telephony/CarrierResolver.java b/src/java/com/android/internal/telephony/CarrierResolver.java
index 207d078..56b37a5 100644
--- a/src/java/com/android/internal/telephony/CarrierResolver.java
+++ b/src/java/com/android/internal/telephony/CarrierResolver.java
@@ -16,7 +16,6 @@
 package com.android.internal.telephony;
 
 import static android.provider.Telephony.CarrierId;
-import static android.provider.Telephony.Carriers.CONTENT_URI;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -41,6 +40,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.metrics.CarrierIdMatchStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.uicc.IccRecords;
 import com.android.internal.telephony.uicc.UiccController;
@@ -78,6 +78,8 @@
     private static final String TEST_ACTION = "com.android.internal.telephony"
             + ".ACTION_TEST_OVERRIDE_CARRIER_ID";
 
+    // cached version of the carrier list, so that we don't need to re-query it every time.
+    private Integer mCarrierListVersion;
     // cached matching rules based mccmnc to speed up resolution
     private List<CarrierMatchingRule> mCarrierMatchingRulesOnMccMnc = new ArrayList<>();
     // cached carrier Id
@@ -275,6 +277,8 @@
                 handleSimLoaded();
                 break;
             case CARRIER_ID_DB_UPDATE_EVENT:
+                // clean the cached carrier list version, so that a new one will be queried.
+                mCarrierListVersion = null;
                 loadCarrierMatchingRulesOnMccMnc(true /* update carrier config*/);
                 break;
             case PREFER_APN_UPDATE_EVENT:
@@ -327,6 +331,9 @@
                         mCarrierMatchingRulesOnMccMnc.add(makeCarrierMatchingRule(cursor));
                     }
                     matchSubscriptionCarrier(updateCarrierConfig);
+
+                    // Generate metrics related to carrier ID table version.
+                    CarrierIdMatchStats.sendCarrierIdTableVersion(getCarrierListVersion());
                 }
             } finally {
                 if (cursor != null) {
@@ -931,14 +938,26 @@
         TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
                 mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId,
                 unknownMccmncToLog, unknownGid1ToLog, simInfo);
+
+        // Generate statsd metrics only when MCC/MNC is unknown or there is no match for GID1.
+        if (unknownMccmncToLog != null || unknownGid1ToLog != null) {
+            // Pass the PNN value to metrics only if the SPN is empty
+            String pnn = TextUtils.isEmpty(subscriptionRule.spn) ? subscriptionRule.plmn : "";
+            CarrierIdMatchStats.onCarrierIdMismatch(
+                    mCarrierId, unknownMccmncToLog, unknownGid1ToLog, subscriptionRule.spn, pnn);
+        }
     }
 
     public int getCarrierListVersion() {
-        final Cursor cursor = mContext.getContentResolver().query(
-                Uri.withAppendedPath(CarrierId.All.CONTENT_URI,
-                "get_version"), null, null, null);
-        cursor.moveToFirst();
-        return cursor.getInt(0);
+        // Use the cached value if it exists, otherwise retrieve it.
+        if (mCarrierListVersion == null) {
+            final Cursor cursor = mContext.getContentResolver().query(
+                    Uri.withAppendedPath(CarrierId.All.CONTENT_URI,
+                    "get_version"), null, null, null);
+            cursor.moveToFirst();
+            mCarrierListVersion = cursor.getInt(0);
+        }
+        return mCarrierListVersion;
     }
 
     public int getCarrierId() {
diff --git a/src/java/com/android/internal/telephony/DeviceStateMonitor.java b/src/java/com/android/internal/telephony/DeviceStateMonitor.java
index 9e95939..94de555 100644
--- a/src/java/com/android/internal/telephony/DeviceStateMonitor.java
+++ b/src/java/com/android/internal/telephony/DeviceStateMonitor.java
@@ -628,26 +628,31 @@
     }
 
     private void setSignalStrengthReportingCriteria() {
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSSI,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI,
                 AccessNetworkThresholds.GERAN, AccessNetworkType.GERAN, true);
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSCP,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP,
                 AccessNetworkThresholds.UTRAN, AccessNetworkType.UTRAN, true);
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSRP,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP,
                 AccessNetworkThresholds.EUTRAN_RSRP, AccessNetworkType.EUTRAN, true);
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSSI,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI,
                 AccessNetworkThresholds.CDMA2000, AccessNetworkType.CDMA2000, true);
         if (mPhone.getHalVersion().greaterOrEqual(RIL.RADIO_HAL_VERSION_1_5)) {
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSRQ,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ,
                     AccessNetworkThresholds.EUTRAN_RSRQ, AccessNetworkType.EUTRAN, false);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSSNR,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR,
                     AccessNetworkThresholds.EUTRAN_RSSNR, AccessNetworkType.EUTRAN, true);
 
             // Defaultly we only need SSRSRP for NGRAN signal criteria reporting
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_SSRSRP,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP,
                     AccessNetworkThresholds.NGRAN_RSRSRP, AccessNetworkType.NGRAN, true);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_SSRSRQ,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ,
                     AccessNetworkThresholds.NGRAN_RSRSRQ, AccessNetworkType.NGRAN, false);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_SSSINR,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR,
                     AccessNetworkThresholds.NGRAN_SSSINR, AccessNetworkType.NGRAN, false);
         }
     }
diff --git a/src/java/com/android/internal/telephony/GsmCdmaPhone.java b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
index 0f59eb2..812a0c6 100644
--- a/src/java/com/android/internal/telephony/GsmCdmaPhone.java
+++ b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
@@ -1876,37 +1876,45 @@
 
     @Override
     public int getCarrierId() {
-        return mCarrierResolver.getCarrierId();
+        return mCarrierResolver != null
+                ? mCarrierResolver.getCarrierId() : super.getCarrierId();
     }
 
     @Override
     public String getCarrierName() {
-        return mCarrierResolver.getCarrierName();
+        return mCarrierResolver != null
+                ? mCarrierResolver.getCarrierName() : super.getCarrierName();
     }
 
     @Override
     public int getMNOCarrierId() {
-        return mCarrierResolver.getMnoCarrierId();
+        return mCarrierResolver != null
+                ? mCarrierResolver.getMnoCarrierId() : super.getMNOCarrierId();
     }
 
     @Override
     public int getSpecificCarrierId() {
-        return mCarrierResolver.getSpecificCarrierId();
+        return mCarrierResolver != null
+                ? mCarrierResolver.getSpecificCarrierId() : super.getSpecificCarrierId();
     }
 
     @Override
     public String getSpecificCarrierName() {
-        return mCarrierResolver.getSpecificCarrierName();
+        return mCarrierResolver != null
+                ? mCarrierResolver.getSpecificCarrierName() : super.getSpecificCarrierName();
     }
 
     @Override
     public void resolveSubscriptionCarrierId(String simState) {
-        mCarrierResolver.resolveSubscriptionCarrierId(simState);
+        if (mCarrierResolver != null) {
+            mCarrierResolver.resolveSubscriptionCarrierId(simState);
+        }
     }
 
     @Override
     public int getCarrierIdListVersion() {
-        return mCarrierResolver.getCarrierListVersion();
+        return mCarrierResolver != null
+                ? mCarrierResolver.getCarrierListVersion() : super.getCarrierIdListVersion();
     }
 
     @Override
@@ -4046,8 +4054,15 @@
     @Override
     public void setSignalStrengthReportingCriteria(
             int signalStrengthMeasure, int[] thresholds, int ran, boolean isEnabled) {
-        mCi.setSignalStrengthReportingCriteria(new SignalThresholdInfo(signalStrengthMeasure,
-                REPORTING_HYSTERESIS_MILLIS, REPORTING_HYSTERESIS_DB, thresholds, isEnabled),
+        mCi.setSignalStrengthReportingCriteria(
+                new SignalThresholdInfo.Builder()
+                        .setRadioAccessNetworkType(ran)
+                        .setSignalMeasurementType(signalStrengthMeasure)
+                        .setHysteresisMs(REPORTING_HYSTERESIS_MILLIS)
+                        .setHysteresisDb(REPORTING_HYSTERESIS_DB)
+                        .setThresholds(thresholds)
+                        .setIsEnabled(isEnabled)
+                        .build(),
                 ran, null);
     }
 
diff --git a/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java b/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java
index 4815dea..a1a9578 100644
--- a/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java
+++ b/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java
@@ -628,7 +628,7 @@
                 "\n format=" + format +
                 "\n receivedIntent=" + receivedIntent);
         }
-        mDispatchersController.injectSmsPdu(pdu, format,
+        mDispatchersController.injectSmsPdu(pdu, format, false /* isOverIms */,
                 result -> {
                     if (receivedIntent != null) {
                         try {
diff --git a/src/java/com/android/internal/telephony/ImsSmsDispatcher.java b/src/java/com/android/internal/telephony/ImsSmsDispatcher.java
index 722390fb..eb96af2 100644
--- a/src/java/com/android/internal/telephony/ImsSmsDispatcher.java
+++ b/src/java/com/android/internal/telephony/ImsSmsDispatcher.java
@@ -23,6 +23,7 @@
 import android.provider.Telephony.Sms.Intents;
 import android.telephony.CarrierConfigManager;
 import android.telephony.ServiceState;
+import android.telephony.SmsManager;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.RegistrationManager;
 import android.telephony.ims.aidl.IImsSmsListener;
@@ -139,7 +140,7 @@
     private final IImsSmsListener mImsSmsListener = new IImsSmsListener.Stub() {
         @Override
         public void onSendSmsResult(int token, int messageRef, @SendStatusResult int status,
-                int reason, int networkReasonCode) {
+                @SmsManager.Result int reason, int networkReasonCode) {
             final long identity = Binder.clearCallingIdentity();
             try {
                 logd("onSendSmsResult token=" + token + " messageRef=" + messageRef
@@ -178,6 +179,13 @@
                         break;
                     default:
                 }
+                mPhone.getSmsStats().onOutgoingSms(
+                        true /* isOverIms */,
+                        SmsConstants.FORMAT_3GPP2.equals(getFormat()),
+                        status == ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK,
+                        reason,
+                        tracker.mMessageId,
+                        tracker.isFromDefaultSmsApplication(mContext));
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
@@ -245,7 +253,7 @@
                     } catch (ImsException e) {
                         loge("Failed to acknowledgeSms(). Error: " + e.getMessage());
                     }
-                }, true);
+                }, true /* ignoreClass */, true /* isOverIms */);
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
@@ -436,6 +444,13 @@
             fallbackToPstn(tracker);
             mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format,
                     ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK, tracker.mMessageId);
+            mPhone.getSmsStats().onOutgoingSms(
+                    true /* isOverIms */,
+                    SmsConstants.FORMAT_3GPP2.equals(format),
+                    true /* fallbackToCs */,
+                    SmsManager.RESULT_SYSTEM_ERROR,
+                    tracker.mMessageId,
+                    tracker.isFromDefaultSmsApplication(mContext));
         }
     }
 
diff --git a/src/java/com/android/internal/telephony/InboundSmsHandler.java b/src/java/com/android/internal/telephony/InboundSmsHandler.java
index f42ba06..c3de17c 100644
--- a/src/java/com/android/internal/telephony/InboundSmsHandler.java
+++ b/src/java/com/android/internal/telephony/InboundSmsHandler.java
@@ -24,6 +24,7 @@
 import static android.service.carrier.CarrierMessagingService.RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE;
 import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.AppOpsManager;
@@ -80,6 +81,8 @@
 import java.io.ByteArrayOutputStream;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -183,6 +186,24 @@
     /** Wakelock release delay when returning to idle state. */
     private static final int WAKELOCK_TIMEOUT = 3000;
 
+    /** Received SMS was not injected. */
+    public static final int SOURCE_NOT_INJECTED = 0;
+
+    /** Received SMS was received over IMS and injected. */
+    public static final int SOURCE_INJECTED_FROM_IMS = 1;
+
+    /** Received SMS was injected from source different than IMS. */
+    public static final int SOURCE_INJECTED_FROM_UNKNOWN = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"SOURCE_"},
+            value = {
+                SOURCE_NOT_INJECTED,
+                SOURCE_INJECTED_FROM_IMS,
+                SOURCE_INJECTED_FROM_UNKNOWN
+    })
+    public @interface SmsSource {}
+
     // The notitfication tag used when showing a notification. The combination of notification tag
     // and notification id should be unique within the phone app.
     private static final String NOTIFICATION_TAG = "InboundSmsHandler";
@@ -255,10 +276,6 @@
     /** Timeout for releasing wakelock */
     private int mWakeLockTimeout;
 
-    /** Indicates if last SMS was injected. This is used to recognize SMS received over IMS from
-        others in order to update metrics. */
-    private boolean mLastSmsWasInjected = false;
-
     private List<SmsFilter> mSmsFilters;
 
     /**
@@ -516,7 +533,7 @@
 
                 case EVENT_INJECT_SMS:
                     // handle new injected SMS
-                    handleInjectSms((AsyncResult) msg.obj);
+                    handleInjectSms((AsyncResult) msg.obj, msg.arg1 == 1 /* isOverIms */);
                     sendMessage(EVENT_RETURN_TO_IDLE);
                     return HANDLED;
 
@@ -641,8 +658,7 @@
         int result;
         try {
             SmsMessage sms = (SmsMessage) ar.result;
-            mLastSmsWasInjected = false;
-            result = dispatchMessage(sms.mWrappedSmsMessage);
+            result = dispatchMessage(sms.mWrappedSmsMessage, SOURCE_NOT_INJECTED);
         } catch (RuntimeException ex) {
             loge("Exception dispatching message", ex);
             result = RESULT_SMS_DISPATCH_FAILURE;
@@ -661,7 +677,7 @@
      * @param ar is the AsyncResult that has the SMS PDU to be injected.
      */
     @UnsupportedAppUsage
-    private void handleInjectSms(AsyncResult ar) {
+    private void handleInjectSms(AsyncResult ar, boolean isOverIms) {
         int result;
         SmsDispatchersController.SmsInjectionCallback callback = null;
         try {
@@ -671,8 +687,9 @@
                 loge("Null injected sms");
                 result = RESULT_SMS_NULL_PDU;
             } else {
-                mLastSmsWasInjected = true;
-                result = dispatchMessage(sms.mWrappedSmsMessage);
+                @SmsSource int smsSource =
+                        isOverIms ? SOURCE_INJECTED_FROM_IMS : SOURCE_INJECTED_FROM_UNKNOWN;
+                result = dispatchMessage(sms.mWrappedSmsMessage, smsSource);
             }
         } catch (RuntimeException ex) {
             loge("Exception dispatching message", ex);
@@ -689,10 +706,11 @@
      * 3GPP2-specific message types.
      *
      * @param smsb the SmsMessageBase object from the RIL
+     * @param smsSource the source of the SMS message
      * @return a result code from {@link android.provider.Telephony.Sms.Intents},
      *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
      */
-    private int dispatchMessage(SmsMessageBase smsb) {
+    private int dispatchMessage(SmsMessageBase smsb, @SmsSource int smsSource) {
         // If sms is null, there was a parsing error.
         if (smsb == null) {
             loge("dispatchSmsMessage: message is null");
@@ -719,12 +737,13 @@
             return Intents.RESULT_SMS_RECEIVED_WHILE_ENCRYPTED;
         }
 
-        int result = dispatchMessageRadioSpecific(smsb);
+        int result = dispatchMessageRadioSpecific(smsb, smsSource);
 
         // In case of error, add to metrics. This is not required in case of success, as the
         // data will be tracked when the message is processed (processMessagePart).
-        if (result != Intents.RESULT_SMS_HANDLED) {
-            mMetrics.writeIncomingSmsError(mPhone.getPhoneId(), mLastSmsWasInjected, result);
+        if (result != Intents.RESULT_SMS_HANDLED && result != Activity.RESULT_OK) {
+            mMetrics.writeIncomingSmsError(mPhone.getPhoneId(), smsSource, result);
+            mPhone.getSmsStats().onIncomingSmsError(is3gpp2(), smsSource, result);
         }
         return result;
     }
@@ -735,10 +754,12 @@
      * {@link #dispatchNormalMessage} from this class.
      *
      * @param smsb the SmsMessageBase object from the RIL
+     * @param smsSource the source of the SMS message
      * @return a result code from {@link android.provider.Telephony.Sms.Intents},
      *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
      */
-    protected abstract int dispatchMessageRadioSpecific(SmsMessageBase smsb);
+    protected abstract int dispatchMessageRadioSpecific(SmsMessageBase smsb,
+            @SmsSource int smsSource);
 
     /**
      * Send an acknowledge message to the SMSC.
@@ -782,10 +803,11 @@
      * {@link #EVENT_BROADCAST_SMS}. Returns {@link Intents#RESULT_SMS_HANDLED} or an error value.
      *
      * @param sms the message to dispatch
+     * @param smsSource the source of the SMS message
      * @return {@link Intents#RESULT_SMS_HANDLED} if the message was accepted, or an error status
      */
     @UnsupportedAppUsage
-    protected int dispatchNormalMessage(SmsMessageBase sms) {
+    protected int dispatchNormalMessage(SmsMessageBase sms, @SmsSource int smsSource) {
         SmsHeader smsHeader = sms.getUserDataHeader();
         InboundSmsTracker tracker;
 
@@ -800,10 +822,10 @@
             tracker = TelephonyComponentFactory.getInstance()
                     .inject(InboundSmsTracker.class.getName())
                     .makeInboundSmsTracker(mContext, sms.getPdu(),
-                    sms.getTimestampMillis(), destPort, is3gpp2(), false,
-                    sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
-                    sms.getMessageBody(), sms.getMessageClass() == MessageClass.CLASS_0,
-                            mPhone.getSubId());
+                            sms.getTimestampMillis(), destPort, is3gpp2(), false,
+                            sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
+                            sms.getMessageBody(), sms.getMessageClass() == MessageClass.CLASS_0,
+                            mPhone.getSubId(), smsSource);
         } else {
             // Create a tracker for this message segment.
             SmsHeader.ConcatRef concatRef = smsHeader.concatRef;
@@ -812,10 +834,11 @@
             tracker = TelephonyComponentFactory.getInstance()
                     .inject(InboundSmsTracker.class.getName())
                     .makeInboundSmsTracker(mContext, sms.getPdu(),
-                    sms.getTimestampMillis(), destPort, is3gpp2(), sms.getOriginatingAddress(),
-                    sms.getDisplayOriginatingAddress(), concatRef.refNumber, concatRef.seqNumber,
-                    concatRef.msgCount, false, sms.getMessageBody(),
-                    sms.getMessageClass() == MessageClass.CLASS_0, mPhone.getSubId());
+                            sms.getTimestampMillis(), destPort, is3gpp2(),
+                            sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
+                            concatRef.refNumber, concatRef.seqNumber, concatRef.msgCount, false,
+                            sms.getMessageBody(), sms.getMessageClass() == MessageClass.CLASS_0,
+                            mPhone.getSubId(), smsSource);
         }
 
         if (VDBG) log("created tracker: " + tracker);
@@ -975,14 +998,7 @@
         }
 
         final boolean isWapPush = (destPort == SmsHeader.PORT_WAP_PUSH);
-
-        // At this point, all parts of the SMS are received. Update metrics for incoming SMS.
-        // WAP-PUSH messages are handled below to also keep track of the result of the processing.
         String format = tracker.getFormat();
-        if (!isWapPush) {
-            mMetrics.writeIncomingSmsSession(mPhone.getPhoneId(), mLastSmsWasInjected,
-                    format, timestamps, block, tracker.getMessageId());
-        }
 
         // Do not process null pdu(s). Check for that and return false in that case.
         List<byte[]> pduList = Arrays.asList(pdus);
@@ -990,6 +1006,8 @@
             String errorMsg = "processMessagePart: returning false due to "
                     + (pduList.size() == 0 ? "pduList.size() == 0" : "pduList.contains(null)");
             logeWithLocalLog(errorMsg, tracker.getMessageId());
+            mPhone.getSmsStats().onIncomingSmsError(
+                    is3gpp2(), tracker.getSource(), RESULT_SMS_NULL_PDU);
             return false;
         }
 
@@ -1004,9 +1022,11 @@
                     } else {
                         loge("processMessagePart: SmsMessage.createFromPdu returned null",
                                 tracker.getMessageId());
-                        mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), mLastSmsWasInjected,
+                        mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), tracker.getSource(),
                                 SmsConstants.FORMAT_3GPP, timestamps, false,
                                 tracker.getMessageId());
+                        mPhone.getSmsStats().onIncomingSmsWapPush(tracker.getSource(),
+                                messageCount, RESULT_SMS_NULL_MESSAGE, tracker.getMessageId());
                         return false;
                     }
                 }
@@ -1035,13 +1055,12 @@
             }
             // Add result of WAP-PUSH into metrics. RESULT_SMS_HANDLED indicates that the WAP-PUSH
             // needs to be ignored, so treating it as a success case.
-            if (result == Activity.RESULT_OK || result == Intents.RESULT_SMS_HANDLED) {
-                mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), mLastSmsWasInjected,
-                        format, timestamps, true, tracker.getMessageId());
-            } else {
-                mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), mLastSmsWasInjected,
-                        format, timestamps, false, tracker.getMessageId());
-            }
+            boolean wapPushResult =
+                    result == Activity.RESULT_OK || result == Intents.RESULT_SMS_HANDLED;
+            mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), tracker.getSource(),
+                    format, timestamps, wapPushResult, tracker.getMessageId());
+            mPhone.getSmsStats().onIncomingSmsWapPush(tracker.getSource(), messageCount,
+                    result, tracker.getMessageId());
             // result is Activity.RESULT_OK if an ordered broadcast was sent
             if (result == Activity.RESULT_OK) {
                 return true;
@@ -1054,6 +1073,15 @@
             }
         }
 
+        // All parts of SMS are received. Update metrics for incoming SMS.
+        // The metrics are generated before SMS filters are invoked.
+        // For messages composed by multiple parts, the metrics are generated considering the
+        // characteristics of the last one.
+        mMetrics.writeIncomingSmsSession(mPhone.getPhoneId(), tracker.getSource(),
+                format, timestamps, block, tracker.getMessageId());
+        mPhone.getSmsStats().onIncomingSmsSuccess(is3gpp2(), tracker.getSource(),
+                messageCount, block, tracker.getMessageId());
+
         // Always invoke SMS filters, even if the number ends up being blocked, to prevent
         // surprising bugs due to blocking numbers that happen to be used for visual voicemail SMS
         // or other carrier system messages.
diff --git a/src/java/com/android/internal/telephony/InboundSmsTracker.java b/src/java/com/android/internal/telephony/InboundSmsTracker.java
index d3306a6..9ddee8d 100644
--- a/src/java/com/android/internal/telephony/InboundSmsTracker.java
+++ b/src/java/com/android/internal/telephony/InboundSmsTracker.java
@@ -56,6 +56,7 @@
     private final boolean mIsClass0;
     private final int mSubId;
     private final long mMessageId;
+    private final @InboundSmsHandler.SmsSource int mSmsSource;
 
     // Fields for concatenating multi-part SMS messages
     private final String mAddress;
@@ -117,10 +118,12 @@
      * @param address originating address
      * @param displayAddress email address if this message was from an email gateway, otherwise same
      *                       as originating address
+     * @param smsSource the source of the SMS message
      */
     public InboundSmsTracker(Context context, byte[] pdu, long timestamp, int destPort,
             boolean is3gpp2, boolean is3gpp2WapPdu, String address, String displayAddress,
-            String messageBody, boolean isClass0, int subId) {
+            String messageBody, boolean isClass0, int subId,
+            @InboundSmsHandler.SmsSource int smsSource) {
         mPdu = pdu;
         mTimestamp = timestamp;
         mDestPort = destPort;
@@ -136,6 +139,7 @@
         mMessageCount = 1;
         mSubId = subId;
         mMessageId = createMessageId(context, timestamp, subId);
+        mSmsSource = smsSource;
     }
 
     /**
@@ -156,11 +160,12 @@
      * @param sequenceNumber the sequence number of this segment (0-based)
      * @param messageCount the total number of segments
      * @param is3gpp2WapPdu true for 3GPP2 format WAP PDU; false otherwise
+     * @param smsSource the source of the SMS message
      */
     public InboundSmsTracker(Context context, byte[] pdu, long timestamp, int destPort,
              boolean is3gpp2, String address, String displayAddress, int referenceNumber,
              int sequenceNumber, int messageCount, boolean is3gpp2WapPdu, String messageBody,
-             boolean isClass0, int subId) {
+             boolean isClass0, int subId, @InboundSmsHandler.SmsSource int smsSource) {
         mPdu = pdu;
         mTimestamp = timestamp;
         mDestPort = destPort;
@@ -177,6 +182,7 @@
         mMessageCount = messageCount;
         mSubId = subId;
         mMessageId = createMessageId(context, timestamp, subId);
+        mSmsSource = smsSource;
     }
 
     /**
@@ -241,6 +247,8 @@
         }
         mMessageBody = cursor.getString(InboundSmsHandler.MESSAGE_BODY_COLUMN);
         mMessageId = createMessageId(context, mTimestamp, mSubId);
+        // TODO(b/167713264): Use the correct SMS source
+        mSmsSource = InboundSmsHandler.SOURCE_NOT_INJECTED;
     }
 
     public ContentValues getContentValues() {
@@ -497,4 +505,8 @@
     public long getMessageId() {
         return mMessageId;
     }
+
+    public @InboundSmsHandler.SmsSource int getSource() {
+        return mSmsSource;
+    }
 }
diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java
index 6180f7a..b7fa377 100644
--- a/src/java/com/android/internal/telephony/Phone.java
+++ b/src/java/com/android/internal/telephony/Phone.java
@@ -81,6 +81,7 @@
 import com.android.internal.telephony.dataconnection.TransportManager;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.imsphone.ImsPhoneCall;
+import com.android.internal.telephony.metrics.SmsStats;
 import com.android.internal.telephony.metrics.VoiceCallSessionStats;
 import com.android.internal.telephony.test.SimulatedRadioControl;
 import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
@@ -444,6 +445,7 @@
     private final CarrierPrivilegesTracker mCarrierPrivilegesTracker;
 
     protected VoiceCallSessionStats mVoiceCallSessionStats;
+    protected SmsStats mSmsStats;
 
     public IccRecords getIccRecords() {
         return mIccRecords.get();
@@ -585,6 +587,9 @@
         mCallRingDelay = TelephonyProperties.call_ring_delay().orElse(3000);
         Rlog.d(LOG_TAG, "mCallRingDelay=" + mCallRingDelay);
 
+        // Initialize SMS stats
+        mSmsStats = new SmsStats(this);
+
         if (getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
             return;
         }
@@ -4573,6 +4578,17 @@
         mVoiceCallSessionStats = voiceCallSessionStats;
     }
 
+    /** Returns the {@link SmsStats} for this phone ID. */
+    public SmsStats getSmsStats() {
+        return mSmsStats;
+    }
+
+    /** Sets the {@link SmsStats} mock for this phone ID during unit testing. */
+    @VisibleForTesting
+    public void setSmsStats(SmsStats smsStats) {
+        mSmsStats = smsStats;
+    }
+
     /** @hide */
     public CarrierPrivilegesTracker getCarrierPrivilegesTracker() {
         return mCarrierPrivilegesTracker;
diff --git a/src/java/com/android/internal/telephony/RIL.java b/src/java/com/android/internal/telephony/RIL.java
index c2481f6..65bd62f 100644
--- a/src/java/com/android/internal/telephony/RIL.java
+++ b/src/java/com/android/internal/telephony/RIL.java
@@ -117,6 +117,7 @@
 import com.android.internal.telephony.cdma.CdmaInformationRecords;
 import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
 import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
+import com.android.internal.telephony.metrics.ModemRestartStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
 import com.android.internal.telephony.uicc.IccCardApplicationStatus.PersoSubState;
@@ -5319,7 +5320,7 @@
             SignalThresholdInfo signalThresholdInfo) {
         android.hardware.radio.V1_5.SignalThresholdInfo signalThresholdInfoHal =
                 new android.hardware.radio.V1_5.SignalThresholdInfo();
-        signalThresholdInfoHal.signalMeasurement = signalThresholdInfo.getSignalMeasurement();
+        signalThresholdInfoHal.signalMeasurement = signalThresholdInfo.getSignalMeasurementType();
         signalThresholdInfoHal.hysteresisMs = signalThresholdInfo.getHysteresisMs();
         signalThresholdInfoHal.hysteresisDb = signalThresholdInfo.getHysteresisDb();
         signalThresholdInfoHal.thresholds = primitiveArrayToArrayList(
@@ -6442,6 +6443,11 @@
 
     void writeMetricsModemRestartEvent(String reason) {
         mMetrics.writeModemRestartEvent(mPhoneId, reason);
+        // Write metrics to statsd. Generate metric only when modem reset is detected by the
+        // first instance of RIL to avoid duplicated events.
+        if (mPhoneId == 0) {
+            ModemRestartStats.onModemRestart(reason);
+        }
     }
 
     /**
diff --git a/src/java/com/android/internal/telephony/SMSDispatcher.java b/src/java/com/android/internal/telephony/SMSDispatcher.java
index d375909..71ca823 100644
--- a/src/java/com/android/internal/telephony/SMSDispatcher.java
+++ b/src/java/com/android/internal/telephony/SMSDispatcher.java
@@ -751,9 +751,10 @@
     protected void handleSendComplete(AsyncResult ar) {
         SmsTracker tracker = (SmsTracker) ar.userObj;
         PendingIntent sentIntent = tracker.mSentIntent;
+        SmsResponse smsResponse = (SmsResponse) ar.result;
 
-        if (ar.result != null) {
-            tracker.mMessageRef = ((SmsResponse)ar.result).mMessageRef;
+        if (smsResponse != null) {
+            tracker.mMessageRef = smsResponse.mMessageRef;
         } else {
             Rlog.d(TAG, "SmsResponse was null");
         }
@@ -770,15 +771,22 @@
             }
             tracker.onSent(mContext);
             mPhone.notifySmsSent(tracker.mDestAddress);
+
+            mPhone.getSmsStats().onOutgoingSms(
+                    tracker.mImsRetry > 0 /* isOverIms */,
+                    SmsConstants.FORMAT_3GPP2.equals(getFormat()),
+                    false /* fallbackToCs */,
+                    SmsManager.RESULT_ERROR_NONE,
+                    tracker.mMessageId,
+                    tracker.isFromDefaultSmsApplication(mContext));
         } else {
             if (DBG) {
-                Rlog.d(TAG, "SMS send failed"
-                        + " id: " + tracker.mMessageId);
+                Rlog.d(TAG, "SMS send failed id: " + tracker.mMessageId);
             }
 
             int ss = mPhone.getServiceState().getState();
 
-            if ( tracker.mImsRetry > 0 && ss != ServiceState.STATE_IN_SERVICE) {
+            if (tracker.mImsRetry > 0 && ss != ServiceState.STATE_IN_SERVICE) {
                 // This is retry after failure over IMS but voice is not available.
                 // Set retry to max allowed, so no retry is sent and
                 //   cause RESULT_ERROR_GENERIC_FAILURE to be returned to app.
@@ -796,6 +804,13 @@
             // if sms over IMS is not supported on data and voice is not available...
             if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
                 tracker.onFailed(mContext, getNotInServiceError(ss), NO_ERROR_CODE);
+                mPhone.getSmsStats().onOutgoingSms(
+                        tracker.mImsRetry > 0 /* isOverIms */,
+                        SmsConstants.FORMAT_3GPP2.equals(getFormat()),
+                        false /* fallbackToCs */,
+                        getNotInServiceError(ss),
+                        tracker.mMessageId,
+                        tracker.isFromDefaultSmsApplication(mContext));
             } else if ((((CommandException)(ar.exception)).getCommandError()
                     == CommandException.Error.SMS_FAIL_RETRY) &&
                    tracker.mRetryCount < MAX_SEND_RETRIES) {
@@ -808,20 +823,35 @@
                 //       message, depending on the failure).  Also, in some
                 //       implementations this retry is handled by the baseband.
                 tracker.mRetryCount++;
+                int errorCode = (smsResponse != null) ? smsResponse.mErrorCode : NO_ERROR_CODE;
                 Message retryMsg = obtainMessage(EVENT_SEND_RETRY, tracker);
                 sendMessageDelayed(retryMsg, SEND_RETRY_DELAY);
+                mPhone.getSmsStats().onOutgoingSms(
+                        tracker.mImsRetry > 0 /* isOverIms */,
+                        SmsConstants.FORMAT_3GPP2.equals(getFormat()),
+                        false /* fallbackToCs */,
+                        SmsManager.RESULT_RIL_SMS_SEND_FAIL_RETRY,
+                        errorCode,
+                        tracker.mMessageId,
+                        tracker.isFromDefaultSmsApplication(mContext));
             } else {
-                int errorCode = NO_ERROR_CODE;
-                if (ar.result != null) {
-                    errorCode = ((SmsResponse)ar.result).mErrorCode;
-                }
+                int errorCode = (smsResponse != null) ? smsResponse.mErrorCode : NO_ERROR_CODE;
                 int error = rilErrorToSmsManagerResult(((CommandException) (ar.exception))
                         .getCommandError());
                 tracker.onFailed(mContext, error, errorCode);
+                mPhone.getSmsStats().onOutgoingSms(
+                        tracker.mImsRetry > 0 /* isOverIms */,
+                        SmsConstants.FORMAT_3GPP2.equals(getFormat()),
+                        false /* fallbackToCs */,
+                        error,
+                        errorCode,
+                        tracker.mMessageId,
+                        tracker.isFromDefaultSmsApplication(mContext));
             }
         }
     }
 
+    @SmsManager.Result
     private static int rilErrorToSmsManagerResult(CommandException.Error rilError) {
         switch (rilError) {
             case RADIO_NOT_AVAILABLE:
@@ -881,6 +911,7 @@
      * @param ss service state
      * @return The result error based on input service state for not in service error
      */
+    @SmsManager.Result
     protected static int getNotInServiceError(int ss) {
         if (ss == ServiceState.STATE_POWER_OFF) {
             return RESULT_ERROR_RADIO_OFF;
@@ -1294,7 +1325,7 @@
      */
     @VisibleForTesting
     public void sendRawPdu(SmsTracker[] trackers) {
-        int error = RESULT_ERROR_NONE;
+        @SmsManager.Result int error = RESULT_ERROR_NONE;
         PackageInfo appInfo = null;
         if (mSmsSendDisabled) {
             Rlog.e(TAG, "Device does not support sending sms.");
@@ -1610,10 +1641,22 @@
         }
     }
 
-    private void handleSmsTrackersFailure(SmsTracker[] trackers, int error, int errorCode) {
+    private void handleSmsTrackersFailure(SmsTracker[] trackers, @SmsManager.Result int error,
+            int errorCode) {
         for (SmsTracker tracker : trackers) {
             tracker.onFailed(mContext, error, errorCode);
         }
+        if (trackers.length > 0) {
+            // This error occurs before the SMS is sent. Make an assumption if it would have
+            // been sent over IMS or not.
+            mPhone.getSmsStats().onOutgoingSms(
+                    isIms(),
+                    SmsConstants.FORMAT_3GPP2.equals(getFormat()),
+                    false /* fallbackToCs */,
+                    error,
+                    trackers[0].mMessageId,
+                    trackers[0].isFromDefaultSmsApplication(mContext));
+        }
     }
 
     /**
@@ -1678,6 +1721,8 @@
 
         public final long mMessageId;
 
+        private Boolean mIsFromDefaultSmsApplication;
+
         // SMS anomaly uuid
         private final UUID mAnomalyUUID = UUID.fromString("43043600-ea7a-44d2-9ae6-a58567ac7886");
 
@@ -1725,6 +1770,16 @@
             return mAppInfo != null ? mAppInfo.packageName : null;
         }
 
+        /** Return if the SMS was originated from the default SMS application. */
+        public boolean isFromDefaultSmsApplication(Context context) {
+            if (mIsFromDefaultSmsApplication == null) {
+                // Perform a lazy initialization, due to the cost of the operation.
+                mIsFromDefaultSmsApplication =
+                        SmsApplication.isDefaultSmsApplication(context, getAppPackageName());
+            }
+            return mIsFromDefaultSmsApplication;
+        }
+
         /**
          * Update the status of this message if we persisted it
          */
@@ -1775,8 +1830,7 @@
          * @return The telephony provider URI if stored
          */
         private Uri persistSentMessageIfRequired(Context context, int messageType, int errorCode) {
-            if (!mIsText || !mPersistMessage ||
-                    !SmsApplication.shouldWriteMessageForPackage(mAppInfo.packageName, context)) {
+            if (!mIsText || !mPersistMessage || isFromDefaultSmsApplication(context)) {
                 return null;
             }
             Rlog.d(TAG, "Persist SMS into "
diff --git a/src/java/com/android/internal/telephony/ServiceStateTracker.java b/src/java/com/android/internal/telephony/ServiceStateTracker.java
index a18c93a..859a8dc 100755
--- a/src/java/com/android/internal/telephony/ServiceStateTracker.java
+++ b/src/java/com/android/internal/telephony/ServiceStateTracker.java
@@ -99,6 +99,7 @@
 import com.android.internal.telephony.dataconnection.DataConnection;
 import com.android.internal.telephony.dataconnection.DcTracker;
 import com.android.internal.telephony.dataconnection.TransportManager;
+import com.android.internal.telephony.metrics.ServiceStateStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
 import com.android.internal.telephony.uicc.IccCardStatus.CardState;
@@ -481,6 +482,8 @@
     private static final int MS_PER_HOUR = 60 * 60 * 1000;
     private final NitzStateMachine mNitzState;
 
+    private ServiceStateStats mServiceStateStats;
+
     /**
      * Holds the last NITZ signal received. Used only for trying to determine an MCC from a CDMA
      * SID.
@@ -647,6 +650,8 @@
         mPhone = phone;
         mCi = ci;
 
+        mServiceStateStats = new ServiceStateStats(mPhone);
+
         mCdnr = new CarrierDisplayNameResolver(mPhone);
 
         mEriManager = TelephonyComponentFactory.getInstance().inject(EriManager.class.getName())
@@ -1717,6 +1722,7 @@
                         TelephonyMetrics.getInstance().writeServiceStateChanged(
                                 mPhone.getPhoneId(), mSS);
                         mPhone.getVoiceCallSessionStats().onServiceStateChanged(mSS);
+                        mServiceStateStats.onServiceStateChanged(mSS);
                     }
                 }
                 break;
@@ -2301,6 +2307,7 @@
                     TelephonyMetrics.getInstance().writeServiceStateChanged(
                             mPhone.getPhoneId(), mSS);
                     mPhone.getVoiceCallSessionStats().onServiceStateChanged(mSS);
+                    mServiceStateStats.onServiceStateChanged(mSS);
                 }
 
                 if (mPhone.isPhoneTypeGsm()) {
@@ -3590,6 +3597,7 @@
         if (hasChanged || hasNrStateChanged) {
             TelephonyMetrics.getInstance().writeServiceStateChanged(mPhone.getPhoneId(), mSS);
             mPhone.getVoiceCallSessionStats().onServiceStateChanged(mSS);
+            mServiceStateStats.onServiceStateChanged(mSS);
         }
 
         boolean shouldLogAttachedChange = false;
@@ -4924,38 +4932,43 @@
     private void updateReportingCriteria(PersistableBundle config) {
         int lteMeasurementEnabled = config.getInt(CarrierConfigManager
                 .KEY_PARAMETERS_USED_FOR_LTE_SIGNAL_BAR_INT, CellSignalStrengthLte.USE_RSRP);
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSRP,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP,
                 config.getIntArray(CarrierConfigManager.KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY),
                 AccessNetworkType.EUTRAN,
                 (lteMeasurementEnabled & CellSignalStrengthLte.USE_RSRP) != 0);
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSCP,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP,
                 config.getIntArray(CarrierConfigManager.KEY_WCDMA_RSCP_THRESHOLDS_INT_ARRAY),
                 AccessNetworkType.UTRAN, true);
-        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSSI,
+        mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI,
                 config.getIntArray(CarrierConfigManager.KEY_GSM_RSSI_THRESHOLDS_INT_ARRAY),
                 AccessNetworkType.GERAN, true);
 
         if (mPhone.getHalVersion().greaterOrEqual(RIL.RADIO_HAL_VERSION_1_5)) {
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSRQ,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ,
                     config.getIntArray(CarrierConfigManager.KEY_LTE_RSRQ_THRESHOLDS_INT_ARRAY),
                     AccessNetworkType.EUTRAN,
                     (lteMeasurementEnabled & CellSignalStrengthLte.USE_RSRQ) != 0);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_RSSNR,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR,
                     config.getIntArray(CarrierConfigManager.KEY_LTE_RSSNR_THRESHOLDS_INT_ARRAY),
                     AccessNetworkType.EUTRAN,
                     (lteMeasurementEnabled & CellSignalStrengthLte.USE_RSSNR) != 0);
 
             int measurementEnabled = config.getInt(CarrierConfigManager
                     .KEY_PARAMETERS_USE_FOR_5G_NR_SIGNAL_BAR_INT, CellSignalStrengthNr.USE_SSRSRP);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_SSRSRP,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP,
                     config.getIntArray(CarrierConfigManager.KEY_5G_NR_SSRSRP_THRESHOLDS_INT_ARRAY),
                     AccessNetworkType.NGRAN,
                     (measurementEnabled & CellSignalStrengthNr.USE_SSRSRP) != 0);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_SSRSRQ,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ,
                     config.getIntArray(CarrierConfigManager.KEY_5G_NR_SSRSRQ_THRESHOLDS_INT_ARRAY),
                     AccessNetworkType.NGRAN,
                     (measurementEnabled & CellSignalStrengthNr.USE_SSRSRQ) != 0);
-            mPhone.setSignalStrengthReportingCriteria(SignalThresholdInfo.SIGNAL_SSSINR,
+            mPhone.setSignalStrengthReportingCriteria(
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR,
                     config.getIntArray(CarrierConfigManager.KEY_5G_NR_SSSINR_THRESHOLDS_INT_ARRAY),
                     AccessNetworkType.NGRAN,
                     (measurementEnabled & CellSignalStrengthNr.USE_SSSINR) != 0);
@@ -5900,6 +5913,17 @@
         tm.setDataNetworkTypeForPhone(mPhone.getPhoneId(), type);
     }
 
+    /** Returns the {@link ServiceStateStats} for the phone tracked. */
+    public ServiceStateStats getServiceStateStats() {
+        return mServiceStateStats;
+    }
+
+    /** Replaces the {@link ServiceStateStats} for testing purposes. */
+    @VisibleForTesting
+    public void setServiceStateStats(ServiceStateStats serviceStateStats) {
+        mServiceStateStats = serviceStateStats;
+    }
+
     /**
      * Used to insert a ServiceState into the ServiceStateProvider as a ContentValues instance.
      *
diff --git a/src/java/com/android/internal/telephony/SmsBroadcastUndelivered.java b/src/java/com/android/internal/telephony/SmsBroadcastUndelivered.java
index 5008665..40a6f03 100644
--- a/src/java/com/android/internal/telephony/SmsBroadcastUndelivered.java
+++ b/src/java/com/android/internal/telephony/SmsBroadcastUndelivered.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.telephony;
 
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -225,8 +226,10 @@
                     }
                 }
             }
-            // Retrieve the phone id, required for metrics
-            int phoneId = getPhoneId(gsmInboundSmsHandler, cdmaInboundSmsHandler);
+            // Retrieve the phone and phone id, required for metrics
+            Phone phone = getPhone(gsmInboundSmsHandler, cdmaInboundSmsHandler);
+            int phoneId = phone != null ? phone.getPhoneId()
+                    : SubscriptionManager.INVALID_PHONE_INDEX;
 
             // Delete old incomplete message segments
             for (SmsReferenceKey message : oldMultiPartMessages) {
@@ -244,6 +247,10 @@
                     TelephonyMetrics metrics = TelephonyMetrics.getInstance();
                     metrics.writeDroppedIncomingMultipartSms(phoneId, message.mFormat, rows,
                             message.mMessageCount);
+                    if (phone != null) {
+                        phone.getSmsStats().onDroppedIncomingMultipartSms(message.mIs3gpp2, rows,
+                                message.mMessageCount);
+                    }
                 }
             }
         } catch (SQLException e) {
@@ -258,17 +265,17 @@
     }
 
     /**
-     * Retrieve the phone id for the GSM or CDMA Inbound SMS handler
+     * Retrieve the phone for the GSM or CDMA Inbound SMS handler
      */
-    private static int getPhoneId(GsmInboundSmsHandler gsmInboundSmsHandler,
+    @Nullable
+    private static Phone getPhone(GsmInboundSmsHandler gsmInboundSmsHandler,
             CdmaInboundSmsHandler cdmaInboundSmsHandler) {
-        int phoneId = SubscriptionManager.INVALID_PHONE_INDEX;
         if (gsmInboundSmsHandler != null) {
-            phoneId = gsmInboundSmsHandler.getPhone().getPhoneId();
+            return gsmInboundSmsHandler.getPhone();
         } else if (cdmaInboundSmsHandler != null) {
-            phoneId = cdmaInboundSmsHandler.getPhone().getPhoneId();
+            return cdmaInboundSmsHandler.getPhone();
         }
-        return phoneId;
+        return null;
     }
 
     /**
@@ -312,6 +319,7 @@
         final int mReferenceNumber;
         final int mMessageCount;
         final String mQuery;
+        final boolean mIs3gpp2;
         final String mFormat;
 
         SmsReferenceKey(InboundSmsTracker tracker) {
@@ -319,6 +327,7 @@
             mReferenceNumber = tracker.getReferenceNumber();
             mMessageCount = tracker.getMessageCount();
             mQuery = tracker.getQueryForSegments();
+            mIs3gpp2 = tracker.is3gpp2();
             mFormat = tracker.getFormat();
         }
 
diff --git a/src/java/com/android/internal/telephony/SmsDispatchersController.java b/src/java/com/android/internal/telephony/SmsDispatchersController.java
index 471b16f..fc3c679 100644
--- a/src/java/com/android/internal/telephony/SmsDispatchersController.java
+++ b/src/java/com/android/internal/telephony/SmsDispatchersController.java
@@ -387,12 +387,13 @@
      *                 the same time an SMS received from radio is responded back.
      */
     @VisibleForTesting
-    public void injectSmsPdu(byte[] pdu, String format, SmsInjectionCallback callback) {
+    public void injectSmsPdu(byte[] pdu, String format, boolean isOverIms,
+            SmsInjectionCallback callback) {
         // TODO We need to decide whether we should allow injecting GSM(3gpp)
         // SMS pdus when the phone is camping on CDMA(3gpp2) network and vice versa.
         android.telephony.SmsMessage msg =
                 android.telephony.SmsMessage.createFromPdu(pdu, format);
-        injectSmsPdu(msg, format, callback, false /* ignoreClass */);
+        injectSmsPdu(msg, format, callback, false /* ignoreClass */, isOverIms);
     }
 
     /**
@@ -407,7 +408,7 @@
      */
     @VisibleForTesting
     public void injectSmsPdu(SmsMessage msg, String format, SmsInjectionCallback callback,
-            boolean ignoreClass) {
+            boolean ignoreClass, boolean isOverIms) {
         Rlog.d(TAG, "SmsDispatchersController:injectSmsPdu");
         try {
             if (msg == null) {
@@ -428,11 +429,13 @@
             if (format.equals(SmsConstants.FORMAT_3GPP)) {
                 Rlog.i(TAG, "SmsDispatchersController:injectSmsText Sending msg=" + msg
                         + ", format=" + format + "to mGsmInboundSmsHandler");
-                mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
+                mGsmInboundSmsHandler.sendMessage(
+                        InboundSmsHandler.EVENT_INJECT_SMS, isOverIms ? 1 : 0, 0, ar);
             } else if (format.equals(SmsConstants.FORMAT_3GPP2)) {
                 Rlog.i(TAG, "SmsDispatchersController:injectSmsText Sending msg=" + msg
                         + ", format=" + format + "to mCdmaInboundSmsHandler");
-                mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
+                mCdmaInboundSmsHandler.sendMessage(
+                        InboundSmsHandler.EVENT_INJECT_SMS, isOverIms ? 1 : 0, 0, ar);
             } else {
                 // Invalid pdu format.
                 Rlog.e(TAG, "Invalid pdu format: " + format);
diff --git a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
index bfa982e..39a36af 100644
--- a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
+++ b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
@@ -350,9 +350,10 @@
      */
     public InboundSmsTracker makeInboundSmsTracker(Context context, byte[] pdu, long timestamp,
             int destPort, boolean is3gpp2, boolean is3gpp2WapPdu, String address,
-            String displayAddr, String messageBody, boolean isClass0, int subId) {
+            String displayAddr, String messageBody, boolean isClass0, int subId,
+            @InboundSmsHandler.SmsSource int smsSource) {
         return new InboundSmsTracker(context, pdu, timestamp, destPort, is3gpp2, is3gpp2WapPdu,
-                address, displayAddr, messageBody, isClass0, subId);
+                address, displayAddr, messageBody, isClass0, subId, smsSource);
     }
 
     /**
@@ -361,10 +362,10 @@
     public InboundSmsTracker makeInboundSmsTracker(Context context, byte[] pdu, long timestamp,
             int destPort, boolean is3gpp2, String address, String displayAddr, int referenceNumber,
             int sequenceNumber, int messageCount, boolean is3gpp2WapPdu, String messageBody,
-            boolean isClass0, int subId) {
+            boolean isClass0, int subId, @InboundSmsHandler.SmsSource int smsSource) {
         return new InboundSmsTracker(context, pdu, timestamp, destPort, is3gpp2, address,
                 displayAddr, referenceNumber, sequenceNumber, messageCount, is3gpp2WapPdu,
-                messageBody, isClass0, subId);
+                messageBody, isClass0, subId, smsSource);
     }
 
     /**
diff --git a/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java b/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
index 61377dc..1597b8a 100644
--- a/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
+++ b/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
@@ -188,10 +188,11 @@
      * Process Cell Broadcast, Voicemail Notification, and other 3GPP/3GPP2-specific messages.
      *
      * @param smsb the SmsMessageBase object from the RIL
+     * @param smsSource the source of the SMS message
      * @return true if the message was handled here; false to continue processing
      */
     @Override
-    protected int dispatchMessageRadioSpecific(SmsMessageBase smsb) {
+    protected int dispatchMessageRadioSpecific(SmsMessageBase smsb, @SmsSource int smsSource) {
         SmsMessage sms = (SmsMessage) smsb;
         boolean isBroadcastType = (SmsEnvelope.MESSAGE_TYPE_BROADCAST == sms.getMessageType());
 
@@ -217,7 +218,7 @@
             case SmsEnvelope.TELESERVICE_VMN:
             case SmsEnvelope.TELESERVICE_MWI:
                 // handle voicemail indication
-                handleVoicemailTeleservice(sms);
+                handleVoicemailTeleservice(sms, smsSource);
                 return Intents.RESULT_SMS_HANDLED;
 
             case SmsEnvelope.TELESERVICE_WMT:
@@ -258,10 +259,10 @@
         if (SmsEnvelope.TELESERVICE_WAP == teleService) {
             return processCdmaWapPdu(sms.getUserData(), sms.mMessageRef,
                     sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
-                    sms.getTimestampMillis());
+                    sms.getTimestampMillis(), smsSource);
         }
 
-        return dispatchNormalMessage(smsb);
+        return dispatchNormalMessage(smsb, smsSource);
     }
 
     /**
@@ -309,7 +310,7 @@
      *
      * @param sms the message to process
      */
-    private void handleVoicemailTeleservice(SmsMessage sms) {
+    private void handleVoicemailTeleservice(SmsMessage sms, @SmsSource int smsSource) {
         int voicemailCount = sms.getNumOfVoicemails();
         if (DBG) log("Voicemail count=" + voicemailCount);
 
@@ -324,7 +325,7 @@
         // update voice mail count in phone
         mPhone.setVoiceMessageCount(voicemailCount);
         // update metrics
-        addVoicemailSmsToMetrics();
+        addVoicemailSmsToMetrics(smsSource);
     }
 
     /**
@@ -338,7 +339,7 @@
      * to applications
      */
     private int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address, String dispAddr,
-            long timestamp) {
+            long timestamp, @SmsSource int smsSource) {
         int index = 0;
 
         int msgType = (0xFF & pdu[index++]);
@@ -386,7 +387,8 @@
                         referenceNumber,
                         segment, totalSegments, true, HexDump.toHexString(userData),
                         false /* isClass0 */,
-                        mPhone.getSubId());
+                        mPhone.getSubId(),
+                        smsSource);
 
         // de-duping is done only for text messages
         return addTrackerToRawTableAndSendMessage(tracker, false /* don't de-dup */);
@@ -431,9 +433,10 @@
     /**
      * Add voicemail indication SMS 0 to metrics.
      */
-    private void addVoicemailSmsToMetrics() {
+    private void addVoicemailSmsToMetrics(@SmsSource int smsSource) {
         mMetrics.writeIncomingVoiceMailSms(mPhone.getPhoneId(),
                 android.telephony.SmsMessage.FORMAT_3GPP2);
+        mPhone.getSmsStats().onIncomingSmsVoicemail(true /* is3gpp2 */, smsSource);
     }
 
     /**
diff --git a/src/java/com/android/internal/telephony/dataconnection/DataConnection.java b/src/java/com/android/internal/telephony/dataconnection/DataConnection.java
index a1e4b91..5b4c85c 100644
--- a/src/java/com/android/internal/telephony/dataconnection/DataConnection.java
+++ b/src/java/com/android/internal/telephony/dataconnection/DataConnection.java
@@ -87,6 +87,7 @@
 import com.android.internal.telephony.TelephonyStatsLog;
 import com.android.internal.telephony.dataconnection.DcTracker.ReleaseNetworkType;
 import com.android.internal.telephony.dataconnection.DcTracker.RequestNetworkType;
+import com.android.internal.telephony.metrics.DataCallSessionStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
 import com.android.internal.util.AsyncChannel;
@@ -198,6 +199,9 @@
 
     private int[] mAdministratorUids = new int[0];
 
+    // stats per data call
+    private DataCallSessionStats mDataCallSessionStats;
+
     /**
      * Used internally for saving connecting parameters.
      */
@@ -692,6 +696,7 @@
         mCid = -1;
         mDataRegState = mPhone.getServiceState().getDataRegistrationState();
         mIsSuspended = false;
+        mDataCallSessionStats = new DataCallSessionStats(mPhone);
 
         int networkType = getNetworkType();
         mRilRat = ServiceState.networkTypeToRilRadioTechnology(networkType);
@@ -1017,6 +1022,7 @@
         if (apnContext != null) apnContext.requestLog(str);
         mDataServiceManager.deactivateDataCall(mCid, discReason,
                 obtainMessage(EVENT_DEACTIVATE_DONE, mTag, 0, o));
+        mDataCallSessionStats.setDeactivateDataCallReason(discReason);
     }
 
     private void notifyAllWithEvent(ApnContext alreadySent, int event, String reason) {
@@ -1975,6 +1981,8 @@
                     if (DBG) log("DcDefaultState EVENT_TEAR_DOWN_NOW");
                     mDataServiceManager.deactivateDataCall(mCid, DataService.REQUEST_REASON_NORMAL,
                             null);
+                    mDataCallSessionStats.setDeactivateDataCallReason(
+                            DataService.REQUEST_REASON_NORMAL);
                     break;
                 case EVENT_LOST_CONNECTION:
                     if (DBG) {
@@ -1997,6 +2005,10 @@
                                 + " drs=" + mDataRegState
                                 + " mRilRat=" + mRilRat);
                     }
+                    // this is for DRS or RAT changes, so only call onRatChanged if RAT is changed
+                    if (mRilRat != 0) {
+                        mDataCallSessionStats.onRatChanged(mRilRat);
+                    }
                     break;
 
                 case EVENT_START_HANDOVER:  //calls startHandover()
@@ -2249,6 +2261,7 @@
                     .registerCarrierPrivilegesListener(
                             getHandler(), EVENT_CARRIER_PRIVILEGED_UIDS_CHANGED, null);
             notifyDataConnectionState();
+            mDataCallSessionStats.onSetupDataCall(mApnSetting.getApnTypeBitmask());
         }
         @Override
         public boolean processMessage(Message msg) {
@@ -2317,8 +2330,10 @@
                             } else if (delay >= 0) {
                                 retryTime = SystemClock.elapsedRealtime() + delay;
                             }
+                            int newRequestType = DcTracker.calculateNewRetryRequestType(
+                                    mHandoverFailureMode, cp.mRequestType, mDcFailCause);
                             mDct.getDataThrottler().setRetryTime(mApnSetting.getApnTypeBitmask(),
-                                    retryTime, dataCallResponse.getHandoverFailureMode());
+                                    retryTime, newRequestType);
 
                             String str = "DcActivatingState: ERROR_DATA_SERVICE_SPECIFIC_ERROR "
                                     + " delay=" + delay
@@ -2346,6 +2361,10 @@
                             throw new RuntimeException("Unknown SetupResult, should not happen");
                     }
                     retVal = HANDLED;
+                    mDataCallSessionStats
+                            .onSetupDataCallResponse(dataCallResponse, cp.mRilRat,
+                                    mApnSetting.getApnTypeBitmask(), mApnSetting.getProtocol(),
+                                    result.mFailCause);
                     break;
                 case EVENT_CARRIER_PRIVILEGED_UIDS_CHANGED:
                     AsyncResult asyncResult = (AsyncResult) msg.obj;
@@ -2498,8 +2517,9 @@
                         getHandler(), DataConnection.EVENT_LINK_CAPACITY_CHANGED, null);
             }
             notifyDataConnectionState();
+            int apnBitMask = mApnSetting.getApnTypeBitmask();
             TelephonyMetrics.getInstance().writeRilDataCallEvent(mPhone.getPhoneId(),
-                    mCid, mApnSetting.getApnTypeBitmask(), RilDataCall.State.CONNECTED);
+                    mCid, apnBitMask, RilDataCall.State.CONNECTED);
         }
 
         @Override
@@ -2527,6 +2547,7 @@
 
             TelephonyMetrics.getInstance().writeRilDataCallEvent(mPhone.getPhoneId(),
                     mCid, mApnSetting.getApnTypeBitmask(), RilDataCall.State.DISCONNECTED);
+            mDataCallSessionStats.onDataCallDisconnected(mCid);
 
             mPhone.getCarrierPrivilegesTracker().unregisterCarrierPrivilegesListener(getHandler());
         }
@@ -2638,6 +2659,10 @@
                         mNetworkAgent.sendLinkProperties(mLinkProperties, DataConnection.this);
                     }
                     retVal = HANDLED;
+                    // this is for DRS or RAT changes, so only call onRatChanged if RAT is changed
+                    if (mRilRat != 0) {
+                        mDataCallSessionStats.onRatChanged(mRilRat);
+                    }
                     break;
                 }
                 case EVENT_NR_FREQUENCY_CHANGED:
@@ -3175,6 +3200,12 @@
         }
     }
 
+    /** Sets the {@link DataCallSessionStats} mock for this phone ID during unit testing. */
+    @VisibleForTesting
+    public void setDataCallSessionStats(DataCallSessionStats dataCallSessionStats) {
+        mDataCallSessionStats = dataCallSessionStats;
+    }
+
     /**
      * @return the string for msg.what as our info.
      */
diff --git a/src/java/com/android/internal/telephony/dataconnection/DataThrottler.java b/src/java/com/android/internal/telephony/dataconnection/DataThrottler.java
index da5b4a1..60e1b58 100644
--- a/src/java/com/android/internal/telephony/dataconnection/DataThrottler.java
+++ b/src/java/com/android/internal/telephony/dataconnection/DataThrottler.java
@@ -22,7 +22,6 @@
 import android.telephony.Annotation.ApnType;
 import android.telephony.data.ApnSetting;
 import android.telephony.data.ApnThrottleStatus;
-import android.telephony.data.DataCallResponse;
 
 import com.android.internal.telephony.RetryManager;
 import com.android.telephony.Rlog;
@@ -67,7 +66,7 @@
      * {@link RetryManager#NO_RETRY} indicates retry should never happen.
      */
     public void setRetryTime(@ApnType int apnTypes, long retryElapsedTime,
-            @DataCallResponse.HandoverFailureMode int handoverFailureMode) {
+            @DcTracker.RequestNetworkType int newRequestType) {
         if (retryElapsedTime < 0) {
             retryElapsedTime = RetryManager.NO_SUGGESTED_RETRY_DELAY;
         }
@@ -79,8 +78,7 @@
             int apnType = apnTypes & -apnTypes;
 
             //Update the apn throttle status
-            ApnThrottleStatus newStatus =
-                    createStatus(apnType, retryElapsedTime, handoverFailureMode);
+            ApnThrottleStatus newStatus = createStatus(apnType, retryElapsedTime, newRequestType);
 
             ApnThrottleStatus oldStatus = mApnThrottleStatus.get(apnType);
 
@@ -130,13 +128,13 @@
     }
 
     private ApnThrottleStatus createStatus(@Annotation.ApnType int apnType, long retryElapsedTime,
-            @DataCallResponse.HandoverFailureMode int handoverFailureMode) {
+            @DcTracker.RequestNetworkType int newRequestType) {
         ApnThrottleStatus.Builder builder = new ApnThrottleStatus.Builder();
 
         if (retryElapsedTime == RetryManager.NO_SUGGESTED_RETRY_DELAY) {
             builder
                     .setNoThrottle()
-                    .setRetryType(getRetryType(handoverFailureMode));
+                    .setRetryType(getRetryType(newRequestType));
         } else if (retryElapsedTime == RetryManager.NO_RETRY) {
             builder
                     .setThrottleExpiryTimeMillis(RetryManager.NO_RETRY)
@@ -144,7 +142,7 @@
         } else {
             builder
                     .setThrottleExpiryTimeMillis(retryElapsedTime)
-                    .setRetryType(getRetryType(handoverFailureMode));
+                    .setRetryType(getRetryType(newRequestType));
         }
         return builder
                 .setSlotIndex(mSlotIndex)
@@ -153,18 +151,17 @@
                 .build();
     }
 
-    private static int getRetryType(@DataCallResponse.HandoverFailureMode int handoverFailureMode) {
-        int retryType;
-        int requestType = DcTracker.calcRequestType(handoverFailureMode);
-        if (requestType == DcTracker.REQUEST_TYPE_NORMAL) {
-            retryType = ApnThrottleStatus.RETRY_TYPE_NEW_CONNECTION;
-        } else if (requestType == DcTracker.REQUEST_TYPE_HANDOVER) {
-            retryType = ApnThrottleStatus.RETRY_TYPE_HANDOVER;
-        } else {
-            loge("createStatus: Unknown requestType=" + requestType);
-            retryType = ApnThrottleStatus.RETRY_TYPE_NEW_CONNECTION;
+    private static int getRetryType(@DcTracker.RequestNetworkType int newRequestType) {
+        if (newRequestType == DcTracker.REQUEST_TYPE_NORMAL) {
+            return ApnThrottleStatus.RETRY_TYPE_NEW_CONNECTION;
         }
-        return retryType;
+
+        if (newRequestType == DcTracker.REQUEST_TYPE_HANDOVER) {
+            return  ApnThrottleStatus.RETRY_TYPE_HANDOVER;
+        }
+
+        loge("createStatus: Unknown requestType=" + newRequestType);
+        return ApnThrottleStatus.RETRY_TYPE_NEW_CONNECTION;
     }
 
     private void sendApnThrottleStatusChanged(List<ApnThrottleStatus> statuses) {
diff --git a/src/java/com/android/internal/telephony/dataconnection/DcTracker.java b/src/java/com/android/internal/telephony/dataconnection/DcTracker.java
index a09a0f6..d3011c7 100644
--- a/src/java/com/android/internal/telephony/dataconnection/DcTracker.java
+++ b/src/java/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -24,6 +24,7 @@
 import static android.telephony.data.ApnSetting.TYPE_IA;
 import static android.telephony.data.DataCallResponse.HANDOVER_FAILURE_MODE_DO_FALLBACK;
 import static android.telephony.data.DataCallResponse.HANDOVER_FAILURE_MODE_LEGACY;
+import static android.telephony.data.DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL;
 
 import static com.android.internal.telephony.RILConstants.DATA_PROFILE_DEFAULT;
 import static com.android.internal.telephony.RILConstants.DATA_PROFILE_INVALID;
@@ -114,6 +115,7 @@
 import com.android.internal.telephony.dataconnection.DataConnectionReasons.DataAllowedReasonType;
 import com.android.internal.telephony.dataconnection.DataConnectionReasons.DataDisallowedReasonType;
 import com.android.internal.telephony.dataconnection.DataEnabledSettings.DataEnabledChangedReason;
+import com.android.internal.telephony.metrics.DataStallRecoveryStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.util.ArrayUtils;
 import com.android.internal.telephony.util.TelephonyUtils;
@@ -340,6 +342,9 @@
     private boolean mNrSaSub6Unmetered = false;
     private boolean mRoamingUnmetered = false;
 
+    // stats per data call recovery event
+    private DataStallRecoveryStats mDataStallRecoveryStats;
+
     /* List of SubscriptionPlans, updated when initialized and when plans are changed. */
     private List<SubscriptionPlan> mSubscriptionPlans = null;
 
@@ -534,7 +539,8 @@
                 if (DBG) log("onDataReconnect: keep associated");
             }
             // TODO: IF already associated should we send the EVENT_TRY_SETUP_DATA???
-            sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA, apnContext));
+            sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA, requestType,
+                    0, apnContext));
         }
     }
 
@@ -2421,7 +2427,7 @@
             if (ac != null) {
                 @ApnType int apnTypes = ac.getApnTypeBitmask();
                 mDataThrottler.setRetryTime(apnTypes, RetryManager.NO_SUGGESTED_RETRY_DELAY,
-                        DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL);
+                        REQUEST_TYPE_NORMAL);
             } else {
                 loge("EVENT_APN_UNTHROTTLED: Invalid APN passed: " + apn);
             }
@@ -2496,20 +2502,19 @@
     private void sendRequestNetworkCompleteMsg(Message message, boolean success,
                                                @TransportType int transport,
                                                @RequestNetworkType int requestType,
-                                               @HandoverFailureMode int handoverFailureMode,
-                                               @DataFailureCause int cause) {
+                                               boolean doFallbackOnFailedHandover) {
         if (message == null) return;
 
         Bundle b = message.getData();
         b.putBoolean(DATA_COMPLETE_MSG_EXTRA_SUCCESS, success);
         b.putInt(DATA_COMPLETE_MSG_EXTRA_REQUEST_TYPE, requestType);
         b.putInt(DATA_COMPLETE_MSG_EXTRA_TRANSPORT_TYPE, transport);
-        b.putBoolean(DATA_COMPLETE_MSG_EXTRA_HANDOVER_FAILURE_FALLBACK,
-                shouldFallbackOnFailedHandover(handoverFailureMode, requestType, cause));
+        b.putBoolean(DATA_COMPLETE_MSG_EXTRA_HANDOVER_FAILURE_FALLBACK, doFallbackOnFailedHandover);
         message.sendToTarget();
     }
 
-    private boolean shouldFallbackOnFailedHandover(@HandoverFailureMode int handoverFailureMode,
+    private static boolean shouldFallbackOnFailedHandover(
+                               @HandoverFailureMode int handoverFailureMode,
                                @RequestNetworkType int requestType,
                                @DataFailureCause int cause) {
         if (requestType != REQUEST_TYPE_HANDOVER) {
@@ -2524,6 +2529,33 @@
         }
     }
 
+    /**
+     * Calculates the new request type that will be used the next time a data connection retries
+     * after a failed data call attempt.
+     */
+    @RequestNetworkType
+    public static int calculateNewRetryRequestType(@HandoverFailureMode int handoverFailureMode,
+            @RequestNetworkType int requestType,
+            @DataFailureCause int cause) {
+        boolean fallbackOnFailedHandover =
+                shouldFallbackOnFailedHandover(handoverFailureMode, requestType, cause);
+        if (requestType != REQUEST_TYPE_HANDOVER) {
+            //The fallback is only relevant if the request is a handover
+            return requestType;
+        }
+
+        if (fallbackOnFailedHandover) {
+            // Since fallback is happening, the request type is really "NONE".
+            return REQUEST_TYPE_NORMAL;
+        }
+
+        if (handoverFailureMode == HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL) {
+            return REQUEST_TYPE_NORMAL;
+        }
+
+        return REQUEST_TYPE_HANDOVER;
+    }
+
     public void enableApn(@ApnType int apnType, @RequestNetworkType int requestType,
             Message onCompleteMsg) {
         sendMessage(obtainMessage(DctConstants.EVENT_ENABLE_APN, apnType, requestType,
@@ -2536,7 +2568,7 @@
         if (apnContext == null) {
             loge("onEnableApn(" + apnType + "): NO ApnContext");
             sendRequestNetworkCompleteMsg(onCompleteMsg, false, mTransportType, requestType,
-                    DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN, DataFailCause.NONE);
+                    false);
             return;
         }
 
@@ -2552,7 +2584,7 @@
             if (DBG) log(str);
             apnContext.requestLog(str);
             sendRequestNetworkCompleteMsg(onCompleteMsg, false, mTransportType, requestType,
-                    DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN, DataFailCause.NONE);
+                    false);
             return;
         }
 
@@ -2568,15 +2600,13 @@
                     if (DBG) log("onEnableApn: 'CONNECTED' so return");
                     // Don't add to local log since this is so common
                     sendRequestNetworkCompleteMsg(onCompleteMsg, true, mTransportType,
-                            requestType, DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN,
-                            DataFailCause.NONE);
+                            requestType, false);
                     return;
                 case DISCONNECTING:
                     if (DBG) log("onEnableApn: 'DISCONNECTING' so return");
                     apnContext.requestLog("onEnableApn state=DISCONNECTING, so return");
                     sendRequestNetworkCompleteMsg(onCompleteMsg, false, mTransportType,
-                            requestType, DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN,
-                            DataFailCause.NONE);
+                            requestType, false);
                     return;
                 case IDLE:
                     // fall through: this is unexpected but if it happens cleanup and try setup
@@ -2605,8 +2635,7 @@
                 addRequestNetworkCompleteMsg(onCompleteMsg, apnType);
             } else {
                 sendRequestNetworkCompleteMsg(onCompleteMsg, false, mTransportType,
-                        requestType, DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN,
-                        DataFailCause.NONE);
+                        requestType, false);
             }
         } else {
             log("onEnableApn: config not ready yet.");
@@ -2879,6 +2908,9 @@
     protected void onDataSetupComplete(ApnContext apnContext, boolean success,
             @DataFailureCause int cause, @RequestNetworkType int requestType,
             @HandoverFailureMode int handoverFailureMode) {
+        boolean fallbackOnFailedHandover = shouldFallbackOnFailedHandover(
+                handoverFailureMode, requestType, cause);
+
         if (success && (handoverFailureMode != DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN
                 && handoverFailureMode != DataCallResponse.HANDOVER_FAILURE_MODE_LEGACY)) {
             Log.wtf(mLogTag, "bad failure mode: "
@@ -2890,7 +2922,7 @@
             if (messageList != null) {
                 for (Message msg : messageList) {
                     sendRequestNetworkCompleteMsg(msg, success, mTransportType, requestType,
-                            handoverFailureMode, cause);
+                            fallbackOnFailedHandover);
                 }
                 messageList.clear();
             }
@@ -3044,22 +3076,13 @@
                 apnContext.markApnPermanentFailed(apn);
             }
 
-            requestType = calcRequestType(handoverFailureMode);
-            onDataSetupCompleteError(apnContext, requestType,
-                    shouldFallbackOnFailedHandover(handoverFailureMode, requestType, cause));
+            int newRequestType = calculateNewRetryRequestType(handoverFailureMode, requestType,
+                    cause);
+            onDataSetupCompleteError(apnContext, newRequestType, fallbackOnFailedHandover);
         }
     }
 
-    /**
-     * Converts the handover failure mode to the corresponding request network type.
-     */
-    @RequestNetworkType
-    public static int calcRequestType(
-            @HandoverFailureMode int handoverFailureMode) {
-        return (handoverFailureMode
-            == DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER)
-            ? REQUEST_TYPE_HANDOVER : REQUEST_TYPE_NORMAL;
-    }
+
 
     /**
      * Error has occurred during the SETUP {aka bringUP} request and the DCT
@@ -3068,11 +3091,10 @@
      * be a delay defined by {@link ApnContext#getDelayForNextApn(boolean)}.
      */
     protected void onDataSetupCompleteError(ApnContext apnContext,
-            @RequestNetworkType int requestType, boolean fallback) {
+            @RequestNetworkType int requestType, boolean fallbackOnFailedHandover) {
         long delay = apnContext.getDelayForNextApn(mFailFast);
-
         // Check if we need to retry or not.
-        if (delay >= 0 && delay != RetryManager.NO_RETRY && !fallback) {
+        if (delay >= 0 && delay != RetryManager.NO_RETRY && !fallbackOnFailedHandover) {
             if (DBG) {
                 log("onDataSetupCompleteError: APN type=" + apnContext.getApnType()
                         + ". Request type=" + requestTypeToString(requestType) + ", Retry in "
@@ -3652,7 +3674,7 @@
                 break;
 
             case DctConstants.EVENT_TRY_SETUP_DATA:
-                trySetupData((ApnContext) msg.obj, REQUEST_TYPE_NORMAL);
+                trySetupData((ApnContext) msg.obj, msg.arg1);
                 break;
 
             case DctConstants.EVENT_CLEAN_UP_CONNECTION:
@@ -4704,7 +4726,7 @@
             RECOVERY_ACTION_RADIO_RESTART
         })
     @Retention(RetentionPolicy.SOURCE)
-    private @interface RecoveryAction {};
+    public @interface RecoveryAction {};
     private static final int RECOVERY_ACTION_GET_DATA_CALL_LIST      = 0;
     private static final int RECOVERY_ACTION_CLEANUP                 = 1;
     private static final int RECOVERY_ACTION_REREGISTER              = 2;
@@ -4806,6 +4828,7 @@
                         mPhone.getPhoneId(), signalStrength);
                 TelephonyMetrics.getInstance().writeDataStallEvent(
                         mPhone.getPhoneId(), recoveryAction);
+                DataStallRecoveryStats.onDataStallEvent(recoveryAction, mPhone);
                 broadcastDataStallDetected(recoveryAction);
 
                 switch (recoveryAction) {
diff --git a/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java b/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
index 7af9ea3..334a537 100644
--- a/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
+++ b/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
@@ -153,11 +153,12 @@
      * are handled by {@link #dispatchNormalMessage} in parent class.
      *
      * @param smsb the SmsMessageBase object from the RIL
+     * @param smsSource the source of the SMS message
      * @return a result code from {@link android.provider.Telephony.Sms.Intents},
      * or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
      */
     @Override
-    protected int dispatchMessageRadioSpecific(SmsMessageBase smsb) {
+    protected int dispatchMessageRadioSpecific(SmsMessageBase smsb, @SmsSource int smsSource) {
         SmsMessage sms = (SmsMessage) smsb;
 
         if (sms.isTypeZero()) {
@@ -174,14 +175,14 @@
             // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be
             // Displayed/Stored/Notified. They should only be acknowledged.
             log("Received short message type 0, Don't display or store it. Send Ack");
-            addSmsTypeZeroToMetrics();
+            addSmsTypeZeroToMetrics(smsSource);
             return Intents.RESULT_SMS_HANDLED;
         }
 
         // Send SMS-PP data download messages to UICC. See 3GPP TS 31.111 section 7.1.1.
         if (sms.isUsimDataDownload()) {
             UsimServiceTable ust = mPhone.getUsimServiceTable();
-            return mDataDownloadHandler.handleUsimDataDownload(ust, sms);
+            return mDataDownloadHandler.handleUsimDataDownload(ust, sms, smsSource);
         }
 
         boolean handled = false;
@@ -195,7 +196,7 @@
             if (DBG) log("Received voice mail indicator clear SMS shouldStore=" + !handled);
         }
         if (handled) {
-            addVoicemailSmsToMetrics();
+            addVoicemailSmsToMetrics(smsSource);
             return Intents.RESULT_SMS_HANDLED;
         }
 
@@ -206,7 +207,7 @@
             return Intents.RESULT_SMS_OUT_OF_MEMORY;
         }
 
-        return dispatchNormalMessage(smsb);
+        return dispatchNormalMessage(smsb, smsSource);
     }
 
     private void updateMessageWaitingIndicator(int voicemailCount) {
@@ -258,16 +259,18 @@
     /**
      * Add SMS of type 0 to metrics.
      */
-    private void addSmsTypeZeroToMetrics() {
+    private void addSmsTypeZeroToMetrics(@SmsSource int smsSource) {
         mMetrics.writeIncomingSmsTypeZero(mPhone.getPhoneId(),
                 android.telephony.SmsMessage.FORMAT_3GPP);
+        mPhone.getSmsStats().onIncomingSmsTypeZero(smsSource);
     }
 
     /**
      * Add voicemail indication SMS 0 to metrics.
      */
-    private void addVoicemailSmsToMetrics() {
+    private void addVoicemailSmsToMetrics(@SmsSource int smsSource) {
         mMetrics.writeIncomingVoiceMailSms(mPhone.getPhoneId(),
                 android.telephony.SmsMessage.FORMAT_3GPP);
+        mPhone.getSmsStats().onIncomingSmsVoicemail(false /* is3gpp2 */, smsSource);
     }
 }
diff --git a/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java b/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java
index 5d08846..ed819c1 100644
--- a/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java
+++ b/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java
@@ -25,6 +25,8 @@
 import android.telephony.SmsManager;
 
 import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.InboundSmsHandler;
+import com.android.internal.telephony.PhoneFactory;
 import com.android.internal.telephony.cat.ComprehensionTlvTag;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.uicc.IccIoResult;
@@ -73,9 +75,11 @@
      *
      * @param ust the UsimServiceTable, to check if data download is enabled
      * @param smsMessage the SMS message to process
+     * @param smsSource the source of the SMS message
      * @return {@code Activity.RESULT_OK} on success; {@code RESULT_SMS_GENERIC_ERROR} on failure
      */
-    int handleUsimDataDownload(UsimServiceTable ust, SmsMessage smsMessage) {
+    int handleUsimDataDownload(UsimServiceTable ust, SmsMessage smsMessage,
+            @InboundSmsHandler.SmsSource int smsSource) {
         // If we receive an SMS-PP message before the UsimServiceTable has been loaded,
         // assume that the data download service is not present. This is very unlikely to
         // happen because the IMS connection will not be established until after the ISIM
@@ -83,7 +87,7 @@
         if (ust != null && ust.isAvailable(
                 UsimServiceTable.UsimService.DATA_DL_VIA_SMS_PP)) {
             Rlog.d(TAG, "Received SMS-PP data download, sending to UICC.");
-            return startDataDownload(smsMessage);
+            return startDataDownload(smsMessage, smsSource);
         } else {
             Rlog.d(TAG, "DATA_DL_VIA_SMS_PP service not available, storing message to UICC.");
             String smsc = IccUtils.bytesToHexString(
@@ -92,7 +96,7 @@
             mCi.writeSmsToSim(SmsManager.STATUS_ON_ICC_UNREAD, smsc,
                     IccUtils.bytesToHexString(smsMessage.getPdu()),
                     obtainMessage(EVENT_WRITE_SMS_COMPLETE));
-            addUsimDataDownloadToMetrics(false);
+            addUsimDataDownloadToMetrics(false, smsSource);
             return Activity.RESULT_OK;  // acknowledge after response from write to USIM
         }
 
@@ -103,10 +107,13 @@
      * thread than this Handler is running on.
      *
      * @param smsMessage the message to process
+     * @param smsSource the source of the SMS message
      * @return {@code Activity.RESULT_OK} on success; {@code RESULT_SMS_GENERIC_ERROR} on failure
      */
-    public int startDataDownload(SmsMessage smsMessage) {
-        if (sendMessage(obtainMessage(EVENT_START_DATA_DOWNLOAD, smsMessage))) {
+    public int startDataDownload(SmsMessage smsMessage,
+            @InboundSmsHandler.SmsSource int smsSource) {
+        if (sendMessage(obtainMessage(EVENT_START_DATA_DOWNLOAD,
+                smsSource, 0 /* unused */, smsMessage))) {
             return Activity.RESULT_OK;  // we will send SMS ACK/ERROR based on UICC response
         } else {
             Rlog.e(TAG, "startDataDownload failed to send message to start data download.");
@@ -114,7 +121,8 @@
         }
     }
 
-    private void handleDataDownload(SmsMessage smsMessage) {
+    private void handleDataDownload(SmsMessage smsMessage,
+            @InboundSmsHandler.SmsSource int smsSource) {
         int dcs = smsMessage.getDataCodingScheme();
         int pid = smsMessage.getProtocolIdentifier();
         byte[] pdu = smsMessage.getPdu();           // includes SC address
@@ -166,7 +174,7 @@
         if (index != envelope.length) {
             Rlog.e(TAG, "startDataDownload() calculated incorrect envelope length, aborting.");
             acknowledgeSmsWithError(CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR);
-            addUsimDataDownloadToMetrics(false);
+            addUsimDataDownloadToMetrics(false, smsSource);
             return;
         }
 
@@ -174,7 +182,7 @@
         mCi.sendEnvelopeWithStatus(encodedEnvelope, obtainMessage(
                 EVENT_SEND_ENVELOPE_RESPONSE, new int[]{ dcs, pid }));
 
-        addUsimDataDownloadToMetrics(true);
+        addUsimDataDownloadToMetrics(true, smsSource);
     }
 
     /**
@@ -284,9 +292,11 @@
      * to the USIM. The metrics does not cover the case where the SMS-PP might be rejected
      * by the USIM itself.
      */
-    private void addUsimDataDownloadToMetrics(boolean result) {
+    private void addUsimDataDownloadToMetrics(boolean result,
+            @InboundSmsHandler.SmsSource int smsSource) {
         TelephonyMetrics metrics = TelephonyMetrics.getInstance();
         metrics.writeIncomingSMSPP(mPhoneId, android.telephony.SmsMessage.FORMAT_3GPP, result);
+        PhoneFactory.getPhone(mPhoneId).getSmsStats().onIncomingSmsPP(smsSource, result);
     }
 
     /**
@@ -300,7 +310,7 @@
 
         switch (msg.what) {
             case EVENT_START_DATA_DOWNLOAD:
-                handleDataDownload((SmsMessage) msg.obj);
+                handleDataDownload((SmsMessage) msg.obj, msg.arg1 /* smsSource */);
                 break;
 
             case EVENT_SEND_ENVELOPE_RESPONSE:
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
index a6daca4..dca68a0 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
@@ -109,6 +109,7 @@
 import com.android.internal.telephony.dataconnection.TransportManager;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.gsm.SuppServiceNotification;
+import com.android.internal.telephony.metrics.ImsStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.metrics.VoiceCallSessionStats;
 import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
@@ -268,6 +269,8 @@
     // List of Registrants to send supplementary service notifications to.
     private RegistrantList mSsnRegistrants = new RegistrantList();
 
+    private ImsStats mImsStats;
+
     // A runnable which is used to automatically exit from Ecm after a period of time.
     private Runnable mExitEcmRunnable = new Runnable() {
         @Override
@@ -423,6 +426,7 @@
         mDefaultPhone = defaultPhone;
         mImsManagerFactory = imsManagerFactory;
         mImsPhoneSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        mImsStats = new ImsStats(this);
         // The ImsExternalCallTracker needs to be defined before the ImsPhoneCallTracker, as the
         // ImsPhoneCallTracker uses a thread to spool up the ImsManager.  Part of this involves
         // setting the multiendpoint listener on the external call tracker.  So we need to ensure
@@ -2390,6 +2394,7 @@
                     + AccessNetworkConstants.transportTypeToString(imsRadioTech));
             setServiceState(ServiceState.STATE_IN_SERVICE);
             mMetrics.writeOnImsConnectionState(mPhoneId, ImsConnectionState.State.CONNECTED, null);
+            mImsStats.onImsRegistered(imsRadioTech);
         }
 
         @Override
@@ -2403,6 +2408,7 @@
             setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
             mMetrics.writeOnImsConnectionState(mPhoneId, ImsConnectionState.State.PROGRESSING,
                     null);
+            mImsStats.onImsRegistering(imsRadioTech);
         }
 
         @Override
@@ -2413,6 +2419,7 @@
             processDisconnectReason(imsReasonInfo);
             mMetrics.writeOnImsConnectionState(mPhoneId, ImsConnectionState.State.DISCONNECTED,
                     imsReasonInfo);
+            mImsStats.onImsUnregistered(imsReasonInfo);
         }
 
         @Override
@@ -2450,6 +2457,17 @@
         return mDefaultPhone.getVoiceCallSessionStats();
     }
 
+    /** Returns the {@link ImsStats} for this IMS phone. */
+    public ImsStats getImsStats() {
+        return mImsStats;
+    }
+
+    /** Sets the {@link ImsStats} mock for this IMS phone during unit testing. */
+    @VisibleForTesting
+    public void setImsStats(ImsStats imsStats) {
+        mImsStats = imsStats;
+    }
+
     public boolean hasAliveCall() {
         return (getForegroundCall().getState() != Call.State.IDLE ||
                 getBackgroundCall().getState() != Call.State.IDLE);
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
index 81f11a8..a6a6514 100755
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -3684,6 +3684,7 @@
             ImsPhoneConnection conn = findConnection(imsCall);
             if (conn != null) {
                 conn.updateMultipartyState(isMultiParty);
+                mPhone.getVoiceCallSessionStats().onMultipartyChange(conn, isMultiParty);
             }
         }
 
@@ -3823,6 +3824,7 @@
         @Override
         public void onSetFeatureResponse(int feature, int network, int value, int status) {
             mMetrics.writeImsSetFeatureValue(mPhone.getPhoneId(), feature, network, value);
+            mPhone.getImsStats().onSetFeatureResponse(feature, network, value);
         }
 
         @Override
@@ -4939,8 +4941,9 @@
 
         mPhone.onFeatureCapabilityChanged();
 
-        mMetrics.writeOnImsCapabilities(mPhone.getPhoneId(), getImsRegistrationTech(),
-                mMmTelCapabilities);
+        int regTech = getImsRegistrationTech();
+        mMetrics.writeOnImsCapabilities(mPhone.getPhoneId(), regTech, mMmTelCapabilities);
+        mPhone.getImsStats().onImsCapabilitiesChanged(regTech, mMmTelCapabilities);
     }
 
     @VisibleForTesting
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
index 83b5ed2..76604a5 100755
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
@@ -1106,6 +1106,7 @@
             mImsVideoCallProviderWrapper.onVideoStateChanged(newVideoState);
         }
         setVideoState(newVideoState);
+        mOwner.getPhone().getVoiceCallSessionStats().onVideoStateChange(this, newVideoState);
     }
 
 
diff --git a/src/java/com/android/internal/telephony/metrics/AirplaneModeStats.java b/src/java/com/android/internal/telephony/metrics/AirplaneModeStats.java
new file mode 100644
index 0000000..1689437
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/AirplaneModeStats.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.internal.telephony.TelephonyStatsLog.AIRPLANE_MODE;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyStatsLog;
+import com.android.telephony.Rlog;
+
+/** Metrics for the usage of airplane mode. */
+public class AirplaneModeStats extends ContentObserver {
+    private static final String TAG = AirplaneModeStats.class.getSimpleName();
+
+    /** Ignore airplane mode events occurring in the first 30 seconds. */
+    private static final long GRACE_PERIOD_MILLIS = 30000L;
+
+    /** An airplane mode toggle is considered short if under 10 seconds. */
+    private static final long SHORT_TOGGLE_MILLIS = 10000L;
+
+    private long mLastActivationTime = 0L;
+
+    private final Context mContext;
+    private final Uri mAirplaneModeSettingUri;
+
+    public AirplaneModeStats(Context context) {
+        super(new Handler(Looper.getMainLooper()));
+
+        mContext = context;
+        mAirplaneModeSettingUri = Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON);
+
+        context.getContentResolver().registerContentObserver(mAirplaneModeSettingUri, false, this);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        if (uri.equals(mAirplaneModeSettingUri)) {
+            onAirplaneModeChanged(isAirplaneModeOn());
+        }
+    }
+
+    private boolean isAirplaneModeOn() {
+        return Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
+    }
+
+    /** Generate metrics when airplane mode is enabled or disabled. */
+    private void onAirplaneModeChanged(boolean isAirplaneModeOn) {
+        Rlog.d(TAG, "Airplane mode change. Value: " + isAirplaneModeOn);
+        long currentTime = SystemClock.elapsedRealtime();
+        if (currentTime < GRACE_PERIOD_MILLIS) {
+            return;
+        }
+
+        boolean isShortToggle = calculateShortToggle(currentTime, isAirplaneModeOn);
+        int carrierId = getCarrierId();
+
+        Rlog.d(TAG, "Airplane mode: " + isAirplaneModeOn + ", short=" + isShortToggle
+                + ", carrierId=" + carrierId);
+        TelephonyStatsLog.write(AIRPLANE_MODE, isAirplaneModeOn, isShortToggle, carrierId);
+    }
+
+
+    /* Keep tracks of time and returns if it was a short toggle. */
+    private boolean calculateShortToggle(long currentTime, boolean isAirplaneModeOn) {
+        boolean isShortToggle = false;
+        if (isAirplaneModeOn) {
+            // When airplane mode is enabled, track the time.
+            if (mLastActivationTime == 0L) {
+                mLastActivationTime = currentTime;
+            }
+            return false;
+        } else {
+            // When airplane mode is disabled, reset the time and check if it was a short toggle.
+            long duration = currentTime - mLastActivationTime;
+            mLastActivationTime = 0L;
+            return duration > 0 && duration < SHORT_TOGGLE_MILLIS;
+        }
+    }
+
+    /**
+     * Returns the carrier ID of the active data subscription. If this is not available,
+     * it returns the carrier ID of the first phone.
+     */
+    private static int getCarrierId() {
+        int dataSubId = SubscriptionManager.getActiveDataSubscriptionId();
+        int phoneId = dataSubId != INVALID_SUBSCRIPTION_ID
+                ? SubscriptionManager.getPhoneId(dataSubId) : 0;
+        Phone phone = PhoneFactory.getPhone(phoneId);
+        return phone != null ? phone.getCarrierId() : INVALID_SUBSCRIPTION_ID;
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/CarrierIdMatchStats.java b/src/java/com/android/internal/telephony/metrics/CarrierIdMatchStats.java
new file mode 100644
index 0000000..4d0397c
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/CarrierIdMatchStats.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static com.android.internal.telephony.TelephonyStatsLog.CARRIER_ID_MISMATCH_REPORTED;
+import static com.android.internal.telephony.TelephonyStatsLog.CARRIER_ID_TABLE_UPDATED;
+
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyStatsLog;
+import com.android.internal.telephony.nano.PersistAtomsProto.CarrierIdMismatch;
+import com.android.telephony.Rlog;
+
+/** Metrics for the carrier id matching. */
+public class CarrierIdMatchStats {
+    private static final String TAG = CarrierIdMatchStats.class.getSimpleName();
+
+    private CarrierIdMatchStats() { }
+
+    /** Generate metrics when carrier ID mismatch occurs. */
+    public static void onCarrierIdMismatch(
+            int cid, String mccMnc, String gid1, String spn, String pnn) {
+        PersistAtomsStorage storage = PhoneFactory.getMetricsCollector().getAtomsStorage();
+
+        CarrierIdMismatch carrierIdMismatch = new CarrierIdMismatch();
+        carrierIdMismatch.mccMnc = nullToEmpty(mccMnc);
+        carrierIdMismatch.gid1 = nullToEmpty(gid1);
+        carrierIdMismatch.spn = nullToEmpty(spn);
+        carrierIdMismatch.pnn = carrierIdMismatch.spn.isEmpty() ? nullToEmpty(pnn) : "";
+
+        // Add to storage and generate atom only if it was added (new SIM card).
+        boolean isAdded = storage.addCarrierIdMismatch(carrierIdMismatch);
+        if (isAdded) {
+            Rlog.d(TAG, "New carrier ID mismatch event: " + carrierIdMismatch.toString());
+            TelephonyStatsLog.write(CARRIER_ID_MISMATCH_REPORTED, cid, mccMnc, gid1, spn, pnn);
+        }
+    }
+
+    /** Generate metrics for the carrier ID table version. */
+    public static void sendCarrierIdTableVersion(int carrierIdTableVersion) {
+        PersistAtomsStorage storage = PhoneFactory.getMetricsCollector().getAtomsStorage();
+
+        if (storage.setCarrierIdTableVersion(carrierIdTableVersion)) {
+            Rlog.d(TAG, "New carrier ID table version: " + carrierIdTableVersion);
+            TelephonyStatsLog.write(CARRIER_ID_TABLE_UPDATED, carrierIdTableVersion);
+        }
+    }
+
+    private static String nullToEmpty(String string) {
+        return string != null ? string : "";
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/DataCallSessionStats.java b/src/java/com/android/internal/telephony/metrics/DataCallSessionStats.java
new file mode 100644
index 0000000..a846d9d
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/DataCallSessionStats.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_HANDOVER;
+import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_NORMAL;
+import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_RADIO_OFF;
+import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_UNKNOWN;
+import static com.android.internal.telephony.TelephonyStatsLog.DATA_CALL_SESSION__IP_TYPE__APN_PROTOCOL_IPV4;
+
+import android.telephony.Annotation.ApnType;
+import android.telephony.Annotation.NetworkType;
+import android.telephony.DataFailCause;
+import android.telephony.ServiceState;
+import android.telephony.data.ApnSetting.ProtocolType;
+import android.telephony.data.DataCallResponse;
+import android.telephony.data.DataService;
+import android.telephony.data.DataService.DeactivateDataReason;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.ServiceStateTracker;
+import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession;
+import com.android.telephony.Rlog;
+
+import java.util.Random;
+
+/** Collects data call change events per DcTracker for the pulled atom. */
+public class DataCallSessionStats {
+    private static final String TAG = DataCallSessionStats.class.getSimpleName();
+
+    private final Phone mPhone;
+    private long mStartTime;
+    private boolean mOnRatChangedCalledBeforeSetup = false;
+    DataCallSession mOngoingDataCall;
+
+    private final PersistAtomsStorage mAtomsStorage =
+            PhoneFactory.getMetricsCollector().getAtomsStorage();
+
+    private static final Random RANDOM = new Random();
+
+    public DataCallSessionStats(Phone phone) {
+        mPhone = phone;
+    }
+
+    /** create a new ongoing atom when data cal is set up */
+    public synchronized void onSetupDataCall(@ApnType int apnTypeBitMask) {
+        if (!mOnRatChangedCalledBeforeSetup) {
+            // there shouldn't be an ongoing dataCall here, if that's the case, it means that
+            // deactivateDataCall hasn't been processed properly, so we save the previous atom here
+            // and move on to create a new atom.
+            if (mOngoingDataCall != null) {
+                mOngoingDataCall.failureCause = DataFailCause.UNKNOWN;
+                mOngoingDataCall.durationMinutes =
+                        convertMillisToMinutes(System.currentTimeMillis() - mStartTime);
+                mOngoingDataCall.ongoing = false;
+                mAtomsStorage.addDataCallSession(mOngoingDataCall);
+                mOngoingDataCall = null;
+            }
+            mOngoingDataCall = getDefaultProto(apnTypeBitMask);
+            mStartTime = System.currentTimeMillis();
+        } else {
+            // if onRatChanged was called before onSetupDataCall, the atom is already initialized
+            // but apnTypeBitMask is initialized to 0, so we need to update it
+            mOngoingDataCall.apnTypeBitmask = apnTypeBitMask;
+        }
+        mOnRatChangedCalledBeforeSetup = false;
+    }
+
+    /**
+     * update the ongoing dataCall's atom for data call response event
+     * @param response setup Data call response
+     * @param radioTechnology The data call RAT
+     * @param apnTypeBitmask APN type bitmask
+     * @param protocol Data connection protocol
+     * @param failureCause failure cause as per android.telephony.DataFailCause
+     */
+    public synchronized void onSetupDataCallResponse(
+            DataCallResponse response,
+            @ServiceState.RilRadioTechnology int radioTechnology,
+            @ApnType int apnTypeBitmask,
+            @ProtocolType int protocol,
+            int failureCause) {
+        // there should've been another call to initiate the atom,
+        // so this method is being called out of order -> no metric will be logged
+        if (mOngoingDataCall == null) {
+            loge("onSetupDataCallResponse: no DataCallSession atom has been initiated.");
+            return;
+        }
+        mOngoingDataCall.ratAtEnd =
+                ServiceState.rilRadioTechnologyToNetworkType(radioTechnology);
+
+        // only set if apn hasn't been set during setup
+        if (mOngoingDataCall.apnTypeBitmask == 0) {
+            mOngoingDataCall.apnTypeBitmask = apnTypeBitmask;
+        }
+
+        mOngoingDataCall.ipType = protocol;
+        mOngoingDataCall.failureCause = failureCause;
+        if (response != null) {
+            mOngoingDataCall.suggestedRetryMillis =
+                    (int) Math.min(response.getRetryDurationMillis(), Integer.MAX_VALUE);
+            if (failureCause != DataFailCause.NONE) {
+                mOngoingDataCall.failureCause = failureCause;
+                mOngoingDataCall.setupFailed = true;
+                // set dataCall as inactive
+                mOngoingDataCall.ongoing = false;
+                // store it only if setup has failed
+                mAtomsStorage.addDataCallSession(mOngoingDataCall);
+                mOngoingDataCall = null;
+            }
+        }
+    }
+
+    /**
+     * update the ongoing dataCall's atom when data call is deactivated
+     *
+     * @param reason Deactivate reason
+     */
+    public void setDeactivateDataCallReason(@DeactivateDataReason int reason) {
+        // there should've been another call to initiate the atom,
+        // so this method is being called out of order -> no metric will be logged
+        if (mOngoingDataCall == null) {
+            loge("onSetupDataCallResponse: no DataCallSession atom has been initiated.");
+            return;
+        }
+        switch (reason) {
+            case DataService.REQUEST_REASON_NORMAL:
+                mOngoingDataCall.deactivateReason =
+                        DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_NORMAL;
+                break;
+            case DataService.REQUEST_REASON_SHUTDOWN:
+                mOngoingDataCall.deactivateReason =
+                        DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_RADIO_OFF;
+                break;
+            case DataService.REQUEST_REASON_HANDOVER:
+                mOngoingDataCall.deactivateReason =
+                        DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_HANDOVER;
+                break;
+            default:
+                mOngoingDataCall.deactivateReason =
+                        DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_UNKNOWN;
+                break;
+        }
+
+        mOngoingDataCall.oosAtEnd = getIsOos();
+    }
+
+    /**
+     * store the atom, when DataConnection reaches DISCONNECTED state
+     *
+     * @param cid Context Id, uniquely identifies the call
+     */
+    public void onDataCallDisconnected(int cid) {
+        // there should've been another call to initiate the atom,
+        // so this method is being called out of order -> no atom will be saved
+        if (mOngoingDataCall == null) {
+            loge("onSetupDataCallResponse: no DataCallSession atom has been initiated.");
+            return;
+        }
+        mOngoingDataCall.carrierId = cid;
+        mOngoingDataCall.ongoing = false;
+        mOngoingDataCall.durationMinutes =
+                convertMillisToMinutes(System.currentTimeMillis() - mStartTime);
+        // store for the data call list event, after DataCall is disconnected and entered into
+        // inactive mode
+        mAtomsStorage.addDataCallSession(mOngoingDataCall);
+        mOngoingDataCall = null;
+    }
+
+    /** Updates this RAT when it changes. */
+    public synchronized void onRatChanged(@ServiceState.RilRadioTechnology int radioTechnology) {
+        // if no data call is initiated, or we have a new data call while the last one has ended
+        // because onRatChanged might be called before onSetupDataCall
+        if (mOngoingDataCall == null) {
+            mOngoingDataCall = getDefaultProto(0);
+            mStartTime = System.currentTimeMillis();
+            mOnRatChangedCalledBeforeSetup = true;
+        }
+        @NetworkType int rat = ServiceState.rilRadioTechnologyToNetworkType(radioTechnology);
+        if (mOngoingDataCall.ratAtEnd != rat) {
+            mOngoingDataCall.ratSwitchCount++;
+            mOngoingDataCall.ratAtEnd = rat;
+        }
+    }
+
+    private static long convertMillisToMinutes(long millis) {
+        return Math.round(millis / 60000);
+    }
+
+    /** Creates a proto for a normal {@code DataCallSession} with default values. */
+    private DataCallSession getDefaultProto(@ApnType int apnTypeBitmask) {
+        DataCallSession proto = new DataCallSession();
+        proto.dimension = RANDOM.nextInt();
+        proto.isMultiSim = SimSlotState.isMultiSim();
+        proto.isEsim = SimSlotState.isEsim(mPhone.getPhoneId());
+        proto.apnTypeBitmask = apnTypeBitmask;
+        proto.carrierId = mPhone.getCarrierId();
+        proto.isRoaming = getIsRoaming();
+        proto.oosAtEnd = false;
+        proto.ratSwitchCount = 0L;
+        proto.isOpportunistic = getIsOpportunistic();
+        proto.ipType = DATA_CALL_SESSION__IP_TYPE__APN_PROTOCOL_IPV4;
+        proto.setupFailed = false;
+        proto.failureCause = DataFailCause.NONE;
+        proto.suggestedRetryMillis = 0;
+        proto.deactivateReason = DATA_CALL_SESSION__DEACTIVATE_REASON__DEACTIVATE_REASON_UNKNOWN;
+        proto.durationMinutes = 0;
+        proto.ongoing = true;
+        return proto;
+    }
+
+    private boolean getIsRoaming() {
+        ServiceStateTracker serviceStateTracker = mPhone.getServiceStateTracker();
+        ServiceState serviceState =
+                serviceStateTracker != null ? serviceStateTracker.getServiceState() : null;
+        return serviceState != null ? serviceState.getRoaming() : false;
+    }
+
+    private boolean getIsOpportunistic() {
+        SubscriptionController subController = SubscriptionController.getInstance();
+        return subController != null ? subController.isOpportunistic(mPhone.getSubId()) : false;
+    }
+
+    private boolean getIsOos() {
+        ServiceStateTracker serviceStateTracker = mPhone.getServiceStateTracker();
+        ServiceState serviceState =
+                serviceStateTracker != null ? serviceStateTracker.getServiceState() : null;
+        return serviceState != null
+                ? serviceState.getDataRegistrationState() == ServiceState.STATE_OUT_OF_SERVICE
+                : false;
+    }
+
+    private void loge(String format, Object... args) {
+        Rlog.e(TAG, "[" + mPhone.getPhoneId() + "]" + String.format(format, args));
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java b/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java
new file mode 100644
index 0000000..7ee6675
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import android.telephony.Annotation.NetworkType;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.ServiceStateTracker;
+import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.TelephonyStatsLog;
+import com.android.internal.telephony.dataconnection.DcTracker;
+
+/** Generates metrics related to data stall recovery events per phone ID for the pushed atom. */
+public class DataStallRecoveryStats {
+    /**
+     * Create and push new atom when there is a data stall recovery event
+     *
+     * @param recoveryAction Data stall recovery action
+     * @param phone
+     */
+    public static void onDataStallEvent(@DcTracker.RecoveryAction int recoveryAction,
+            Phone phone) {
+        if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
+            phone = phone.getDefaultPhone();
+        }
+
+        int carrierId = phone.getCarrierId();
+        int rat = getRat(phone);
+        // the number returned here matches the SignalStrength enum we have
+        int signalStrength = phone.getSignalStrength().getLevel();
+        boolean isOpportunistic = getIsOpportunistic(phone);
+        boolean isMultiSim = SimSlotState.getCurrentState().numActiveSims > 1;
+
+        TelephonyStatsLog.write(TelephonyStatsLog.DATA_STALL_RECOVERY_REPORTED, carrierId, rat,
+                signalStrength, recoveryAction, isOpportunistic, isMultiSim);
+    }
+
+    private static @NetworkType int getRat(Phone phone) {
+        ServiceStateTracker serviceStateTracker = phone.getServiceStateTracker();
+        ServiceState serviceState =
+                serviceStateTracker != null ? serviceStateTracker.getServiceState() : null;
+        return serviceState != null ? serviceState.getVoiceNetworkType()
+                : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+    }
+
+    private static boolean getIsOpportunistic(Phone phone) {
+        SubscriptionController subController = SubscriptionController.getInstance();
+        return subController != null ? subController.isOpportunistic(phone.getSubId()) : false;
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/ImsStats.java b/src/java/com/android/internal/telephony/metrics/ImsStats.java
new file mode 100644
index 0000000..b20c598
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/ImsStats.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static android.telephony.ims.RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED;
+import static android.telephony.ims.RegistrationManager.REGISTRATION_STATE_REGISTERED;
+import static android.telephony.ims.RegistrationManager.REGISTRATION_STATE_REGISTERING;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE;
+import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
+import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
+import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.AccessNetworkConstants.TransportType;
+import android.telephony.Annotation.NetworkType;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RegistrationManager.ImsRegistrationState;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability;
+import android.telephony.ims.stub.ImsRegistrationImplBase.ImsRegistrationTech;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
+import com.android.telephony.Rlog;
+
+/** Tracks IMS registration metrics for each phone. */
+public class ImsStats {
+    private static final String TAG = ImsStats.class.getSimpleName();
+
+    /**
+     * Minimal duration of the registration state.
+     *
+     * <p>Registration state (including changes in capable/available features) with duration shorter
+     * than this will be ignored as they are considered transient states.
+     */
+    private static final long MIN_REGISTRATION_DURATION_MILLIS = 1L * SECOND_IN_MILLIS;
+
+    /**
+     * Maximum length of the extra message in the termination reason.
+     *
+     * <p>If the extra message is longer than this length, it will be truncated.
+     */
+    private static final int MAX_EXTRA_MESSAGE_LENGTH = 128;
+
+    private final ImsPhone mPhone;
+    private final PersistAtomsStorage mStorage;
+
+    @ImsRegistrationState private int mLastRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
+
+    private long mLastTimestamp;
+    @Nullable private ImsRegistrationStats mLastRegistrationStats;
+
+    // Available features are those reported by ImsService to be available for use.
+    private MmTelCapabilities mLastAvailableFeatures = new MmTelCapabilities();
+
+    // Capable features (enabled by device/carrier). Theses are available before IMS is registered
+    // and not necessarily updated when RAT changes.
+    private final MmTelCapabilities mLastWwanCapableFeatures = new MmTelCapabilities();
+    private final MmTelCapabilities mLastWlanCapableFeatures = new MmTelCapabilities();
+
+    public ImsStats(ImsPhone phone) {
+        mPhone = phone;
+        mStorage = PhoneFactory.getMetricsCollector().getAtomsStorage();
+    }
+
+    /**
+     * Finalizes the durations of the current IMS registration stats segment.
+     *
+     * <p>This method is also invoked whenever the registration state, feature capability, or
+     * feature availability changes.
+     */
+    public synchronized void conclude() {
+        long now = getTimeMillis();
+
+        // Currently not tracking time spent on registering.
+        if (mLastRegistrationState == REGISTRATION_STATE_REGISTERED) {
+            ImsRegistrationStats stats = copyOf(mLastRegistrationStats);
+            long duration = now - mLastTimestamp;
+
+            if (duration < MIN_REGISTRATION_DURATION_MILLIS) {
+                logw("conclude: discarding transient stats, duration=%d", duration);
+            } else {
+                stats.registeredMillis = duration;
+
+                stats.voiceAvailableMillis =
+                        mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_VOICE) ? duration : 0;
+                stats.videoAvailableMillis =
+                        mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_VIDEO) ? duration : 0;
+                stats.utAvailableMillis =
+                        mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_UT) ? duration : 0;
+                stats.smsAvailableMillis =
+                        mLastAvailableFeatures.isCapable(CAPABILITY_TYPE_SMS) ? duration : 0;
+
+                MmTelCapabilities lastCapableFeatures =
+                        stats.rat == TelephonyManager.NETWORK_TYPE_IWLAN
+                                ? mLastWlanCapableFeatures
+                                : mLastWwanCapableFeatures;
+                stats.voiceCapableMillis =
+                        lastCapableFeatures.isCapable(CAPABILITY_TYPE_VOICE) ? duration : 0;
+                stats.videoCapableMillis =
+                        lastCapableFeatures.isCapable(CAPABILITY_TYPE_VIDEO) ? duration : 0;
+                stats.utCapableMillis =
+                        lastCapableFeatures.isCapable(CAPABILITY_TYPE_UT) ? duration : 0;
+                stats.smsCapableMillis =
+                        lastCapableFeatures.isCapable(CAPABILITY_TYPE_SMS) ? duration : 0;
+
+                mStorage.addImsRegistrationStats(stats);
+            }
+        }
+
+        mLastTimestamp = now;
+    }
+
+    /** Updates the stats when registered features changed. */
+    public synchronized void onImsCapabilitiesChanged(
+            @ImsRegistrationTech int radioTech, MmTelCapabilities capabilities) {
+        conclude();
+
+        if (mLastRegistrationStats != null) {
+            mLastRegistrationStats.rat = convertRegistrationTechToNetworkType(radioTech);
+        }
+        mLastAvailableFeatures = capabilities;
+    }
+
+    /** Updates the stats when capable features changed. */
+    public synchronized void onSetFeatureResponse(
+            @MmTelCapability int feature, @ImsRegistrationTech int network, int value) {
+        MmTelCapabilities lastCapableFeatures = getLastCapableFeaturesForTech(network);
+        if (lastCapableFeatures != null) {
+            conclude();
+            if (value == ProvisioningManager.PROVISIONING_VALUE_ENABLED) {
+                lastCapableFeatures.addCapabilities(feature);
+            } else {
+                lastCapableFeatures.removeCapabilities(feature);
+            }
+        }
+    }
+
+    /** Updates the stats when IMS registration is progressing. */
+    public synchronized void onImsRegistering(@TransportType int imsRadioTech) {
+        conclude();
+
+        mLastRegistrationStats = getDefaultImsRegistrationStats();
+        mLastRegistrationStats.rat = convertTransportTypeToNetworkType(imsRadioTech);
+        mLastRegistrationState = REGISTRATION_STATE_REGISTERING;
+    }
+
+    /** Updates the stats when IMS registration succeeds. */
+    public synchronized void onImsRegistered(@TransportType int imsRadioTech) {
+        conclude();
+
+        // NOTE: mLastRegistrationStats can be null (no registering phase).
+        if (mLastRegistrationStats == null) {
+            mLastRegistrationStats = getDefaultImsRegistrationStats();
+        }
+        mLastRegistrationStats.rat = convertTransportTypeToNetworkType(imsRadioTech);
+        mLastRegistrationState = REGISTRATION_STATE_REGISTERED;
+    }
+
+    /** Updates the stats and generates a termination atom when IMS registration fails/ends. */
+    public synchronized void onImsUnregistered(ImsReasonInfo reasonInfo) {
+        conclude();
+
+        // Generate end reason atom.
+        // NOTE: mLastRegistrationStats can be null (no registering phase).
+        ImsRegistrationTermination termination = new ImsRegistrationTermination();
+        if (mLastRegistrationStats != null) {
+            termination.carrierId = mLastRegistrationStats.carrierId;
+            termination.ratAtEnd = getRatAtEnd(mLastRegistrationStats.rat);
+        } else {
+            termination.carrierId = mPhone.getDefaultPhone().getCarrierId();
+            // We cannot tell whether the registration was intended for WWAN or WLAN
+            termination.ratAtEnd = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        }
+        termination.isMultiSim = SimSlotState.isMultiSim();
+        termination.setupFailed = (mLastRegistrationState != REGISTRATION_STATE_REGISTERED);
+        termination.reasonCode = reasonInfo.getCode();
+        termination.extraCode = reasonInfo.getExtraCode();
+        termination.extraMessage = sanitizeExtraMessage(reasonInfo.getExtraMessage());
+        termination.count = 1;
+        mStorage.addImsRegistrationTermination(termination);
+
+        // Reset state to unregistered.
+        mLastRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
+        mLastRegistrationStats = null;
+        mLastAvailableFeatures = new MmTelCapabilities();
+    }
+
+    @NetworkType
+    private int getRatAtEnd(@NetworkType int lastStateRat) {
+        return lastStateRat == TelephonyManager.NETWORK_TYPE_IWLAN ? lastStateRat : getWwanPsRat();
+    }
+
+    @NetworkType
+    private int convertTransportTypeToNetworkType(@TransportType int transportType) {
+        switch (transportType) {
+            case AccessNetworkConstants.TRANSPORT_TYPE_WWAN:
+                return getWwanPsRat();
+            case AccessNetworkConstants.TRANSPORT_TYPE_WLAN:
+                return TelephonyManager.NETWORK_TYPE_IWLAN;
+            default:
+                return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        }
+    }
+
+    @NetworkType
+    private int getWwanPsRat() {
+        ServiceState state = mPhone.getServiceStateTracker().getServiceState();
+        final NetworkRegistrationInfo wwanRegInfo =
+                state.getNetworkRegistrationInfo(
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+        return wwanRegInfo != null
+                ? wwanRegInfo.getAccessNetworkTechnology()
+                : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+    }
+
+    private ImsRegistrationStats getDefaultImsRegistrationStats() {
+        Phone phone = mPhone.getDefaultPhone();
+        ImsRegistrationStats stats = new ImsRegistrationStats();
+        stats.carrierId = phone.getCarrierId();
+        stats.simSlotIndex = phone.getPhoneId();
+        return stats;
+    }
+
+    @Nullable
+    private MmTelCapabilities getLastCapableFeaturesForTech(@ImsRegistrationTech int radioTech) {
+        switch (radioTech) {
+            case REGISTRATION_TECH_NONE:
+                return null;
+            case REGISTRATION_TECH_IWLAN:
+                return mLastWlanCapableFeatures;
+            default:
+                return mLastWwanCapableFeatures;
+        }
+    }
+
+    @NetworkType
+    private int convertRegistrationTechToNetworkType(@ImsRegistrationTech int radioTech) {
+        switch (radioTech) {
+            case REGISTRATION_TECH_NONE:
+                return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+            case REGISTRATION_TECH_LTE:
+                return TelephonyManager.NETWORK_TYPE_LTE;
+            case REGISTRATION_TECH_IWLAN:
+                return TelephonyManager.NETWORK_TYPE_IWLAN;
+            default:
+                // TODO: for VoNR, need to add registration tech to ImsRegistrationImplBase
+                loge("convertRegistrationTechToNetworkType: unknown radio tech %d", radioTech);
+                return getWwanPsRat();
+        }
+    }
+
+    private static ImsRegistrationStats copyOf(ImsRegistrationStats source) {
+        ImsRegistrationStats dest = new ImsRegistrationStats();
+
+        dest.carrierId = source.carrierId;
+        dest.simSlotIndex = source.simSlotIndex;
+        dest.rat = source.rat;
+        dest.registeredMillis = source.registeredMillis;
+        dest.voiceCapableMillis = source.voiceCapableMillis;
+        dest.voiceAvailableMillis = source.voiceAvailableMillis;
+        dest.smsCapableMillis = source.smsCapableMillis;
+        dest.smsAvailableMillis = source.smsAvailableMillis;
+        dest.videoCapableMillis = source.videoCapableMillis;
+        dest.videoAvailableMillis = source.videoAvailableMillis;
+        dest.utCapableMillis = source.utCapableMillis;
+        dest.utAvailableMillis = source.utAvailableMillis;
+
+        return dest;
+    }
+
+    @VisibleForTesting
+    protected long getTimeMillis() {
+        return SystemClock.elapsedRealtime();
+    }
+
+    private static String sanitizeExtraMessage(@Nullable String str) {
+        if (str == null) {
+            return "";
+        }
+        return str.length() > MAX_EXTRA_MESSAGE_LENGTH
+                ? str.substring(0, MAX_EXTRA_MESSAGE_LENGTH)
+                : str;
+    }
+
+    private void logw(String format, Object... args) {
+        Rlog.w(TAG, "[" + mPhone.getPhoneId() + "] " + String.format(format, args));
+    }
+
+    private void loge(String format, Object... args) {
+        Rlog.e(TAG, "[" + mPhone.getPhoneId() + "] " + String.format(format, args));
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/MetricsCollector.java b/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
index af71c54..8b2796e 100644
--- a/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
+++ b/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
@@ -20,6 +20,14 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 
+import static com.android.internal.telephony.TelephonyStatsLog.CARRIER_ID_TABLE_VERSION;
+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.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.SIM_SLOT_STATE;
 import static com.android.internal.telephony.TelephonyStatsLog.SUPPORTED_RADIO_ACCESS_FAMILY;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_RAT_USAGE;
@@ -33,7 +41,15 @@
 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.RawVoiceCallRatUsage;
+import com.android.internal.telephony.imsphone.ImsPhone;
+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.ImsRegistrationStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
+import com.android.internal.telephony.nano.PersistAtomsProto.IncomingSms;
+import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms;
+import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
 import com.android.internal.util.ConcurrentUtils;
 import com.android.telephony.Rlog;
@@ -82,20 +98,32 @@
 
     private PersistAtomsStorage mStorage;
     private final StatsManager mStatsManager;
+    private final AirplaneModeStats mAirplaneModeStats;
     private static final Random sRandom = new Random();
 
     public MetricsCollector(Context context) {
         mStorage = new PersistAtomsStorage(context);
         mStatsManager = (StatsManager) context.getSystemService(Context.STATS_MANAGER);
         if (mStatsManager != null) {
+            registerAtom(CELLULAR_DATA_SERVICE_SWITCH, POLICY_PULL_DAILY);
+            registerAtom(CELLULAR_SERVICE_STATE, POLICY_PULL_DAILY);
             registerAtom(SIM_SLOT_STATE, null);
             registerAtom(SUPPORTED_RADIO_ACCESS_FAMILY, null);
             registerAtom(VOICE_CALL_RAT_USAGE, POLICY_PULL_DAILY);
             registerAtom(VOICE_CALL_SESSION, POLICY_PULL_DAILY);
+            registerAtom(INCOMING_SMS, POLICY_PULL_DAILY);
+            registerAtom(OUTGOING_SMS, POLICY_PULL_DAILY);
+            registerAtom(CARRIER_ID_TABLE_VERSION, null);
+            registerAtom(DATA_CALL_SESSION, POLICY_PULL_DAILY);
+            registerAtom(IMS_REGISTRATION_STATS, POLICY_PULL_DAILY);
+            registerAtom(IMS_REGISTRATION_TERMINATION, POLICY_PULL_DAILY);
+
             Rlog.d(TAG, "registered");
         } else {
             Rlog.e(TAG, "could not get StatsManager, atoms not registered");
         }
+
+        mAirplaneModeStats = new AirplaneModeStats(context);
     }
 
     /** Replaces the {@link PersistAtomsStorage} backing the puller. Used during unit tests. */
@@ -114,6 +142,10 @@
     @Override
     public int onPullAtom(int atomTag, List<StatsEvent> data) {
         switch (atomTag) {
+            case CELLULAR_DATA_SERVICE_SWITCH:
+                return pullCellularDataServiceSwitch(data);
+            case CELLULAR_SERVICE_STATE:
+                return pullCellularServiceState(data);
             case SIM_SLOT_STATE:
                 return pullSimSlotState(data);
             case SUPPORTED_RADIO_ACCESS_FAMILY:
@@ -122,6 +154,18 @@
                 return pullVoiceCallRatUsages(data);
             case VOICE_CALL_SESSION:
                 return pullVoiceCallSessions(data);
+            case INCOMING_SMS:
+                return pullIncomingSms(data);
+            case OUTGOING_SMS:
+                return pullOutgoingSms(data);
+            case CARRIER_ID_TABLE_VERSION:
+                return pullCarrierIdTableVersion(data);
+            case DATA_CALL_SESSION:
+                return pullDataCallSession(data);
+            case IMS_REGISTRATION_STATS:
+                return pullImsRegistrationStats(data);
+            case IMS_REGISTRATION_TERMINATION:
+                return pullImsRegistrationTermination(data);
             default:
                 Rlog.e(TAG, String.format("unexpected atom ID %d", atomTag));
                 return StatsManager.PULL_SKIP;
@@ -154,17 +198,17 @@
     }
 
     private static int pullSupportedRadioAccessFamily(List<StatsEvent> data) {
-        long rafSupported = 0L;
-        try {
-            // The bitmask is defined in android.telephony.TelephonyManager.NetworkTypeBitMask
-            for (Phone phone : PhoneFactory.getPhones()) {
-                rafSupported |= phone.getRadioAccessFamily();
-            }
-        } catch (IllegalStateException e) {
-            // Phones have not been made yet
+        Phone[] phones = getPhonesIfAny();
+        if (phones.length == 0) {
             return StatsManager.PULL_SKIP;
         }
 
+        // The bitmask is defined in android.telephony.TelephonyManager.NetworkTypeBitMask
+        long rafSupported = 0L;
+        for (Phone phone : PhoneFactory.getPhones()) {
+            rafSupported |= phone.getRadioAccessFamily();
+        }
+
         StatsEvent e =
                 StatsEvent.newBuilder()
                         .setAtomId(SUPPORTED_RADIO_ACCESS_FAMILY)
@@ -174,8 +218,25 @@
         return StatsManager.PULL_SUCCESS;
     }
 
+    private static int pullCarrierIdTableVersion(List<StatsEvent> data) {
+        Phone[] phones = getPhonesIfAny();
+        if (phones.length == 0) {
+            return StatsManager.PULL_SKIP;
+        } else {
+            // All phones should have the same version of the carrier ID table, so only query the
+            // first one.
+            int version = phones[0].getCarrierIdListVersion();
+            data.add(
+                    StatsEvent.newBuilder()
+                            .setAtomId(CARRIER_ID_TABLE_VERSION)
+                            .writeInt(version)
+                            .build());
+            return StatsManager.PULL_SUCCESS;
+        }
+    }
+
     private int pullVoiceCallRatUsages(List<StatsEvent> data) {
-        RawVoiceCallRatUsage[] usages = mStorage.getVoiceCallRatUsages(MIN_COOLDOWN_MILLIS);
+        VoiceCallRatUsage[] usages = mStorage.getVoiceCallRatUsages(MIN_COOLDOWN_MILLIS);
         if (usages != null) {
             // sort by carrier/RAT and remove buckets with insufficient number of calls
             Arrays.stream(usages)
@@ -199,7 +260,7 @@
     private int pullVoiceCallSessions(List<StatsEvent> data) {
         VoiceCallSession[] calls = mStorage.getVoiceCallSessions(MIN_COOLDOWN_MILLIS);
         if (calls != null) {
-            // call session list is already shuffled when calls inserted
+            // call session list is already shuffled when calls were inserted
             Arrays.stream(calls).forEach(call -> data.add(buildStatsEvent(call)));
             return StatsManager.PULL_SUCCESS;
         } else {
@@ -208,12 +269,146 @@
         }
     }
 
+    private int pullIncomingSms(List<StatsEvent> data) {
+        IncomingSms[] smsList = mStorage.getIncomingSms(MIN_COOLDOWN_MILLIS);
+        if (smsList != null) {
+            // SMS list is already shuffled when SMS were inserted
+            Arrays.stream(smsList).forEach(sms -> data.add(buildStatsEvent(sms)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "INCOMING_SMS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullOutgoingSms(List<StatsEvent> data) {
+        OutgoingSms[] smsList = mStorage.getOutgoingSms(MIN_COOLDOWN_MILLIS);
+        if (smsList != null) {
+            // SMS list is already shuffled when SMS were inserted
+            Arrays.stream(smsList).forEach(sms -> data.add(buildStatsEvent(sms)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "OUTGOING_SMS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullDataCallSession(List<StatsEvent> data) {
+        DataCallSession[] dataCallSessions = mStorage.getDataCallSessions(MIN_COOLDOWN_MILLIS);
+        if (dataCallSessions != null) {
+            Arrays.stream(dataCallSessions)
+                    .forEach(dataCall -> data.add(buildStatsEvent(dataCall)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "DATA_CALL_SESSION pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullCellularDataServiceSwitch(List<StatsEvent> data) {
+        CellularDataServiceSwitch[] persistAtoms =
+                mStorage.getCellularDataServiceSwitches(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            // list is already shuffled when instances were inserted
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "CELLULAR_DATA_SERVICE_SWITCH pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullCellularServiceState(List<StatsEvent> data) {
+        // Include the latest durations
+        for (Phone phone : getPhonesIfAny()) {
+            phone.getServiceStateTracker().getServiceStateStats().conclude();
+        }
+
+        CellularServiceState[] persistAtoms =
+                mStorage.getCellularServiceStates(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            // list is already shuffled when instances were inserted
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "CELLULAR_SERVICE_STATE pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullImsRegistrationStats(List<StatsEvent> data) {
+        // Include the latest durations
+        for (Phone phone : getPhonesIfAny()) {
+            ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
+            if (imsPhone != null) {
+                imsPhone.getImsStats().conclude();
+            }
+        }
+
+        ImsRegistrationStats[] persistAtoms = mStorage.getImsRegistrationStats(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            // list is already shuffled when instances were inserted
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "IMS_REGISTRATION_STATS pull too frequent, skipping");
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    private int pullImsRegistrationTermination(List<StatsEvent> data) {
+        ImsRegistrationTermination[] persistAtoms =
+                mStorage.getImsRegistrationTerminations(MIN_COOLDOWN_MILLIS);
+        if (persistAtoms != null) {
+            // list is already shuffled when instances were inserted
+            Arrays.stream(persistAtoms)
+                    .forEach(persistAtom -> data.add(buildStatsEvent(persistAtom)));
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            Rlog.w(TAG, "IMS_REGISTRATION_TERMINATION 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);
     }
 
-    private static StatsEvent buildStatsEvent(RawVoiceCallRatUsage usage) {
+    private static StatsEvent buildStatsEvent(CellularDataServiceSwitch serviceSwitch) {
+        return StatsEvent.newBuilder()
+                .setAtomId(CELLULAR_DATA_SERVICE_SWITCH)
+                .writeInt(serviceSwitch.ratFrom)
+                .writeInt(serviceSwitch.ratTo)
+                .writeInt(serviceSwitch.simSlotIndex)
+                .writeBoolean(serviceSwitch.isMultiSim)
+                .writeInt(serviceSwitch.carrierId)
+                .writeInt(serviceSwitch.switchCount)
+                .build();
+    }
+
+    private static StatsEvent buildStatsEvent(CellularServiceState state) {
+        return StatsEvent.newBuilder()
+                .setAtomId(CELLULAR_SERVICE_STATE)
+                .writeInt(state.voiceRat)
+                .writeInt(state.dataRat)
+                .writeInt(state.voiceRoamingType)
+                .writeInt(state.dataRoamingType)
+                .writeBoolean(state.isEndc)
+                .writeInt(state.simSlotIndex)
+                .writeBoolean(state.isMultiSim)
+                .writeInt(state.carrierId)
+                .writeInt(
+                        (int)
+                                (round(state.totalTimeMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .build();
+    }
+
+    private static StatsEvent buildStatsEvent(VoiceCallRatUsage usage) {
         return StatsEvent.newBuilder()
                 .setAtomId(VOICE_CALL_RAT_USAGE)
                 .writeInt(usage.carrierId)
@@ -253,9 +448,149 @@
                 .writeBoolean(session.isRoaming)
                 // workaround: dimension required for keeping multiple pulled atoms
                 .writeInt(sRandom.nextInt())
+                // New fields introduced in Android S
+                .writeInt(session.signalStrengthAtEnd)
+                .writeInt(session.bandAtEnd)
+                .writeInt(session.setupDurationMillis)
+                .writeInt(session.mainCodecQuality)
+                .writeBoolean(session.videoEnabled)
+                .writeInt(session.ratAtConnected)
+                .writeBoolean(session.isMultiparty)
                 .build();
     }
 
+    private static StatsEvent buildStatsEvent(IncomingSms sms) {
+        return StatsEvent.newBuilder()
+                .setAtomId(INCOMING_SMS)
+                .writeInt(sms.smsFormat)
+                .writeInt(sms.smsTech)
+                .writeInt(sms.rat)
+                .writeInt(sms.smsType)
+                .writeInt(sms.totalParts)
+                .writeInt(sms.receivedParts)
+                .writeBoolean(sms.blocked)
+                .writeInt(sms.error)
+                .writeBoolean(sms.isRoaming)
+                .writeInt(sms.simSlotIndex)
+                .writeBoolean(sms.isMultiSim)
+                .writeBoolean(sms.isEsim)
+                .writeInt(sms.carrierId)
+                .writeLong(sms.messageId)
+                .build();
+    }
+
+    private static StatsEvent buildStatsEvent(OutgoingSms sms) {
+        return StatsEvent.newBuilder()
+                .setAtomId(OUTGOING_SMS)
+                .writeInt(sms.smsFormat)
+                .writeInt(sms.smsTech)
+                .writeInt(sms.rat)
+                .writeInt(sms.sendResult)
+                .writeInt(sms.errorCode)
+                .writeBoolean(sms.isRoaming)
+                .writeBoolean(sms.isFromDefaultApp)
+                .writeInt(sms.simSlotIndex)
+                .writeBoolean(sms.isMultiSim)
+                .writeBoolean(sms.isEsim)
+                .writeInt(sms.carrierId)
+                .writeLong(sms.messageId)
+                .writeInt(sms.retryId)
+                .build();
+    }
+
+    private static StatsEvent buildStatsEvent(DataCallSession dataCallSession) {
+        return StatsEvent.newBuilder()
+                .setAtomId(DATA_CALL_SESSION)
+                .writeInt(dataCallSession.dimension)
+                .writeBoolean(dataCallSession.isMultiSim)
+                .writeBoolean(dataCallSession.isEsim)
+                .writeInt(0) // profile is deprecated, so we default to 0
+                .writeInt(dataCallSession.apnTypeBitmask)
+                .writeInt(dataCallSession.carrierId)
+                .writeBoolean(dataCallSession.isRoaming)
+                .writeInt(dataCallSession.ratAtEnd)
+                .writeBoolean(dataCallSession.oosAtEnd)
+                .writeLong(dataCallSession.ratSwitchCount)
+                .writeBoolean(dataCallSession.isOpportunistic)
+                .writeInt(dataCallSession.ipType)
+                .writeBoolean(dataCallSession.setupFailed)
+                .writeInt(dataCallSession.failureCause)
+                .writeInt(dataCallSession.suggestedRetryMillis)
+                .writeInt(dataCallSession.deactivateReason)
+                .writeLong(dataCallSession.durationMinutes)
+                .writeBoolean(dataCallSession.ongoing)
+                .build();
+    }
+
+    private static StatsEvent buildStatsEvent(ImsRegistrationStats stats) {
+        return StatsEvent.newBuilder()
+                .setAtomId(IMS_REGISTRATION_STATS)
+                .writeInt(stats.carrierId)
+                .writeInt(stats.simSlotIndex)
+                .writeInt(stats.rat)
+                .writeInt(
+                        (int)
+                                (round(stats.registeredMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.voiceCapableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.voiceAvailableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.smsCapableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.smsAvailableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.videoCapableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.videoAvailableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.utCapableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .writeInt(
+                        (int)
+                                (round(stats.utAvailableMillis, DURATION_BUCKET_MILLIS)
+                                        / SECOND_IN_MILLIS))
+                .build();
+    }
+
+    private static StatsEvent buildStatsEvent(ImsRegistrationTermination termination) {
+        return StatsEvent.newBuilder()
+                .setAtomId(IMS_REGISTRATION_TERMINATION)
+                .writeInt(termination.carrierId)
+                .writeBoolean(termination.isMultiSim)
+                .writeInt(termination.ratAtEnd)
+                .writeBoolean(termination.setupFailed)
+                .writeInt(termination.reasonCode)
+                .writeInt(termination.extraCode)
+                .writeString(termination.extraMessage)
+                .writeInt(termination.count)
+                .build();
+    }
+
+    /** Returns all phones in {@link PhoneFactory}, or an empty array if phones not made yet. */
+    private static Phone[] getPhonesIfAny() {
+        try {
+            return PhoneFactory.getPhones();
+        } catch (IllegalStateException e) {
+            // Phones have not been made yet
+            return new Phone[0];
+        }
+    }
+
     /** Returns the value rounded to the bucket. */
     private static long round(long value, long bucket) {
         return ((value + bucket / 2) / bucket) * bucket;
diff --git a/src/java/com/android/internal/telephony/metrics/ModemRestartStats.java b/src/java/com/android/internal/telephony/metrics/ModemRestartStats.java
new file mode 100644
index 0000000..343bd42
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/ModemRestartStats.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.internal.telephony.TelephonyStatsLog.MODEM_RESTART;
+
+import android.os.Build;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyStatsLog;
+import com.android.telephony.Rlog;
+
+/** Metrics for the modem restarts. */
+public class ModemRestartStats {
+    private static final String TAG = ModemRestartStats.class.getSimpleName();
+
+    /* Maximum length of the baseband version. */
+    private static final int MAX_BASEBAND_LEN = 100;
+
+    /* Maximum length of the modem restart reason. */
+    private static final int MAX_REASON_LEN = 100;
+
+    private ModemRestartStats() { }
+
+    /** Generate metrics when modem restart occurs. */
+    public static void onModemRestart(String reason) {
+        reason = truncateString(reason, MAX_REASON_LEN);
+        String basebandVersion = truncateString(Build.getRadioVersion(), MAX_BASEBAND_LEN);
+        int carrierId = getCarrierId();
+
+        Rlog.d(TAG, "Modem restart (carrier=" + carrierId + "): " + reason);
+        TelephonyStatsLog.write(MODEM_RESTART, basebandVersion, reason, carrierId);
+    }
+
+    private static String truncateString(String string, int maxLen) {
+        string = nullToEmpty(string);
+        if (string.length() > maxLen) {
+            string = string.substring(0, maxLen);
+        }
+        return string;
+    }
+
+    private static String nullToEmpty(String string) {
+        return string != null ? string : "";
+    }
+
+    /** Returns the carrier ID of the first SIM card for which carrier ID is available. */
+    private static int getCarrierId() {
+        int carrierId = INVALID_SUBSCRIPTION_ID;
+        try {
+            for (Phone phone : PhoneFactory.getPhones()) {
+                carrierId = phone.getCarrierId();
+                if (carrierId != INVALID_SUBSCRIPTION_ID) {
+                    break;
+                }
+            }
+        } catch (IllegalStateException e) {
+            // Nothing to do here.
+        }
+        return carrierId;
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java b/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
index f208369..3aca215 100644
--- a/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
+++ b/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
@@ -18,18 +18,32 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.telephony.TelephonyManager;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.nano.PersistAtomsProto.CarrierIdMismatch;
+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.ImsRegistrationStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
+import com.android.internal.telephony.nano.PersistAtomsProto.IncomingSms;
+import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms;
 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms;
-import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
+import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
+import com.android.internal.util.ArrayUtils;
 import com.android.telephony.Rlog;
 
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.security.SecureRandom;
 import java.util.Arrays;
+import java.util.stream.IntStream;
 
 /**
  * Stores and aggregates metrics that should not be pulled at arbitrary frequency.
@@ -43,51 +57,239 @@
     /** Name of the file where cached statistics are saved to. */
     private static final String FILENAME = "persist_atoms.pb";
 
-    /** Maximum number of call sessions to store during pulls. */
+    /** Delay to store atoms to persistent storage to bundle multiple operations together. */
+    private static final int SAVE_TO_FILE_DELAY_MILLIS = 30000;
+
+    /** Maximum number of call sessions to store between pulls. */
     private static final int MAX_NUM_CALL_SESSIONS = 50;
 
+    /**
+     * Maximum number of SMS to store between pulls. Incoming messages and outgoing messages are
+     * counted separately.
+     */
+    private static final int MAX_NUM_SMS = 25;
+
+    /**
+     * Maximum number of carrier ID mismatch events stored on the device to avoid sending duplicated
+     * metrics.
+     */
+    private static final int MAX_CARRIER_ID_MISMATCH = 40;
+
+    /** Maximum number of data call sessions to store during pulls. */
+    private static final int MAX_NUM_DATA_CALL_SESSIONS = 15;
+
+    /** Maximum number of service states to store between pulls. */
+    private static final int MAX_NUM_CELLULAR_SERVICE_STATES = 50;
+
+    /** Maximum number of data service switches to store between pulls. */
+    private static final int MAX_NUM_CELLULAR_DATA_SERVICE_SWITCHES = 50;
+
+    /** Maximum number of IMS registration stats to store between pulls. */
+    private static final int MAX_NUM_IMS_REGISTRATION_STATS = 10;
+
+    /** Maximum number of IMS registration terminations to store between pulls. */
+    private static final int MAX_NUM_IMS_REGISTRATION_TERMINATIONS = 10;
+
     /** Stores persist atoms and persist states of the puller. */
     @VisibleForTesting protected final PersistAtoms mAtoms;
 
     /** Aggregates RAT duration and call count. */
     private final VoiceCallRatTracker mVoiceCallRatTracker;
 
+    /** Delay before data is stored persistenly to storage. */
+    @VisibleForTesting protected int mSaveDelay;
+
     private final Context mContext;
+    private final Handler mHandler;
+    private final HandlerThread mHandlerThread;
     private static final SecureRandom sRandom = new SecureRandom();
 
+    private Runnable mSaveRunnable =
+            new Runnable() {
+                @Override
+                public void run() {
+                    saveAtomsToFileNow();
+                }
+            };
+
     public PersistAtomsStorage(Context context) {
         mContext = context;
         mAtoms = loadAtomsFromFile();
-        mVoiceCallRatTracker = VoiceCallRatTracker.fromProto(mAtoms.rawVoiceCallRatUsage);
+        mVoiceCallRatTracker = VoiceCallRatTracker.fromProto(mAtoms.voiceCallRatUsage);
+
+        mHandlerThread = new HandlerThread("PersistAtomsThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mSaveDelay = SAVE_TO_FILE_DELAY_MILLIS;
     }
 
     /** Adds a call to the storage. */
     public synchronized void addVoiceCallSession(VoiceCallSession call) {
-        int newLength = mAtoms.voiceCallSession.length + 1;
-        if (newLength > MAX_NUM_CALL_SESSIONS) {
-            // will evict one previous call randomly instead of making the array larger
-            newLength = MAX_NUM_CALL_SESSIONS;
-        } else {
-            mAtoms.voiceCallSession = Arrays.copyOf(mAtoms.voiceCallSession, newLength);
-        }
-        int insertAt = 0;
-        if (newLength > 1) {
-            // shuffle when each call is added, or randomly replace a previous call instead if
-            // MAX_NUM_CALL_SESSIONS is reached (call at the last index is evicted).
-            insertAt = sRandom.nextInt(newLength);
-            mAtoms.voiceCallSession[newLength - 1] = mAtoms.voiceCallSession[insertAt];
-        }
-        mAtoms.voiceCallSession[insertAt] = call;
+        mAtoms.voiceCallSession =
+                insertAtRandomPlace(mAtoms.voiceCallSession, call, MAX_NUM_CALL_SESSIONS);
         saveAtomsToFile();
+
+        Rlog.d(TAG, "Add new voice call session: " + call.toString());
     }
 
     /** Adds RAT usages to the storage when a call session ends. */
     public synchronized void addVoiceCallRatUsage(VoiceCallRatTracker ratUsages) {
         mVoiceCallRatTracker.mergeWith(ratUsages);
-        mAtoms.rawVoiceCallRatUsage = mVoiceCallRatTracker.toProto();
+        mAtoms.voiceCallRatUsage = mVoiceCallRatTracker.toProto();
         saveAtomsToFile();
     }
 
+    /** Adds an incoming SMS to the storage. */
+    public synchronized void addIncomingSms(IncomingSms sms) {
+        mAtoms.incomingSms = insertAtRandomPlace(mAtoms.incomingSms, sms, MAX_NUM_SMS);
+        saveAtomsToFile();
+
+        // To be removed
+        Rlog.d(TAG, "Add new incoming SMS atom: " + sms.toString());
+    }
+
+    /** Adds an outgoing SMS to the storage. */
+    public synchronized void addOutgoingSms(OutgoingSms sms) {
+        // Update the retry id, if needed, so that it's unique and larger than all
+        // previous ones. (this algorithm ignores the fact that some SMS atoms might
+        // be dropped due to limit in size of the array).
+        for (OutgoingSms storedSms : mAtoms.outgoingSms) {
+            if (storedSms.messageId == sms.messageId && storedSms.retryId >= sms.retryId) {
+                sms.retryId = storedSms.retryId + 1;
+            }
+        }
+
+        mAtoms.outgoingSms = insertAtRandomPlace(mAtoms.outgoingSms, sms, MAX_NUM_SMS);
+        saveAtomsToFile();
+
+        // To be removed
+        Rlog.d(TAG, "Add new outgoing SMS atom: " + sms.toString());
+    }
+
+    /** Adds a service state to the storage, together with data service switch if any. */
+    public synchronized void addCellularServiceStateAndCellularDataServiceSwitch(
+            CellularServiceState state, @Nullable CellularDataServiceSwitch serviceSwitch) {
+        CellularServiceState existingState = find(state);
+        if (existingState != null) {
+            existingState.totalTimeMillis += state.totalTimeMillis;
+            existingState.lastUsedMillis = getWallTimeMillis();
+        } else {
+            state.lastUsedMillis = getWallTimeMillis();
+            mAtoms.cellularServiceState =
+                    insertAtRandomPlace(
+                            mAtoms.cellularServiceState, state, MAX_NUM_CELLULAR_SERVICE_STATES);
+        }
+
+        if (serviceSwitch != null) {
+            CellularDataServiceSwitch existingSwitch = find(serviceSwitch);
+            if (existingSwitch != null) {
+                existingSwitch.switchCount += serviceSwitch.switchCount;
+                existingSwitch.lastUsedMillis = getWallTimeMillis();
+            } else {
+                serviceSwitch.lastUsedMillis = getWallTimeMillis();
+                mAtoms.cellularDataServiceSwitch =
+                        insertAtRandomPlace(
+                                mAtoms.cellularDataServiceSwitch,
+                                serviceSwitch,
+                                MAX_NUM_CELLULAR_DATA_SERVICE_SWITCHES);
+            }
+        }
+
+        saveAtomsToFile();
+    }
+
+    /** Adds a data call session to the storage. */
+    public synchronized void addDataCallSession(DataCallSession dataCall) {
+        mAtoms.dataCallSession =
+                insertAtRandomPlace(mAtoms.dataCallSession, dataCall, MAX_NUM_DATA_CALL_SESSIONS);
+        saveAtomsToFile();
+    }
+
+    /**
+     * Adds a new carrier ID mismatch event to the storage.
+     *
+     * @return true if the item was not present and was added to the persistent storage, false
+     *     otherwise.
+     */
+    public synchronized boolean addCarrierIdMismatch(CarrierIdMismatch carrierIdMismatch) {
+        // Check if the details of the SIM cards are already present and in case return.
+        if (find(carrierIdMismatch) != null) {
+            return false;
+        }
+        // Add the new CarrierIdMismatch at the end of the array, so that the same atom will not be
+        // sent again in future.
+        if (mAtoms.carrierIdMismatch.length == MAX_CARRIER_ID_MISMATCH) {
+            System.arraycopy(
+                    mAtoms.carrierIdMismatch,
+                    1,
+                    mAtoms.carrierIdMismatch,
+                    0,
+                    MAX_CARRIER_ID_MISMATCH - 1);
+            mAtoms.carrierIdMismatch[MAX_CARRIER_ID_MISMATCH - 1] = carrierIdMismatch;
+        } else {
+            int newLength = mAtoms.carrierIdMismatch.length + 1;
+            mAtoms.carrierIdMismatch = Arrays.copyOf(mAtoms.carrierIdMismatch, newLength);
+            mAtoms.carrierIdMismatch[newLength - 1] = carrierIdMismatch;
+        }
+        saveAtomsToFile();
+        return true;
+    }
+
+    /** Adds IMS registration stats to the storage. */
+    public synchronized void addImsRegistrationStats(ImsRegistrationStats stats) {
+        ImsRegistrationStats existingStats = find(stats);
+        if (existingStats != null) {
+            existingStats.registeredMillis += stats.registeredMillis;
+            existingStats.voiceCapableMillis += stats.voiceCapableMillis;
+            existingStats.voiceAvailableMillis += stats.voiceAvailableMillis;
+            existingStats.smsCapableMillis += stats.smsCapableMillis;
+            existingStats.smsAvailableMillis += stats.smsAvailableMillis;
+            existingStats.videoCapableMillis += stats.videoCapableMillis;
+            existingStats.videoAvailableMillis += stats.videoAvailableMillis;
+            existingStats.utCapableMillis += stats.utCapableMillis;
+            existingStats.utAvailableMillis += stats.utAvailableMillis;
+            existingStats.lastUsedMillis = getWallTimeMillis();
+        } else {
+            stats.lastUsedMillis = getWallTimeMillis();
+            mAtoms.imsRegistrationStats =
+                    insertAtRandomPlace(
+                            mAtoms.imsRegistrationStats, stats, MAX_NUM_IMS_REGISTRATION_STATS);
+        }
+        saveAtomsToFile();
+    }
+
+    /** Adds IMS registration termination to the storage. */
+    public synchronized void addImsRegistrationTermination(ImsRegistrationTermination termination) {
+        ImsRegistrationTermination existingTermination = find(termination);
+        if (existingTermination != null) {
+            existingTermination.count += termination.count;
+            existingTermination.lastUsedMillis = getWallTimeMillis();
+        } else {
+            termination.lastUsedMillis = getWallTimeMillis();
+            mAtoms.imsRegistrationTermination =
+                    insertAtRandomPlace(
+                            mAtoms.imsRegistrationTermination,
+                            termination,
+                            MAX_NUM_IMS_REGISTRATION_TERMINATIONS);
+        }
+        saveAtomsToFile();
+    }
+
+    /**
+     * Stores the version of the carrier ID matching table.
+     *
+     * @return true if the version is newer than last available version, false otherwise.
+     */
+    public synchronized boolean setCarrierIdTableVersion(int carrierIdTableVersion) {
+        if (mAtoms.carrierIdTableVersion < carrierIdTableVersion) {
+            mAtoms.carrierIdTableVersion = carrierIdTableVersion;
+            saveAtomsToFile();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Returns and clears the voice call sessions if last pulled longer than {@code
      * minIntervalMillis} ago, otherwise returns {@code null}.
@@ -110,13 +312,12 @@
      * minIntervalMillis} ago, otherwise returns {@code null}.
      */
     @Nullable
-    public synchronized RawVoiceCallRatUsage[] getVoiceCallRatUsages(long minIntervalMillis) {
-        if (getWallTimeMillis() - mAtoms.rawVoiceCallRatUsagePullTimestampMillis
-                > minIntervalMillis) {
-            mAtoms.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
-            RawVoiceCallRatUsage[] previousUsages = mAtoms.rawVoiceCallRatUsage;
+    public synchronized VoiceCallRatUsage[] getVoiceCallRatUsages(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.voiceCallRatUsagePullTimestampMillis > minIntervalMillis) {
+            mAtoms.voiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
+            VoiceCallRatUsage[] previousUsages = mAtoms.voiceCallRatUsage;
             mVoiceCallRatTracker.clear();
-            mAtoms.rawVoiceCallRatUsage = new RawVoiceCallRatUsage[0];
+            mAtoms.voiceCallRatUsage = new VoiceCallRatUsage[0];
             saveAtomsToFile();
             return previousUsages;
         } else {
@@ -124,39 +325,228 @@
         }
     }
 
+    /**
+     * Returns and clears the incoming SMS if last pulled longer than {@code minIntervalMillis} ago,
+     * otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized IncomingSms[] getIncomingSms(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.incomingSmsPullTimestampMillis > minIntervalMillis) {
+            mAtoms.incomingSmsPullTimestampMillis = getWallTimeMillis();
+            IncomingSms[] previousIncomingSms = mAtoms.incomingSms;
+            mAtoms.incomingSms = new IncomingSms[0];
+            saveAtomsToFile();
+            return previousIncomingSms;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the outgoing SMS if last pulled longer than {@code minIntervalMillis} ago,
+     * otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized OutgoingSms[] getOutgoingSms(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.outgoingSmsPullTimestampMillis > minIntervalMillis) {
+            mAtoms.outgoingSmsPullTimestampMillis = getWallTimeMillis();
+            OutgoingSms[] previousOutgoingSms = mAtoms.outgoingSms;
+            mAtoms.outgoingSms = new OutgoingSms[0];
+            saveAtomsToFile();
+            return previousOutgoingSms;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the data call session if last pulled longer than {@code minIntervalMillis}
+     * ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized DataCallSession[] getDataCallSessions(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.dataCallSessionPullTimestampMillis > minIntervalMillis) {
+            mAtoms.dataCallSessionPullTimestampMillis = getWallTimeMillis();
+            DataCallSession[] previousDataCallSession = mAtoms.dataCallSession;
+            mAtoms.dataCallSession = new DataCallSession[0];
+            saveAtomsToFile();
+            return previousDataCallSession;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the service state durations if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized CellularServiceState[] getCellularServiceStates(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.cellularServiceStatePullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.cellularServiceStatePullTimestampMillis = getWallTimeMillis();
+            CellularServiceState[] previousStates = mAtoms.cellularServiceState;
+            Arrays.stream(previousStates).forEach(state -> state.lastUsedMillis = 0L);
+            mAtoms.cellularServiceState = new CellularServiceState[0];
+            saveAtomsToFile();
+            return previousStates;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the service state durations if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized CellularDataServiceSwitch[] getCellularDataServiceSwitches(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.cellularDataServiceSwitchPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.cellularDataServiceSwitchPullTimestampMillis = getWallTimeMillis();
+            CellularDataServiceSwitch[] previousSwitches = mAtoms.cellularDataServiceSwitch;
+            Arrays.stream(previousSwitches)
+                    .forEach(serviceSwitch -> serviceSwitch.lastUsedMillis = 0L);
+            mAtoms.cellularDataServiceSwitch = new CellularDataServiceSwitch[0];
+            saveAtomsToFile();
+            return previousSwitches;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the IMS registration statistics if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized ImsRegistrationStats[] getImsRegistrationStats(long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.imsRegistrationStatsPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.imsRegistrationStatsPullTimestampMillis = getWallTimeMillis();
+            ImsRegistrationStats[] previousStats = mAtoms.imsRegistrationStats;
+            Arrays.stream(previousStats).forEach(stats -> stats.lastUsedMillis = 0L);
+            mAtoms.imsRegistrationStats = new ImsRegistrationStats[0];
+            saveAtomsToFile();
+            return previousStats;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns and clears the IMS registration terminations if last pulled longer than {@code
+     * minIntervalMillis} ago, otherwise returns {@code null}.
+     */
+    @Nullable
+    public synchronized ImsRegistrationTermination[] getImsRegistrationTerminations(
+            long minIntervalMillis) {
+        if (getWallTimeMillis() - mAtoms.imsRegistrationTerminationPullTimestampMillis
+                > minIntervalMillis) {
+            mAtoms.imsRegistrationTerminationPullTimestampMillis = getWallTimeMillis();
+            ImsRegistrationTermination[] previousTerminations = mAtoms.imsRegistrationTermination;
+            Arrays.stream(previousTerminations)
+                    .forEach(termination -> termination.lastUsedMillis = 0L);
+            mAtoms.imsRegistrationTermination = new ImsRegistrationTermination[0];
+            saveAtomsToFile();
+            return previousTerminations;
+        } else {
+            return null;
+        }
+    }
+
     /** Loads {@link PersistAtoms} from a file in private storage. */
     private PersistAtoms loadAtomsFromFile() {
         try {
-            PersistAtoms atomsFromFile =
+            PersistAtoms atoms =
                     PersistAtoms.parseFrom(
                             Files.readAllBytes(mContext.getFileStreamPath(FILENAME).toPath()));
             // check all the fields in case of situations such as OTA or crash during saving
-            if (atomsFromFile.rawVoiceCallRatUsage == null) {
-                atomsFromFile.rawVoiceCallRatUsage = new RawVoiceCallRatUsage[0];
-            }
-            if (atomsFromFile.voiceCallSession == null) {
-                atomsFromFile.voiceCallSession = new VoiceCallSession[0];
-            }
-            if (atomsFromFile.voiceCallSession.length > MAX_NUM_CALL_SESSIONS) {
-                atomsFromFile.voiceCallSession =
-                        Arrays.copyOf(atomsFromFile.voiceCallSession, MAX_NUM_CALL_SESSIONS);
-            }
-            // out of caution, set timestamps to now if they are missing
-            if (atomsFromFile.rawVoiceCallRatUsagePullTimestampMillis == 0L) {
-                atomsFromFile.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
-            }
-            if (atomsFromFile.voiceCallSessionPullTimestampMillis == 0L) {
-                atomsFromFile.voiceCallSessionPullTimestampMillis = getWallTimeMillis();
-            }
-            return atomsFromFile;
+            atoms.voiceCallRatUsage =
+                    sanitizeAtoms(atoms.voiceCallRatUsage, VoiceCallRatUsage.class);
+            atoms.voiceCallSession =
+                    sanitizeAtoms(
+                            atoms.voiceCallSession, VoiceCallSession.class, MAX_NUM_CALL_SESSIONS);
+            atoms.incomingSms = sanitizeAtoms(atoms.incomingSms, IncomingSms.class, MAX_NUM_SMS);
+            atoms.outgoingSms = sanitizeAtoms(atoms.outgoingSms, OutgoingSms.class, MAX_NUM_SMS);
+            atoms.carrierIdMismatch =
+                    sanitizeAtoms(
+                            atoms.carrierIdMismatch,
+                            CarrierIdMismatch.class,
+                            MAX_CARRIER_ID_MISMATCH);
+            atoms.dataCallSession =
+                    sanitizeAtoms(
+                            atoms.dataCallSession,
+                            DataCallSession.class,
+                            MAX_NUM_DATA_CALL_SESSIONS);
+            atoms.cellularServiceState =
+                    sanitizeAtoms(
+                            atoms.cellularServiceState,
+                            CellularServiceState.class,
+                            MAX_NUM_CELLULAR_SERVICE_STATES);
+            atoms.cellularDataServiceSwitch =
+                    sanitizeAtoms(
+                            atoms.cellularDataServiceSwitch,
+                            CellularDataServiceSwitch.class,
+                            MAX_NUM_CELLULAR_DATA_SERVICE_SWITCHES);
+            atoms.imsRegistrationStats =
+                    sanitizeAtoms(
+                            atoms.imsRegistrationStats,
+                            ImsRegistrationStats.class,
+                            MAX_NUM_IMS_REGISTRATION_STATS);
+            atoms.imsRegistrationTermination =
+                    sanitizeAtoms(
+                            atoms.imsRegistrationTermination,
+                            ImsRegistrationTermination.class,
+                            MAX_NUM_IMS_REGISTRATION_TERMINATIONS);
+            // out of caution, sanitize also the timestamps
+            atoms.voiceCallRatUsagePullTimestampMillis =
+                    sanitizeTimestamp(atoms.voiceCallRatUsagePullTimestampMillis);
+            atoms.voiceCallSessionPullTimestampMillis =
+                    sanitizeTimestamp(atoms.voiceCallSessionPullTimestampMillis);
+            atoms.incomingSmsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.incomingSmsPullTimestampMillis);
+            atoms.outgoingSmsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.outgoingSmsPullTimestampMillis);
+            atoms.dataCallSessionPullTimestampMillis =
+                    sanitizeTimestamp(atoms.dataCallSessionPullTimestampMillis);
+            atoms.cellularServiceStatePullTimestampMillis =
+                    sanitizeTimestamp(atoms.cellularServiceStatePullTimestampMillis);
+            atoms.cellularDataServiceSwitchPullTimestampMillis =
+                    sanitizeTimestamp(atoms.cellularDataServiceSwitchPullTimestampMillis);
+            atoms.imsRegistrationStatsPullTimestampMillis =
+                    sanitizeTimestamp(atoms.imsRegistrationStatsPullTimestampMillis);
+            atoms.imsRegistrationTerminationPullTimestampMillis =
+                    sanitizeTimestamp(atoms.imsRegistrationTerminationPullTimestampMillis);
+            return atoms;
+        } catch (NoSuchFileException e) {
+            Rlog.d(TAG, "PersistAtoms file not found");
         } catch (IOException | NullPointerException e) {
             Rlog.e(TAG, "cannot load/parse PersistAtoms", e);
-            return makeNewPersistAtoms();
         }
+        return makeNewPersistAtoms();
+    }
+
+    /**
+     * Posts message to save a copy of {@link PersistAtoms} to a file after a delay.
+     *
+     * <p>The delay is introduced to avoid too frequent operations to disk, which would negatively
+     * impact the power consumption.
+     */
+    private void saveAtomsToFile() {
+        if (mSaveDelay > 0) {
+            mHandler.removeCallbacks(mSaveRunnable);
+            if (mHandler.postDelayed(mSaveRunnable, mSaveDelay)) {
+                return;
+            }
+        }
+        // In case of error posting the event or if delay is 0, save immediately
+        saveAtomsToFileNow();
     }
 
     /** Saves a copy of {@link PersistAtoms} to a file in private storage. */
-    private void saveAtomsToFile() {
+    private synchronized void saveAtomsToFileNow() {
         try (FileOutputStream stream = mContext.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
             stream.write(PersistAtoms.toByteArray(mAtoms));
         } catch (IOException e) {
@@ -164,19 +554,188 @@
         }
     }
 
+    /**
+     * Returns the service state that has the same dimension values with the given one, or {@code
+     * null} if it does not exist.
+     */
+    private @Nullable CellularServiceState find(CellularServiceState key) {
+        for (CellularServiceState state : mAtoms.cellularServiceState) {
+            if (state.voiceRat == key.voiceRat
+                    && state.dataRat == key.dataRat
+                    && state.voiceRoamingType == key.voiceRoamingType
+                    && state.dataRoamingType == key.dataRoamingType
+                    && state.isEndc == key.isEndc
+                    && state.simSlotIndex == key.simSlotIndex
+                    && state.isMultiSim == key.isMultiSim
+                    && state.carrierId == key.carrierId) {
+                return state;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the data service switch that has the same dimension values with the given one, or
+     * {@code null} if it does not exist.
+     */
+    private @Nullable CellularDataServiceSwitch find(CellularDataServiceSwitch key) {
+        for (CellularDataServiceSwitch serviceSwitch : mAtoms.cellularDataServiceSwitch) {
+            if (serviceSwitch.ratFrom == key.ratFrom
+                    && serviceSwitch.ratTo == key.ratTo
+                    && serviceSwitch.simSlotIndex == key.simSlotIndex
+                    && serviceSwitch.isMultiSim == key.isMultiSim
+                    && serviceSwitch.carrierId == key.carrierId) {
+                return serviceSwitch;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the carrier ID mismatch event that has the same dimension values with the given one,
+     * or {@code null} if it does not exist.
+     */
+    private @Nullable CarrierIdMismatch find(CarrierIdMismatch key) {
+        for (CarrierIdMismatch mismatch : mAtoms.carrierIdMismatch) {
+            if (mismatch.mccMnc.equals(key.mccMnc)
+                    && mismatch.gid1.equals(key.gid1)
+                    && mismatch.spn.equals(key.spn)
+                    && mismatch.pnn.equals(key.pnn)) {
+                return mismatch;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the IMS registration stats that has the same dimension values with the given one, or
+     * {@code null} if it does not exist.
+     */
+    private @Nullable ImsRegistrationStats find(ImsRegistrationStats key) {
+        for (ImsRegistrationStats stats : mAtoms.imsRegistrationStats) {
+            if (stats.carrierId == key.carrierId
+                    && stats.simSlotIndex == key.simSlotIndex
+                    && stats.rat == key.rat) {
+                return stats;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the IMS registration termination that has the same dimension values with the given
+     * one, or {@code null} if it does not exist.
+     */
+    private @Nullable ImsRegistrationTermination find(ImsRegistrationTermination key) {
+        for (ImsRegistrationTermination termination : mAtoms.imsRegistrationTermination) {
+            if (termination.carrierId == key.carrierId
+                    && termination.isMultiSim == key.isMultiSim
+                    && termination.ratAtEnd == key.ratAtEnd
+                    && termination.setupFailed == key.setupFailed
+                    && termination.reasonCode == key.reasonCode
+                    && termination.extraCode == key.extraCode
+                    && termination.extraMessage.equals(key.extraMessage)) {
+                return termination;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Inserts a new element in a random position in an array with a maximum size, replacing the
+     * least recent item if possible.
+     */
+    private static <T> T[] insertAtRandomPlace(T[] storage, T instance, int maxLength) {
+        final int newLength = storage.length + 1;
+        final boolean arrayFull = (newLength > maxLength);
+        T[] result = Arrays.copyOf(storage, arrayFull ? maxLength : newLength);
+        if (newLength == 1) {
+            result[0] = instance;
+        } else if (arrayFull) {
+            result[findItemToEvict(storage)] = instance;
+        } else {
+            // insert at random place (by moving the item at the random place to the end)
+            int insertAt = sRandom.nextInt(newLength);
+            result[newLength - 1] = result[insertAt];
+            result[insertAt] = instance;
+        }
+        return result;
+    }
+
+    /** Returns index of the item suitable for eviction when the array is full. */
+    private static <T> int findItemToEvict(T[] array) {
+        if (array instanceof CellularServiceState[]) {
+            CellularServiceState[] arr = (CellularServiceState[]) array;
+            return IntStream.range(0, arr.length)
+                    .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j)
+                    .getAsInt();
+        }
+
+        if (array instanceof CellularDataServiceSwitch[]) {
+            CellularDataServiceSwitch[] arr = (CellularDataServiceSwitch[]) array;
+            return IntStream.range(0, arr.length)
+                    .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j)
+                    .getAsInt();
+        }
+
+        if (array instanceof ImsRegistrationStats[]) {
+            ImsRegistrationStats[] arr = (ImsRegistrationStats[]) array;
+            return IntStream.range(0, arr.length)
+                    .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j)
+                    .getAsInt();
+        }
+
+        if (array instanceof ImsRegistrationTermination[]) {
+            ImsRegistrationTermination[] arr = (ImsRegistrationTermination[]) array;
+            return IntStream.range(0, arr.length)
+                    .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j)
+                    .getAsInt();
+        }
+
+        return sRandom.nextInt(array.length);
+    }
+
+    /** Sanitizes the loaded array of atoms to avoid null values. */
+    private <T> T[] sanitizeAtoms(T[] array, Class<T> cl) {
+        return ArrayUtils.emptyIfNull(array, cl);
+    }
+
+    /** Sanitizes the loaded array of atoms loaded to avoid null values and enforce max length. */
+    private <T> T[] sanitizeAtoms(T[] array, Class<T> cl, int maxLength) {
+        array = sanitizeAtoms(array, cl);
+        if (array.length > maxLength) {
+            return Arrays.copyOf(array, maxLength);
+        }
+        return array;
+    }
+
+    /** Sanitizes the timestamp of the last pull loaded from persistent storage. */
+    private long sanitizeTimestamp(long timestamp) {
+        return timestamp <= 0L ? getWallTimeMillis() : timestamp;
+    }
+
     /** Returns an empty PersistAtoms with pull timestamp set to current time. */
     private PersistAtoms makeNewPersistAtoms() {
         PersistAtoms atoms = new PersistAtoms();
         // allow pulling only after some time so data are sufficiently aggregated
-        atoms.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
-        atoms.voiceCallSessionPullTimestampMillis = getWallTimeMillis();
+        long currentTime = getWallTimeMillis();
+        atoms.voiceCallRatUsagePullTimestampMillis = currentTime;
+        atoms.voiceCallSessionPullTimestampMillis = currentTime;
+        atoms.incomingSmsPullTimestampMillis = currentTime;
+        atoms.outgoingSmsPullTimestampMillis = currentTime;
+        atoms.carrierIdTableVersion = TelephonyManager.UNKNOWN_CARRIER_ID_LIST_VERSION;
+        atoms.dataCallSessionPullTimestampMillis = currentTime;
+        atoms.cellularServiceStatePullTimestampMillis = currentTime;
+        atoms.cellularDataServiceSwitchPullTimestampMillis = currentTime;
+        atoms.imsRegistrationStatsPullTimestampMillis = currentTime;
+        atoms.imsRegistrationTerminationPullTimestampMillis = currentTime;
         Rlog.d(TAG, "created new PersistAtoms");
         return atoms;
     }
 
     @VisibleForTesting
     protected long getWallTimeMillis() {
-        // epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP
+        // Epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP
         return System.currentTimeMillis();
     }
 }
diff --git a/src/java/com/android/internal/telephony/metrics/ServiceStateStats.java b/src/java/com/android/internal/telephony/metrics/ServiceStateStats.java
new file mode 100644
index 0000000..9cbc406
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/ServiceStateStats.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.Annotation.NetworkType;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+
+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.CellularDataServiceSwitch;
+import com.android.internal.telephony.nano.PersistAtomsProto.CellularServiceState;
+import com.android.telephony.Rlog;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Tracks service state duration and switch metrics for each phone. */
+public class ServiceStateStats {
+    private static final String TAG = ServiceStateStats.class.getSimpleName();
+
+    private final AtomicReference<TimestampedServiceState> mLastState =
+            new AtomicReference<>(new TimestampedServiceState(null, 0L));
+    private final Phone mPhone;
+    private final PersistAtomsStorage mStorage;
+
+    public ServiceStateStats(Phone phone) {
+        mPhone = phone;
+        mStorage = PhoneFactory.getMetricsCollector().getAtomsStorage();
+    }
+
+    /** Finalizes the durations of the current service state segment. */
+    public void conclude() {
+        final long now = getTimeMillis();
+        TimestampedServiceState lastState =
+                mLastState.getAndUpdate(
+                        state -> new TimestampedServiceState(state.mServiceState, now));
+        addServiceState(lastState, now);
+    }
+
+    /** Updates the current service state. */
+    public void onServiceStateChanged(ServiceState serviceState) {
+        final long now = getTimeMillis();
+        if (isModemOff(serviceState)) {
+            // Finish the duration of last service state and mark modem off
+            addServiceState(mLastState.getAndSet(new TimestampedServiceState(null, now)), now);
+        } else {
+            CellularServiceState newState = new CellularServiceState();
+            newState.voiceRat = getVoiceRat(mPhone, serviceState);
+            newState.dataRat = getDataRat(serviceState);
+            newState.voiceRoamingType = serviceState.getVoiceRoamingType();
+            newState.dataRoamingType = serviceState.getDataRoamingType();
+            newState.isEndc = isEndc(serviceState);
+            newState.simSlotIndex = mPhone.getPhoneId();
+            newState.isMultiSim = SimSlotState.isMultiSim();
+            newState.carrierId = mPhone.getCarrierId();
+
+            TimestampedServiceState prevState =
+                    mLastState.getAndSet(new TimestampedServiceState(newState, now));
+            addServiceStateAndSwitch(
+                    prevState, now, getDataServiceSwitch(prevState.mServiceState, newState));
+        }
+    }
+
+    private void addServiceState(TimestampedServiceState prevState, long now) {
+        addServiceStateAndSwitch(prevState, now, null);
+    }
+
+    private void addServiceStateAndSwitch(
+            TimestampedServiceState prevState,
+            long now,
+            @Nullable CellularDataServiceSwitch serviceSwitch) {
+        if (prevState.mServiceState == null) {
+            // Skip duration when modem is off
+            return;
+        }
+        if (now >= prevState.mTimestamp) {
+            CellularServiceState state = copyOf(prevState.mServiceState);
+            state.totalTimeMillis = now - prevState.mTimestamp;
+            mStorage.addCellularServiceStateAndCellularDataServiceSwitch(state, serviceSwitch);
+        } else {
+            Rlog.e(TAG, "addServiceState: durationMillis<0");
+        }
+    }
+
+    @Nullable
+    private CellularDataServiceSwitch getDataServiceSwitch(
+            @Nullable CellularServiceState prevState, CellularServiceState nextState) {
+        // Record switch only if multi-SIM state and carrier ID are the same and data RAT differs.
+        if (prevState != null
+                && prevState.isMultiSim == nextState.isMultiSim
+                && prevState.carrierId == nextState.carrierId
+                && prevState.dataRat != nextState.dataRat) {
+            CellularDataServiceSwitch serviceSwitch = new CellularDataServiceSwitch();
+            serviceSwitch.ratFrom = prevState.dataRat;
+            serviceSwitch.ratTo = nextState.dataRat;
+            serviceSwitch.isMultiSim = nextState.isMultiSim;
+            serviceSwitch.simSlotIndex = nextState.simSlotIndex;
+            serviceSwitch.carrierId = nextState.carrierId;
+            serviceSwitch.switchCount = 1;
+            return serviceSwitch;
+        } else {
+            return null;
+        }
+    }
+
+    private static CellularServiceState copyOf(CellularServiceState state) {
+        // MessageNano does not support clone, have to copy manually
+        CellularServiceState copy = new CellularServiceState();
+        copy.voiceRat = state.voiceRat;
+        copy.dataRat = state.dataRat;
+        copy.voiceRoamingType = state.voiceRoamingType;
+        copy.dataRoamingType = state.dataRoamingType;
+        copy.isEndc = state.isEndc;
+        copy.simSlotIndex = state.simSlotIndex;
+        copy.isMultiSim = state.isMultiSim;
+        copy.carrierId = state.carrierId;
+        copy.totalTimeMillis = state.totalTimeMillis;
+        return copy;
+    }
+
+    private static boolean isModemOff(ServiceState state) {
+        // NOTE: Wifi calls can be made in airplane mode, where voice reg state is POWER_OFF but
+        // data reg state is IN_SERVICE. In this case, service state should still be tracked.
+        return state.getVoiceRegState() == ServiceState.STATE_POWER_OFF
+                && state.getDataRegState() == ServiceState.STATE_POWER_OFF;
+    }
+
+    private static @NetworkType int getVoiceRat(Phone phone, ServiceState state) {
+        boolean isWifiCall =
+                phone.getImsPhone() != null
+                        && phone.getImsPhone().isWifiCallingEnabled()
+                        && state.getDataNetworkType() == TelephonyManager.NETWORK_TYPE_IWLAN;
+        return isWifiCall ? TelephonyManager.NETWORK_TYPE_IWLAN : state.getVoiceNetworkType();
+    }
+
+    private static @NetworkType int getDataRat(ServiceState state) {
+        final NetworkRegistrationInfo wwanRegInfo =
+                state.getNetworkRegistrationInfo(
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+        return wwanRegInfo != null
+                ? wwanRegInfo.getAccessNetworkTechnology()
+                : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+    }
+
+    private static boolean isEndc(ServiceState state) {
+        if (getDataRat(state) != TelephonyManager.NETWORK_TYPE_LTE) {
+            return false;
+        }
+        int nrState = state.getNrState();
+        return nrState == NetworkRegistrationInfo.NR_STATE_CONNECTED
+                || nrState == NetworkRegistrationInfo.NR_STATE_NOT_RESTRICTED;
+    }
+
+    @VisibleForTesting
+    protected long getTimeMillis() {
+        return SystemClock.elapsedRealtime();
+    }
+
+    private static final class TimestampedServiceState {
+        private final CellularServiceState mServiceState;
+        private final long mTimestamp; // Start time of the service state segment
+
+        TimestampedServiceState(CellularServiceState serviceState, long timestamp) {
+            mServiceState = serviceState;
+            mTimestamp = timestamp;
+        }
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/SimSlotState.java b/src/java/com/android/internal/telephony/metrics/SimSlotState.java
index 3c3ae62..95fa042 100644
--- a/src/java/com/android/internal/telephony/metrics/SimSlotState.java
+++ b/src/java/com/android/internal/telephony/metrics/SimSlotState.java
@@ -20,9 +20,12 @@
 import com.android.internal.telephony.uicc.UiccCard;
 import com.android.internal.telephony.uicc.UiccController;
 import com.android.internal.telephony.uicc.UiccSlot;
+import com.android.telephony.Rlog;
 
 /** Snapshots and stores the current SIM state. */
 public class SimSlotState {
+    private static final String TAG = SimSlotState.class.getSimpleName();
+
     public final int numActiveSlots;
     public final int numActiveSims;
     public final int numActiveEsims;
@@ -62,4 +65,21 @@
         this.numActiveSims = numActiveSims;
         this.numActiveEsims = numActiveEsims;
     }
+
+    /** Returns whether the given phone is using a eSIM. */
+    public static boolean isEsim(int phoneId) {
+        UiccSlot slot = UiccController.getInstance().getUiccSlotForPhone(phoneId);
+        if (slot != null) {
+            return slot.isEuicc();
+        } else {
+            // should not happen, but assume we are not using eSIM
+            Rlog.e(TAG, "isEsim: slot=null for phone " + phoneId);
+            return false;
+        }
+    }
+
+    /** Returns whether the device has multiple active SIM profiles. */
+    public static boolean isMultiSim() {
+        return (getCurrentState().numActiveSims > 1);
+    }
 }
diff --git a/src/java/com/android/internal/telephony/metrics/SmsStats.java b/src/java/com/android/internal/telephony/metrics/SmsStats.java
new file mode 100644
index 0000000..44182c8
--- /dev/null
+++ b/src/java/com/android/internal/telephony/metrics/SmsStats.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static com.android.internal.telephony.InboundSmsHandler.SOURCE_INJECTED_FROM_IMS;
+import static com.android.internal.telephony.InboundSmsHandler.SOURCE_INJECTED_FROM_UNKNOWN;
+import static com.android.internal.telephony.InboundSmsHandler.SOURCE_NOT_INJECTED;
+import static com.android.internal.telephony.SmsResponse.NO_ERROR_CODE;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__ERROR__SMS_ERROR_GENERIC;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__ERROR__SMS_ERROR_NOT_SUPPORTED;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__ERROR__SMS_ERROR_NO_MEMORY;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__ERROR__SMS_SUCCESS;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_FORMAT__SMS_FORMAT_3GPP;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_FORMAT__SMS_FORMAT_3GPP2;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TECH__SMS_TECH_CS_3GPP;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TECH__SMS_TECH_CS_3GPP2;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TECH__SMS_TECH_IMS;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TECH__SMS_TECH_UNKNOWN;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TYPE__SMS_TYPE_NORMAL;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TYPE__SMS_TYPE_SMS_PP;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TYPE__SMS_TYPE_VOICEMAIL_INDICATION;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TYPE__SMS_TYPE_WAP_PUSH;
+import static com.android.internal.telephony.TelephonyStatsLog.INCOMING_SMS__SMS_TYPE__SMS_TYPE_ZERO;
+import static com.android.internal.telephony.TelephonyStatsLog.OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR;
+import static com.android.internal.telephony.TelephonyStatsLog.OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_FALLBACK;
+import static com.android.internal.telephony.TelephonyStatsLog.OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_RETRY;
+import static com.android.internal.telephony.TelephonyStatsLog.OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_SUCCESS;
+import static com.android.internal.telephony.TelephonyStatsLog.OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_UNKNOWN;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Annotation.NetworkType;
+import android.telephony.ServiceState;
+import android.telephony.SmsManager;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.telephony.ims.stub.ImsSmsImplBase;
+import android.telephony.ims.stub.ImsSmsImplBase.SendStatusResult;
+
+import com.android.internal.telephony.InboundSmsHandler;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.ServiceStateTracker;
+import com.android.internal.telephony.nano.PersistAtomsProto.IncomingSms;
+import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms;
+import com.android.telephony.Rlog;
+
+import java.util.Random;
+
+/** Collects voice call events per phone ID for the pulled atom. */
+public class SmsStats {
+    private static final String TAG = SmsStats.class.getSimpleName();
+
+    /** 3GPP error for out of service: "no network service" in TS 27.005 cl 3.2.5 */
+    private static final int NO_NETWORK_ERROR_3GPP = 331;
+
+    /** 3GPP2 error for out of service: "Other radio interface problem" in N.S0005 Table 171 */
+    private static final int NO_NETWORK_ERROR_3GPP2 = 66;
+
+    private final Phone mPhone;
+
+    private final PersistAtomsStorage mAtomsStorage =
+            PhoneFactory.getMetricsCollector().getAtomsStorage();
+
+    private static final Random RANDOM = new Random();
+
+    public SmsStats(Phone phone) {
+        mPhone = phone;
+    }
+
+    /** Create a new atom when multi-part incoming SMS is dropped due to missing parts. */
+    public void onDroppedIncomingMultipartSms(boolean is3gpp2, int receivedCount, int totalCount) {
+        IncomingSms proto = getIncomingDefaultProto(is3gpp2, SOURCE_NOT_INJECTED);
+        // Keep SMS tech as unknown because it's possible that it changed overtime and is not
+        // necessarily the current one. Similarly mark the RAT as unknown.
+        proto.smsTech = INCOMING_SMS__SMS_TECH__SMS_TECH_UNKNOWN;
+        proto.rat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        proto.error = INCOMING_SMS__ERROR__SMS_ERROR_GENERIC;
+        proto.totalParts = totalCount;
+        proto.receivedParts = receivedCount;
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an SMS for the voicemail indicator is received. */
+    public void onIncomingSmsVoicemail(boolean is3gpp2,
+            @InboundSmsHandler.SmsSource int smsSource) {
+        IncomingSms proto = getIncomingDefaultProto(is3gpp2, smsSource);
+        proto.smsType = INCOMING_SMS__SMS_TYPE__SMS_TYPE_VOICEMAIL_INDICATION;
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an SMS of type zero is received. */
+    public void onIncomingSmsTypeZero(@InboundSmsHandler.SmsSource int smsSource) {
+        IncomingSms proto = getIncomingDefaultProto(false /* is3gpp2 */, smsSource);
+        proto.smsType = INCOMING_SMS__SMS_TYPE__SMS_TYPE_ZERO;
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an SMS-PP for the SIM card is received. */
+    public void onIncomingSmsPP(@InboundSmsHandler.SmsSource int smsSource, boolean success) {
+        IncomingSms proto = getIncomingDefaultProto(false /* is3gpp2 */, smsSource);
+        proto.smsType = INCOMING_SMS__SMS_TYPE__SMS_TYPE_SMS_PP;
+        proto.error = getIncomingSmsError(success);
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an SMS is received successfully. */
+    public void onIncomingSmsSuccess(boolean is3gpp2,
+            @InboundSmsHandler.SmsSource int smsSource, int messageCount,
+            boolean blocked, long messageId) {
+        IncomingSms proto = getIncomingDefaultProto(is3gpp2, smsSource);
+        proto.totalParts = messageCount;
+        proto.receivedParts = messageCount;
+        proto.blocked = blocked;
+        proto.messageId = messageId;
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an incoming SMS has an error. */
+    public void onIncomingSmsError(boolean is3gpp2,
+            @InboundSmsHandler.SmsSource int smsSource, int result) {
+        IncomingSms proto = getIncomingDefaultProto(is3gpp2, smsSource);
+        proto.error = getIncomingSmsError(result);
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an incoming WAP_PUSH SMS is received. */
+    public void onIncomingSmsWapPush(@InboundSmsHandler.SmsSource int smsSource,
+            int messageCount, int result, long messageId) {
+        IncomingSms proto = getIncomingDefaultProto(false, smsSource);
+        proto.smsType = INCOMING_SMS__SMS_TYPE__SMS_TYPE_WAP_PUSH;
+        proto.totalParts = messageCount;
+        proto.receivedParts = messageCount;
+        proto.error = getIncomingSmsError(result);
+        proto.messageId = messageId;
+        mAtomsStorage.addIncomingSms(proto);
+    }
+
+    /** Create a new atom when an outgoing SMS is sent. */
+    public void onOutgoingSms(boolean isOverIms, boolean is3gpp2, boolean fallbackToCs,
+            @SmsManager.Result int errorCode, long messageId, boolean isFromDefaultApp) {
+        onOutgoingSms(isOverIms, is3gpp2, fallbackToCs, errorCode, NO_ERROR_CODE,
+                messageId, isFromDefaultApp);
+    }
+
+    /** Create a new atom when an outgoing SMS is sent. */
+    public void onOutgoingSms(boolean isOverIms, boolean is3gpp2, boolean fallbackToCs,
+            @SmsManager.Result int errorCode, int radioSpecificErrorCode, long messageId,
+            boolean isFromDefaultApp) {
+        OutgoingSms proto =
+                getOutgoingDefaultProto(is3gpp2, isOverIms, messageId, isFromDefaultApp);
+
+        if (isOverIms) {
+            // Populate error code and result for IMS case
+            proto.errorCode = errorCode;
+            if (fallbackToCs) {
+                proto.sendResult = OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_FALLBACK;
+            } else if (errorCode == SmsManager.RESULT_RIL_SMS_SEND_FAIL_RETRY) {
+                proto.sendResult = OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_RETRY;
+            } else if (errorCode != SmsManager.RESULT_ERROR_NONE) {
+                proto.sendResult = OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR;
+            }
+        } else {
+            // Populate error code and result for CS case
+            if (errorCode == SmsManager.RESULT_RIL_SMS_SEND_FAIL_RETRY) {
+                proto.sendResult = OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_RETRY;
+            } else if (errorCode != SmsManager.RESULT_ERROR_NONE) {
+                proto.sendResult = OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR;
+            }
+            proto.errorCode = radioSpecificErrorCode;
+            if (errorCode == SmsManager.RESULT_RIL_RADIO_NOT_AVAILABLE
+                    && radioSpecificErrorCode == NO_ERROR_CODE) {
+                proto.errorCode = is3gpp2 ? NO_NETWORK_ERROR_3GPP2 : NO_NETWORK_ERROR_3GPP;
+            }
+        }
+        mAtomsStorage.addOutgoingSms(proto);
+    }
+
+    /** Creates a proto for a normal single-part {@code IncomingSms} with default values. */
+    private IncomingSms getIncomingDefaultProto(boolean is3gpp2,
+            @InboundSmsHandler.SmsSource int smsSource) {
+        IncomingSms proto = new IncomingSms();
+        proto.smsFormat = getSmsFormat(is3gpp2);
+        proto.smsTech = getSmsTech(smsSource, is3gpp2);
+        proto.rat = getRat(smsSource);
+        proto.smsType = INCOMING_SMS__SMS_TYPE__SMS_TYPE_NORMAL;
+        proto.totalParts = 1;
+        proto.receivedParts = 1;
+        proto.blocked = false;
+        proto.error = INCOMING_SMS__ERROR__SMS_SUCCESS;
+        proto.isRoaming = getIsRoaming();
+        proto.simSlotIndex = getPhoneId();
+        proto.isMultiSim = SimSlotState.isMultiSim();
+        proto.isEsim = SimSlotState.isEsim(getPhoneId());
+        proto.carrierId = getCarrierId();
+        // Message ID is initialized with random number, as it is not available for all incoming
+        // SMS messages (e.g. those handled by OS or error cases).
+        proto.messageId = RANDOM.nextLong();
+        return proto;
+    }
+
+    /** Create a proto for a normal {@code OutgoingSms} with default values. */
+    private OutgoingSms getOutgoingDefaultProto(boolean is3gpp2, boolean isOverIms,
+            long messageId, boolean isFromDefaultApp) {
+        OutgoingSms proto = new OutgoingSms();
+        proto.smsFormat = getSmsFormat(is3gpp2);
+        proto.smsTech = getSmsTech(isOverIms, is3gpp2);
+        proto.rat = getRat(isOverIms);
+        proto.sendResult = OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_SUCCESS;
+        proto.errorCode = isOverIms ? SmsManager.RESULT_ERROR_NONE : NO_ERROR_CODE;
+        proto.isRoaming = getIsRoaming();
+        proto.isFromDefaultApp = isFromDefaultApp;
+        proto.simSlotIndex = getPhoneId();
+        proto.isMultiSim = SimSlotState.isMultiSim();
+        proto.isEsim = SimSlotState.isEsim(getPhoneId());
+        proto.carrierId = getCarrierId();
+        // If the message ID is invalid, generate a random value
+        proto.messageId = messageId != 0L ? messageId : RANDOM.nextLong();
+        // Setting the retry ID to zero. If needed, it will be incremented when the atom is added
+        // in the persistent storage.
+        proto.retryId = 0;
+        return proto;
+    }
+
+    private static int getSmsFormat(boolean is3gpp2) {
+        if (is3gpp2) {
+            return INCOMING_SMS__SMS_FORMAT__SMS_FORMAT_3GPP2;
+        } else {
+            return INCOMING_SMS__SMS_FORMAT__SMS_FORMAT_3GPP;
+        }
+    }
+
+    private int getSmsTech(@InboundSmsHandler.SmsSource int smsSource, boolean is3gpp2) {
+        if (smsSource == SOURCE_INJECTED_FROM_UNKNOWN) {
+            return INCOMING_SMS__SMS_TECH__SMS_TECH_UNKNOWN;
+        }
+        return getSmsTech(smsSource == SOURCE_INJECTED_FROM_IMS, is3gpp2);
+    }
+
+    private int getSmsTech(boolean isOverIms, boolean is3gpp2) {
+        if (isOverIms) {
+            return INCOMING_SMS__SMS_TECH__SMS_TECH_IMS;
+        } else if (is3gpp2) {
+            return INCOMING_SMS__SMS_TECH__SMS_TECH_CS_3GPP2;
+        } else {
+            return INCOMING_SMS__SMS_TECH__SMS_TECH_CS_3GPP;
+        }
+    }
+
+    private static int getIncomingSmsError(int result) {
+        switch (result) {
+            case Activity.RESULT_OK:
+            case Intents.RESULT_SMS_HANDLED:
+                return INCOMING_SMS__ERROR__SMS_SUCCESS;
+            case Intents.RESULT_SMS_OUT_OF_MEMORY:
+                return INCOMING_SMS__ERROR__SMS_ERROR_NO_MEMORY;
+            case Intents.RESULT_SMS_UNSUPPORTED:
+                return INCOMING_SMS__ERROR__SMS_ERROR_NOT_SUPPORTED;
+            case Intents.RESULT_SMS_GENERIC_ERROR:
+            default:
+                return INCOMING_SMS__ERROR__SMS_ERROR_GENERIC;
+        }
+    }
+
+    private static int getIncomingSmsError(boolean success) {
+        if (success) {
+            return INCOMING_SMS__ERROR__SMS_SUCCESS;
+        } else {
+            return INCOMING_SMS__ERROR__SMS_ERROR_GENERIC;
+        }
+    }
+
+    private static int getOutgoingSmsError(@SendStatusResult int imsSendResult) {
+        switch (imsSendResult) {
+            case ImsSmsImplBase.SEND_STATUS_OK:
+                return OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_SUCCESS;
+            case ImsSmsImplBase.SEND_STATUS_ERROR:
+                return OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR;
+            case ImsSmsImplBase.SEND_STATUS_ERROR_RETRY:
+                return OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_RETRY;
+            case ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK:
+                return OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_ERROR_FALLBACK;
+            default:
+                return OUTGOING_SMS__SEND_RESULT__SMS_SEND_RESULT_UNKNOWN;
+        }
+    }
+
+    private int getPhoneId() {
+        Phone phone = mPhone;
+        if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
+            phone = mPhone.getDefaultPhone();
+        }
+        return phone.getPhoneId();
+    }
+
+    @Nullable
+    private ServiceState getServiceState() {
+        Phone phone = mPhone;
+        if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
+            phone = mPhone.getDefaultPhone();
+        }
+        ServiceStateTracker serviceStateTracker = phone.getServiceStateTracker();
+        return serviceStateTracker != null ? serviceStateTracker.getServiceState() : null;
+    }
+
+    private @NetworkType int getRat(@InboundSmsHandler.SmsSource int smsSource) {
+        if (smsSource == SOURCE_INJECTED_FROM_UNKNOWN) {
+            return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        }
+        return getRat(smsSource == SOURCE_INJECTED_FROM_IMS);
+    }
+
+    private @NetworkType int getRat(boolean isOverIms) {
+        if (isOverIms) {
+            if (mPhone.getImsRegistrationTech()
+                    == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN) {
+                return TelephonyManager.NETWORK_TYPE_IWLAN;
+            }
+        }
+        // TODO(b/168837897): Returns the RAT at the time the SMS was received..
+        ServiceState serviceState = getServiceState();
+        return serviceState != null
+                ? serviceState.getVoiceNetworkType() : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+    }
+
+    private boolean getIsRoaming() {
+        ServiceState serviceState = getServiceState();
+        return serviceState != null ? serviceState.getRoaming() : false;
+    }
+
+    private int getCarrierId() {
+        Phone phone = mPhone;
+        if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
+            phone = mPhone.getDefaultPhone();
+        }
+        return phone.getCarrierId();
+    }
+
+    private void loge(String format, Object... args) {
+        Rlog.e(TAG, "[" + mPhone.getPhoneId() + "]" + String.format(format, args));
+    }
+}
diff --git a/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java b/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
index fa75bc8..14ab36c 100644
--- a/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -18,6 +18,7 @@
 
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
+import static com.android.internal.telephony.InboundSmsHandler.SOURCE_NOT_INJECTED;
 import static com.android.internal.telephony.RILConstants.RIL_REQUEST_ANSWER;
 import static com.android.internal.telephony.RILConstants.RIL_REQUEST_CDMA_SEND_SMS;
 import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DEACTIVATE_DATA_CALL;
@@ -72,6 +73,7 @@
 import com.android.internal.telephony.CarrierResolver;
 import com.android.internal.telephony.DriverCall;
 import com.android.internal.telephony.GsmCdmaConnection;
+import com.android.internal.telephony.InboundSmsHandler;
 import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.RIL;
 import com.android.internal.telephony.RILConstants;
@@ -2407,18 +2409,18 @@
      *
      * @param phoneId Phone id
      * @param type Type of the SMS.
-     * @param smsOverIms true if the SMS was received over SMS, false otherwise
+     * @param smsSource the source of the SMS message
      * @param format SMS format. Either 3GPP or 3GPP2.
      * @param timestamps array with timestamps of each incoming SMS part. It contains a single
      * @param blocked indicates if the message was blocked or not.
      * @param success Indicates if the SMS-PP was successfully delivered to the USIM.
      * @param messageId Unique id for this message.
      */
-    private void writeIncomingSmsSessionWithType(int phoneId, int type, boolean smsOverIms,
-            String format, long[] timestamps, boolean blocked, boolean success,
-            long messageId) {
+    private void writeIncomingSmsSessionWithType(int phoneId, int type,
+            @InboundSmsHandler.SmsSource int smsSource, String format, long[] timestamps,
+            boolean blocked, boolean success, long messageId) {
         logv("Logged SMS session consisting of " + timestamps.length
-                + " parts, over IMS = " + smsOverIms
+                + " parts, source = " + smsSource
                 + " blocked = " + blocked
                 + " type = " + type
                 + " messageId = " + messageId);
@@ -2428,8 +2430,9 @@
             SmsSessionEventBuilder eventBuilder =
                     new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_RECEIVED)
                         .setFormat(convertSmsFormat(format))
-                        .setTech(smsOverIms ? SmsSession.Event.Tech.SMS_IMS :
-                            SmsSession.Event.Tech.SMS_GSM)
+                        .setTech(smsSource != SOURCE_NOT_INJECTED
+                            ? SmsSession.Event.Tech.SMS_IMS
+                            : SmsSession.Event.Tech.SMS_GSM)
                         .setErrorCode(success ? SmsManager.RESULT_ERROR_NONE :
                             SmsManager.RESULT_ERROR_GENERIC_FAILURE)
                         .setSmsType(type)
@@ -2444,42 +2447,43 @@
      * Write an incoming WAP-PUSH message.
      *
      * @param phoneId Phone id
-     * @param smsOverIms true if the SMS was received over SMS, false otherwise
+     * @param smsSource the source of the SMS message
      * @param format SMS format. Either 3GPP or 3GPP2.
      * @param timestamps array with timestamps of each incoming SMS part. It contains a single
      * @param success Indicates if the SMS-PP was successfully delivered to the USIM.
      * @param messageId Unique id for this message.
      */
-    public void writeIncomingWapPush(int phoneId, boolean smsOverIms, String format,
-            long[] timestamps, boolean success, long messageId) {
+    public void writeIncomingWapPush(int phoneId, @InboundSmsHandler.SmsSource int smsSource,
+            String format, long[] timestamps, boolean success, long messageId) {
         writeIncomingSmsSessionWithType(phoneId, SmsSession.Event.SmsType.SMS_TYPE_WAP_PUSH,
-                smsOverIms, format, timestamps, false, success, messageId);
+                smsSource, format, timestamps, false, success, messageId);
     }
 
     /**
      * Write a successful incoming SMS session
      *
      * @param phoneId Phone id
-     * @param smsOverIms true if the SMS was received over SMS, false otherwise
+     * @param smsSource the source of the SMS message
      * @param format SMS format. Either 3GPP or 3GPP2.
      * @param timestamps array with timestamps of each incoming SMS part. It contains a single
      * @param blocked indicates if the message was blocked or not.
      * @param messageId Unique id for this message.
      */
-    public void writeIncomingSmsSession(int phoneId, boolean smsOverIms, String format,
-            long[] timestamps, boolean blocked, long messageId) {
+    public void writeIncomingSmsSession(int phoneId, @InboundSmsHandler.SmsSource int smsSource,
+            String format, long[] timestamps, boolean blocked, long messageId) {
         writeIncomingSmsSessionWithType(phoneId, SmsSession.Event.SmsType.SMS_TYPE_NORMAL,
-                smsOverIms, format, timestamps, blocked, true, messageId);
+                smsSource, format, timestamps, blocked, true, messageId);
     }
 
     /**
      * Write an error incoming SMS
      *
      * @param phoneId Phone id
-     * @param smsOverIms true if the SMS was received over SMS, false otherwise
+     * @param smsSource the source of the SMS message
      * @param result Indicates the reason of the failure.
      */
-    public void writeIncomingSmsError(int phoneId, boolean smsOverIms, int result) {
+    public void writeIncomingSmsError(int phoneId, @InboundSmsHandler.SmsSource int smsSource,
+            int result) {
         logv("Incoming SMS error = " + result);
 
         int smsError = SmsManager.RESULT_ERROR_GENERIC_FAILURE;
@@ -2504,8 +2508,9 @@
         SmsSessionEventBuilder eventBuilder =
                 new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_RECEIVED)
                     .setErrorCode(smsError)
-                    .setTech(smsOverIms ? SmsSession.Event.Tech.SMS_IMS :
-                        SmsSession.Event.Tech.SMS_GSM);
+                    .setTech(smsSource != SOURCE_NOT_INJECTED
+                        ? SmsSession.Event.Tech.SMS_IMS
+                        : SmsSession.Event.Tech.SMS_GSM);
         smsSession.addEvent(eventBuilder);
         finishSmsSession(smsSession);
     }
diff --git a/src/java/com/android/internal/telephony/metrics/VoiceCallRatTracker.java b/src/java/com/android/internal/telephony/metrics/VoiceCallRatTracker.java
index 7d7cd0e..5183ea4 100644
--- a/src/java/com/android/internal/telephony/metrics/VoiceCallRatTracker.java
+++ b/src/java/com/android/internal/telephony/metrics/VoiceCallRatTracker.java
@@ -16,7 +16,7 @@
 
 package com.android.internal.telephony.metrics;
 
-import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
+import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.telephony.Rlog;
 
 import java.util.Arrays;
@@ -52,7 +52,7 @@
     }
 
     /** Creates an RAT tracker from saved atoms at startup. */
-    public static VoiceCallRatTracker fromProto(RawVoiceCallRatUsage[] usages) {
+    public static VoiceCallRatTracker fromProto(VoiceCallRatUsage[] usages) {
         VoiceCallRatTracker tracker = new VoiceCallRatTracker();
         if (usages == null) {
             Rlog.e(TAG, "fromProto: usages=null");
@@ -63,10 +63,10 @@
     }
 
     /** Append the map to javanano persist atoms. */
-    public RawVoiceCallRatUsage[] toProto() {
+    public VoiceCallRatUsage[] toProto() {
         return mRatUsageMap.entrySet().stream()
                 .map(VoiceCallRatTracker::entryToProto)
-                .toArray(RawVoiceCallRatUsage[]::new);
+                .toArray(VoiceCallRatUsage[]::new);
     }
 
     /** Resets the tracker. */
@@ -140,14 +140,14 @@
         }
     }
 
-    private void addProto(RawVoiceCallRatUsage usage) {
+    private void addProto(VoiceCallRatUsage usage) {
         mRatUsageMap.put(Key.fromProto(usage), Value.fromProto(usage));
     }
 
-    private static RawVoiceCallRatUsage entryToProto(Map.Entry<Key, Value> entry) {
+    private static VoiceCallRatUsage entryToProto(Map.Entry<Key, Value> entry) {
         Key key = entry.getKey();
         Value value = entry.getValue();
-        RawVoiceCallRatUsage usage = new RawVoiceCallRatUsage();
+        VoiceCallRatUsage usage = new VoiceCallRatUsage();
         usage.carrierId = key.carrierId;
         usage.rat = key.rat;
         if (value.mConnectionIds != null) {
@@ -172,7 +172,7 @@
             this.rat = rat;
         }
 
-        static Key fromProto(RawVoiceCallRatUsage usage) {
+        static Key fromProto(VoiceCallRatUsage usage) {
             return new Key(usage.carrierId, usage.rat);
         }
 
@@ -226,7 +226,7 @@
             }
         }
 
-        static Value fromProto(RawVoiceCallRatUsage usage) {
+        static Value fromProto(VoiceCallRatUsage usage) {
             Value value = new Value(usage.totalDurationMillis, usage.callCount);
             return value;
         }
diff --git a/src/java/com/android/internal/telephony/metrics/VoiceCallSessionStats.java b/src/java/com/android/internal/telephony/metrics/VoiceCallSessionStats.java
index 927a67e..6b6da10 100644
--- a/src/java/com/android/internal/telephony/metrics/VoiceCallSessionStats.java
+++ b/src/java/com/android/internal/telephony/metrics/VoiceCallSessionStats.java
@@ -21,6 +21,11 @@
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_UNKNOWN;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MO;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MT;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_FULLBAND;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_WIDEBAND;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_SLOW;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_FAST;
@@ -31,18 +36,26 @@
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_FAST;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_SLOW;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_GREAT;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
 
 import android.annotation.Nullable;
+import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
 import android.os.SystemClock;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.VideoState;
+import android.telephony.AccessNetworkUtils;
 import android.telephony.Annotation.NetworkType;
 import android.telephony.DisconnectCause;
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsStreamMediaProfile;
+import android.util.LongSparseArray;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.SparseLongArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.Call;
@@ -57,10 +70,10 @@
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
 import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.AudioCodec;
 import com.android.internal.telephony.uicc.UiccController;
-import com.android.internal.telephony.uicc.UiccSlot;
 import com.android.telephony.Rlog;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -70,28 +83,36 @@
 public class VoiceCallSessionStats {
     private static final String TAG = VoiceCallSessionStats.class.getSimpleName();
 
-    /** Bitmask value of unknown audio codecs. */
-    private static final long AUDIO_CODEC_UNKNOWN = 1L << AudioCodec.AUDIO_CODEC_UNKNOWN;
-
     /** Upper bounds of each call setup duration category in milliseconds. */
     private static final int CALL_SETUP_DURATION_UNKNOWN = 0;
-    private static final int CALL_SETUP_DURATION_EXTREMELY_FAST = 60;
-    private static final int CALL_SETUP_DURATION_ULTRA_FAST = 100;
-    private static final int CALL_SETUP_DURATION_VERY_FAST = 300;
-    private static final int CALL_SETUP_DURATION_FAST = 600;
-    private static final int CALL_SETUP_DURATION_NORMAL = 1000;
-    private static final int CALL_SETUP_DURATION_SLOW = 3000;
+    private static final int CALL_SETUP_DURATION_EXTREMELY_FAST = 400;
+    private static final int CALL_SETUP_DURATION_ULTRA_FAST = 700;
+    private static final int CALL_SETUP_DURATION_VERY_FAST = 1000;
+    private static final int CALL_SETUP_DURATION_FAST = 1500;
+    private static final int CALL_SETUP_DURATION_NORMAL = 2500;
+    private static final int CALL_SETUP_DURATION_SLOW = 4000;
     private static final int CALL_SETUP_DURATION_VERY_SLOW = 6000;
     private static final int CALL_SETUP_DURATION_ULTRA_SLOW = 10000;
     // CALL_SETUP_DURATION_EXTREMELY_SLOW has no upper bound (it includes everything above 10000)
 
-    /** Holds the audio codec bitmask value for CS calls. */
-    private static final SparseLongArray CS_CODEC_MAP = buildGsmCdmaCodecMap();
+    /** Number of buckets for codec quality, from UNKNOWN to FULLBAND. */
+    private static final int CODEC_QUALITY_COUNT = 5;
 
-    /** Holds the audio codec bitmask value for IMS calls. */
-    private static final SparseLongArray IMS_CODEC_MAP = buildImsCodecMap();
+    /**
+     * Threshold to calculate the main audio codec quality of the call.
+     *
+     * The audio codec quality was equal to or greater than the main audio codec quality for
+     * at least 70% of the call.
+     */
+    private static final int MAIN_CODEC_QUALITY_THRESHOLD = 70;
 
-    /** Holds setup duration buckets with keys as their lower bounds in milliseconds. */
+    /** Holds the audio codec value for CS calls. */
+    private static final SparseIntArray CS_CODEC_MAP = buildGsmCdmaCodecMap();
+
+    /** Holds the audio codec value for IMS calls. */
+    private static final SparseIntArray IMS_CODEC_MAP = buildImsCodecMap();
+
+    /** Holds setup duration buckets with values as their upper bounds in milliseconds. */
     private static final SparseIntArray CALL_SETUP_DURATION_MAP = buildCallSetupDurationMap();
 
     /**
@@ -101,6 +122,13 @@
     private final SparseArray<VoiceCallSession> mCallProtos = new SparseArray<>();
 
     /**
+     * Tracks usage of codecs for each call. The outer array is used to map each connection id to
+     * the corresponding codec usage. The inner array is used to map timestamp (key) with the
+     * codec in use (value).
+     */
+    private final SparseArray<LongSparseArray<Integer>> mCodecUsage = new SparseArray<>();
+
+    /**
      * Tracks call RAT usage.
      *
      * <p>RAT usage is mainly tied to phones rather than calls, since each phone can have multiple
@@ -224,12 +252,51 @@
 
     /** Updates internal states when audio codec for a call is changed. */
     public synchronized void onAudioCodecChanged(Connection conn, int audioQuality) {
-        VoiceCallSession proto = mCallProtos.get(getConnectionId(conn));
+        int id = getConnectionId(conn);
+        VoiceCallSession proto = mCallProtos.get(id);
         if (proto == null) {
             loge("onAudioCodecChanged: untracked connection");
             return;
         }
-        proto.codecBitmask |= audioQualityToCodecBitmask(proto.bearerAtEnd, audioQuality);
+        int codec = audioQualityToCodec(proto.bearerAtEnd, audioQuality);
+        proto.codecBitmask |= (1L << codec);
+
+        if (mCodecUsage.contains(id)) {
+            mCodecUsage.get(id).append(getTimeMillis(), codec);
+        } else {
+            LongSparseArray<Integer> arr = new LongSparseArray<>();
+            arr.append(getTimeMillis(), codec);
+            mCodecUsage.put(id, arr);
+        }
+    }
+
+    /** Updates internal states when video state changes. */
+    public synchronized void onVideoStateChange(
+            ImsPhoneConnection conn, @VideoState int videoState) {
+        int id = getConnectionId(conn);
+        VoiceCallSession proto = mCallProtos.get(id);
+        if (proto == null) {
+            loge("onVideoStateChange: untracked connection");
+            return;
+        }
+        logd(TAG, "Video state = " + videoState);
+        if (videoState != VideoProfile.STATE_AUDIO_ONLY) {
+            proto.videoEnabled = true;
+        }
+    }
+
+    /** Updates internal states when multiparty state changes. */
+    public synchronized void onMultipartyChange(ImsPhoneConnection conn, boolean isMultiParty) {
+        int id = getConnectionId(conn);
+        VoiceCallSession proto = mCallProtos.get(id);
+        if (proto == null) {
+            loge("onMultipartyChange: untracked connection");
+            return;
+        }
+        logd(TAG, "Multiparty = " + isMultiParty);
+        if (isMultiParty) {
+            proto.isMultiparty = true;
+        }
     }
 
     /**
@@ -316,7 +383,7 @@
         } else {
             int bearer = getBearer(conn);
             ServiceState serviceState = getServiceState();
-            int rat = getRat(serviceState);
+            @NetworkType int rat = getRat(serviceState);
 
             VoiceCallSession proto = new VoiceCallSession();
 
@@ -329,12 +396,13 @@
             proto.disconnectExtraCode = conn.getPreciseDisconnectCause();
             proto.disconnectExtraMessage = conn.getVendorDisconnectCause();
             proto.ratAtStart = rat;
+            proto.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
             proto.ratAtEnd = rat;
             proto.ratSwitchCount = 0L;
             proto.codecBitmask = 0L;
-            proto.simSlotIndex = getSimSlotId();
-            proto.isMultiSim = SimSlotState.getCurrentState().numActiveSims > 1;
-            proto.isEsim = isEsim();
+            proto.simSlotIndex = mPhoneId;
+            proto.isMultiSim = SimSlotState.isMultiSim();
+            proto.isEsim = SimSlotState.isEsim(mPhoneId);
             proto.carrierId = mPhone.getCarrierId();
             proto.srvccCompleted = false;
             proto.srvccFailureCount = 0L;
@@ -342,6 +410,7 @@
             proto.rttEnabled = false;
             proto.isEmergency = conn.isEmergencyCall();
             proto.isRoaming = serviceState != null ? serviceState.getVoiceRoaming() : false;
+            proto.isMultiparty = conn.isMultiparty();
 
             // internal fields for tracking
             proto.setupBeginMillis = getTimeMillis();
@@ -364,6 +433,12 @@
         mCallProtos.delete(connectionId);
         proto.concurrentCallCountAtEnd = mCallProtos.size();
 
+        // Calculate signal strength at the end of the call
+        proto.signalStrengthAtEnd = getSignalStrength(proto.ratAtEnd);
+
+        // Calculate main codec quality
+        proto.mainCodecQuality = finalizeMainCodecQuality(connectionId);
+
         // ensure internal fields are cleared
         proto.setupBeginMillis = 0L;
 
@@ -421,17 +496,27 @@
 
     private void checkCallSetup(Connection conn, VoiceCallSession proto) {
         if (proto.setupBeginMillis != 0L && isSetupFinished(conn.getCall())) {
-            proto.setupDuration = classifySetupDuration(getTimeMillis() - proto.setupBeginMillis);
+            proto.setupDurationMillis = (int) (getTimeMillis() - proto.setupBeginMillis);
+            proto.setupDuration = classifySetupDuration(proto.setupDurationMillis);
             proto.setupBeginMillis = 0L;
         }
-        // clear setupFailed if call now active, but otherwise leave it unchanged
-        if (conn.getState() == Call.State.ACTIVE) {
+        // Clear setupFailed if call now active, but otherwise leave it unchanged
+        // This block is executed only once, when call becomes active for the first time.
+        if (proto.setupFailed && conn.getState() == Call.State.ACTIVE) {
             proto.setupFailed = false;
+            // Track RAT when voice call is connected.
+            ServiceState serviceState = getServiceState();
+            proto.ratAtConnected = getRat(serviceState);
+            // Reset list of codecs with the last codec at the present time. In this way, we
+            // track codec quality only after call is connected and not while ringing.
+            resetCodecList(conn);
         }
     }
 
     private void updateRatTracker(ServiceState state) {
-        int rat = getRat(state);
+        @NetworkType int rat = getRat(state);
+        int band = getBand(rat, state.getChannelNumber());
+
         mRatUsage.add(mPhone.getCarrierId(), rat, getTimeMillis(), getConnectionIds());
         for (int i = 0; i < mCallProtos.size(); i++) {
             VoiceCallSession proto = mCallProtos.valueAt(i);
@@ -439,6 +524,7 @@
                 proto.ratSwitchCount++;
                 proto.ratAtEnd = rat;
             }
+            proto.bandAtEnd = band;
             // assuming that SIM carrier ID does not change during the call
         }
     }
@@ -452,23 +538,6 @@
         finishCall(id);
     }
 
-    private boolean isEsim() {
-        int slotId = getSimSlotId();
-        UiccSlot slot = mUiccController.getUiccSlot(slotId);
-        if (slot != null) {
-            return slot.isEuicc();
-        } else {
-            // should not happen, but assume we are not using eSIM
-            loge("isEsim: slot %d is null", slotId);
-            return false;
-        }
-    }
-
-    private int getSimSlotId() {
-        // NOTE: UiccController's mapping hasn't be initialized when Phone was created
-        return mUiccController.getSlotIdFromPhoneId(mPhoneId);
-    }
-
     private @Nullable ServiceState getServiceState() {
         ServiceStateTracker tracker = mPhone.getServiceStateTracker();
         return tracker != null ? tracker.getServiceState() : null;
@@ -505,8 +574,146 @@
         return isWifiCall ? TelephonyManager.NETWORK_TYPE_IWLAN : state.getVoiceNetworkType();
     }
 
-    // NOTE: when setup is finished for MO calls, it is not successful yet.
+    /** Returns the band associated with a given rat and channel number. */
+    private int getBand(@NetworkType int rat, int chNumber) {
+        int band;
+        switch (rat) {
+            case TelephonyManager.NETWORK_TYPE_GSM:
+            case TelephonyManager.NETWORK_TYPE_GPRS:
+            case TelephonyManager.NETWORK_TYPE_EDGE:
+                band = AccessNetworkUtils.getOperatingBandForArfcn(chNumber);
+                break;
+            case TelephonyManager.NETWORK_TYPE_UMTS:
+            case TelephonyManager.NETWORK_TYPE_HSDPA:
+            case TelephonyManager.NETWORK_TYPE_HSUPA:
+            case TelephonyManager.NETWORK_TYPE_HSPA:
+            case TelephonyManager.NETWORK_TYPE_HSPAP:
+                band = AccessNetworkUtils.getOperatingBandForUarfcn(chNumber);
+                break;
+            case TelephonyManager.NETWORK_TYPE_LTE:
+            case TelephonyManager.NETWORK_TYPE_LTE_CA:
+                band = AccessNetworkUtils.getOperatingBandForEarfcn(chNumber);
+                break;
+            default:
+                band = 0;
+                break;
+        }
+        return band == AccessNetworkUtils.INVALID_BAND ? 0 : band;
+    }
+
+    /** Returns the signal strength. */
+    private int getSignalStrength(@NetworkType int rat) {
+        if (rat == TelephonyManager.NETWORK_TYPE_IWLAN) {
+            return getSignalStrengthWifi();
+        } else {
+            return getSignalStrengthCellular();
+        }
+    }
+
+    /** Returns the signal strength of WiFi. */
+    private int getSignalStrengthWifi() {
+        WifiManager wifiManager =
+                (WifiManager) mPhone.getContext().getSystemService(Context.WIFI_SERVICE);
+        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+        int result = VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
+        if (wifiInfo != null) {
+            int level = wifiManager.calculateSignalLevel(wifiInfo.getRssi());
+            int max = wifiManager.getMaxSignalLevel();
+            // Scale result into 0 to 4 range.
+            result = VOICE_CALL_SESSION__SIGNAL_STRENGTH_AT_END__SIGNAL_STRENGTH_GREAT
+                    * level / max;
+            logd(TAG, "WiFi level: " + result + " (" + level + "/" + max + ")");
+        }
+        return result;
+    }
+
+    /** Returns the signal strength of cellular RAT. */
+    private int getSignalStrengthCellular() {
+        return mPhone.getSignalStrength().getLevel();
+    }
+
+    /** Resets the list of codecs used for the connection with only the codec currently in use. */
+    private void resetCodecList(Connection conn) {
+        int id = getConnectionId(conn);
+        LongSparseArray<Integer> codecUsage = mCodecUsage.get(id);
+        if (codecUsage != null) {
+            int lastCodec = codecUsage.valueAt(codecUsage.size() - 1);
+            LongSparseArray<Integer> arr = new LongSparseArray<>();
+            arr.append(getTimeMillis(), lastCodec);
+            mCodecUsage.put(id, arr);
+        }
+    }
+
+    /** Returns the main codec quality used during the call. */
+    private int finalizeMainCodecQuality(int connectionId) {
+        // Retrieve information about codec usage for this call and remove it from main array.
+        if (!mCodecUsage.contains(connectionId)) {
+            return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
+        }
+        LongSparseArray<Integer> codecUsage = mCodecUsage.get(connectionId);
+        mCodecUsage.delete(connectionId);
+
+        // Append fake entry at the end, to facilitate the calculation of time for each codec.
+        codecUsage.put(getTimeMillis(), AudioCodec.AUDIO_CODEC_UNKNOWN);
+
+        // Calculate array with time for each quality
+        int totalTime = 0;
+        long[] timePerQuality = new long[CODEC_QUALITY_COUNT];
+        for (int i = 0; i < codecUsage.size() - 1; i++) {
+            long time = codecUsage.keyAt(i + 1) - codecUsage.keyAt(i);
+            int quality = getCodecQuality(codecUsage.valueAt(i));
+            timePerQuality[quality] += time;
+            totalTime += time;
+        }
+        logd(TAG, "Time per codec quality = " + Arrays.toString(timePerQuality));
+
+        // We calculate 70% duration of the call as the threshold for the main audio codec quality
+        // and iterate on all codec qualities. As soon as the sum of codec duration is greater than
+        // the threshold, we have identified the main codec quality.
+        long timeAtMinimumQuality = 0;
+        long timeThreshold = totalTime * MAIN_CODEC_QUALITY_THRESHOLD / 100;
+        for (int i = CODEC_QUALITY_COUNT - 1; i >= 0; i--) {
+            timeAtMinimumQuality += timePerQuality[i];
+            if (timeAtMinimumQuality >= timeThreshold) {
+                return i;
+            }
+        }
+        return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
+    }
+
+    private int getCodecQuality(int codec) {
+        switch(codec) {
+            case AudioCodec.AUDIO_CODEC_AMR:
+            case AudioCodec.AUDIO_CODEC_QCELP13K:
+            case AudioCodec.AUDIO_CODEC_EVRC:
+            case AudioCodec.AUDIO_CODEC_EVRC_B:
+            case AudioCodec.AUDIO_CODEC_EVRC_NW:
+            case AudioCodec.AUDIO_CODEC_GSM_EFR:
+            case AudioCodec.AUDIO_CODEC_GSM_FR:
+            case AudioCodec.AUDIO_CODEC_GSM_HR:
+            case AudioCodec.AUDIO_CODEC_G711U:
+            case AudioCodec.AUDIO_CODEC_G723:
+            case AudioCodec.AUDIO_CODEC_G711A:
+            case AudioCodec.AUDIO_CODEC_G722:
+            case AudioCodec.AUDIO_CODEC_G711AB:
+            case AudioCodec.AUDIO_CODEC_G729:
+            case AudioCodec.AUDIO_CODEC_EVS_NB:
+                return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+            case AudioCodec.AUDIO_CODEC_AMR_WB:
+            case AudioCodec.AUDIO_CODEC_EVS_WB:
+            case AudioCodec.AUDIO_CODEC_EVRC_WB:
+                return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_WIDEBAND;
+            case AudioCodec.AUDIO_CODEC_EVS_SWB:
+                return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
+            case AudioCodec.AUDIO_CODEC_EVS_FB:
+                return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_FULLBAND;
+            default:
+                return VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
+        }
+    }
+
     private static boolean isSetupFinished(@Nullable Call call) {
+        // NOTE: when setup is finished for MO calls, it is not successful yet.
         if (call != null) {
             switch (call.getState()) {
                 case ACTIVE: // MT setup: accepted to ACTIVE
@@ -518,19 +725,19 @@
         return false;
     }
 
-    private static long audioQualityToCodecBitmask(int bearer, int audioQuality) {
+    private static int audioQualityToCodec(int bearer, int audioQuality) {
         switch (bearer) {
             case VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS:
-                return CS_CODEC_MAP.get(audioQuality, AUDIO_CODEC_UNKNOWN);
+                return CS_CODEC_MAP.get(audioQuality, AudioCodec.AUDIO_CODEC_UNKNOWN);
             case VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_IMS:
-                return IMS_CODEC_MAP.get(audioQuality, AUDIO_CODEC_UNKNOWN);
+                return IMS_CODEC_MAP.get(audioQuality, AudioCodec.AUDIO_CODEC_UNKNOWN);
             default:
-                loge("audioQualityToCodecBitmask: unknown bearer %d", bearer);
-                return AUDIO_CODEC_UNKNOWN;
+                loge("audioQualityToCodec: unknown bearer %d", bearer);
+                return AudioCodec.AUDIO_CODEC_UNKNOWN;
         }
     }
 
-    private static int classifySetupDuration(long durationMillis) {
+    private static int classifySetupDuration(int durationMillis) {
         // keys in CALL_SETUP_DURATION_MAP are upper bounds in ascending order
         for (int i = 0; i < CALL_SETUP_DURATION_MAP.size(); i++) {
             if (durationMillis < CALL_SETUP_DURATION_MAP.keyAt(i)) {
@@ -566,48 +773,42 @@
         Rlog.e(TAG, String.format(format, args));
     }
 
-    private static SparseLongArray buildGsmCdmaCodecMap() {
-        SparseLongArray map = new SparseLongArray();
-
-        map.put(DriverCall.AUDIO_QUALITY_AMR, 1L << AudioCodec.AUDIO_CODEC_AMR);
-        map.put(DriverCall.AUDIO_QUALITY_AMR_WB, 1L << AudioCodec.AUDIO_CODEC_AMR_WB);
-        map.put(DriverCall.AUDIO_QUALITY_GSM_EFR, 1L << AudioCodec.AUDIO_CODEC_GSM_EFR);
-        map.put(DriverCall.AUDIO_QUALITY_GSM_FR, 1L << AudioCodec.AUDIO_CODEC_GSM_FR);
-        map.put(DriverCall.AUDIO_QUALITY_GSM_HR, 1L << AudioCodec.AUDIO_CODEC_GSM_HR);
-        map.put(DriverCall.AUDIO_QUALITY_EVRC, 1L << AudioCodec.AUDIO_CODEC_EVRC);
-        map.put(DriverCall.AUDIO_QUALITY_EVRC_B, 1L << AudioCodec.AUDIO_CODEC_EVRC_B);
-        map.put(DriverCall.AUDIO_QUALITY_EVRC_WB, 1L << AudioCodec.AUDIO_CODEC_EVRC_WB);
-        map.put(DriverCall.AUDIO_QUALITY_EVRC_NW, 1L << AudioCodec.AUDIO_CODEC_EVRC_NW);
-
+    private static SparseIntArray buildGsmCdmaCodecMap() {
+        SparseIntArray map = new SparseIntArray();
+        map.put(DriverCall.AUDIO_QUALITY_AMR, AudioCodec.AUDIO_CODEC_AMR);
+        map.put(DriverCall.AUDIO_QUALITY_AMR_WB, AudioCodec.AUDIO_CODEC_AMR_WB);
+        map.put(DriverCall.AUDIO_QUALITY_GSM_EFR, AudioCodec.AUDIO_CODEC_GSM_EFR);
+        map.put(DriverCall.AUDIO_QUALITY_GSM_FR, AudioCodec.AUDIO_CODEC_GSM_FR);
+        map.put(DriverCall.AUDIO_QUALITY_GSM_HR, AudioCodec.AUDIO_CODEC_GSM_HR);
+        map.put(DriverCall.AUDIO_QUALITY_EVRC, AudioCodec.AUDIO_CODEC_EVRC);
+        map.put(DriverCall.AUDIO_QUALITY_EVRC_B, AudioCodec.AUDIO_CODEC_EVRC_B);
+        map.put(DriverCall.AUDIO_QUALITY_EVRC_WB, AudioCodec.AUDIO_CODEC_EVRC_WB);
+        map.put(DriverCall.AUDIO_QUALITY_EVRC_NW, AudioCodec.AUDIO_CODEC_EVRC_NW);
         return map;
     }
 
-    private static SparseLongArray buildImsCodecMap() {
-        SparseLongArray map = new SparseLongArray();
-
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_AMR, 1L << AudioCodec.AUDIO_CODEC_AMR);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB, 1L << AudioCodec.AUDIO_CODEC_AMR_WB);
-        map.put(
-                ImsStreamMediaProfile.AUDIO_QUALITY_QCELP13K,
-                1L << AudioCodec.AUDIO_CODEC_QCELP13K);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC, 1L << AudioCodec.AUDIO_CODEC_EVRC);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_B, 1L << AudioCodec.AUDIO_CODEC_EVRC_B);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB, 1L << AudioCodec.AUDIO_CODEC_EVRC_WB);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_NW, 1L << AudioCodec.AUDIO_CODEC_EVRC_NW);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_EFR, 1L << AudioCodec.AUDIO_CODEC_GSM_EFR);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_FR, 1L << AudioCodec.AUDIO_CODEC_GSM_FR);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_HR, 1L << AudioCodec.AUDIO_CODEC_GSM_HR);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711U, 1L << AudioCodec.AUDIO_CODEC_G711U);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G723, 1L << AudioCodec.AUDIO_CODEC_G723);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711A, 1L << AudioCodec.AUDIO_CODEC_G711A);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G722, 1L << AudioCodec.AUDIO_CODEC_G722);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711AB, 1L << AudioCodec.AUDIO_CODEC_G711AB);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G729, 1L << AudioCodec.AUDIO_CODEC_G729);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_NB, 1L << AudioCodec.AUDIO_CODEC_EVS_NB);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB, 1L << AudioCodec.AUDIO_CODEC_EVS_WB);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB, 1L << AudioCodec.AUDIO_CODEC_EVS_SWB);
-        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB, 1L << AudioCodec.AUDIO_CODEC_EVS_FB);
-
+    private static SparseIntArray buildImsCodecMap() {
+        SparseIntArray map = new SparseIntArray();
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_AMR, AudioCodec.AUDIO_CODEC_AMR);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB, AudioCodec.AUDIO_CODEC_AMR_WB);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_QCELP13K, AudioCodec.AUDIO_CODEC_QCELP13K);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC, AudioCodec.AUDIO_CODEC_EVRC);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_B, AudioCodec.AUDIO_CODEC_EVRC_B);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB, AudioCodec.AUDIO_CODEC_EVRC_WB);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_NW, AudioCodec.AUDIO_CODEC_EVRC_NW);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_EFR, AudioCodec.AUDIO_CODEC_GSM_EFR);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_FR, AudioCodec.AUDIO_CODEC_GSM_FR);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_GSM_HR, AudioCodec.AUDIO_CODEC_GSM_HR);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711U, AudioCodec.AUDIO_CODEC_G711U);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G723, AudioCodec.AUDIO_CODEC_G723);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711A, AudioCodec.AUDIO_CODEC_G711A);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G722, AudioCodec.AUDIO_CODEC_G722);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G711AB, AudioCodec.AUDIO_CODEC_G711AB);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_G729, AudioCodec.AUDIO_CODEC_G729);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_NB, AudioCodec.AUDIO_CODEC_EVS_NB);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB, AudioCodec.AUDIO_CODEC_EVS_WB);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB, AudioCodec.AUDIO_CODEC_EVS_SWB);
+        map.put(ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB, AudioCodec.AUDIO_CODEC_EVS_FB);
         return map;
     }
 
diff --git a/src/java/com/android/internal/telephony/uicc/UiccPkcs15.java b/src/java/com/android/internal/telephony/uicc/UiccPkcs15.java
index 3513bb9..1254067 100644
--- a/src/java/com/android/internal/telephony/uicc/UiccPkcs15.java
+++ b/src/java/com/android/internal/telephony/uicc/UiccPkcs15.java
@@ -53,7 +53,7 @@
     private class FileHandler extends Handler {
         // EF path for PKCS15 root, eg. "3F007F50"
         // null if logical channel is used for PKCS15 access.
-        private final String mPkcs15Path;
+        final String mPkcs15Path;
         // Message to send when file has been parsed.
         private Message mCallback;
         // File id to read data from, eg. "5031"
@@ -90,7 +90,7 @@
         private void readBinary() {
             if (mChannelId >=0 ) {
                 mUiccProfile.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xB0, 0x00, 0x00, 0x00,
-                        "", obtainMessage(EVENT_READ_BINARY_DONE));
+                        mFileId, obtainMessage(EVENT_READ_BINARY_DONE));
             } else {
                 log("EF based");
             }
@@ -200,8 +200,8 @@
                     // ar.result is null if using logical channel,
                     // or string for pkcs15 path if using file access.
                     mFh = new FileHandler((String) ar.result);
-                    if (!mFh.loadFile(ID_ACRF, obtainMessage(EVENT_LOAD_ACRF_DONE))) {
-                        cleanUp();
+                    if (!mFh.loadFile(EFODF_PATH, obtainMessage(EVENT_LOAD_ODF_DONE))) {
+                        startFromAcrf();
                     }
                 } else {
                     log("select pkcs15 failed: " + ar.exception);
@@ -210,6 +210,39 @@
                 }
                 break;
 
+            case EVENT_LOAD_ODF_DONE:
+                if (ar.exception == null && ar.result != null) {
+                    String idDodf = parseOdf((String) ar.result);
+                    if (!mFh.loadFile(idDodf, obtainMessage(EVENT_LOAD_DODF_DONE))) {
+                        startFromAcrf();
+                    }
+                } else {
+                    startFromAcrf();
+                }
+                break;
+
+            case EVENT_LOAD_DODF_DONE:
+                if (ar.exception == null && ar.result != null) {
+                    String idAcmf = parseDodf((String) ar.result);
+                    if (!mFh.loadFile(idAcmf, obtainMessage(EVENT_LOAD_ACMF_DONE))) {
+                        startFromAcrf();
+                    }
+                } else {
+                    startFromAcrf();
+                }
+                break;
+
+            case EVENT_LOAD_ACMF_DONE:
+                if (ar.exception == null && ar.result != null) {
+                    String idAcrf = parseAcmf((String) ar.result);
+                    if (!mFh.loadFile(idAcrf, obtainMessage(EVENT_LOAD_ACRF_DONE))) {
+                        startFromAcrf();
+                    }
+                } else {
+                    startFromAcrf();
+                }
+                break;
+
             case EVENT_LOAD_ACRF_DONE:
                 if (ar.exception == null && ar.result != null) {
                     mRules = new ArrayList<String>();
@@ -238,6 +271,13 @@
         }
     }
 
+    private void startFromAcrf() {
+        log("Fallback to use ACRF_PATH");
+        if (!mFh.loadFile(ACRF_PATH, obtainMessage(EVENT_LOAD_ACRF_DONE))) {
+            cleanUp();
+        }
+    }
+
     private void cleanUp() {
         log("cleanUp");
         if (mChannelId >= 0) {
@@ -250,10 +290,125 @@
 
     // Constants defined in specs, needed for parsing
     private static final String CARRIER_RULE_AID = "FFFFFFFFFFFF"; // AID for carrier privilege rule
-    private static final String ID_ACRF = "4300";
+    private static final String ACRF_PATH = "4300";
+    private static final String EFODF_PATH = "5031";
     private static final String TAG_ASN_SEQUENCE = "30";
     private static final String TAG_ASN_OCTET_STRING = "04";
+    private static final String TAG_ASN_OID = "06";
     private static final String TAG_TARGET_AID = "A0";
+    private static final String TAG_ODF = "A7";
+    private static final String TAG_DODF = "A1";
+    private static final String REFRESH_TAG_LEN = "08";
+    // OID defined by Global Platform for the "Access Control". The hexstring here can be converted
+    // to OID string value 1.2.840.114283.200.1.1
+    public static final String AC_OID = "060A2A864886FC6B81480101";
+
+
+    // parse ODF file to get file id for DODF file
+    // data is hex string, return file id if parse success, null otherwise
+    private String parseOdf(String data) {
+        // Example:
+        // [A7] 06 [30] 04 [04] 02 52 07
+        try {
+            TLV tlvRule = new TLV(TAG_ODF); // A7
+            tlvRule.parse(data, false);
+            String ruleString = tlvRule.getValue();
+            TLV tlvAsnPath = new TLV(TAG_ASN_SEQUENCE); // 30
+            TLV tlvPath = new TLV(TAG_ASN_OCTET_STRING);  // 04
+            tlvAsnPath.parse(ruleString, true);
+            tlvPath.parse(tlvAsnPath.getValue(), true);
+            return tlvPath.getValue();
+        } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
+            log("Error: " + ex);
+            return null;
+        }
+    }
+
+    // parse DODF file to get file id for ACMF file
+    // data is hex string, return file id if parse success, null otherwise
+    private String parseDodf(String data) {
+        // Example:
+        // [A1] 29 [30] 00 [30] 0F 0C 0D 47 50 20 53 45 20 41 63 63 20 43 74 6C [A1] 14 [30] 12
+        // [06] 0A 2A 86 48 86 FC 6B 81 48 01 01 [30] 04 04 02 42 00
+        String ret = null;
+        String acRules = data;
+        while (!acRules.isEmpty()) {
+            TLV dodfTag = new TLV(TAG_DODF); // A1
+            try {
+                acRules = dodfTag.parse(acRules, false);
+                String ruleString = dodfTag.getValue();
+                // Skip the Common Object Attributes
+                TLV commonObjectAttributes = new TLV(TAG_ASN_SEQUENCE); // 30
+                ruleString = commonObjectAttributes.parse(ruleString, false);
+
+                // Skip the Common Data Object Attributes
+                TLV commonDataObjectAttributes = new TLV(TAG_ASN_SEQUENCE); // 30
+                ruleString = commonDataObjectAttributes.parse(ruleString, false);
+
+                if (ruleString.startsWith(TAG_TARGET_AID)) {
+                    // Skip SubClassAttributes [Optional]
+                    TLV subClassAttributes = new TLV(TAG_TARGET_AID); // A0
+                    ruleString = subClassAttributes.parse(ruleString, false);
+                }
+
+                if (ruleString.startsWith(TAG_DODF)) {
+                    TLV oidDoTag = new TLV(TAG_DODF); // A1
+                    oidDoTag.parse(ruleString, true);
+                    ruleString = oidDoTag.getValue();
+
+                    TLV oidDo = new TLV(TAG_ASN_SEQUENCE); // 30
+                    oidDo.parse(ruleString, true);
+                    ruleString = oidDo.getValue();
+
+                    TLV oidTag = new TLV(TAG_ASN_OID); // 06
+                    oidTag.parse(ruleString, false);
+                    // Example : [06] 0A 2A 86 48 86 FC 6B 81 48 01 01
+                    String oid = oidTag.getValue();
+                    if (oid.equals(AC_OID)) {
+                        // Skip OID and get the AC to the ACCM
+                        ruleString = oidTag.parse(ruleString, false);
+                        TLV tlvAsnPath = new TLV(TAG_ASN_SEQUENCE); // 30
+                        TLV tlvPath = new TLV(TAG_ASN_OCTET_STRING);  // 04
+                        tlvAsnPath.parse(ruleString, true);
+                        tlvPath.parse(tlvAsnPath.getValue(), true);
+                        return tlvPath.getValue();
+                    }
+                }
+                continue; // skip current rule as it doesn't have expected TAG
+            } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
+                log("Error: " + ex);
+                break; // Bad data, ignore all remaining ACRules
+            }
+        }
+        return ret;
+    }
+
+    // parse ACMF file to get file id for ACRF file
+    // data is hex string, return file id if parse success, null otherwise
+    private String parseAcmf(String data) {
+        try {
+            // [30] 10 [04] 08 01 02 03 04 05 06 07 08 [30] 04 [04] 02 43 00
+            TLV acmfTag = new TLV(TAG_ASN_SEQUENCE); // 30
+            acmfTag.parse(data, false);
+            String ruleString = acmfTag.getValue();
+            TLV refreshTag = new TLV(TAG_ASN_OCTET_STRING); // 04
+            String refreshTagLength = refreshTag.parseLength(ruleString);
+            if (!refreshTagLength.equals(REFRESH_TAG_LEN)) {
+                log("Error: refresh tag in ACMF must be 8.");
+                return null;
+            }
+            ruleString = refreshTag.parse(ruleString, false);
+            TLV tlvAsnPath = new TLV(TAG_ASN_SEQUENCE); // 30
+            TLV tlvPath = new TLV(TAG_ASN_OCTET_STRING);  // 04
+            tlvAsnPath.parse(ruleString, true);
+            tlvPath.parse(tlvAsnPath.getValue(), true);
+            return tlvPath.getValue();
+        } catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
+            log("Error: " + ex);
+            return null;
+        }
+
+    }
 
     // parse ACRF file to get file id for ACCF file
     // data is hex string, return file id if parse success, null otherwise
@@ -262,14 +417,14 @@
 
         String acRules = data;
         while (!acRules.isEmpty()) {
+            // Example:
+            // [30] 10 [A0] 08 04 06 FF FF FF FF FF FF [30] 04 [04] 02 43 10
+            // bytes in [] are tags for the data
             TLV tlvRule = new TLV(TAG_ASN_SEQUENCE);
             try {
                 acRules = tlvRule.parse(acRules, false);
                 String ruleString = tlvRule.getValue();
                 if (ruleString.startsWith(TAG_TARGET_AID)) {
-                    // rule string consists of target AID + path, example:
-                    // [A0] 08 [04] 06 FF FF FF FF FF FF [30] 04 [04] 02 43 10
-                    // bytes in [] are tags for the data
                     TLV tlvTarget = new TLV(TAG_TARGET_AID); // A0
                     TLV tlvAid = new TLV(TAG_ASN_OCTET_STRING); // 04
                     TLV tlvAsnPath = new TLV(TAG_ASN_SEQUENCE); // 30
diff --git a/src/java/com/android/internal/telephony/vendor/dataconnection/VendorDcTracker.java b/src/java/com/android/internal/telephony/vendor/dataconnection/VendorDcTracker.java
index 315a13c..d219df9 100644
--- a/src/java/com/android/internal/telephony/vendor/dataconnection/VendorDcTracker.java
+++ b/src/java/com/android/internal/telephony/vendor/dataconnection/VendorDcTracker.java
@@ -148,7 +148,7 @@
 
     @Override
     protected void onDataSetupCompleteError(ApnContext apnContext,
-            @RequestNetworkType int requestType, boolean fallback) {
+            @RequestNetworkType int requestType, boolean fallbackOnFailedHandover) {
         long delay = apnContext.getDelayForNextApn(mFailFast);
         if (mPhone.getContext().getResources().getBoolean(
                 com.android.internal.R.bool.config_pdp_reject_enable_retry)) {
@@ -275,7 +275,8 @@
                     cancelReconnect(apnContext);
                     if (retry) {
                         if (DBG) log("onResetEvent: retry data call on apnContext=" + apnContext);
-                        sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA, apnContext));
+                        sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA,
+                                REQUEST_TYPE_NORMAL, 0, apnContext));
                     }
                 }
             }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/InboundSmsTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/InboundSmsTrackerTest.java
index cd97bdc..7bc26b7 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/InboundSmsTrackerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/InboundSmsTrackerTest.java
@@ -51,7 +51,8 @@
         mInboundSmsTracker = new InboundSmsTracker(InstrumentationRegistry.getContext(),
                 FAKE_PDU, FAKE_TIMESTAMP, FAKE_DEST_PORT, false,
                 FAKE_ADDRESS, FAKE_DISPLAY_ADDRESS, FAKE_REFERENCE_NUMBER, FAKE_SEQUENCE_NUMBER,
-                FAKE_MESSAGE_COUNT, false, FAKE_MESSAGE_BODY, false /* isClass0 */, FAKE_SUBID);
+                FAKE_MESSAGE_COUNT, false, FAKE_MESSAGE_BODY, false /* isClass0 */, FAKE_SUBID,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
     }
 
     public static MatrixCursor createFakeCursor() {
@@ -87,6 +88,7 @@
         assertEquals(FAKE_DISPLAY_ADDRESS, mInboundSmsTracker.getDisplayAddress());
         assertEquals(false, mInboundSmsTracker.isClass0());
         assertEquals(FAKE_SUBID, mInboundSmsTracker.getSubId());
+        assertEquals(InboundSmsHandler.SOURCE_NOT_INJECTED, mInboundSmsTracker.getSource());
 //        assertNotEquals(0L, mInboundSmsTracker.getMessageId());
 
         String[] args = new String[]{"123"};
diff --git a/tests/telephonytests/src/com/android/internal/telephony/ServiceStateTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/ServiceStateTrackerTest.java
index 3098a8c..583eb82 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/ServiceStateTrackerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/ServiceStateTrackerTest.java
@@ -98,6 +98,7 @@
 
 import com.android.internal.R;
 import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
+import com.android.internal.telephony.metrics.ServiceStateStats;
 import com.android.internal.telephony.test.SimulatedCommands;
 import com.android.internal.telephony.uicc.IccCardApplicationStatus;
 import com.android.internal.telephony.uicc.IccRecords;
@@ -136,6 +137,9 @@
     @Mock
     private SubscriptionInfo mSubInfo;
 
+    @Mock
+    private ServiceStateStats mServiceStateStats;
+
     private ServiceStateTracker sst;
     private ServiceStateTrackerTestHandler mSSTTestHandler;
     private PersistableBundle mBundle;
@@ -185,6 +189,7 @@
         @Override
         public void onLooperPrepared() {
             sst = new ServiceStateTracker(mPhone, mSimulatedCommands);
+            sst.setServiceStateStats(mServiceStateStats);
             setReady(true);
         }
     }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SignalThresholdInfoTest.java b/tests/telephonytests/src/com/android/internal/telephony/SignalThresholdInfoTest.java
index ddf5bcd..0f0a031 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/SignalThresholdInfoTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/SignalThresholdInfoTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.os.Parcel;
+import android.telephony.AccessNetworkConstants;
 import android.telephony.SignalThresholdInfo;
 
 import androidx.test.filters.SmallTest;
@@ -33,32 +34,94 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @RunWith(JUnit4.class)
 public class SignalThresholdInfoTest extends TestCase {
     private static final int HYSTERESIS_DB = 2;
     private static final int HYSTERESIS_MS = 30;
-    private static final int[] SSRSRP_THRESHOLDS = new int[] {-30, 10, 45, 130};
+    private static final int[] SSRSRP_THRESHOLDS = new int[]{-120, -100, -80, -60};
 
-    private final int[] mRssiThresholds = new int[] {-109, -103, -97, -89};
-    private final int[] mRscpThresholds = new int[] {-115, -105, -95, -85};
-    private final int[] mRsrpThresholds = new int[] {-128, -118, -108, -98};
-    private final int[] mRsrqThresholds = new int[] {-19, -17, -14, -12};
-    private final int[] mRssnrThresholds = new int[] {-30, 10, 45, 130};
-    private final int[][] mThresholds = new int[5][];
+    // Map of SignalMeasurementType to invalid thresholds edge values.
+    // Each invalid value will be constructed with a thresholds array to test separately.
+    private static final Map<Integer, List<Integer>> INVALID_THRESHOLDS_MAP = Map.of(
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI,
+            List.of(SignalThresholdInfo.SIGNAL_RSSI_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_RSSI_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP,
+            List.of(SignalThresholdInfo.SIGNAL_RSCP_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_RSCP_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP,
+            List.of(SignalThresholdInfo.SIGNAL_RSRP_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_RSRP_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ,
+            List.of(SignalThresholdInfo.SIGNAL_RSRQ_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_RSRQ_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR,
+            List.of(SignalThresholdInfo.SIGNAL_RSSNR_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_RSSNR_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP,
+            List.of(SignalThresholdInfo.SIGNAL_SSRSRP_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_SSRSRP_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ,
+            List.of(SignalThresholdInfo.SIGNAL_SSRSRQ_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_SSRSRQ_MAX_VALUE + 1),
+            SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR,
+            List.of(SignalThresholdInfo.SIGNAL_SSSINR_MIN_VALUE - 1,
+                    SignalThresholdInfo.SIGNAL_SSSINR_MAX_VALUE + 1)
+    );
+
+    // Map of RAN to allowed SignalMeasurementType set.
+    // RAN/TYPE pair will be used to verify the validation of the combo
+    private static final Map<Integer, Set<Integer>> VALID_RAN_TO_MEASUREMENT_TYPE_MAP = Map.of(
+            AccessNetworkConstants.AccessNetworkType.GERAN,
+            Set.of(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI),
+            AccessNetworkConstants.AccessNetworkType.CDMA2000,
+            Set.of(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI),
+            AccessNetworkConstants.AccessNetworkType.UTRAN,
+            Set.of(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP),
+            AccessNetworkConstants.AccessNetworkType.EUTRAN,
+            Set.of(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP,
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ,
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR),
+            AccessNetworkConstants.AccessNetworkType.NGRAN,
+            Set.of(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP,
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ,
+                    SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR)
+    );
+
+    // Deliberately picking up the max/min value in each range to test the edge cases
+    private final int[] mRssiThresholds = new int[]{-113, -103, -97, -51};
+    private final int[] mRscpThresholds = new int[]{-120, -105, -95, -25};
+    private final int[] mRsrpThresholds = new int[]{-140, -118, -108, -44};
+    private final int[] mRsrqThresholds = new int[]{-34, -17, -14, 3};
+    private final int[] mRssnrThresholds = new int[]{-20, 10, 20, 30};
+    private final int[] mSsrsrpThresholds = new int[]{-140, -118, -98, -44};
+    private final int[] mSsrsrqThresholds = new int[]{-43, -17, -14, 20};
+    private final int[] mSssinrThresholds = new int[]{-23, -16, -10, 40};
+
+    private final int[][] mThresholds = {mRssiThresholds, mRscpThresholds, mRsrpThresholds,
+            mRsrqThresholds, mRssnrThresholds, mSsrsrpThresholds, mSsrsrqThresholds,
+            mSssinrThresholds};
 
     @Test
     @SmallTest
     public void testSignalThresholdInfo() throws Exception {
-        SignalThresholdInfo signalThresholdInfo = new SignalThresholdInfo(
-                SignalThresholdInfo.SIGNAL_SSRSRP,
-                HYSTERESIS_MS,
-                HYSTERESIS_DB,
-                SSRSRP_THRESHOLDS,
-                false);
+        SignalThresholdInfo signalThresholdInfo =
+                new SignalThresholdInfo.Builder()
+                        .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                        .setSignalMeasurementType(
+                                SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP)
+                        .setHysteresisMs(HYSTERESIS_MS)
+                        .setHysteresisDb(HYSTERESIS_DB)
+                        .setThresholds(SSRSRP_THRESHOLDS)
+                        .setIsEnabled(false)
+                        .build();
 
-        assertEquals(SignalThresholdInfo.SIGNAL_SSRSRP,
-                signalThresholdInfo.getSignalMeasurement());
+        assertEquals(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP,
+                signalThresholdInfo.getSignalMeasurementType());
         assertEquals(HYSTERESIS_MS, signalThresholdInfo.getHysteresisMs());
         assertEquals(HYSTERESIS_DB, signalThresholdInfo.getHysteresisDb());
         assertEquals(Arrays.toString(SSRSRP_THRESHOLDS), Arrays.toString(
@@ -68,9 +131,8 @@
 
     @Test
     @SmallTest
-    public void testDefaultThresholdsConstruction() {
-        setThresholds();
-        ArrayList<SignalThresholdInfo> stList = setSignalThresholdInfoConstructor();
+    public void testBuilderWithAllFields() {
+        ArrayList<SignalThresholdInfo> stList = buildSignalThresholdInfoWithAllFields();
 
         int count = 0;
         for (SignalThresholdInfo st : stList) {
@@ -82,7 +144,7 @@
     @Test
     @SmallTest
     public void testDefaultThresholdsParcel() {
-        ArrayList<SignalThresholdInfo> stList = setSignalThresholdInfoConstructor();
+        ArrayList<SignalThresholdInfo> stList = buildSignalThresholdInfoWithAllFields();
 
         for (SignalThresholdInfo st : stList) {
             Parcel p = Parcel.obtain();
@@ -98,58 +160,265 @@
     @SmallTest
     public void testGetSignalThresholdInfo() {
         ArrayList<SignalThresholdInfo> stList = new ArrayList<>();
-        stList.add(new SignalThresholdInfo(0, 0, 0, null, false));
-        stList.add(new SignalThresholdInfo(SignalThresholdInfo.SIGNAL_RSSI, HYSTERESIS_MS,
-                HYSTERESIS_DB, mRssiThresholds, false));
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.GERAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI)
+                .setHysteresisMs(0)
+                .setHysteresisDb(0)
+                .setThresholds(new int[]{})
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.GERAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI)
+                .setHysteresisMs(HYSTERESIS_MS).setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRssiThresholds)
+                .setIsEnabled(false)
+                .build());
 
-        assertThat(stList.get(0).getThresholds()).isEqualTo(null);
-        assertThat(stList.get(1).getSignalMeasurement()).isEqualTo(SignalThresholdInfo.SIGNAL_RSSI);
+        assertThat(stList.get(0).getThresholds()).isEqualTo(new int[]{});
+        assertThat(stList.get(1).getSignalMeasurementType()).isEqualTo(
+                SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI);
         assertThat(stList.get(1).getThresholds()).isEqualTo(mRssiThresholds);
     }
 
     @Test
     @SmallTest
     public void testEqualsSignalThresholdInfo() {
-        final int[] dummyThresholds = new int[] {-100, -1, 1, 100};
-        SignalThresholdInfo st1 = new SignalThresholdInfo(1, HYSTERESIS_MS, HYSTERESIS_DB,
-                mRssiThresholds, false);
-        SignalThresholdInfo st2 = new SignalThresholdInfo(2, HYSTERESIS_MS, HYSTERESIS_DB,
-                mRssiThresholds, false);
-        SignalThresholdInfo st3 = new SignalThresholdInfo(1, HYSTERESIS_MS, HYSTERESIS_DB,
-                dummyThresholds, false);
-        SignalThresholdInfo st4 = new SignalThresholdInfo(1, HYSTERESIS_MS, HYSTERESIS_DB,
-                mRssiThresholds, false);
+        final int[] dummyThresholds = new int[]{-100, -90, -70, -60};
+        final int[] dummyThreholdsDisordered = new int[]{-60, -90, -100, -70};
+        SignalThresholdInfo st1 = new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(1).setSignalMeasurementType(1)
+                .setHysteresisMs(HYSTERESIS_MS).setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRssiThresholds).setIsEnabled(false)
+                .build();
+        SignalThresholdInfo st2 = new SignalThresholdInfo.Builder().setRadioAccessNetworkType(2)
+                .setSignalMeasurementType(2).setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB).setThresholds(mRssiThresholds).setIsEnabled(false)
+                .build();
+        SignalThresholdInfo st3 = new SignalThresholdInfo.Builder().setRadioAccessNetworkType(1)
+                .setSignalMeasurementType(1).setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB).setThresholds(dummyThresholds).setIsEnabled(false)
+                .build();
+        SignalThresholdInfo st4 = new SignalThresholdInfo.Builder().setRadioAccessNetworkType(1)
+                .setSignalMeasurementType(1).setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB).setThresholds(mRssiThresholds).setIsEnabled(false)
+                .build();
+        SignalThresholdInfo st5 = new SignalThresholdInfo.Builder().setRadioAccessNetworkType(1)
+                .setSignalMeasurementType(1).setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB).setThresholds(dummyThreholdsDisordered)
+                .setIsEnabled(false).build();
 
         //Return true if all SignalThresholdInfo values match.
         assertTrue(st1.equals(st1));
         assertFalse(st1.equals(st2));
         assertFalse(st1.equals(st3));
         assertTrue(st1.equals(st4));
+        //Threshold values ordering doesn't matter
+        assertTrue(st3.equals(st5));
         //Return false if the object of argument is other than SignalThresholdInfo.
         assertFalse(st1.equals(new String("test")));
     }
 
-    private void setThresholds() {
-        mThresholds[0] = mRssiThresholds;
-        mThresholds[1] = mRscpThresholds;
-        mThresholds[2] = mRsrpThresholds;
-        mThresholds[3] = mRsrqThresholds;
-        mThresholds[4] = mRssnrThresholds;
+    @Test
+    @SmallTest
+    public void testBuilderWithValidParameters() {
+        ArrayList<SignalThresholdInfo> stList = buildSignalThresholdInfoWithPublicFields();
+
+        for (int i = 0; i < stList.size(); i++) {
+            SignalThresholdInfo st = stList.get(i);
+            assertThat(st.getThresholds()).isEqualTo(mThresholds[i]);
+            assertThat(st.getHysteresisMs()).isEqualTo(SignalThresholdInfo.HYSTERESIS_MS_DISABLED);
+            assertThat(st.getHysteresisDb()).isEqualTo(SignalThresholdInfo.HYSTERESIS_DB_DISABLED);
+            assertFalse(st.isEnabled());
+        }
     }
 
-    private ArrayList<SignalThresholdInfo> setSignalThresholdInfoConstructor() {
+    @Test
+    @SmallTest
+    public void testBuilderWithInvalidParameter() {
+        // Invalid signal measurement type
+        int[] invalidSignalMeasurementTypes = new int[]{-1, 0, 9};
+        for (int signalMeasurementType : invalidSignalMeasurementTypes) {
+            buildWithInvalidParameterThrowException(
+                    AccessNetworkConstants.AccessNetworkType.GERAN, signalMeasurementType,
+                    new int[]{-1});
+        }
+
+        // Null thresholds array
+        buildWithInvalidParameterThrowException(AccessNetworkConstants.AccessNetworkType.GERAN,
+                SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI, null);
+
+        // Thresholds value out of range
+        for (int signalMeasurementType : INVALID_THRESHOLDS_MAP.keySet()) {
+            List<Integer> invalidThresholds = INVALID_THRESHOLDS_MAP.get(signalMeasurementType);
+            for (int threshold : invalidThresholds) {
+                buildWithInvalidParameterThrowException(getValidRan(signalMeasurementType),
+                        signalMeasurementType, new int[]{threshold});
+            }
+        }
+
+        // Invalid RAN/Measurement type combos
+        for (int ran : VALID_RAN_TO_MEASUREMENT_TYPE_MAP.keySet()) {
+            Set validTypes = VALID_RAN_TO_MEASUREMENT_TYPE_MAP.get(ran);
+            for (int type = SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI;
+                    type <= SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR; type++) {
+                if (!validTypes.contains(type)) {
+                    buildWithInvalidParameterThrowException(ran, type, new int[]{-1});
+                }
+            }
+        }
+    }
+
+    private void buildWithInvalidParameterThrowException(int ran, int signalMeasurementType,
+            int[] thresholds) {
+        try {
+            new SignalThresholdInfo.Builder()
+                    .setRadioAccessNetworkType(ran)
+                    .setSignalMeasurementType(signalMeasurementType)
+                    .setThresholds(thresholds)
+                    .build();
+            fail("exception expected");
+        } catch (IllegalArgumentException | NullPointerException expected) {
+        }
+    }
+
+    private ArrayList<SignalThresholdInfo> buildSignalThresholdInfoWithAllFields() {
         ArrayList<SignalThresholdInfo> stList = new ArrayList<>();
-        stList.add(new SignalThresholdInfo(SignalThresholdInfo.SIGNAL_RSSI, HYSTERESIS_MS,
-                HYSTERESIS_DB, mRssiThresholds, false));
-        stList.add(new SignalThresholdInfo(SignalThresholdInfo.SIGNAL_RSCP, HYSTERESIS_MS,
-                HYSTERESIS_DB, mRscpThresholds, false));
-        stList.add(new SignalThresholdInfo(SignalThresholdInfo.SIGNAL_RSRP, HYSTERESIS_MS,
-                HYSTERESIS_DB, mRsrpThresholds, false));
-        stList.add(new SignalThresholdInfo(SignalThresholdInfo.SIGNAL_RSRQ, HYSTERESIS_MS,
-                HYSTERESIS_DB, mRsrqThresholds, false));
-        stList.add(new SignalThresholdInfo(SignalThresholdInfo.SIGNAL_RSSNR, HYSTERESIS_MS,
-                HYSTERESIS_DB, mRssnrThresholds, false));
+
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.GERAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI)
+                .setHysteresisMs(HYSTERESIS_MS).setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRssiThresholds).setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.UTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRscpThresholds)
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.EUTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRsrpThresholds)
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.EUTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRsrqThresholds)
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.EUTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mRssnrThresholds)
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mSsrsrpThresholds)
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mSsrsrqThresholds)
+                .setIsEnabled(false)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR)
+                .setHysteresisMs(HYSTERESIS_MS)
+                .setHysteresisDb(HYSTERESIS_DB)
+                .setThresholds(mSssinrThresholds)
+                .setIsEnabled(false)
+                .build());
 
         return stList;
     }
+
+    private ArrayList<SignalThresholdInfo> buildSignalThresholdInfoWithPublicFields() {
+        ArrayList<SignalThresholdInfo> stList = new ArrayList<>();
+
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.GERAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI)
+                .setThresholds(mRssiThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.UTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP)
+                .setThresholds(mRscpThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.EUTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP)
+                .setThresholds(mRsrpThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.EUTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ)
+                .setThresholds(mRsrqThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.EUTRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR)
+                .setThresholds(mRssnrThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP)
+                .setThresholds(mSsrsrpThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ)
+                .setThresholds(mSsrsrqThresholds)
+                .build());
+        stList.add(new SignalThresholdInfo.Builder()
+                .setRadioAccessNetworkType(AccessNetworkConstants.AccessNetworkType.NGRAN)
+                .setSignalMeasurementType(SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR)
+                .setThresholds(mSssinrThresholds)
+                .build());
+
+        return stList;
+    }
+
+    /**
+     * Return a possible valid RAN value for the measurement type. This is used to prevent the
+     * invalid ran/type causing IllegalArgumentException when testing other invalid input cases.
+     */
+    private static int getValidRan(@SignalThresholdInfo.SignalMeasurementType int type) {
+        switch (type) {
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSI:
+                return AccessNetworkConstants.AccessNetworkType.GERAN;
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSCP:
+                return AccessNetworkConstants.AccessNetworkType.UTRAN;
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRP:
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSRQ:
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_RSSNR:
+                return AccessNetworkConstants.AccessNetworkType.EUTRAN;
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRP:
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSRSRQ:
+            case SignalThresholdInfo.SIGNAL_MEASUREMENT_TYPE_SSSINR:
+                return AccessNetworkConstants.AccessNetworkType.NGRAN;
+            default:
+                return AccessNetworkConstants.AccessNetworkType.UNKNOWN;
+        }
+    }
 }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SipMessageParsingUtilsTest.java b/tests/telephonytests/src/com/android/internal/telephony/SipMessageParsingUtilsTest.java
new file mode 100644
index 0000000..50ca36a
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/SipMessageParsingUtilsTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import android.telephony.ims.SipMessage;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test SIP Message parsing utilities in frameworks/base. see {@link SipMessageParsingUtils}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SipMessageParsingUtilsTest {
+
+    // Sample messages from RFC 3261 modified for parsing use cases.
+    public static final SipMessage SIP_MESSAGE_1 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            // Typical Via
+            "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n"
+                    + "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142",
+            new byte[0]);
+    public static final String SIP_MESSAGE_1_TRANSACTION_ID = "z9hG4bK776asdhds";
+    public static final SipMessage SIP_MESSAGE_2 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            // include leading whitespace.
+            " Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142\n"
+                    // transaction ID should be returned for the first "Via"
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKabcdefghi\n"
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds\n",
+
+            new byte[0]);
+    public static final String SIP_MESSAGE_2_TRANSACTION_ID = "z9hG4bKabcdefghi";
+    public static final SipMessage SIP_MESSAGE_3 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    // Subject line is split into multiple lines via space and tab.
+                    + "Subject: I know you're there,\n"
+                    + " pick up the phone\n"
+                    + "\tand talk to me!\n"
+                    // transaction ID should be returned for the first "Via"
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKzyxwvutsr\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142\n"
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKabcdefghi\n",
+            new byte[0]);
+    public static final String SIP_MESSAGE_3_TRANSACTION_ID = "z9hG4bKzyxwvutsr";
+    // compact form
+    public static final SipMessage SIP_MESSAGE_4 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            "Max-Forwards: 70\n"
+                    + "t: Bob <sip:bob@biloxi.com>\n"
+                    + "f: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    // compat form of via
+                    + "v: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKAbCdEfGiJ\n"
+                    + "v: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKabcdefghi\n"
+                    + "i: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "m: <sip:alice@pc33.atlanta.com>\n"
+                    + "c: application/sdp\n"
+                    + "l: 142\n",
+            new byte[0]);
+    public static final String SIP_MESSAGE_4_TRANSACTION_ID = "z9hG4bKAbCdEfGiJ";
+    public static final SipMessage SIP_MESSAGE_5 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    // Malformed lines
+                    + "Subject: I know you're there,\n"
+                    + "pick up the phone\n"
+                    + "and talk to me!\n"
+                    // transaction ID should be returned for the first "Via"
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKzyxwvutsr\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142\n"
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKabcdefghi\n",
+            new byte[0]);
+    public static final String SIP_MESSAGE_5_TRANSACTION_ID = "z9hG4bKzyxwvutsr";
+    // Not practical, but ensure that parsing works, even in special cases like one line.
+    public static final SipMessage SIP_MESSAGE_6 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+            "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKabcdefghi\n",
+            new byte[0]);
+    public static final String SIP_MESSAGE_6_TRANSACTION_ID = "z9hG4bKabcdefghi";
+    public static final SipMessage SIP_MESSAGE_7 = new SipMessage(
+            "INVITE sip:bob@biloxi.com SIP/2.0",
+                    "Max-Forwards: 70\n"
+                    + "To: Bob <sip:bob@biloxi.com>\n"
+                    + "From: Alice <sip:alice@atlanta.com>;tag=1928301774\n"
+                    + "Call-ID: a84b4c76e66710@pc33.atlanta.com\n"
+                    + "CSeq: 314159 INVITE\n"
+                    + "Contact: <sip:alice@pc33.atlanta.com>\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Content-Length: 142\n"
+                    // Typical Via, but on last line to test edge conditions
+                    + "Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds",
+            new byte[0]);
+    public static final String SIP_MESSAGE_7_TRANSACTION_ID = "z9hG4bK776asdhds";
+    // SIP Message from RFC 4475 "A Short Tortuous INVITE"
+    public static final SipMessage SIP_MESSAGE_8 = new SipMessage(
+            "INVITE sip:vivekg@chair-dnrc.example.com;unknownparam SIP/2.0",
+            "TO :\n"
+                    + " sip:vivekg@chair-dnrc.example.com ;   tag    = 1918181833n\n"
+                    + "from   : \"J Rosenberg \\\\\\\"\"       <sip:jdrosen@example.com>\n"
+                    + "  ;\n"
+                    + "  tag = 98asjd8\n"
+                    + "MaX-fOrWaRdS: 0068\n"
+                    + "Call-ID: wsinv.ndaksdj@192.0.2.1\n"
+                    + "Content-Length   : 150\n"
+                    + "cseq: 0009\n"
+                    + "  INVITE\n"
+                    + "s :\n"
+                    + "NewFangledHeader:   newfangled value\n"
+                    + " continued newfangled value\n"
+                    + "UnknownHeaderWithUnusualValue: ;;,,;;,;\n"
+                    + "Content-Type: application/sdp\n"
+                    + "Route:\n"
+                    + " <sip:services.example.com;lr;unknownwith=value;unknown-no-value>\n"
+                    // Note, this has multiple Via headers concatenated with one header key, we
+                    // should return the first in the list.
+                    + "v:  SIP  / 2.0  / TCP     spindle.example.com   ;\n"
+                    + "  branch  =   z9hG4bK9ikj8  ,\n"
+                    + " SIP  /    2.0   / UDP  192.168.255.111   ; branch=\n"
+                    + " z9hG4bK30239\n"
+                    + "Via  : SIP  /   2.0\n"
+                    + " /UDP\n"
+                    + "    192.0.2.2;branch=z9hG4bK390skdjuw\n"
+                    + "m:\"Quoted string \\\"\\\"\" <sip:jdrosen@example.com> ; newparam =\n"
+                    + "      newvalue ;\n"
+                    + "   secondparam ; q = 0.33",
+            new byte[0]);
+    public static final String SIP_MESSAGE_8_TRANSACTION_ID = "z9hG4bK9ikj8";
+
+    @Test
+    @SmallTest
+    public void testGetTransactionId() {
+        assertEquals(SIP_MESSAGE_1_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_1.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_2_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_2.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_3_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_3.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_4_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_4.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_5_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_5.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_6_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_6.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_7_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_7.getHeaderSection()));
+        assertEquals(SIP_MESSAGE_8_TRANSACTION_ID, SipMessageParsingUtils.getTransactionId(
+                SIP_MESSAGE_8.getHeaderSection()));
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SmsDispatchersControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/SmsDispatchersControllerTest.java
index 1097324..ee9e2b8 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/SmsDispatchersControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/SmsDispatchersControllerTest.java
@@ -167,7 +167,7 @@
         restoreInstance(ActivityManager.class, "IActivityManagerSingleton", null);
 
         // inject null sms pdu. This should cause intent to be received since pdu is null.
-        mSmsDispatchersController.injectSmsPdu(null, SmsConstants.FORMAT_3GPP,
+        mSmsDispatchersController.injectSmsPdu(null, SmsConstants.FORMAT_3GPP, true,
                 (SmsDispatchersController.SmsInjectionCallback) result -> {
                     mInjectionCallbackTriggered = true;
                    assertEquals(Intents.RESULT_SMS_GENERIC_ERROR, result);
diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
index 0d00988..2aceb20 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
@@ -41,6 +41,8 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -59,6 +61,7 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.NetworkRegistrationInfo;
 import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.TelephonyRegistryManager;
@@ -85,8 +88,10 @@
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
+import com.android.internal.telephony.metrics.ImsStats;
 import com.android.internal.telephony.metrics.MetricsCollector;
 import com.android.internal.telephony.metrics.PersistAtomsStorage;
+import com.android.internal.telephony.metrics.SmsStats;
 import com.android.internal.telephony.metrics.VoiceCallSessionStats;
 import com.android.internal.telephony.test.SimulatedCommands;
 import com.android.internal.telephony.test.SimulatedCommandsVerifier;
@@ -99,6 +104,7 @@
 import com.android.internal.telephony.uicc.UiccCardApplication;
 import com.android.internal.telephony.uicc.UiccController;
 import com.android.internal.telephony.uicc.UiccProfile;
+import com.android.internal.telephony.uicc.UiccSlot;
 import com.android.server.pm.PackageManagerService;
 import com.android.server.pm.permission.PermissionManagerService;
 
@@ -296,7 +302,17 @@
     @Mock
     protected MetricsCollector mMetricsCollector;
     @Mock
+    protected SmsStats mSmsStats;
+    @Mock
     protected DataThrottler mDataThrottler;
+    @Mock
+    protected SignalStrength mSignalStrength;
+    @Mock
+    protected WifiManager mWifiManager;
+    @Mock
+    protected WifiInfo mWifiInfo;
+    @Mock
+    protected ImsStats mImsStats;
 
     protected ActivityManager mActivityManager;
     protected ImsCallProfile mImsCallProfile;
@@ -525,8 +541,11 @@
         doReturn(mDataEnabledSettings).when(mPhone).getDataEnabledSettings();
         doReturn(mDcTracker).when(mPhone).getDcTracker(anyInt());
         doReturn(mCarrierPrivilegesTracker).when(mPhone).getCarrierPrivilegesTracker();
+        doReturn(mSignalStrength).when(mPhone).getSignalStrength();
         doReturn(mVoiceCallSessionStats).when(mPhone).getVoiceCallSessionStats();
         doReturn(mVoiceCallSessionStats).when(mImsPhone).getVoiceCallSessionStats();
+        doReturn(mSmsStats).when(mPhone).getSmsStats();
+        doReturn(mImsStats).when(mImsPhone).getImsStats();
         mIccSmsInterfaceManager.mDispatchersController = mSmsDispatchersController;
 
         //mUiccController
@@ -552,6 +571,7 @@
                 }
             }
         }).when(mUiccController).getIccRecords(anyInt(), anyInt());
+        doReturn(new UiccSlot[] {}).when(mUiccController).getUiccSlots();
 
         //UiccCardApplication
         doReturn(mSimRecords).when(mUiccCardApplication3gpp).getIccRecords();
@@ -615,6 +635,12 @@
         doReturn(mNetworkRegistrationInfo).when(mServiceState).getNetworkRegistrationInfo(
                 anyInt(), anyInt());
         doReturn(new HalVersion(1, 4)).when(mPhone).getHalVersion();
+        doReturn(2).when(mSignalStrength).getLevel();
+
+        // WiFi
+        doReturn(mWifiInfo).when(mWifiManager).getConnectionInfo();
+        doReturn(2).when(mWifiManager).calculateSignalLevel(anyInt());
+        doReturn(4).when(mWifiManager).getMaxSignalLevel();
 
         //SIM
         doReturn(1).when(mTelephonyManager).getSimCount();
@@ -644,6 +670,7 @@
         // Metrics
         doReturn(null).when(mContext).getFileStreamPath(anyString());
         doReturn(mPersistAtomsStorage).when(mMetricsCollector).getAtomsStorage();
+        doReturn(mWifiManager).when(mContext).getSystemService(eq(Context.WIFI_SERVICE));
 
         //Use reflection to mock singletons
         replaceInstance(CallManager.class, "INSTANCE", null, mCallManager);
diff --git a/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaInboundSmsHandlerTest.java b/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaInboundSmsHandlerTest.java
index a6d30f9..68831f6 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaInboundSmsHandlerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaInboundSmsHandlerTest.java
@@ -130,18 +130,20 @@
                 "1234567890", /* displayAddress */
                 "This is the message body of a single-part message" /* messageBody */,
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
 
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                 anyInt(), anyBoolean(),
                 anyBoolean(), nullable(String.class), nullable(String.class),
-                nullable(String.class), anyBoolean(), anyInt());
+                nullable(String.class), anyBoolean(), anyInt(), anyInt());
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                 anyInt(), anyBoolean(),
                 nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                anyInt());
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(Cursor.class), anyBoolean());
 
@@ -234,12 +236,13 @@
                 blockedNumber, /* displayAddress */
                 "This is the message body of a single-part message" /* messageBody */,
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                 anyInt(), anyBoolean(),
                 anyBoolean(), nullable(String.class), nullable(String.class),
-                nullable(String.class), anyBoolean(), anyInt());
+                nullable(String.class), anyBoolean(), anyInt(), anyInt());
         mFakeBlockedNumberContentProvider.mBlockedNumbers.add(blockedNumber);
 
         transitionFromStartupToIdle();
diff --git a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataConnectionTest.java b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataConnectionTest.java
index 40cb0be..2298f1b 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataConnectionTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataConnectionTest.java
@@ -68,6 +68,7 @@
 import com.android.internal.telephony.dataconnection.DataConnection.ConnectionParams;
 import com.android.internal.telephony.dataconnection.DataConnection.DisconnectParams;
 import com.android.internal.telephony.dataconnection.DataConnection.SetupResult;
+import com.android.internal.telephony.metrics.DataCallSessionStats;
 import com.android.internal.util.IState;
 import com.android.internal.util.StateMachine;
 
@@ -93,6 +94,8 @@
     ApnContext mApnContext;
     @Mock
     DcFailBringUp mDcFailBringUp;
+    @Mock
+    DataCallSessionStats mDataCallSessionStats;
 
     private DataConnection mDc;
     private DataConnectionTestHandler mDataConnectionTestHandler;
@@ -311,6 +314,8 @@
         mDataConnectionTestHandler.start();
 
         waitForMs(200);
+        mDc.setDataCallSessionStats(mDataCallSessionStats);
+
         logd("-Setup!");
     }
 
diff --git a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataThrottlerTest.java b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataThrottlerTest.java
index 44847c8..0af680d 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataThrottlerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/dataconnection/DataThrottlerTest.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.telephony.dataconnection;
 
+import static com.android.internal.telephony.dataconnection.DcTracker.REQUEST_TYPE_HANDOVER;
+import static com.android.internal.telephony.dataconnection.DcTracker.REQUEST_TYPE_NORMAL;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -23,7 +26,6 @@
 import android.telephony.AccessNetworkConstants;
 import android.telephony.data.ApnSetting;
 import android.telephony.data.ApnThrottleStatus;
-import android.telephony.data.DataCallResponse;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -87,7 +89,7 @@
 
 
         mDataThrottler.setRetryTime(ApnSetting.TYPE_DEFAULT, 1234567890L,
-                DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN);
+                REQUEST_TYPE_NORMAL);
         assertEquals(1234567890L, mDataThrottler.getRetryTime(ApnSetting.TYPE_DEFAULT));
         assertEquals(RetryManager.NO_SUGGESTED_RETRY_DELAY,
                 mDataThrottler.getRetryTime(ApnSetting.TYPE_MMS));
@@ -112,7 +114,7 @@
 
 
         mDataThrottler.setRetryTime(ApnSetting.TYPE_DEFAULT | ApnSetting.TYPE_DUN, 13579L,
-                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER);
+                REQUEST_TYPE_HANDOVER);
         assertEquals(13579L, mDataThrottler.getRetryTime(ApnSetting.TYPE_DEFAULT));
         assertEquals(13579L, mDataThrottler.getRetryTime(ApnSetting.TYPE_DUN));
 
@@ -143,7 +145,7 @@
 
 
         mDataThrottler.setRetryTime(ApnSetting.TYPE_MMS, -10,
-                DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN);
+                REQUEST_TYPE_NORMAL);
         assertEquals(RetryManager.NO_SUGGESTED_RETRY_DELAY,
                 mDataThrottler.getRetryTime(ApnSetting.TYPE_MMS));
         processAllMessages();
@@ -158,8 +160,7 @@
         ));
 
         mDataThrottler.setRetryTime(ApnSetting.TYPE_FOTA | ApnSetting.TYPE_EMERGENCY,
-                RetryManager.NO_RETRY,
-                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER);
+                RetryManager.NO_RETRY, REQUEST_TYPE_HANDOVER);
 
         processAllMessages();
         expectedStatuses.add(List.of(
diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmInboundSmsHandlerTest.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmInboundSmsHandlerTest.java
index 54d4c80..d201bcf 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmInboundSmsHandlerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmInboundSmsHandlerTest.java
@@ -231,12 +231,13 @@
                 "1234567890", /* displayAddress */
                 mMessageBody, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                 anyInt(), anyBoolean(),
                 anyBoolean(), nullable(String.class), nullable(String.class),
-                nullable(String.class), anyBoolean(), anyInt());
+                nullable(String.class), anyBoolean(), anyInt(), anyInt());
 
         createMockInboundSmsTracker();
 
@@ -469,12 +470,13 @@
                 "1234567890", /* displayAddress */
                 mMessageBody, /* messageBody */
                 true, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         anyBoolean(), nullable(String.class), nullable(String.class),
-                        nullable(String.class), anyBoolean(), anyInt());
+                        nullable(String.class), anyBoolean(), anyInt(), anyInt());
         mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS,
                 mInboundSmsTracker);
         processAllMessages();
@@ -499,12 +501,13 @@
                 "1234567890", /* displayAddress */
                 mMessageBody, /* messageBody */
                 false, /* isClass0 */
-                mSubId0));
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED));
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         anyBoolean(), nullable(String.class), nullable(String.class),
-                        nullable(String.class), anyBoolean(), anyInt());
+                        nullable(String.class), anyBoolean(), anyInt(), anyInt());
         doReturn(2131L).when(mInboundSmsTracker).getMessageId();
         mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS,
                 mInboundSmsTracker);
@@ -561,7 +564,8 @@
                 is3gpp2WapPush, /* is3gpp2WapPdu */
                 mMessageBodyPart1, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
 
         // Part 2
         mInboundSmsTrackerPart2 = new InboundSmsTracker(
@@ -578,7 +582,8 @@
                 is3gpp2WapPush, /* is3gpp2WapPdu */
                 mMessageBodyPart2, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
     }
 
     @Test
@@ -602,7 +607,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // State machine should go back to idle and wait for second part
@@ -614,7 +620,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // State machine should go back to idle and wait for second part
@@ -630,7 +637,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify broadcast intents
@@ -659,7 +667,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // State machine should go back to idle and wait for second part
@@ -669,7 +678,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify broadcast intents
@@ -685,7 +695,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify no additional broadcasts sent
@@ -703,7 +714,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify no additional broadcasts sent
@@ -739,7 +751,8 @@
                 false, /* is3gpp2WapPdu */
                 mMessageBodyPart2, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
 
         mSmsHeader.concatRef = new SmsHeader.ConcatRef();
         doReturn(mSmsHeader).when(mGsmSmsMessage).getUserDataHeader();
@@ -748,7 +761,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // State machine should go back to idle and wait for second part
@@ -758,7 +772,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify no broadcasts sent
@@ -789,7 +804,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify the message is stored in the raw table
@@ -814,13 +830,15 @@
                 false, /* is3gpp2WapPdu */
                 mMessageBodyPart2, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
 
         doReturn(mInboundSmsTrackerPart2).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify no broadcasts sent
@@ -846,7 +864,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
 
         sendNewSms();
 
@@ -857,7 +876,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         verify(mContext, never()).sendBroadcast(any(Intent.class));
@@ -890,7 +910,8 @@
                 false, /* is3gpp2WapPdu */
                 mMessageBodyPart1, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
 
         mSmsHeader.concatRef = new SmsHeader.ConcatRef();
         doReturn(mSmsHeader).when(mGsmSmsMessage).getUserDataHeader();
@@ -898,7 +919,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
 
         sendNewSms();
 
@@ -909,7 +931,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         verify(mContext, never()).sendBroadcast(any(Intent.class));
@@ -942,7 +965,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // State machine should go back to idle and wait for second part
@@ -952,7 +976,8 @@
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         nullable(String.class), nullable(String.class), anyInt(), anyInt(),
-                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt());
+                        anyInt(), anyBoolean(), nullable(String.class), anyBoolean(), anyInt(),
+                        anyInt());
         sendNewSms();
 
         // verify no broadcasts sent
@@ -1042,12 +1067,13 @@
                 "1234567890", /* displayAddress */
                 mMessageBody, /* messageBody */
                 false, /* isClass0 */
-                mSubId0);
+                mSubId0,
+                InboundSmsHandler.SOURCE_NOT_INJECTED);
         doReturn(mInboundSmsTracker).when(mTelephonyComponentFactory)
                 .makeInboundSmsTracker(any(Context.class), nullable(byte[].class), anyLong(),
                         anyInt(), anyBoolean(),
                         anyBoolean(), nullable(String.class), nullable(String.class),
-                        nullable(String.class), anyBoolean(), anyInt());
+                        nullable(String.class), anyBoolean(), anyInt(), anyInt());
 
         //add a fake entry to db
         ContentValues rawSms = new ContentValues();
diff --git a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneTest.java b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneTest.java
index 99c50d2..921a78b 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneTest.java
@@ -157,6 +157,7 @@
         mImsPhoneUT.registerForIncomingRing(mTestHandler,
                 EVENT_INCOMING_RING, null);
         mImsPhoneUT.setVoiceCallSessionStats(mVoiceCallSessionStats);
+        mImsPhoneUT.setImsStats(mImsStats);
         doReturn(mImsUtInterface).when(mImsCT).getUtInterface();
         // When the mock GsmCdmaPhone gets setIsInEcbm called, ensure isInEcm matches.
         doAnswer(invocation -> {
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/ImsStatsTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/ImsStatsTest.java
new file mode 100644
index 0000000..6223329
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/ImsStatsTest.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN;
+import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
+import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO;
+import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE;
+import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
+import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyTest;
+import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
+import com.android.internal.telephony.uicc.UiccSlot;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+public class ImsStatsTest extends TelephonyTest {
+    private static final long START_TIME_MILLIS = 2000L;
+    private static final int CARRIER1_ID = 1;
+    private static final int CARRIER2_ID = 1187;
+
+    @MmTelCapability
+    private static final int CAPABILITY_TYPE_ALL =
+            MmTelCapabilities.CAPABILITY_TYPE_VOICE
+                    | MmTelCapabilities.CAPABILITY_TYPE_VIDEO
+                    | MmTelCapabilities.CAPABILITY_TYPE_SMS
+                    | MmTelCapabilities.CAPABILITY_TYPE_UT;
+
+    @Mock private UiccSlot mPhysicalSlot0;
+    @Mock private UiccSlot mPhysicalSlot1;
+    @Mock private Phone mSecondPhone;
+    @Mock private ImsPhone mSecondImsPhone;
+
+    private TestableImsStats mImsStats;
+
+    private static class TestableImsStats extends ImsStats {
+        private long mTimeMillis = START_TIME_MILLIS;
+
+        TestableImsStats(ImsPhone imsPhone) {
+            super(imsPhone);
+        }
+
+        @Override
+        protected long getTimeMillis() {
+            // 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)
+            return mTimeMillis == 0L ? START_TIME_MILLIS : mTimeMillis;
+        }
+
+        private void setTimeMillis(long timeMillis) {
+            mTimeMillis = timeMillis;
+        }
+
+        private void incTimeMillis(long timeMillis) {
+            mTimeMillis += timeMillis;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp(getClass().getSimpleName());
+
+        doReturn(CARRIER1_ID).when(mPhone).getCarrierId();
+        doReturn(mImsPhone).when(mPhone).getImsPhone();
+        doReturn(mSST).when(mImsPhone).getServiceStateTracker();
+
+        // WWAN PS RAT is LTE
+        doReturn(
+                        new NetworkRegistrationInfo.Builder()
+                                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+                                .build())
+                .when(mServiceState)
+                .getNetworkRegistrationInfo(DOMAIN_PS, TRANSPORT_TYPE_WWAN);
+
+        // Single physical SIM
+        doReturn(true).when(mPhysicalSlot0).isActive();
+        doReturn(CardState.CARDSTATE_PRESENT).when(mPhysicalSlot0).getCardState();
+        doReturn(false).when(mPhysicalSlot0).isEuicc();
+        doReturn(new UiccSlot[] {mPhysicalSlot0}).when(mUiccController).getUiccSlots();
+        doReturn(mPhysicalSlot0).when(mUiccController).getUiccSlot(0);
+        doReturn(mPhysicalSlot0).when(mUiccController).getUiccSlotForPhone(0);
+
+        mImsStats = new TestableImsStats(mImsPhone);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_registered() throws Exception {
+        // IMS over LTE
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VIDEO,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_UT,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_SMS,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+        mImsStats.onImsCapabilitiesChanged(
+                REGISTRATION_TECH_LTE, new MmTelCapabilities(CAPABILITY_TYPE_ALL));
+
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.conclude();
+
+        // Duration should be counted
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(captor.capture());
+        ImsRegistrationStats stats = captor.getValue();
+        assertEquals(CARRIER1_ID, stats.carrierId);
+        assertEquals(0, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(2000L, stats.voiceCapableMillis);
+        assertEquals(2000L, stats.voiceAvailableMillis);
+        assertEquals(2000L, stats.videoCapableMillis);
+        assertEquals(2000L, stats.videoAvailableMillis);
+        assertEquals(2000L, stats.utCapableMillis);
+        assertEquals(2000L, stats.utAvailableMillis);
+        assertEquals(2000L, stats.smsCapableMillis);
+        assertEquals(2000L, stats.smsAvailableMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_registeredPartialFeatures() throws Exception {
+        // IMS over LTE
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VIDEO,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_UT,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_SMS,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+        mImsStats.onImsCapabilitiesChanged(
+                REGISTRATION_TECH_LTE, new MmTelCapabilities(CAPABILITY_TYPE_VOICE));
+
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.conclude();
+
+        // Duration should be counted
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(captor.capture());
+        ImsRegistrationStats stats = captor.getValue();
+        assertEquals(CARRIER1_ID, stats.carrierId);
+        assertEquals(0, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(2000L, stats.voiceCapableMillis);
+        assertEquals(2000L, stats.voiceAvailableMillis);
+        assertEquals(2000L, stats.videoCapableMillis);
+        assertEquals(0L, stats.videoAvailableMillis);
+        assertEquals(2000L, stats.utCapableMillis);
+        assertEquals(0L, stats.utAvailableMillis);
+        assertEquals(2000L, stats.smsCapableMillis);
+        assertEquals(0L, stats.smsAvailableMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_registeredVoiceOnly() throws Exception {
+        // Wifi calling
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_IWLAN,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VIDEO,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_UT,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WLAN);
+        mImsStats.onImsCapabilitiesChanged(
+                REGISTRATION_TECH_IWLAN, new MmTelCapabilities(CAPABILITY_TYPE_VOICE));
+
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.conclude();
+
+        // Duration should be counted
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(captor.capture());
+        ImsRegistrationStats stats = captor.getValue();
+        assertEquals(CARRIER1_ID, stats.carrierId);
+        assertEquals(0, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_IWLAN, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(2000L, stats.voiceCapableMillis);
+        assertEquals(2000L, stats.voiceAvailableMillis);
+        assertEquals(0L, stats.videoCapableMillis);
+        assertEquals(0L, stats.videoAvailableMillis);
+        assertEquals(0L, stats.utCapableMillis);
+        assertEquals(0L, stats.utAvailableMillis);
+        assertEquals(0L, stats.smsCapableMillis);
+        assertEquals(0L, stats.smsAvailableMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_notRegistered() throws Exception {
+        // IMS over LTE
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VIDEO,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_UT,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_SMS,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onImsCapabilitiesChanged(
+                REGISTRATION_TECH_LTE, new MmTelCapabilities(CAPABILITY_TYPE_ALL));
+
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.conclude();
+
+        // No atom should be generated
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsCapabilitiesChanged_sameTech() throws Exception {
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onImsCapabilitiesChanged(
+                REGISTRATION_TECH_LTE, new MmTelCapabilities(CAPABILITY_TYPE_VOICE));
+
+        // Atom with previous feature availability should be generated
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(captor.capture());
+        ImsRegistrationStats stats = captor.getValue();
+        assertEquals(CARRIER1_ID, stats.carrierId);
+        assertEquals(0, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(2000L, stats.voiceCapableMillis);
+        assertEquals(0L, stats.voiceAvailableMillis);
+        assertEquals(0L, stats.videoCapableMillis);
+        assertEquals(0L, stats.videoAvailableMillis);
+        assertEquals(0L, stats.utCapableMillis);
+        assertEquals(0L, stats.utAvailableMillis);
+        assertEquals(0L, stats.smsCapableMillis);
+        assertEquals(0L, stats.smsAvailableMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onSetFeatureResponse_sameTech() throws Exception {
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VOICE,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onSetFeatureResponse(
+                CAPABILITY_TYPE_VIDEO,
+                REGISTRATION_TECH_LTE,
+                ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+
+        // Atom with previous capability should be generated
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(captor.capture());
+        ImsRegistrationStats stats = captor.getValue();
+        assertEquals(CARRIER1_ID, stats.carrierId);
+        assertEquals(0, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(2000L, stats.voiceCapableMillis);
+        assertEquals(0L, stats.voiceAvailableMillis);
+        assertEquals(0L, stats.videoCapableMillis);
+        assertEquals(0L, stats.videoAvailableMillis);
+        assertEquals(0L, stats.utCapableMillis);
+        assertEquals(0L, stats.utAvailableMillis);
+        assertEquals(0L, stats.smsCapableMillis);
+        assertEquals(0L, stats.smsAvailableMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsRegistered_differentTech() throws Exception {
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WLAN);
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+
+        // At this point, the first 2 registrations should have their durations counted
+        ArgumentCaptor<ImsRegistrationStats> captor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage, times(2)).addImsRegistrationStats(captor.capture());
+        assertEquals(2, captor.getAllValues().size());
+        ImsRegistrationStats statsLte = captor.getAllValues().get(0);
+        assertEquals(CARRIER1_ID, statsLte.carrierId);
+        assertEquals(0, statsLte.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, statsLte.rat);
+        assertEquals(2000L, statsLte.registeredMillis);
+        assertEquals(0L, statsLte.voiceCapableMillis);
+        assertEquals(0L, statsLte.voiceAvailableMillis);
+        assertEquals(0L, statsLte.videoCapableMillis);
+        assertEquals(0L, statsLte.videoAvailableMillis);
+        assertEquals(0L, statsLte.utCapableMillis);
+        assertEquals(0L, statsLte.utAvailableMillis);
+        assertEquals(0L, statsLte.smsCapableMillis);
+        assertEquals(0L, statsLte.smsAvailableMillis);
+        ImsRegistrationStats statsWifi = captor.getAllValues().get(1);
+        assertEquals(CARRIER1_ID, statsWifi.carrierId);
+        assertEquals(0, statsWifi.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_IWLAN, statsWifi.rat);
+        assertEquals(2000L, statsWifi.registeredMillis);
+        assertEquals(0L, statsWifi.voiceCapableMillis);
+        assertEquals(0L, statsWifi.voiceAvailableMillis);
+        assertEquals(0L, statsWifi.videoCapableMillis);
+        assertEquals(0L, statsWifi.videoAvailableMillis);
+        assertEquals(0L, statsWifi.utCapableMillis);
+        assertEquals(0L, statsWifi.utAvailableMillis);
+        assertEquals(0L, statsWifi.smsCapableMillis);
+        assertEquals(0L, statsWifi.smsAvailableMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsUnregistered_setupFailure() throws Exception {
+        mImsStats.onImsUnregistered(
+                new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 999, "Timeout"));
+
+        // Atom with termination info should be generated
+        ArgumentCaptor<ImsRegistrationTermination> captor =
+                ArgumentCaptor.forClass(ImsRegistrationTermination.class);
+        verify(mPersistAtomsStorage).addImsRegistrationTermination(captor.capture());
+        ImsRegistrationTermination termination = captor.getValue();
+        assertEquals(CARRIER1_ID, termination.carrierId);
+        assertFalse(termination.isMultiSim);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, termination.ratAtEnd);
+        assertTrue(termination.setupFailed);
+        assertEquals(ImsReasonInfo.CODE_REGISTRATION_ERROR, termination.reasonCode);
+        assertEquals(999, termination.extraCode);
+        assertEquals("Timeout", termination.extraMessage);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsUnregistered_setupFailureWithProgress() throws Exception {
+        mImsStats.onImsRegistering(REGISTRATION_TECH_LTE);
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onImsUnregistered(
+                new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 999, "Timeout"));
+
+        // Atom with termination info should be generated
+        ArgumentCaptor<ImsRegistrationTermination> captor =
+                ArgumentCaptor.forClass(ImsRegistrationTermination.class);
+        verify(mPersistAtomsStorage).addImsRegistrationTermination(captor.capture());
+        ImsRegistrationTermination termination = captor.getValue();
+        assertEquals(CARRIER1_ID, termination.carrierId);
+        assertFalse(termination.isMultiSim);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, termination.ratAtEnd);
+        assertTrue(termination.setupFailed);
+        assertEquals(ImsReasonInfo.CODE_REGISTRATION_ERROR, termination.reasonCode);
+        assertEquals(999, termination.extraCode);
+        assertEquals("Timeout", termination.extraMessage);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsUnregistered_afterRegistered() throws Exception {
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onImsUnregistered(
+                new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 999, "Timeout"));
+
+        // Atom with termination info and durations should be generated
+        ArgumentCaptor<ImsRegistrationStats> statsCaptor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(statsCaptor.capture());
+        ImsRegistrationStats stats = statsCaptor.getValue();
+        assertEquals(CARRIER1_ID, stats.carrierId);
+        assertEquals(0, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(0L, stats.voiceCapableMillis);
+        assertEquals(0L, stats.voiceAvailableMillis);
+        assertEquals(0L, stats.videoCapableMillis);
+        assertEquals(0L, stats.videoAvailableMillis);
+        assertEquals(0L, stats.utCapableMillis);
+        assertEquals(0L, stats.utAvailableMillis);
+        assertEquals(0L, stats.smsCapableMillis);
+        assertEquals(0L, stats.smsAvailableMillis);
+        ArgumentCaptor<ImsRegistrationTermination> terminationCaptor =
+                ArgumentCaptor.forClass(ImsRegistrationTermination.class);
+        verify(mPersistAtomsStorage).addImsRegistrationTermination(terminationCaptor.capture());
+        ImsRegistrationTermination termination = terminationCaptor.getValue();
+        assertEquals(CARRIER1_ID, termination.carrierId);
+        assertFalse(termination.isMultiSim);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, termination.ratAtEnd);
+        assertFalse(termination.setupFailed);
+        assertEquals(ImsReasonInfo.CODE_REGISTRATION_ERROR, termination.reasonCode);
+        assertEquals(999, termination.extraCode);
+        assertEquals("Timeout", termination.extraMessage);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsUnregistered_nullMessage() throws Exception {
+        mImsStats.onImsUnregistered(
+                new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 0, null));
+
+        // Atom with termination info should be generated, null string should be sanitized
+        ArgumentCaptor<ImsRegistrationTermination> captor =
+                ArgumentCaptor.forClass(ImsRegistrationTermination.class);
+        verify(mPersistAtomsStorage).addImsRegistrationTermination(captor.capture());
+        ImsRegistrationTermination termination = captor.getValue();
+        assertEquals(CARRIER1_ID, termination.carrierId);
+        assertFalse(termination.isMultiSim);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, termination.ratAtEnd);
+        assertTrue(termination.setupFailed);
+        assertEquals(ImsReasonInfo.CODE_REGISTRATION_ERROR, termination.reasonCode);
+        assertEquals(0, termination.extraCode);
+        assertEquals("", termination.extraMessage);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsUnregistered_longMessage() throws Exception {
+        String longExtraMessage =
+                "This message is too long -- it has more than 128 characters: "
+                        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+                        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+                        + " This is the end of the message.";
+        mImsStats.onImsUnregistered(
+                new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 0, longExtraMessage));
+
+        // Atom with termination info should be generated, null string should be sanitized
+        ArgumentCaptor<ImsRegistrationTermination> captor =
+                ArgumentCaptor.forClass(ImsRegistrationTermination.class);
+        verify(mPersistAtomsStorage).addImsRegistrationTermination(captor.capture());
+        ImsRegistrationTermination termination = captor.getValue();
+        assertEquals(CARRIER1_ID, termination.carrierId);
+        assertFalse(termination.isMultiSim);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, termination.ratAtEnd);
+        assertTrue(termination.setupFailed);
+        assertEquals(ImsReasonInfo.CODE_REGISTRATION_ERROR, termination.reasonCode);
+        assertEquals(0, termination.extraCode);
+        assertEquals(128, termination.extraMessage.length());
+        assertTrue(longExtraMessage.startsWith(termination.extraMessage));
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onImsUnregistered_multiSim() throws Exception {
+        doReturn(mSecondImsPhone).when(mSecondPhone).getImsPhone();
+        doReturn(mSecondPhone).when(mSecondImsPhone).getDefaultPhone();
+        doReturn(1).when(mSecondPhone).getPhoneId();
+        doReturn(1).when(mSecondImsPhone).getPhoneId();
+        doReturn(CARRIER2_ID).when(mSecondPhone).getCarrierId();
+        doReturn(true).when(mPhysicalSlot1).isActive();
+        doReturn(CardState.CARDSTATE_PRESENT).when(mPhysicalSlot1).getCardState();
+        doReturn(false).when(mPhysicalSlot1).isEuicc();
+        doReturn(new UiccSlot[] {mPhysicalSlot0, mPhysicalSlot1})
+                .when(mUiccController)
+                .getUiccSlots();
+        doReturn(mPhysicalSlot1).when(mUiccController).getUiccSlot(1);
+        doReturn(mPhysicalSlot1).when(mUiccController).getUiccSlotForPhone(1);
+        // Reusing service state tracker from phone 0 for simplicity
+        doReturn(mSST).when(mSecondPhone).getServiceStateTracker();
+        doReturn(mSST).when(mSecondImsPhone).getServiceStateTracker();
+        mImsStats = new TestableImsStats(mSecondImsPhone);
+        mImsStats.onImsRegistered(TRANSPORT_TYPE_WWAN);
+        mImsStats.incTimeMillis(2000L);
+        mImsStats.onImsUnregistered(
+                new ImsReasonInfo(ImsReasonInfo.CODE_REGISTRATION_ERROR, 999, "Timeout"));
+
+        // Atom with termination info and durations should be generated
+        ArgumentCaptor<ImsRegistrationStats> statsCaptor =
+                ArgumentCaptor.forClass(ImsRegistrationStats.class);
+        verify(mPersistAtomsStorage).addImsRegistrationStats(statsCaptor.capture());
+        ImsRegistrationStats stats = statsCaptor.getValue();
+        assertEquals(CARRIER2_ID, stats.carrierId);
+        assertEquals(1, stats.simSlotIndex);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, stats.rat);
+        assertEquals(2000L, stats.registeredMillis);
+        assertEquals(0L, stats.voiceCapableMillis);
+        assertEquals(0L, stats.voiceAvailableMillis);
+        assertEquals(0L, stats.videoCapableMillis);
+        assertEquals(0L, stats.videoAvailableMillis);
+        assertEquals(0L, stats.utCapableMillis);
+        assertEquals(0L, stats.utAvailableMillis);
+        assertEquals(0L, stats.smsCapableMillis);
+        assertEquals(0L, stats.smsAvailableMillis);
+        ArgumentCaptor<ImsRegistrationTermination> terminationCaptor =
+                ArgumentCaptor.forClass(ImsRegistrationTermination.class);
+        verify(mPersistAtomsStorage).addImsRegistrationTermination(terminationCaptor.capture());
+        ImsRegistrationTermination termination = terminationCaptor.getValue();
+        assertEquals(CARRIER2_ID, termination.carrierId);
+        assertTrue(termination.isMultiSim);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, termination.ratAtEnd);
+        assertFalse(termination.setupFailed);
+        assertEquals(ImsReasonInfo.CODE_REGISTRATION_ERROR, termination.reasonCode);
+        assertEquals(999, termination.extraCode);
+        assertEquals("Timeout", termination.extraMessage);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/MetricsCollectorTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/MetricsCollectorTest.java
index 103cae1..cd15686 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/metrics/MetricsCollectorTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/MetricsCollectorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.telephony.metrics;
 
+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.SIM_SLOT_STATE;
 import static com.android.internal.telephony.TelephonyStatsLog.SUPPORTED_RADIO_ACCESS_FAMILY;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_RAT_USAGE;
@@ -38,7 +40,9 @@
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneFactory;
 import com.android.internal.telephony.TelephonyTest;
-import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
+import com.android.internal.telephony.nano.PersistAtomsProto.CellularDataServiceSwitch;
+import com.android.internal.telephony.nano.PersistAtomsProto.CellularServiceState;
+import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage;
 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
 import com.android.internal.telephony.uicc.IccCardStatus.CardState;
 import com.android.internal.telephony.uicc.UiccCard;
@@ -82,6 +86,8 @@
     @Mock private UiccSlot mEsimSlot;
     @Mock private UiccCard mActiveCard;
 
+    @Mock private ServiceStateStats mServiceStateStats;
+
     private MetricsCollector mMetricsCollector;
 
     @Before
@@ -89,6 +95,8 @@
         super.setUp(getClass().getSimpleName());
         mMetricsCollector = new MetricsCollector(mContext);
         mMetricsCollector.setPersistAtomsStorage(mPersistAtomsStorage);
+        doReturn(mSST).when(mSecondPhone).getServiceStateTracker();
+        doReturn(mServiceStateStats).when(mSST).getServiceStateStats();
     }
 
     @After
@@ -216,7 +224,7 @@
     @Test
     @SmallTest
     public void onPullAtom_voiceCallRatUsage_empty() throws Exception {
-        doReturn(new RawVoiceCallRatUsage[0])
+        doReturn(new VoiceCallRatUsage[0])
                 .when(mPersistAtomsStorage)
                 .getVoiceCallRatUsages(anyLong());
         List<StatsEvent> actualAtoms = new ArrayList<>();
@@ -244,11 +252,11 @@
     @Test
     @SmallTest
     public void onPullAtom_voiceCallRatUsage_bucketWithTooFewCalls() throws Exception {
-        RawVoiceCallRatUsage usage1 = new RawVoiceCallRatUsage();
+        VoiceCallRatUsage usage1 = new VoiceCallRatUsage();
         usage1.callCount = MIN_CALLS_PER_BUCKET;
-        RawVoiceCallRatUsage usage2 = new RawVoiceCallRatUsage();
+        VoiceCallRatUsage usage2 = new VoiceCallRatUsage();
         usage2.callCount = MIN_CALLS_PER_BUCKET - 1L;
-        doReturn(new RawVoiceCallRatUsage[] {usage1, usage1, usage1, usage2})
+        doReturn(new VoiceCallRatUsage[] {usage1, usage1, usage1, usage2})
                 .when(mPersistAtomsStorage)
                 .getVoiceCallRatUsages(anyLong());
         List<StatsEvent> actualAtoms = new ArrayList<>();
@@ -303,4 +311,95 @@
         assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
         // TODO(b/153196254): verify atom contents
     }
+
+    @Test
+    @SmallTest
+    public void onPullAtom_cellularDataServiceSwitch_empty() throws Exception {
+        doReturn(new CellularDataServiceSwitch[0])
+                .when(mPersistAtomsStorage)
+                .getCellularDataServiceSwitches(anyLong());
+        List<StatsEvent> actualAtoms = new ArrayList<>();
+
+        int result = mMetricsCollector.onPullAtom(CELLULAR_DATA_SERVICE_SWITCH, actualAtoms);
+
+        assertThat(actualAtoms).hasSize(0);
+        assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+        // TODO(b/153196254): verify atom contents
+    }
+
+    @Test
+    @SmallTest
+    public void onPullAtom_cellularDataServiceSwitch_tooFrequent() throws Exception {
+        doReturn(null).when(mPersistAtomsStorage).getCellularDataServiceSwitches(anyLong());
+        List<StatsEvent> actualAtoms = new ArrayList<>();
+
+        int result = mMetricsCollector.onPullAtom(CELLULAR_DATA_SERVICE_SWITCH, actualAtoms);
+
+        assertThat(actualAtoms).hasSize(0);
+        assertThat(result).isEqualTo(StatsManager.PULL_SKIP);
+        verify(mPersistAtomsStorage, times(1))
+                .getCellularDataServiceSwitches(eq(MIN_COOLDOWN_MILLIS));
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onPullAtom_cellularDataServiceSwitch_multipleSwitches() throws Exception {
+        CellularDataServiceSwitch serviceSwitch = new CellularDataServiceSwitch();
+        doReturn(new CellularDataServiceSwitch[] {serviceSwitch, serviceSwitch, serviceSwitch})
+                .when(mPersistAtomsStorage)
+                .getCellularDataServiceSwitches(anyLong());
+        List<StatsEvent> actualAtoms = new ArrayList<>();
+
+        int result = mMetricsCollector.onPullAtom(CELLULAR_DATA_SERVICE_SWITCH, actualAtoms);
+
+        assertThat(actualAtoms).hasSize(3);
+        assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+        // TODO(b/153196254): verify atom contents
+    }
+
+    @Test
+    @SmallTest
+    public void onPullAtom_cellularServiceState_empty() throws Exception {
+        doReturn(new CellularServiceState[0])
+                .when(mPersistAtomsStorage)
+                .getCellularServiceStates(anyLong());
+        List<StatsEvent> actualAtoms = new ArrayList<>();
+
+        int result = mMetricsCollector.onPullAtom(CELLULAR_SERVICE_STATE, actualAtoms);
+
+        assertThat(actualAtoms).hasSize(0);
+        assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+        // TODO(b/153196254): verify atom contents
+    }
+
+    @Test
+    @SmallTest
+    public void onPullAtom_cellularServiceState_tooFrequent() throws Exception {
+        doReturn(null).when(mPersistAtomsStorage).getCellularServiceStates(anyLong());
+        List<StatsEvent> actualAtoms = new ArrayList<>();
+
+        int result = mMetricsCollector.onPullAtom(CELLULAR_SERVICE_STATE, actualAtoms);
+
+        assertThat(actualAtoms).hasSize(0);
+        assertThat(result).isEqualTo(StatsManager.PULL_SKIP);
+        verify(mPersistAtomsStorage, times(1)).getCellularServiceStates(eq(MIN_COOLDOWN_MILLIS));
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void onPullAtom_cellularServiceState_multipleStates() throws Exception {
+        CellularServiceState state = new CellularServiceState();
+        doReturn(new CellularServiceState[] {state, state, state})
+                .when(mPersistAtomsStorage)
+                .getCellularServiceStates(anyLong());
+        List<StatsEvent> actualAtoms = new ArrayList<>();
+
+        int result = mMetricsCollector.onPullAtom(CELLULAR_SERVICE_STATE, actualAtoms);
+
+        assertThat(actualAtoms).hasSize(3);
+        assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+        // TODO(b/153196254): verify atom contents
+    }
 }
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 ac1d73f..54acb92 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/metrics/PersistAtomsStorageTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/PersistAtomsStorageTest.java
@@ -38,13 +38,18 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.telephony.DisconnectCause;
+import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsReasonInfo;
 import android.test.suitebuilder.annotation.SmallTest;
 
 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.ImsRegistrationStats;
+import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination;
 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms;
-import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
+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;
 import com.android.internal.telephony.protobuf.nano.MessageNano;
@@ -63,6 +68,8 @@
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.Queue;
 
 public class PersistAtomsStorageTest extends TelephonyTest {
     private static final String TEST_FILE = "PersistAtomsStorageTest.pb";
@@ -88,14 +95,39 @@
     // failed call
     private VoiceCallSession mCall4Proto;
 
-    private RawVoiceCallRatUsage mCarrier1LteUsageProto;
-    private RawVoiceCallRatUsage mCarrier1UmtsUsageProto;
-    private RawVoiceCallRatUsage mCarrier2LteUsageProto;
-    private RawVoiceCallRatUsage mCarrier3LteUsageProto;
-    private RawVoiceCallRatUsage mCarrier3GsmUsageProto;
+    private VoiceCallRatUsage mCarrier1LteUsageProto;
+    private VoiceCallRatUsage mCarrier1UmtsUsageProto;
+    private VoiceCallRatUsage mCarrier2LteUsageProto;
+    private VoiceCallRatUsage mCarrier3LteUsageProto;
+    private VoiceCallRatUsage mCarrier3GsmUsageProto;
 
     private VoiceCallSession[] mVoiceCallSessions;
-    private RawVoiceCallRatUsage[] mVoiceCallRatUsages;
+    private VoiceCallRatUsage[] mVoiceCallRatUsages;
+
+    // data service state switch for slot 0 and 1
+    private CellularDataServiceSwitch mServiceSwitch1Proto;
+    private CellularDataServiceSwitch mServiceSwitch2Proto;
+
+    // service states for slot 0 and 1
+    private CellularServiceState mServiceState1Proto;
+    private CellularServiceState mServiceState2Proto;
+    private CellularServiceState mServiceState3Proto;
+    private CellularServiceState mServiceState4Proto;
+
+    private CellularDataServiceSwitch[] mServiceSwitches;
+    private CellularServiceState[] mServiceStates;
+
+    // IMS registrations for slot 0 and 1
+    private ImsRegistrationStats mImsRegistrationStatsLte0;
+    private ImsRegistrationStats mImsRegistrationStatsWifi0;
+    private ImsRegistrationStats mImsRegistrationStatsLte1;
+
+    // IMS registration terminations for slot 0 and 1
+    private ImsRegistrationTermination mImsRegistrationTerminationLte;
+    private ImsRegistrationTermination mImsRegistrationTerminationWifi;
+
+    private ImsRegistrationStats[] mImsRegistrationStats;
+    private ImsRegistrationTermination[] mImsRegistrationTerminations;
 
     private void makeTestData() {
         // MO call with SRVCC (LTE to UMTS)
@@ -213,38 +245,38 @@
         mCall4Proto.isEmergency = false;
         mCall4Proto.isRoaming = true;
 
-        mCarrier1LteUsageProto = new RawVoiceCallRatUsage();
+        mCarrier1LteUsageProto = new VoiceCallRatUsage();
         mCarrier1LteUsageProto.carrierId = CARRIER1_ID;
         mCarrier1LteUsageProto.rat = TelephonyManager.NETWORK_TYPE_LTE;
         mCarrier1LteUsageProto.callCount = 1L;
         mCarrier1LteUsageProto.totalDurationMillis = 8000L;
 
-        mCarrier1UmtsUsageProto = new RawVoiceCallRatUsage();
+        mCarrier1UmtsUsageProto = new VoiceCallRatUsage();
         mCarrier1UmtsUsageProto.carrierId = CARRIER1_ID;
         mCarrier1UmtsUsageProto.rat = TelephonyManager.NETWORK_TYPE_UMTS;
         mCarrier1UmtsUsageProto.callCount = 1L;
         mCarrier1UmtsUsageProto.totalDurationMillis = 6000L;
 
-        mCarrier2LteUsageProto = new RawVoiceCallRatUsage();
+        mCarrier2LteUsageProto = new VoiceCallRatUsage();
         mCarrier2LteUsageProto.carrierId = CARRIER2_ID;
         mCarrier2LteUsageProto.rat = TelephonyManager.NETWORK_TYPE_LTE;
         mCarrier2LteUsageProto.callCount = 2L;
         mCarrier2LteUsageProto.totalDurationMillis = 20000L;
 
-        mCarrier3LteUsageProto = new RawVoiceCallRatUsage();
+        mCarrier3LteUsageProto = new VoiceCallRatUsage();
         mCarrier3LteUsageProto.carrierId = CARRIER3_ID;
         mCarrier3LteUsageProto.rat = TelephonyManager.NETWORK_TYPE_LTE;
         mCarrier3LteUsageProto.callCount = 1L;
         mCarrier3LteUsageProto.totalDurationMillis = 1000L;
 
-        mCarrier3GsmUsageProto = new RawVoiceCallRatUsage();
+        mCarrier3GsmUsageProto = new VoiceCallRatUsage();
         mCarrier3GsmUsageProto.carrierId = CARRIER3_ID;
         mCarrier3GsmUsageProto.rat = TelephonyManager.NETWORK_TYPE_GSM;
         mCarrier3GsmUsageProto.callCount = 1L;
         mCarrier3GsmUsageProto.totalDurationMillis = 100000L;
 
         mVoiceCallRatUsages =
-                new RawVoiceCallRatUsage[] {
+                new VoiceCallRatUsage[] {
                     mCarrier1UmtsUsageProto,
                     mCarrier1LteUsageProto,
                     mCarrier2LteUsageProto,
@@ -253,6 +285,152 @@
                 };
         mVoiceCallSessions =
                 new VoiceCallSession[] {mCall1Proto, mCall2Proto, mCall3Proto, mCall4Proto};
+
+        // OOS to LTE on slot 0
+        mServiceSwitch1Proto = new CellularDataServiceSwitch();
+        mServiceSwitch1Proto.ratFrom = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        mServiceSwitch1Proto.ratTo = TelephonyManager.NETWORK_TYPE_LTE;
+        mServiceSwitch1Proto.simSlotIndex = 0;
+        mServiceSwitch1Proto.isMultiSim = true;
+        mServiceSwitch1Proto.carrierId = CARRIER1_ID;
+        mServiceSwitch1Proto.switchCount = 1;
+
+        // LTE to UMTS on slot 1
+        mServiceSwitch2Proto = new CellularDataServiceSwitch();
+        mServiceSwitch2Proto.ratFrom = TelephonyManager.NETWORK_TYPE_LTE;
+        mServiceSwitch2Proto.ratTo = TelephonyManager.NETWORK_TYPE_UMTS;
+        mServiceSwitch2Proto.simSlotIndex = 0;
+        mServiceSwitch2Proto.isMultiSim = true;
+        mServiceSwitch2Proto.carrierId = CARRIER2_ID;
+        mServiceSwitch2Proto.switchCount = 2;
+
+        // OOS on slot 0
+        mServiceState1Proto = new CellularServiceState();
+        mServiceState1Proto.voiceRat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        mServiceState1Proto.dataRat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        mServiceState1Proto.voiceRoamingType = ServiceState.ROAMING_TYPE_NOT_ROAMING;
+        mServiceState1Proto.dataRoamingType = ServiceState.ROAMING_TYPE_NOT_ROAMING;
+        mServiceState1Proto.isEndc = false;
+        mServiceState1Proto.simSlotIndex = 0;
+        mServiceState1Proto.isMultiSim = true;
+        mServiceState1Proto.carrierId = CARRIER1_ID;
+        mServiceState1Proto.totalTimeMillis = 5000L;
+
+        // LTE with ENDC on slot 0
+        mServiceState2Proto = new CellularServiceState();
+        mServiceState2Proto.voiceRat = TelephonyManager.NETWORK_TYPE_LTE;
+        mServiceState2Proto.dataRat = TelephonyManager.NETWORK_TYPE_LTE;
+        mServiceState2Proto.voiceRoamingType = ServiceState.ROAMING_TYPE_NOT_ROAMING;
+        mServiceState2Proto.dataRoamingType = ServiceState.ROAMING_TYPE_NOT_ROAMING;
+        mServiceState2Proto.isEndc = true;
+        mServiceState2Proto.simSlotIndex = 0;
+        mServiceState2Proto.isMultiSim = true;
+        mServiceState2Proto.carrierId = CARRIER1_ID;
+        mServiceState2Proto.totalTimeMillis = 15000L;
+
+        // LTE with WFC and roaming on slot 1
+        mServiceState3Proto = new CellularServiceState();
+        mServiceState3Proto.voiceRat = TelephonyManager.NETWORK_TYPE_IWLAN;
+        mServiceState3Proto.dataRat = TelephonyManager.NETWORK_TYPE_LTE;
+        mServiceState3Proto.voiceRoamingType = ServiceState.ROAMING_TYPE_INTERNATIONAL;
+        mServiceState3Proto.dataRoamingType = ServiceState.ROAMING_TYPE_INTERNATIONAL;
+        mServiceState3Proto.isEndc = false;
+        mServiceState3Proto.simSlotIndex = 1;
+        mServiceState3Proto.isMultiSim = true;
+        mServiceState3Proto.carrierId = CARRIER2_ID;
+        mServiceState3Proto.totalTimeMillis = 10000L;
+
+        // UMTS with roaming on slot 1
+        mServiceState4Proto = new CellularServiceState();
+        mServiceState4Proto.voiceRat = TelephonyManager.NETWORK_TYPE_UMTS;
+        mServiceState4Proto.dataRat = TelephonyManager.NETWORK_TYPE_UMTS;
+        mServiceState4Proto.voiceRoamingType = ServiceState.ROAMING_TYPE_INTERNATIONAL;
+        mServiceState4Proto.dataRoamingType = ServiceState.ROAMING_TYPE_INTERNATIONAL;
+        mServiceState4Proto.isEndc = false;
+        mServiceState4Proto.simSlotIndex = 1;
+        mServiceState4Proto.isMultiSim = true;
+        mServiceState4Proto.carrierId = CARRIER2_ID;
+        mServiceState4Proto.totalTimeMillis = 10000L;
+
+        mServiceSwitches =
+                new CellularDataServiceSwitch[] {mServiceSwitch1Proto, mServiceSwitch2Proto};
+        mServiceStates =
+                new CellularServiceState[] {
+                    mServiceState1Proto,
+                    mServiceState2Proto,
+                    mServiceState3Proto,
+                    mServiceState4Proto
+                };
+
+        // IMS over LTE on slot 0, registered for 5 seconds
+        mImsRegistrationStatsLte0 = new ImsRegistrationStats();
+        mImsRegistrationStatsLte0.carrierId = CARRIER1_ID;
+        mImsRegistrationStatsLte0.simSlotIndex = 0;
+        mImsRegistrationStatsLte0.rat = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsRegistrationStatsLte0.registeredMillis = 5000L;
+        mImsRegistrationStatsLte0.voiceCapableMillis = 5000L;
+        mImsRegistrationStatsLte0.voiceAvailableMillis = 5000L;
+        mImsRegistrationStatsLte0.smsCapableMillis = 5000L;
+        mImsRegistrationStatsLte0.smsAvailableMillis = 5000L;
+        mImsRegistrationStatsLte0.videoCapableMillis = 5000L;
+        mImsRegistrationStatsLte0.videoAvailableMillis = 5000L;
+        mImsRegistrationStatsLte0.utCapableMillis = 5000L;
+        mImsRegistrationStatsLte0.utAvailableMillis = 5000L;
+
+        // IMS over WiFi on slot 0, registered for 10 seconds (voice only)
+        mImsRegistrationStatsWifi0 = new ImsRegistrationStats();
+        mImsRegistrationStatsWifi0.carrierId = CARRIER2_ID;
+        mImsRegistrationStatsWifi0.simSlotIndex = 0;
+        mImsRegistrationStatsWifi0.rat = TelephonyManager.NETWORK_TYPE_IWLAN;
+        mImsRegistrationStatsWifi0.registeredMillis = 10000L;
+        mImsRegistrationStatsWifi0.voiceCapableMillis = 10000L;
+        mImsRegistrationStatsWifi0.voiceAvailableMillis = 10000L;
+
+        // IMS over LTE on slot 1, registered for 20 seconds
+        mImsRegistrationStatsLte1 = new ImsRegistrationStats();
+        mImsRegistrationStatsLte1.carrierId = CARRIER1_ID;
+        mImsRegistrationStatsLte1.simSlotIndex = 0;
+        mImsRegistrationStatsLte1.rat = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsRegistrationStatsLte1.registeredMillis = 20000L;
+        mImsRegistrationStatsLte1.voiceCapableMillis = 20000L;
+        mImsRegistrationStatsLte1.voiceAvailableMillis = 20000L;
+        mImsRegistrationStatsLte1.smsCapableMillis = 20000L;
+        mImsRegistrationStatsLte1.smsAvailableMillis = 20000L;
+        mImsRegistrationStatsLte1.videoCapableMillis = 20000L;
+        mImsRegistrationStatsLte1.videoAvailableMillis = 20000L;
+        mImsRegistrationStatsLte1.utCapableMillis = 20000L;
+        mImsRegistrationStatsLte1.utAvailableMillis = 20000L;
+
+        // IMS terminations on LTE
+        mImsRegistrationTerminationLte = new ImsRegistrationTermination();
+        mImsRegistrationTerminationLte.carrierId = CARRIER1_ID;
+        mImsRegistrationTerminationLte.isMultiSim = true;
+        mImsRegistrationTerminationLte.ratAtEnd = TelephonyManager.NETWORK_TYPE_LTE;
+        mImsRegistrationTerminationLte.setupFailed = false;
+        mImsRegistrationTerminationLte.reasonCode = ImsReasonInfo.CODE_REGISTRATION_ERROR;
+        mImsRegistrationTerminationLte.extraCode = 999;
+        mImsRegistrationTerminationLte.extraMessage = "Request Timeout";
+        mImsRegistrationTerminationLte.count = 2;
+
+        // IMS terminations on WiFi
+        mImsRegistrationTerminationWifi = new ImsRegistrationTermination();
+        mImsRegistrationTerminationWifi.carrierId = CARRIER2_ID;
+        mImsRegistrationTerminationWifi.isMultiSim = true;
+        mImsRegistrationTerminationWifi.ratAtEnd = TelephonyManager.NETWORK_TYPE_IWLAN;
+        mImsRegistrationTerminationWifi.setupFailed = false;
+        mImsRegistrationTerminationWifi.reasonCode = ImsReasonInfo.CODE_REGISTRATION_ERROR;
+        mImsRegistrationTerminationWifi.extraCode = 0;
+        mImsRegistrationTerminationWifi.extraMessage = "";
+        mImsRegistrationTerminationWifi.count = 1;
+
+        mImsRegistrationStats =
+                new ImsRegistrationStats[] {
+                    mImsRegistrationStatsLte0, mImsRegistrationStatsWifi0, mImsRegistrationStatsLte1
+                };
+        mImsRegistrationTerminations =
+                new ImsRegistrationTermination[] {
+                    mImsRegistrationTerminationLte, mImsRegistrationTerminationWifi
+                };
     }
 
     private static class TestablePersistAtomsStorage extends PersistAtomsStorage {
@@ -260,6 +438,8 @@
 
         TestablePersistAtomsStorage(Context context) {
             super(context);
+            // Remove delay for saving to persistent storage during tests.
+            mSaveDelay = 0;
         }
 
         @Override
@@ -278,7 +458,8 @@
         }
 
         private PersistAtoms getAtomsProto() {
-            // NOTE: not guarded by mLock as usual, should be fine since the test is single-threaded
+            // NOTE: unlike other methods in PersistAtomsStorage, this is not synchronized, but
+            // should be fine since the test is single-threaded
             return mAtoms;
         }
     }
@@ -329,18 +510,8 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // no exception should be thrown, storage should be empty, pull time should be start time
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(0, voiceCallRatUsage.length);
-        assertNotNull(voiceCallSession);
-        assertEquals(0, voiceCallSession.length);
+        assertAllPullTimestampEquals(START_TIME_MILLIS);
+        assertStorageIsEmptyForAllAtoms();
     }
 
     @Test
@@ -353,18 +524,8 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // no exception should be thrown, storage should be empty, pull time should be start time
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(0, voiceCallRatUsage.length);
-        assertNotNull(voiceCallSession);
-        assertEquals(0, voiceCallSession.length);
+        assertAllPullTimestampEquals(START_TIME_MILLIS);
+        assertStorageIsEmptyForAllAtoms();
     }
 
     @Test
@@ -376,18 +537,8 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // no exception should be thrown, storage should be empty, pull time should be start time
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(0, voiceCallRatUsage.length);
-        assertNotNull(voiceCallSession);
-        assertEquals(0, voiceCallSession.length);
+        assertAllPullTimestampEquals(START_TIME_MILLIS);
+        assertStorageIsEmptyForAllAtoms();
     }
 
     @Test
@@ -401,57 +552,44 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // no exception should be thrown, storage should be empty, pull time should be start time
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(0, voiceCallRatUsage.length);
-        assertNotNull(voiceCallSession);
-        assertEquals(0, voiceCallSession.length);
+        assertAllPullTimestampEquals(START_TIME_MILLIS);
+        assertStorageIsEmptyForAllAtoms();
     }
 
     @Test
     @SmallTest
     public void loadAtoms_pullTimeMissing() throws Exception {
+        // create test file with lastPullTimeMillis = 0L, i.e. default/unknown
         createTestFile(0L);
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // no exception should be thrown, storage should be match, pull time should be start time
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
-        assertEquals(
-                START_TIME_MILLIS,
-                mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertProtoArrayEquals(mVoiceCallRatUsages, voiceCallRatUsage);
-        assertProtoArrayEquals(mVoiceCallSessions, voiceCallSession);
+        assertAllPullTimestampEquals(START_TIME_MILLIS);
+        assertProtoArrayEquals(mVoiceCallRatUsages, mPersistAtomsStorage.getVoiceCallRatUsages(0L));
+        assertProtoArrayEquals(mVoiceCallSessions, mPersistAtomsStorage.getVoiceCallSessions(0L));
+        assertProtoArrayEqualsIgnoringOrder(
+                mServiceStates, mPersistAtomsStorage.getCellularServiceStates(0L));
+        assertProtoArrayEqualsIgnoringOrder(
+                mServiceSwitches, mPersistAtomsStorage.getCellularDataServiceSwitches(0L));
     }
 
     @Test
     @SmallTest
     public void loadAtoms_validContents() throws Exception {
-        createTestFile(100L);
+        createTestFile(/* lastPullTimeMillis= */ 100L);
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
 
         // no exception should be thrown, storage and pull time should match
-        assertEquals(
-                100L, mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
-        assertEquals(
-                100L, mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertProtoArrayEquals(mVoiceCallRatUsages, voiceCallRatUsage);
-        assertProtoArrayEquals(mVoiceCallSessions, voiceCallSession);
+        assertAllPullTimestampEquals(100L);
+        assertProtoArrayEquals(mVoiceCallRatUsages, mPersistAtomsStorage.getVoiceCallRatUsages(0L));
+        assertProtoArrayEquals(mVoiceCallSessions, mPersistAtomsStorage.getVoiceCallSessions(0L));
+        assertProtoArrayEqualsIgnoringOrder(
+                mServiceStates, mPersistAtomsStorage.getCellularServiceStates(0L));
+        assertProtoArrayEqualsIgnoringOrder(
+                mServiceSwitches, mPersistAtomsStorage.getCellularDataServiceSwitches(0L));
     }
 
     @Test
@@ -464,46 +602,31 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // call should be added successfully, there should be no RAT usage, changes should be saved
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
+        verifyCurrentStateSavedToFileOnce();
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getVoiceCallRatUsages(0L));
         VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(0, voiceCallRatUsage.length);
         assertProtoArrayEquals(new VoiceCallSession[] {mCall1Proto}, voiceCallSession);
-        InOrder inOrder = inOrder(mTestFileOutputStream);
-        inOrder.verify(mTestFileOutputStream, times(1))
-                .write(eq(PersistAtoms.toByteArray(mPersistAtomsStorage.getAtomsProto())));
-        inOrder.verify(mTestFileOutputStream, times(1)).close();
-        inOrder.verifyNoMoreInteractions();
     }
 
     @Test
     @SmallTest
     public void addVoiceCallSession_withExistingCalls() throws Exception {
-        createTestFile(100L);
+        createTestFile(START_TIME_MILLIS);
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
         mPersistAtomsStorage.addVoiceCallSession(mCall1Proto);
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // call should be added successfully, RAT usages should not change, changes should be saved
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(mVoiceCallRatUsages.length, voiceCallRatUsage.length);
-        assertNotNull(voiceCallSession);
-        // call lists are randomized, but sorted version should be identical
+        assertProtoArrayEquals(mVoiceCallRatUsages, mPersistAtomsStorage.getVoiceCallRatUsages(0L));
         VoiceCallSession[] expectedVoiceCallSessions =
                 new VoiceCallSession[] {
                     mCall1Proto, mCall1Proto, mCall2Proto, mCall3Proto, mCall4Proto
                 };
-        Arrays.sort(expectedVoiceCallSessions, sProtoComparator);
-        Arrays.sort(voiceCallSession, sProtoComparator);
-        assertProtoArrayEquals(expectedVoiceCallSessions, voiceCallSession);
-        InOrder inOrder = inOrder(mTestFileOutputStream);
-        inOrder.verify(mTestFileOutputStream, times(1))
-                .write(eq(PersistAtoms.toByteArray(mPersistAtomsStorage.getAtomsProto())));
-        inOrder.verify(mTestFileOutputStream, times(1)).close();
-        inOrder.verifyNoMoreInteractions();
+        // call list is randomized at this point
+        verifyCurrentStateSavedToFileOnce();
+        assertProtoArrayEqualsIgnoringOrder(
+                expectedVoiceCallSessions, mPersistAtomsStorage.getVoiceCallSessions(0L));
     }
 
     @Test
@@ -517,9 +640,10 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // one previous call should be evicted, the new call should be added
+        verifyCurrentStateSavedToFileOnce();
         VoiceCallSession[] calls = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertHasCall(calls, mCall1Proto, 49);
-        assertHasCall(calls, mCall2Proto, 1);
+        assertHasCall(calls, mCall1Proto, /* expectedCount= */ 49);
+        assertHasCall(calls, mCall2Proto, /* expectedCount= */ 1);
     }
 
     @Test
@@ -533,25 +657,16 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // RAT should be added successfully, calls should not change, changes should be saved
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        RawVoiceCallRatUsage[] expectedVoiceCallRatUsage = mVoiceCallRatUsages.clone();
-        Arrays.sort(expectedVoiceCallRatUsage, sProtoComparator);
-        Arrays.sort(voiceCallRatUsage, sProtoComparator);
-        assertProtoArrayEquals(expectedVoiceCallRatUsage, voiceCallRatUsage);
-        assertNotNull(voiceCallSession);
-        assertEquals(0, voiceCallSession.length);
-        InOrder inOrder = inOrder(mTestFileOutputStream);
-        inOrder.verify(mTestFileOutputStream, times(1))
-                .write(eq(PersistAtoms.toByteArray(mPersistAtomsStorage.getAtomsProto())));
-        inOrder.verify(mTestFileOutputStream, times(1)).close();
-        inOrder.verifyNoMoreInteractions();
+        verifyCurrentStateSavedToFileOnce();
+        assertProtoArrayEqualsIgnoringOrder(
+                mVoiceCallRatUsages, mPersistAtomsStorage.getVoiceCallRatUsages(0L));
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getVoiceCallSessions(0L));
     }
 
     @Test
     @SmallTest
     public void addVoiceCallRatUsage_withExistingUsages() throws Exception {
-        createTestFile(100L);
+        createTestFile(START_TIME_MILLIS);
         VoiceCallRatTracker ratTracker = VoiceCallRatTracker.fromProto(mVoiceCallRatUsages);
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
@@ -559,42 +674,30 @@
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // RAT should be added successfully, calls should not change, changes should be saved
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
         // call count and duration should become doubled since mVoiceCallRatUsages applied through
         // both file and addVoiceCallRatUsage()
-        RawVoiceCallRatUsage[] expectedVoiceCallRatUsage =
+        verifyCurrentStateSavedToFileOnce();
+        VoiceCallRatUsage[] expectedVoiceCallRatUsage =
                 multiplyVoiceCallRatUsage(mVoiceCallRatUsages, 2);
-        Arrays.sort(expectedVoiceCallRatUsage, sProtoComparator);
-        Arrays.sort(voiceCallRatUsage, sProtoComparator);
-        assertProtoArrayEquals(expectedVoiceCallRatUsage, voiceCallRatUsage);
-        assertNotNull(voiceCallSession);
-        assertEquals(mVoiceCallSessions.length, voiceCallSession.length);
-        InOrder inOrder = inOrder(mTestFileOutputStream);
-        inOrder.verify(mTestFileOutputStream, times(1))
-                .write(eq(PersistAtoms.toByteArray(mPersistAtomsStorage.getAtomsProto())));
-        inOrder.verify(mTestFileOutputStream, times(1)).close();
-        inOrder.verifyNoMoreInteractions();
+        assertProtoArrayEqualsIgnoringOrder(
+                expectedVoiceCallRatUsage, mPersistAtomsStorage.getVoiceCallRatUsages(0L));
+        assertProtoArrayEquals(mVoiceCallSessions, mPersistAtomsStorage.getVoiceCallSessions(0L));
     }
 
     @Test
     @SmallTest
     public void addVoiceCallRatUsage_empty() throws Exception {
         createEmptyTestFile();
-        VoiceCallRatTracker ratTracker = VoiceCallRatTracker.fromProto(new RawVoiceCallRatUsage[0]);
+        VoiceCallRatTracker ratTracker = VoiceCallRatTracker.fromProto(new VoiceCallRatUsage[0]);
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
         mPersistAtomsStorage.addVoiceCallRatUsage(ratTracker);
         mPersistAtomsStorage.incTimeMillis(100L);
 
         // RAT should be added successfully, calls should not change
-        // in this case it does not necessarily need to save
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(0L);
-        VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(0L);
-        assertNotNull(voiceCallRatUsage);
-        assertEquals(0, voiceCallRatUsage.length);
-        assertNotNull(voiceCallSession);
-        assertEquals(0, voiceCallSession.length);
+        // in this case saving is unnecessarily
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getVoiceCallRatUsages(0L));
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getVoiceCallSessions(0L));
     }
 
     @Test
@@ -604,7 +707,7 @@
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
         mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(100L);
+        VoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(100L);
 
         // should be denied
         assertNull(voiceCallRatUsage);
@@ -617,9 +720,9 @@
 
         mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
         mPersistAtomsStorage.incTimeMillis(100L);
-        RawVoiceCallRatUsage[] voiceCallRatUsage1 = mPersistAtomsStorage.getVoiceCallRatUsages(50L);
+        VoiceCallRatUsage[] voiceCallRatUsage1 = mPersistAtomsStorage.getVoiceCallRatUsages(50L);
         mPersistAtomsStorage.incTimeMillis(100L);
-        RawVoiceCallRatUsage[] voiceCallRatUsage2 = mPersistAtomsStorage.getVoiceCallRatUsages(50L);
+        VoiceCallRatUsage[] voiceCallRatUsage2 = mPersistAtomsStorage.getVoiceCallRatUsages(50L);
         long voiceCallSessionPullTimestampMillis =
                 mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis;
         VoiceCallSession[] voiceCallSession = mPersistAtomsStorage.getVoiceCallSessions(50L);
@@ -627,19 +730,19 @@
         // first set of results should equal to file contents, second should be empty, corresponding
         // pull timestamp should be updated and saved, other fields should be unaffected
         assertProtoArrayEquals(mVoiceCallRatUsages, voiceCallRatUsage1);
-        assertProtoArrayEquals(new RawVoiceCallRatUsage[0], voiceCallRatUsage2);
+        assertProtoArrayIsEmpty(voiceCallRatUsage2);
         assertEquals(
                 START_TIME_MILLIS + 200L,
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis);
+                mPersistAtomsStorage.getAtomsProto().voiceCallRatUsagePullTimestampMillis);
         assertProtoArrayEquals(mVoiceCallSessions, voiceCallSession);
         assertEquals(START_TIME_MILLIS, voiceCallSessionPullTimestampMillis);
         InOrder inOrder = inOrder(mTestFileOutputStream);
         assertEquals(
                 START_TIME_MILLIS + 100L,
-                getAtomsWritten(inOrder).rawVoiceCallRatUsagePullTimestampMillis);
+                getAtomsWritten(inOrder).voiceCallRatUsagePullTimestampMillis);
         assertEquals(
                 START_TIME_MILLIS + 200L,
-                getAtomsWritten(inOrder).rawVoiceCallRatUsagePullTimestampMillis);
+                getAtomsWritten(inOrder).voiceCallRatUsagePullTimestampMillis);
         assertEquals(
                 START_TIME_MILLIS + 200L,
                 getAtomsWritten(inOrder).voiceCallSessionPullTimestampMillis);
@@ -670,13 +773,13 @@
         mPersistAtomsStorage.incTimeMillis(100L);
         VoiceCallSession[] voiceCallSession2 = mPersistAtomsStorage.getVoiceCallSessions(50L);
         long voiceCallRatUsagePullTimestampMillis =
-                mPersistAtomsStorage.getAtomsProto().rawVoiceCallRatUsagePullTimestampMillis;
-        RawVoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(50L);
+                mPersistAtomsStorage.getAtomsProto().voiceCallRatUsagePullTimestampMillis;
+        VoiceCallRatUsage[] voiceCallRatUsage = mPersistAtomsStorage.getVoiceCallRatUsages(50L);
 
         // first set of results should equal to file contents, second should be empty, corresponding
         // pull timestamp should be updated and saved, other fields should be unaffected
         assertProtoArrayEquals(mVoiceCallSessions, voiceCallSession1);
-        assertProtoArrayEquals(new VoiceCallSession[0], voiceCallSession2);
+        assertProtoArrayIsEmpty(voiceCallSession2);
         assertEquals(
                 START_TIME_MILLIS + 200L,
                 mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
@@ -691,10 +794,486 @@
                 getAtomsWritten(inOrder).voiceCallSessionPullTimestampMillis);
         assertEquals(
                 START_TIME_MILLIS + 200L,
-                getAtomsWritten(inOrder).rawVoiceCallRatUsagePullTimestampMillis);
+                getAtomsWritten(inOrder).voiceCallRatUsagePullTimestampMillis);
         inOrder.verifyNoMoreInteractions();
     }
 
+    @Test
+    @SmallTest
+    public void addCellularServiceStateAndCellularDataServiceSwitch_emptyProto() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addCellularServiceStateAndCellularDataServiceSwitch(
+                mServiceState1Proto, mServiceSwitch1Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        CellularServiceState[] serviceStates = mPersistAtomsStorage.getCellularServiceStates(0L);
+        CellularDataServiceSwitch[] serviceSwitches =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(0L);
+        assertProtoArrayEquals(new CellularServiceState[] {mServiceState1Proto}, serviceStates);
+        assertProtoArrayEquals(
+                new CellularDataServiceSwitch[] {mServiceSwitch1Proto}, serviceSwitches);
+    }
+
+    @Test
+    @SmallTest
+    public void addCellularServiceStateAndCellularDataServiceSwitch_withExistingEntries()
+            throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addCellularServiceStateAndCellularDataServiceSwitch(
+                mServiceState1Proto, mServiceSwitch1Proto);
+
+        mPersistAtomsStorage.addCellularServiceStateAndCellularDataServiceSwitch(
+                mServiceState2Proto, mServiceSwitch2Proto);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        CellularServiceState[] serviceStates = mPersistAtomsStorage.getCellularServiceStates(0L);
+        CellularDataServiceSwitch[] serviceSwitches =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new CellularServiceState[] {mServiceState1Proto, mServiceState2Proto},
+                serviceStates);
+        assertProtoArrayEqualsIgnoringOrder(
+                new CellularDataServiceSwitch[] {mServiceSwitch1Proto, mServiceSwitch2Proto},
+                serviceSwitches);
+    }
+
+    @Test
+    @SmallTest
+    public void addCellularServiceStateAndCellularDataServiceSwitch_updateExistingEntries()
+            throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        CellularServiceState newServiceState1Proto = copyOf(mServiceState1Proto);
+        CellularDataServiceSwitch newServiceSwitch1Proto = copyOf(mServiceSwitch1Proto);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addCellularServiceStateAndCellularDataServiceSwitch(
+                copyOf(mServiceState1Proto), copyOf(mServiceSwitch1Proto));
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // mServiceState1Proto's duration and mServiceSwitch1Proto's switch should be doubled
+        verifyCurrentStateSavedToFileOnce();
+        CellularServiceState[] serviceStates = mPersistAtomsStorage.getCellularServiceStates(0L);
+        newServiceState1Proto.totalTimeMillis *= 2;
+        assertProtoArrayEqualsIgnoringOrder(
+                new CellularServiceState[] {
+                    newServiceState1Proto,
+                    mServiceState2Proto,
+                    mServiceState3Proto,
+                    mServiceState4Proto
+                },
+                serviceStates);
+        CellularDataServiceSwitch[] serviceSwitches =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(0L);
+        newServiceSwitch1Proto.switchCount *= 2;
+        assertProtoArrayEqualsIgnoringOrder(
+                new CellularDataServiceSwitch[] {newServiceSwitch1Proto, mServiceSwitch2Proto},
+                serviceSwitches);
+    }
+
+    @Test
+    @SmallTest
+    public void addCellularServiceStateAndCellularDataServiceSwitch_tooManyServiceStates()
+            throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        Queue<CellularServiceState> expectedServiceStates = new LinkedList<>();
+        Queue<CellularDataServiceSwitch> expectedServiceSwitches = new LinkedList<>();
+
+        // Add 51 service states, with the first being least recent
+        for (int i = 0; i < 51; i++) {
+            CellularServiceState state = new CellularServiceState();
+            state.voiceRat = i / 10;
+            state.dataRat = i % 10;
+            expectedServiceStates.add(state);
+            CellularDataServiceSwitch serviceSwitch = new CellularDataServiceSwitch();
+            serviceSwitch.ratFrom = i / 10;
+            serviceSwitch.ratTo = i % 10;
+            expectedServiceSwitches.add(serviceSwitch);
+            mPersistAtomsStorage.addCellularServiceStateAndCellularDataServiceSwitch(
+                    copyOf(state), copyOf(serviceSwitch));
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+
+        // The least recent (the first) service state should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        CellularServiceState[] serviceStates = mPersistAtomsStorage.getCellularServiceStates(0L);
+        expectedServiceStates.remove();
+        assertProtoArrayEqualsIgnoringOrder(
+                expectedServiceStates.toArray(new CellularServiceState[0]), serviceStates);
+        CellularDataServiceSwitch[] serviceSwitches =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(0L);
+        expectedServiceSwitches.remove();
+        assertProtoArrayEqualsIgnoringOrder(
+                expectedServiceSwitches.toArray(new CellularDataServiceSwitch[0]), serviceSwitches);
+    }
+
+    @Test
+    @SmallTest
+    public void getCellularDataServiceSwitches_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        CellularDataServiceSwitch[] serviceSwitches =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(100L);
+
+        // should be denied
+        assertNull(serviceSwitches);
+    }
+
+    @Test
+    @SmallTest
+    public void getCellularDataServiceSwitches_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        CellularDataServiceSwitch[] serviceSwitches1 =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        CellularDataServiceSwitch[] serviceSwitches2 =
+                mPersistAtomsStorage.getCellularDataServiceSwitches(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new CellularDataServiceSwitch[] {mServiceSwitch1Proto, mServiceSwitch2Proto},
+                serviceSwitches1);
+        assertProtoArrayEquals(new CellularDataServiceSwitch[0], serviceSwitches2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto().cellularDataServiceSwitchPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).cellularDataServiceSwitchPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).cellularDataServiceSwitchPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void getCellularServiceStates_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        CellularServiceState[] serviceStates = mPersistAtomsStorage.getCellularServiceStates(100L);
+
+        // should be denied
+        assertNull(serviceStates);
+    }
+
+    @Test
+    @SmallTest
+    public void getCellularServiceStates_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        CellularServiceState[] serviceStates1 = mPersistAtomsStorage.getCellularServiceStates(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        CellularServiceState[] serviceStates2 = mPersistAtomsStorage.getCellularServiceStates(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new CellularServiceState[] {
+                    mServiceState1Proto,
+                    mServiceState2Proto,
+                    mServiceState3Proto,
+                    mServiceState4Proto
+                },
+                serviceStates1);
+        assertProtoArrayEquals(new CellularServiceState[0], serviceStates2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto().cellularServiceStatePullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).cellularServiceStatePullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).cellularServiceStatePullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationStats_emptyProto() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsRegistrationStats(mImsRegistrationStatsLte0);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationStats[] regStats = mPersistAtomsStorage.getImsRegistrationStats(0L);
+        assertProtoArrayEquals(new ImsRegistrationStats[] {mImsRegistrationStatsLte0}, regStats);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationStats_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsRegistrationStats(mImsRegistrationStatsLte0);
+
+        mPersistAtomsStorage.addImsRegistrationStats(mImsRegistrationStatsWifi0);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationStats[] regStats = mPersistAtomsStorage.getImsRegistrationStats(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationStats[] {mImsRegistrationStatsLte0, mImsRegistrationStatsWifi0},
+                regStats);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationStats_updateExistingEntries() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        ImsRegistrationStats newImsRegistrationStatsLte0 = copyOf(mImsRegistrationStatsLte0);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addImsRegistrationStats(copyOf(mImsRegistrationStatsLte0));
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // mImsRegistrationStatsLte0's durations should be doubled
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationStats[] serviceStates = mPersistAtomsStorage.getImsRegistrationStats(0L);
+        newImsRegistrationStatsLte0.registeredMillis *= 2;
+        newImsRegistrationStatsLte0.voiceCapableMillis *= 2;
+        newImsRegistrationStatsLte0.voiceAvailableMillis *= 2;
+        newImsRegistrationStatsLte0.smsCapableMillis *= 2;
+        newImsRegistrationStatsLte0.smsAvailableMillis *= 2;
+        newImsRegistrationStatsLte0.videoCapableMillis *= 2;
+        newImsRegistrationStatsLte0.videoAvailableMillis *= 2;
+        newImsRegistrationStatsLte0.utCapableMillis *= 2;
+        newImsRegistrationStatsLte0.utAvailableMillis *= 2;
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationStats[] {
+                    newImsRegistrationStatsLte0,
+                    mImsRegistrationStatsWifi0,
+                    mImsRegistrationStatsLte1
+                },
+                serviceStates);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationStats_tooManyRegistrationStats() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        Queue<ImsRegistrationStats> expectedRegistrationStats = new LinkedList<>();
+
+        // Add 11 registration stats
+        for (int i = 0; i < 11; i++) {
+            ImsRegistrationStats stats = copyOf(mImsRegistrationStatsLte0);
+            stats.rat = i;
+            expectedRegistrationStats.add(stats);
+            mPersistAtomsStorage.addImsRegistrationStats(stats);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+
+        // The least recent (the first) registration stats should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationStats[] stats = mPersistAtomsStorage.getImsRegistrationStats(0L);
+        expectedRegistrationStats.remove();
+        assertProtoArrayEqualsIgnoringOrder(
+                expectedRegistrationStats.toArray(new ImsRegistrationStats[0]), stats);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationTermination_emptyProto() throws Exception {
+        createEmptyTestFile();
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsRegistrationTermination(mImsRegistrationTerminationLte);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationTermination[] terminations =
+                mPersistAtomsStorage.getImsRegistrationTerminations(0L);
+        assertProtoArrayEquals(
+                new ImsRegistrationTermination[] {mImsRegistrationTerminationLte}, terminations);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationTermination_withExistingEntries() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.addImsRegistrationTermination(mImsRegistrationTerminationLte);
+
+        mPersistAtomsStorage.addImsRegistrationTermination(mImsRegistrationTerminationWifi);
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // service state and service switch should be added successfully
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationTermination[] terminations =
+                mPersistAtomsStorage.getImsRegistrationTerminations(0L);
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationTermination[] {
+                    mImsRegistrationTerminationLte, mImsRegistrationTerminationWifi
+                },
+                terminations);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationTermination_updateExistingEntries() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+        ImsRegistrationTermination newTermination = copyOf(mImsRegistrationTerminationWifi);
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+
+        mPersistAtomsStorage.addImsRegistrationTermination(copyOf(mImsRegistrationTerminationWifi));
+        mPersistAtomsStorage.incTimeMillis(100L);
+
+        // mImsRegistrationTerminationWifi's count should be doubled
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationTermination[] terminations =
+                mPersistAtomsStorage.getImsRegistrationTerminations(0L);
+        newTermination.count *= 2;
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationTermination[] {mImsRegistrationTerminationLte, newTermination},
+                terminations);
+    }
+
+    @Test
+    @SmallTest
+    public void addImsRegistrationTermination_tooManyTerminations() throws Exception {
+        createEmptyTestFile();
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        Queue<ImsRegistrationTermination> expectedTerminations = new LinkedList<>();
+
+        // Add 11 registration terminations
+        for (int i = 0; i < 11; i++) {
+            ImsRegistrationTermination termination = copyOf(mImsRegistrationTerminationLte);
+            termination.reasonCode = i;
+            expectedTerminations.add(termination);
+            mPersistAtomsStorage.addImsRegistrationTermination(termination);
+            mPersistAtomsStorage.incTimeMillis(100L);
+        }
+
+        // The least recent (the first) registration termination should be evicted
+        verifyCurrentStateSavedToFileOnce();
+        ImsRegistrationTermination[] terminations =
+                mPersistAtomsStorage.getImsRegistrationTerminations(0L);
+        expectedTerminations.remove();
+        assertProtoArrayEqualsIgnoringOrder(
+                expectedTerminations.toArray(new ImsRegistrationTermination[0]), terminations);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationStats_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        ImsRegistrationStats[] stats = mPersistAtomsStorage.getImsRegistrationStats(100L);
+
+        // should be denied
+        assertNull(stats);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationStats_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationStats[] stats1 = mPersistAtomsStorage.getImsRegistrationStats(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationStats[] stats2 = mPersistAtomsStorage.getImsRegistrationStats(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationStats[] {
+                    mImsRegistrationStatsLte0, mImsRegistrationStatsWifi0, mImsRegistrationStatsLte1
+                },
+                stats1);
+        assertProtoArrayEquals(new ImsRegistrationStats[0], stats2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto().imsRegistrationStatsPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).imsRegistrationStatsPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).imsRegistrationStatsPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationTerminations_tooFrequent() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(50L); // pull interval less than minimum
+        ImsRegistrationTermination[] terminations =
+                mPersistAtomsStorage.getImsRegistrationTerminations(100L);
+
+        // should be denied
+        assertNull(terminations);
+    }
+
+    @Test
+    @SmallTest
+    public void getImsRegistrationTerminations_withSavedAtoms() throws Exception {
+        createTestFile(START_TIME_MILLIS);
+
+        mPersistAtomsStorage = new TestablePersistAtomsStorage(mContext);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationTermination[] terminations1 =
+                mPersistAtomsStorage.getImsRegistrationTerminations(50L);
+        mPersistAtomsStorage.incTimeMillis(100L);
+        ImsRegistrationTermination[] terminations2 =
+                mPersistAtomsStorage.getImsRegistrationTerminations(50L);
+
+        // first set of results should equal to file contents, second should be empty, corresponding
+        // pull timestamp should be updated and saved
+        assertProtoArrayEqualsIgnoringOrder(
+                new ImsRegistrationTermination[] {
+                    mImsRegistrationTerminationLte, mImsRegistrationTerminationWifi
+                },
+                terminations1);
+        assertProtoArrayEquals(new ImsRegistrationTermination[0], terminations2);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                mPersistAtomsStorage.getAtomsProto().imsRegistrationTerminationPullTimestampMillis);
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        assertEquals(
+                START_TIME_MILLIS + 100L,
+                getAtomsWritten(inOrder).imsRegistrationTerminationPullTimestampMillis);
+        assertEquals(
+                START_TIME_MILLIS + 200L,
+                getAtomsWritten(inOrder).imsRegistrationTerminationPullTimestampMillis);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    /* Utilities */
+
     private void createEmptyTestFile() throws Exception {
         PersistAtoms atoms = new PersistAtoms();
         FileOutputStream stream = new FileOutputStream(mTestFile);
@@ -704,10 +1283,18 @@
 
     private void createTestFile(long lastPullTimeMillis) throws Exception {
         PersistAtoms atoms = new PersistAtoms();
-        atoms.rawVoiceCallRatUsagePullTimestampMillis = lastPullTimeMillis;
+        atoms.voiceCallRatUsagePullTimestampMillis = lastPullTimeMillis;
+        atoms.voiceCallRatUsage = mVoiceCallRatUsages;
         atoms.voiceCallSessionPullTimestampMillis = lastPullTimeMillis;
-        atoms.rawVoiceCallRatUsage = mVoiceCallRatUsages;
         atoms.voiceCallSession = mVoiceCallSessions;
+        atoms.cellularServiceStatePullTimestampMillis = lastPullTimeMillis;
+        atoms.cellularServiceState = mServiceStates;
+        atoms.cellularDataServiceSwitchPullTimestampMillis = lastPullTimeMillis;
+        atoms.cellularDataServiceSwitch = mServiceSwitches;
+        atoms.imsRegistrationStatsPullTimestampMillis = lastPullTimeMillis;
+        atoms.imsRegistrationStats = mImsRegistrationStats;
+        atoms.imsRegistrationTerminationPullTimestampMillis = lastPullTimeMillis;
+        atoms.imsRegistrationTermination = mImsRegistrationTerminations;
         FileOutputStream stream = new FileOutputStream(mTestFile);
         stream.write(PersistAtoms.toByteArray(atoms));
         stream.close();
@@ -731,11 +1318,11 @@
         }
     }
 
-    private static RawVoiceCallRatUsage[] multiplyVoiceCallRatUsage(
-            RawVoiceCallRatUsage[] usages, int times) {
-        RawVoiceCallRatUsage[] multipliedUsages = new RawVoiceCallRatUsage[usages.length];
+    private static VoiceCallRatUsage[] multiplyVoiceCallRatUsage(
+            VoiceCallRatUsage[] usages, int times) {
+        VoiceCallRatUsage[] multipliedUsages = new VoiceCallRatUsage[usages.length];
         for (int i = 0; i < usages.length; i++) {
-            multipliedUsages[i] = new RawVoiceCallRatUsage();
+            multipliedUsages[i] = new VoiceCallRatUsage();
             multipliedUsages[i].carrierId = usages[i].carrierId;
             multipliedUsages[i].rat = usages[i].rat;
             multipliedUsages[i].callCount = usages[i].callCount * 2;
@@ -744,19 +1331,73 @@
         return multipliedUsages;
     }
 
+    private static CellularServiceState copyOf(CellularServiceState source) throws Exception {
+        return CellularServiceState.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static CellularDataServiceSwitch copyOf(CellularDataServiceSwitch source)
+            throws Exception {
+        return CellularDataServiceSwitch.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static ImsRegistrationStats copyOf(ImsRegistrationStats source) throws Exception {
+        return ImsRegistrationStats.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private static ImsRegistrationTermination copyOf(ImsRegistrationTermination source)
+            throws Exception {
+        return ImsRegistrationTermination.parseFrom(MessageNano.toByteArray(source));
+    }
+
+    private void assertAllPullTimestampEquals(long timestamp) {
+        assertEquals(
+                timestamp,
+                mPersistAtomsStorage.getAtomsProto().voiceCallRatUsagePullTimestampMillis);
+        assertEquals(
+                timestamp,
+                mPersistAtomsStorage.getAtomsProto().voiceCallSessionPullTimestampMillis);
+        assertEquals(
+                timestamp,
+                mPersistAtomsStorage.getAtomsProto().cellularServiceStatePullTimestampMillis);
+        assertEquals(
+                timestamp,
+                mPersistAtomsStorage.getAtomsProto().cellularDataServiceSwitchPullTimestampMillis);
+    }
+
+    private void assertStorageIsEmptyForAllAtoms() {
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getVoiceCallRatUsages(0L));
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getVoiceCallSessions(0L));
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getCellularServiceStates(0L));
+        assertProtoArrayIsEmpty(mPersistAtomsStorage.getCellularDataServiceSwitches(0L));
+    }
+
+    private static <T extends MessageNano> void assertProtoArrayIsEmpty(T[] array) {
+        assertNotNull(array);
+        assertEquals(0, array.length);
+    }
+
     private static void assertProtoArrayEquals(MessageNano[] expected, MessageNano[] actual) {
         assertNotNull(expected);
         assertNotNull(actual);
-        assertEquals(expected.length, actual.length);
+        String message =
+                "Expected:\n" + Arrays.toString(expected) + "\nGot:\n" + Arrays.toString(actual);
+        assertEquals(message, expected.length, actual.length);
         for (int i = 0; i < expected.length; i++) {
-            assertTrue(
-                    String.format(
-                            "Message %d of %d differs:\n=== expected ===\n%s=== got ===\n%s",
-                            i + 1, expected.length, expected[i].toString(), actual[i].toString()),
-                    MessageNano.messageNanoEquals(expected[i], actual[i]));
+            assertTrue(message, MessageNano.messageNanoEquals(expected[i], actual[i]));
         }
     }
 
+    private static void assertProtoArrayEqualsIgnoringOrder(
+            MessageNano[] expected, MessageNano[] actual) {
+        assertNotNull(expected);
+        assertNotNull(actual);
+        expected = expected.clone();
+        actual = actual.clone();
+        Arrays.sort(expected, sProtoComparator);
+        Arrays.sort(actual, sProtoComparator);
+        assertProtoArrayEquals(expected, actual);
+    }
+
     private static void assertHasCall(
             VoiceCallSession[] calls, @Nullable VoiceCallSession expectedCall, int expectedCount) {
         assertNotNull(calls);
@@ -770,4 +1411,12 @@
         }
         assertEquals(expectedCount, actualCount);
     }
+
+    private void verifyCurrentStateSavedToFileOnce() throws Exception {
+        InOrder inOrder = inOrder(mTestFileOutputStream);
+        inOrder.verify(mTestFileOutputStream, times(1))
+                .write(eq(PersistAtoms.toByteArray(mPersistAtomsStorage.getAtomsProto())));
+        inOrder.verify(mTestFileOutputStream, times(1)).close();
+        inOrder.verifyNoMoreInteractions();
+    }
 }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/ServiceStateStatsTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/ServiceStateStatsTest.java
new file mode 100644
index 0000000..ca967ab
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/ServiceStateStatsTest.java
@@ -0,0 +1,840 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.metrics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.telephony.AccessNetworkConstants;
+import android.telephony.Annotation.NetworkType;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telephony.Phone;
+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.uicc.IccCardStatus.CardState;
+import com.android.internal.telephony.uicc.UiccSlot;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+public class ServiceStateStatsTest extends TelephonyTest {
+    private static final long START_TIME_MILLIS = 2000L;
+    private static final int CARRIER1_ID = 1;
+    private static final int CARRIER2_ID = 1187;
+
+    @Mock private UiccSlot mPhysicalSlot0;
+    @Mock private UiccSlot mPhysicalSlot1;
+    @Mock private Phone mSecondPhone;
+
+    private TestableServiceStateStats mServiceStateStats;
+
+    private static class TestableServiceStateStats extends ServiceStateStats {
+        private long mTimeMillis = START_TIME_MILLIS;
+
+        TestableServiceStateStats(Phone phone) {
+            super(phone);
+        }
+
+        @Override
+        protected long getTimeMillis() {
+            // 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)
+            return mTimeMillis == 0L ? START_TIME_MILLIS : mTimeMillis;
+        }
+
+        private void setTimeMillis(long timeMillis) {
+            mTimeMillis = timeMillis;
+        }
+
+        private void incTimeMillis(long timeMillis) {
+            mTimeMillis += timeMillis;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp(getClass().getSimpleName());
+
+        doReturn(CARRIER1_ID).when(mPhone).getCarrierId();
+        doReturn(mImsPhone).when(mPhone).getImsPhone();
+
+        // Single physical SIM
+        doReturn(true).when(mPhysicalSlot0).isActive();
+        doReturn(CardState.CARDSTATE_PRESENT).when(mPhysicalSlot0).getCardState();
+        doReturn(false).when(mPhysicalSlot0).isEuicc();
+        doReturn(new UiccSlot[] {mPhysicalSlot0}).when(mUiccController).getUiccSlots();
+        doReturn(mPhysicalSlot0).when(mUiccController).getUiccSlot(0);
+        doReturn(mPhysicalSlot0).when(mUiccController).getUiccSlotForPhone(0);
+
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_LTE);
+
+        mServiceStateStats = new TestableServiceStateStats(mPhone);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_inService() throws Exception {
+        // Using default service state for LTE
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // Duration should be counted and there should not be any switch
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_outOfService() throws Exception {
+        doReturn(ServiceState.STATE_OUT_OF_SERVICE).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_OUT_OF_SERVICE).when(mServiceState).getDataRegState();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // Duration should be counted and there should not be any switch
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_airplaneMode() throws Exception {
+        doReturn(ServiceState.STATE_POWER_OFF).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_POWER_OFF).when(mServiceState).getDataRegState();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        doReturn(-1).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // There should be no new switches, service states, or added durations
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_airplaneModeWithWifiCalling() throws Exception {
+        doReturn(ServiceState.STATE_POWER_OFF).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_IN_SERVICE).when(mServiceState).getDataRegState();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_IWLAN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        doReturn(true).when(mImsPhone).isWifiCallingEnabled();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // Duration for Wifi calling should be counted and there should not be any switch
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_IWLAN, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_noSimCardEmergencyOnly() throws Exception {
+        // Using default service state for LTE
+        doReturn(CardState.CARDSTATE_ABSENT).when(mPhysicalSlot0).getCardState();
+        doReturn(ServiceState.STATE_EMERGENCY_ONLY).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_EMERGENCY_ONLY).when(mServiceState).getDataRegState();
+        doReturn(-1).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // Duration should be counted and there should not be any switch
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(-1, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_noSimCardOutOfService() throws Exception {
+        doReturn(CardState.CARDSTATE_ABSENT).when(mPhysicalSlot0).getCardState();
+        doReturn(ServiceState.STATE_OUT_OF_SERVICE).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_OUT_OF_SERVICE).when(mServiceState).getDataRegState();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        doReturn(-1).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // Duration should be counted and there should not be any switch
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(-1, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void conclude_longOnGoingServiceState() throws Exception {
+        // Using default service state for LTE
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.conclude();
+
+        // There should be 2 separate service state updates, which should be different objects
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage, times(2))
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        assertNotSame(captor.getAllValues().get(0), captor.getAllValues().get(1));
+        CellularServiceState state = captor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = captor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_sameRats() throws Exception {
+        // Using default service state for LTE
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        // Should produce 1 service state atom with LTE and no data service switch
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_differentDataRats() throws Exception {
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_LTE);
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+
+        // There should be 2 service states and a data service switch
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> serviceStateCaptor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        ArgumentCaptor<CellularDataServiceSwitch> serviceSwitchCaptor =
+                ArgumentCaptor.forClass(CellularDataServiceSwitch.class);
+        verify(mPersistAtomsStorage, times(2))
+                .addCellularServiceStateAndCellularDataServiceSwitch(
+                        serviceStateCaptor.capture(), serviceSwitchCaptor.capture());
+        CellularServiceState state = serviceStateCaptor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = serviceStateCaptor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        CellularDataServiceSwitch serviceSwitch = serviceSwitchCaptor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, serviceSwitch.ratFrom);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, serviceSwitch.ratTo);
+        assertEquals(0, serviceSwitch.simSlotIndex);
+        assertFalse(serviceSwitch.isMultiSim);
+        assertEquals(CARRIER1_ID, serviceSwitch.carrierId);
+        assertEquals(1, serviceSwitch.switchCount);
+        assertNull(serviceSwitchCaptor.getAllValues().get(1)); // produced by conclude()
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_differentVoiceRats() throws Exception {
+        // Using default service state for LTE
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        // Voice RAT changes to IWLAN and data RAT stays in LTE according to WWAN PS RAT
+        doReturn(TelephonyManager.NETWORK_TYPE_IWLAN).when(mServiceState).getDataNetworkType();
+        doReturn(true).when(mImsPhone).isWifiCallingEnabled();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+
+        // There should be 2 service states but no data service switch
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage, times(2))
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = captor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_IWLAN, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_iwlanButNotWifiCalling() throws Exception {
+        // Using default service state for LTE as WWAN PS RAT
+        doReturn(TelephonyManager.NETWORK_TYPE_IWLAN).when(mServiceState).getDataNetworkType();
+        doReturn(false).when(mImsPhone).isWifiCallingEnabled();
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+
+        // Should produce 1 service state atom with voice and data (WWAN PS) RAT as LTE
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage)
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getValue();
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(0L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_endc() throws Exception {
+        // Using default service state for LTE without ENDC
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        // ENDC should stay false
+        doReturn(NetworkRegistrationInfo.NR_STATE_RESTRICTED).when(mServiceState).getNrState();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(200L);
+        // ENDC should become true
+        doReturn(NetworkRegistrationInfo.NR_STATE_NOT_RESTRICTED).when(mServiceState).getNrState();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(400L);
+        // ENDC should stay true
+        doReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED).when(mServiceState).getNrState();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(800L);
+
+        // There should be 4 service state updates (2 distinct service states) but no service switch
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage, times(4))
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = captor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(200L, state.totalTimeMillis);
+        state = captor.getAllValues().get(2);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertTrue(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(400L, state.totalTimeMillis);
+        state = captor.getAllValues().get(3);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertTrue(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(800L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_simSwapSameRat() throws Exception {
+        // Using default service state for LTE
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        // SIM removed, emergency call only
+        doReturn(CardState.CARDSTATE_ABSENT).when(mPhysicalSlot0).getCardState();
+        doReturn(TelephonyManager.NETWORK_TYPE_UMTS).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        doReturn(-1).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(5000L);
+        // New SIM inserted
+        doReturn(CardState.CARDSTATE_PRESENT).when(mPhysicalSlot0).getCardState();
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_LTE);
+        doReturn(CARRIER2_ID).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(200L);
+
+        // There should be 3 service states, but there should be no switches due to carrier change
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage, times(3))
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = captor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UNKNOWN, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(-1, state.carrierId);
+        assertEquals(5000L, state.totalTimeMillis);
+        state = captor.getAllValues().get(2);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER2_ID, state.carrierId);
+        assertEquals(200L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_roaming() throws Exception {
+        // Using default service state for LTE
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        // Voice roaming
+        doReturn(TelephonyManager.NETWORK_TYPE_UMTS).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UMTS).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UMTS);
+        doReturn(ServiceState.ROAMING_TYPE_INTERNATIONAL).when(mServiceState).getVoiceRoamingType();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(200L);
+        // Voice and data roaming
+        doReturn(ServiceState.ROAMING_TYPE_INTERNATIONAL).when(mServiceState).getDataRoamingType();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(400L);
+
+        // There should be 3 service states and 1 data service switch (LTE to UMTS)
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> serviceStateCaptor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        ArgumentCaptor<CellularDataServiceSwitch> serviceSwitchCaptor =
+                ArgumentCaptor.forClass(CellularDataServiceSwitch.class);
+        verify(mPersistAtomsStorage, times(3))
+                .addCellularServiceStateAndCellularDataServiceSwitch(
+                        serviceStateCaptor.capture(), serviceSwitchCaptor.capture());
+        CellularServiceState state = serviceStateCaptor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = serviceStateCaptor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_INTERNATIONAL, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(200L, state.totalTimeMillis);
+        state = serviceStateCaptor.getAllValues().get(2);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_INTERNATIONAL, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_INTERNATIONAL, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(400L, state.totalTimeMillis);
+        CellularDataServiceSwitch serviceSwitch = serviceSwitchCaptor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, serviceSwitch.ratFrom);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, serviceSwitch.ratTo);
+        assertEquals(0, serviceSwitch.simSlotIndex);
+        assertFalse(serviceSwitch.isMultiSim);
+        assertEquals(CARRIER1_ID, serviceSwitch.carrierId);
+        assertEquals(1, serviceSwitch.switchCount);
+        assertNull(serviceSwitchCaptor.getAllValues().get(1));
+        assertNull(serviceSwitchCaptor.getAllValues().get(2)); // produced by conclude()
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_dualSim() throws Exception {
+        // Using default service state for LTE
+        // Only difference between the 2 slots is slot index
+        mockDualSim(CARRIER1_ID);
+        TestableServiceStateStats mSecondServiceStateStats =
+                new TestableServiceStateStats(mSecondPhone);
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        mSecondServiceStateStats.onServiceStateChanged(mServiceState);
+        mSecondServiceStateStats.incTimeMillis(100L);
+        doReturn(TelephonyManager.NETWORK_TYPE_UMTS).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UMTS).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UMTS);
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(200L);
+        mSecondServiceStateStats.onServiceStateChanged(mServiceState);
+        mSecondServiceStateStats.incTimeMillis(200L);
+
+        // There should be 4 service states and 2 data service switches
+        mServiceStateStats.conclude();
+        mSecondServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> serviceStateCaptor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        ArgumentCaptor<CellularDataServiceSwitch> serviceSwitchCaptor =
+                ArgumentCaptor.forClass(CellularDataServiceSwitch.class);
+        verify(mPersistAtomsStorage, times(4))
+                .addCellularServiceStateAndCellularDataServiceSwitch(
+                        serviceStateCaptor.capture(), serviceSwitchCaptor.capture());
+        CellularServiceState state = serviceStateCaptor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertTrue(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = serviceStateCaptor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(1, state.simSlotIndex);
+        assertTrue(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = serviceStateCaptor.getAllValues().get(2);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertTrue(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(200L, state.totalTimeMillis);
+        state = serviceStateCaptor.getAllValues().get(3);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(1, state.simSlotIndex);
+        assertTrue(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(200L, state.totalTimeMillis);
+        CellularDataServiceSwitch serviceSwitch = serviceSwitchCaptor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, serviceSwitch.ratFrom);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, serviceSwitch.ratTo);
+        assertEquals(0, serviceSwitch.simSlotIndex);
+        assertTrue(serviceSwitch.isMultiSim);
+        assertEquals(CARRIER1_ID, serviceSwitch.carrierId);
+        assertEquals(1, serviceSwitch.switchCount);
+        serviceSwitch = serviceSwitchCaptor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, serviceSwitch.ratFrom);
+        assertEquals(TelephonyManager.NETWORK_TYPE_UMTS, serviceSwitch.ratTo);
+        assertEquals(1, serviceSwitch.simSlotIndex);
+        assertTrue(serviceSwitch.isMultiSim);
+        assertEquals(CARRIER1_ID, serviceSwitch.carrierId);
+        assertEquals(1, serviceSwitch.switchCount);
+        assertNull(serviceSwitchCaptor.getAllValues().get(2)); // produced by conclude()
+        assertNull(serviceSwitchCaptor.getAllValues().get(3)); // produced by conclude()
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    @Test
+    @SmallTest
+    public void update_airplaneMode() throws Exception {
+        // Using default service state for LTE
+
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(100L);
+        doReturn(ServiceState.STATE_POWER_OFF).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_POWER_OFF).when(mServiceState).getDataRegState();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_UNKNOWN).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        doReturn(-1).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(5000L);
+        doReturn(ServiceState.STATE_IN_SERVICE).when(mServiceState).getVoiceRegState();
+        doReturn(ServiceState.STATE_IN_SERVICE).when(mServiceState).getDataRegState();
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
+        doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getDataNetworkType();
+        mockWwanPsRat(TelephonyManager.NETWORK_TYPE_LTE);
+        doReturn(CARRIER1_ID).when(mPhone).getCarrierId();
+        mServiceStateStats.onServiceStateChanged(mServiceState);
+        mServiceStateStats.incTimeMillis(200L);
+
+        // There should be 2 service state updates (1 distinct service state) and no switches
+        mServiceStateStats.conclude();
+        ArgumentCaptor<CellularServiceState> captor =
+                ArgumentCaptor.forClass(CellularServiceState.class);
+        verify(mPersistAtomsStorage, times(2))
+                .addCellularServiceStateAndCellularDataServiceSwitch(captor.capture(), eq(null));
+        CellularServiceState state = captor.getAllValues().get(0);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(100L, state.totalTimeMillis);
+        state = captor.getAllValues().get(1);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.voiceRat);
+        assertEquals(TelephonyManager.NETWORK_TYPE_LTE, state.dataRat);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.voiceRoamingType);
+        assertEquals(ServiceState.ROAMING_TYPE_NOT_ROAMING, state.dataRoamingType);
+        assertFalse(state.isEndc);
+        assertEquals(0, state.simSlotIndex);
+        assertFalse(state.isMultiSim);
+        assertEquals(CARRIER1_ID, state.carrierId);
+        assertEquals(200L, state.totalTimeMillis);
+        verifyNoMoreInteractions(mPersistAtomsStorage);
+    }
+
+    private void mockWwanPsRat(@NetworkType int rat) {
+        doReturn(new NetworkRegistrationInfo.Builder().setAccessNetworkTechnology(rat).build())
+                .when(mServiceState)
+                .getNetworkRegistrationInfo(
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+    }
+
+    private void mockDualSim(int carrierId) {
+        doReturn(1).when(mSecondPhone).getPhoneId();
+        doReturn(1).when(mUiccController).getSlotIdFromPhoneId(1);
+        doReturn(carrierId).when(mSecondPhone).getCarrierId();
+
+        doReturn(true).when(mPhysicalSlot1).isActive();
+        doReturn(CardState.CARDSTATE_PRESENT).when(mPhysicalSlot1).getCardState();
+        doReturn(false).when(mPhysicalSlot1).isEuicc();
+        doReturn(new UiccSlot[] {mPhysicalSlot0, mPhysicalSlot1})
+                .when(mUiccController)
+                .getUiccSlots();
+        doReturn(mPhysicalSlot1).when(mUiccController).getUiccSlot(1);
+        doReturn(mPhysicalSlot1).when(mUiccController).getUiccSlotForPhone(1);
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/SimSlotStateTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/SimSlotStateTest.java
index 0f1196f..e05faec 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/metrics/SimSlotStateTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/SimSlotStateTest.java
@@ -17,6 +17,8 @@
 package com.android.internal.telephony.metrics;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 
@@ -73,10 +75,12 @@
         doReturn(new UiccSlot[] {}).when(mUiccController).getUiccSlots();
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(0, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -85,10 +89,12 @@
         setupSingleSim(null);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(0, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -97,10 +103,12 @@
         setupSingleSim(mInactiveSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(0, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -109,10 +117,12 @@
         setupSingleSim(mEmptySlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -122,10 +132,12 @@
         setupSingleSim(mPhysicalSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(1, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -135,10 +147,12 @@
         setupSingleSim(mPhysicalSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -148,11 +162,13 @@
         setupSingleSim(mPhysicalSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         // the metrics should not count restricted cards since they cannot be used
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -162,10 +178,12 @@
         setupSingleSim(mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -175,10 +193,12 @@
         setupSingleSim(mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -188,10 +208,12 @@
         setupSingleSim(mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(1, state.numActiveSims);
         assertEquals(1, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -200,10 +222,12 @@
         setupDualSim(mEmptySlot, mInactiveSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -212,10 +236,12 @@
         setupDualSim(mPhysicalSlot, mInactiveSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(1, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -225,10 +251,12 @@
         setupDualSim(mInactiveSlot, mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -238,10 +266,12 @@
         setupDualSim(mInactiveSlot, mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(1, state.numActiveSlots);
         assertEquals(1, state.numActiveSims);
         assertEquals(1, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -251,10 +281,12 @@
         setupDualSim(mEmptySlot, mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(2, state.numActiveSlots);
         assertEquals(0, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -264,10 +296,12 @@
         setupDualSim(mPhysicalSlot, mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(2, state.numActiveSlots);
         assertEquals(1, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -277,10 +311,12 @@
         setupDualSim(mEmptySlot, mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(2, state.numActiveSlots);
         assertEquals(1, state.numActiveSims);
         assertEquals(1, state.numActiveEsims);
+        assertFalse(isMultiSim);
     }
 
     @Test
@@ -290,10 +326,12 @@
         setupDualSim(mPhysicalSlot, mEsimSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(2, state.numActiveSlots);
         assertEquals(2, state.numActiveSims);
         assertEquals(1, state.numActiveEsims);
+        assertTrue(isMultiSim);
     }
 
     @Test
@@ -302,10 +340,45 @@
         setupDualSim(mPhysicalSlot, mPhysicalSlot);
 
         SimSlotState state = SimSlotState.getCurrentState();
+        boolean isMultiSim = SimSlotState.isMultiSim();
 
         assertEquals(2, state.numActiveSlots);
         assertEquals(2, state.numActiveSims);
         assertEquals(0, state.numActiveEsims);
+        assertTrue(isMultiSim);
+    }
+
+    @Test
+    @SmallTest
+    public void isEsim_singlePhysicalSim() {
+        doReturn(mPhysicalSlot).when(mUiccController).getUiccSlotForPhone(eq(0));
+
+        boolean isEsim = SimSlotState.isEsim(0);
+
+        assertFalse(isEsim);
+    }
+
+    @Test
+    @SmallTest
+    public void isEsim_singleEsim() {
+        doReturn(mEsimSlot).when(mUiccController).getUiccSlotForPhone(eq(0));
+
+        boolean isEsim = SimSlotState.isEsim(0);
+
+        assertTrue(isEsim);
+    }
+
+    @Test
+    @SmallTest
+    public void isEsim_dualSim() {
+        doReturn(mPhysicalSlot).when(mUiccController).getUiccSlotForPhone(eq(0));
+        doReturn(mEsimSlot).when(mUiccController).getUiccSlotForPhone(eq(1));
+
+        boolean isEsim0 = SimSlotState.isEsim(0);
+        boolean isEsim1 = SimSlotState.isEsim(1);
+
+        assertFalse(isEsim0);
+        assertTrue(isEsim1);
     }
 
     private void setupSingleSim(UiccSlot slot0) {
diff --git a/tests/telephonytests/src/com/android/internal/telephony/metrics/VoiceCallSessionStatsTest.java b/tests/telephonytests/src/com/android/internal/telephony/metrics/VoiceCallSessionStatsTest.java
index e11cddd..f64bc6e 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/metrics/VoiceCallSessionStatsTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/metrics/VoiceCallSessionStatsTest.java
@@ -20,11 +20,12 @@
 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;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MT;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
+import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
-import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_FAST;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
-import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_FAST;
 import static com.android.internal.telephony.TelephonyStatsLog.VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_SLOW;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -57,7 +58,7 @@
 import com.android.internal.telephony.TelephonyTest;
 import com.android.internal.telephony.imsphone.ImsPhoneCall;
 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
-import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
+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;
 import com.android.internal.telephony.protobuf.nano.MessageNano;
@@ -133,6 +134,7 @@
         doReturn(CARRIER_ID_SLOT_0).when(mPhone).getCarrierId();
         // mPhone's mSST/mServiceState has been set up by TelephonyTest
         doReturn(CARRIER_ID_SLOT_1).when(mSecondPhone).getCarrierId();
+        doReturn(mSignalStrength).when(mSecondPhone).getSignalStrength();
         doReturn(mSecondServiceStateTracker).when(mSecondPhone).getServiceStateTracker();
         doReturn(mSecondServiceState).when(mSecondServiceStateTracker).getServiceState();
 
@@ -160,6 +162,7 @@
 
         doReturn(new UiccSlot[] {mPhysicalSlot}).when(mUiccController).getUiccSlots();
         doReturn(mPhysicalSlot).when(mUiccController).getUiccSlot(eq(0));
+        doReturn(mPhysicalSlot).when(mUiccController).getUiccSlotForPhone(eq(0));
 
         doReturn(0).when(mUiccController).getSlotIdFromPhoneId(0);
         doReturn(1).when(mUiccController).getSlotIdFromPhoneId(1);
@@ -199,13 +202,18 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_REMOTE_CALL_DECLINE);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 200;
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_EVS_SWB;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 12000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.DIALING).when(mImsCall0).getState();
@@ -247,10 +255,11 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_SIP_FORBIDDEN);
         expectedCall.setupFailed = true;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 2200L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.DIALING).when(mImsCall0).getState();
@@ -285,14 +294,17 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 200;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_EVS_SWB;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_SUPER_WIDEBAND;
         expectedCall.disconnectExtraMessage = "normal call clearing";
-        RawVoiceCallRatUsage expectedRatUsage =
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 100000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.DIALING).when(mImsCall0).getState();
@@ -340,11 +352,14 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 8000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -382,13 +397,16 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 12000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -423,6 +441,7 @@
         doReturn(mInactiveCard).when(mEsimSlot).getUiccCard();
         doReturn(new UiccSlot[] {mPhysicalSlot, mEsimSlot}).when(mUiccController).getUiccSlots();
         doReturn(mEsimSlot).when(mUiccController).getUiccSlot(eq(1));
+        doReturn(mEsimSlot).when(mUiccController).getUiccSlotForPhone(eq(1));
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
         doReturn(true).when(mImsConnection0).isIncoming();
         doReturn(2000L).when(mImsConnection0).getCreateTime();
@@ -435,7 +454,10 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.isMultiSim = false; // DSDS with one active SIM profile should not count
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -463,6 +485,7 @@
         doReturn(mActiveCard).when(mEsimSlot).getUiccCard();
         doReturn(new UiccSlot[] {mPhysicalSlot, mEsimSlot}).when(mUiccController).getUiccSlots();
         doReturn(mEsimSlot).when(mUiccController).getUiccSlot(eq(1));
+        doReturn(mEsimSlot).when(mUiccController).getUiccSlotForPhone(eq(1));
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
         doReturn(true).when(mImsConnection0).isIncoming();
         doReturn(2000L).when(mImsConnection0).getCreateTime();
@@ -475,7 +498,10 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.isMultiSim = true;
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -503,6 +529,7 @@
         doReturn(mActiveCard).when(mEsimSlot).getUiccCard();
         doReturn(new UiccSlot[] {mPhysicalSlot, mEsimSlot}).when(mUiccController).getUiccSlots();
         doReturn(mEsimSlot).when(mUiccController).getUiccSlot(eq(1));
+        doReturn(mEsimSlot).when(mUiccController).getUiccSlotForPhone(eq(1));
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mSecondServiceState).getVoiceNetworkType();
         doReturn(true).when(mImsConnection1).isIncoming();
         doReturn(2000L).when(mImsConnection1).getCreateTime();
@@ -515,7 +542,10 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
 
         mVoiceCallSessionStats1.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall1).getState();
@@ -552,7 +582,10 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.isEmergency = true;
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -590,7 +623,10 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.isRoaming = true;
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -627,10 +663,13 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask =
                 1L << AudioCodec.AUDIO_CODEC_AMR | 1L << AudioCodec.AUDIO_CODEC_EVS_SWB;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -675,21 +714,25 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.ratSwitchCount = 2L;
         expectedCall.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall.bandAtEnd = 0;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 4000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageHspa =
+        VoiceCallRatUsage expectedRatUsageHspa =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_HSPA, 4000L, 6000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 6000L, 8000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -721,7 +764,7 @@
         verifyNoMoreInteractions(mPersistAtomsStorage);
         assertProtoEquals(expectedCall, callCaptor.getValue());
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {
+                new VoiceCallRatUsage[] {
                     expectedRatUsageLte, expectedRatUsageHspa, expectedRatUsageUmts
                 },
                 ratUsage.get());
@@ -743,7 +786,10 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.rttEnabled = true;
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -780,9 +826,12 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall.rttEnabled = true;
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -828,13 +877,17 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE);
         expectedCall0.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall0.setupDurationMillis = 80;
         expectedCall0.setupFailed = false;
         expectedCall0.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall0.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall0.concurrentCallCountAtStart = 0;
         expectedCall0.concurrentCallCountAtEnd = 1;
         expectedCall0.ratSwitchCount = 1L;
         expectedCall0.ratAtEnd = TelephonyManager.NETWORK_TYPE_HSPA;
+        expectedCall0.bandAtEnd = 0;
         // call 1 starts later, MT
         doReturn(true).when(mImsConnection1).isIncoming();
         doReturn(60000L).when(mImsConnection1).getCreateTime();
@@ -848,26 +901,30 @@
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall1.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall1.setupDurationMillis = 20;
         expectedCall1.setupFailed = false;
         expectedCall1.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall1.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall1.concurrentCallCountAtStart = 1;
         expectedCall1.concurrentCallCountAtEnd = 0;
         expectedCall1.ratSwitchCount = 2L;
         expectedCall1.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall1.bandAtEnd = 0;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 80000L, 2L);
-        RawVoiceCallRatUsage expectedRatUsageHspa =
+        VoiceCallRatUsage expectedRatUsageHspa =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_HSPA, 80000L, 100000L, 2L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0,
                         TelephonyManager.NETWORK_TYPE_UMTS,
                         100000L,
                         120000L,
                         1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         // call 0 dial
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -926,7 +983,7 @@
                 new VoiceCallSession[] {expectedCall0, expectedCall1},
                 callCaptor.getAllValues().stream().toArray(VoiceCallSession[]::new));
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {
+                new VoiceCallRatUsage[] {
                     expectedRatUsageLte, expectedRatUsageHspa, expectedRatUsageUmts
                 },
                 ratUsage.get());
@@ -948,13 +1005,17 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall0.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall0.setupDurationMillis = 80;
         expectedCall0.setupFailed = false;
         expectedCall0.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall0.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall0.concurrentCallCountAtStart = 0;
         expectedCall0.concurrentCallCountAtEnd = 0;
         expectedCall0.ratSwitchCount = 2L;
         expectedCall0.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall0.bandAtEnd = 0;
         // call 1 starts later, MT
         doReturn(true).when(mImsConnection1).isIncoming();
         doReturn(60000L).when(mImsConnection1).getCreateTime();
@@ -968,26 +1029,30 @@
                         ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE);
         expectedCall1.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall1.setupDurationMillis = 20;
         expectedCall1.setupFailed = false;
         expectedCall1.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall1.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall1.concurrentCallCountAtStart = 1;
         expectedCall1.concurrentCallCountAtEnd = 1;
         expectedCall1.ratSwitchCount = 1L;
         expectedCall1.ratAtEnd = TelephonyManager.NETWORK_TYPE_HSPA;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall1.bandAtEnd = 0;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 80000L, 2L);
-        RawVoiceCallRatUsage expectedRatUsageHspa =
+        VoiceCallRatUsage expectedRatUsageHspa =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_HSPA, 80000L, 100000L, 2L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0,
                         TelephonyManager.NETWORK_TYPE_UMTS,
                         100000L,
                         120000L,
                         1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         // call 0 dial
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -1046,7 +1111,7 @@
                 new VoiceCallSession[] {expectedCall0, expectedCall1},
                 callCaptor.getAllValues().stream().toArray(VoiceCallSession[]::new));
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {
+                new VoiceCallRatUsage[] {
                     expectedRatUsageLte, expectedRatUsageHspa, expectedRatUsageUmts
                 },
                 ratUsage.get());
@@ -1068,9 +1133,12 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall0.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall0.setupDurationMillis = 80;
         expectedCall0.setupFailed = false;
         expectedCall0.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall0.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall0.concurrentCallCountAtStart = 0;
         expectedCall0.concurrentCallCountAtEnd = 1;
         expectedCall0.ratSwitchCount = 0L;
@@ -1088,19 +1156,23 @@
                         ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE);
         expectedCall1.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall1.setupDurationMillis = 20;
         expectedCall1.setupFailed = false;
         expectedCall1.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall1.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall1.concurrentCallCountAtStart = 1;
         expectedCall1.concurrentCallCountAtEnd = 0;
         expectedCall1.ratSwitchCount = 1L;
         expectedCall1.ratAtEnd = TelephonyManager.NETWORK_TYPE_HSPA;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall1.bandAtEnd = 0;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 80000L, 2L);
-        RawVoiceCallRatUsage expectedRatUsageHspa =
+        VoiceCallRatUsage expectedRatUsageHspa =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_HSPA, 80000L, 90000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         // call 0 dial
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -1155,7 +1227,7 @@
                 new VoiceCallSession[] {expectedCall0, expectedCall1},
                 callCaptor.getAllValues().stream().toArray(VoiceCallSession[]::new));
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageHspa},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageHspa},
                 ratUsage.get());
     }
 
@@ -1172,19 +1244,24 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         DisconnectCause.NORMAL);
         expectedCall.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall.bandAtEnd = 0;
         expectedCall.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_SLOW;
+        expectedCall.setupDurationMillis = 5000;
         expectedCall.disconnectExtraCode = PreciseDisconnectCause.CALL_REJECTED;
         expectedCall.ratSwitchCount = 1L;
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 3000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 3000L, 15000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
@@ -1215,7 +1292,7 @@
         verifyNoMoreInteractions(mPersistAtomsStorage);
         assertProtoEquals(expectedCall, callCaptor.getValue());
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
                 ratUsage.get());
     }
 
@@ -1232,16 +1309,20 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         DisconnectCause.LOST_SIGNAL);
         expectedCall.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall.bandAtEnd = 0;
         expectedCall.ratSwitchCount = 1L;
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 3000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 3000L, 15000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
@@ -1265,7 +1346,7 @@
         verifyNoMoreInteractions(mPersistAtomsStorage);
         assertProtoEquals(expectedCall, callCaptor.getValue());
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
                 ratUsage.get());
     }
 
@@ -1281,20 +1362,25 @@
                         VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MO,
                         TelephonyManager.NETWORK_TYPE_LTE,
                         DisconnectCause.NORMAL);
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UMTS;
         expectedCall.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall.bandAtEnd = 0;
         expectedCall.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_VERY_SLOW;
+        expectedCall.setupDurationMillis = 5000;
         expectedCall.disconnectExtraCode = PreciseDisconnectCause.NORMAL;
         expectedCall.ratSwitchCount = 1L;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 3000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 3000L, 100000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mServiceState).getVoiceNetworkType();
@@ -1327,7 +1413,7 @@
         verifyNoMoreInteractions(mPersistAtomsStorage);
         assertProtoEquals(expectedCall, callCaptor.getValue());
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
                 ratUsage.get());
     }
 
@@ -1346,13 +1432,18 @@
                         DisconnectCause.NORMAL);
         expectedCall.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
+        expectedCall.setupDurationMillis = 0;
         expectedCall.disconnectExtraCode = PreciseDisconnectCause.CALL_REJECTED;
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        expectedCall.bandAtEnd = 0;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 2500L, 15000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         mVoiceCallSessionStats0.onServiceStateChanged(mServiceState);
@@ -1393,14 +1484,19 @@
                         VOICE_CALL_SESSION__DIRECTION__CALL_DIRECTION_MT,
                         TelephonyManager.NETWORK_TYPE_UMTS,
                         DisconnectCause.NORMAL);
-        expectedCall.setupDuration = VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_FAST;
+        expectedCall.setupDuration =
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+        expectedCall.setupDurationMillis = 500;
         expectedCall.disconnectExtraCode = PreciseDisconnectCause.NORMAL;
         expectedCall.setupFailed = false;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        expectedCall.bandAtEnd = 0;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 2500L, 100000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         mVoiceCallSessionStats0.onServiceStateChanged(mServiceState);
@@ -1449,19 +1545,23 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_LOCAL_HO_NOT_FEASIBLE);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.srvccFailureCount = 2L;
         expectedCall.ratSwitchCount = 1L;
         expectedCall.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall.bandAtEnd = 0;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 10000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 10000L, 12000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -1502,7 +1602,7 @@
         verifyNoMoreInteractions(mPersistAtomsStorage);
         assertProtoEquals(expectedCall, callCaptor.getValue());
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
                 ratUsage.get());
     }
 
@@ -1523,14 +1623,17 @@
                         TelephonyManager.NETWORK_TYPE_LTE,
                         ImsReasonInfo.CODE_USER_TERMINATED);
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.srvccCancellationCount = 2L;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 12000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -1591,7 +1694,8 @@
                         DisconnectCause.NORMAL);
         expectedCall.disconnectExtraCode = PreciseDisconnectCause.NORMAL;
         expectedCall.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall.setupDurationMillis = 80;
         expectedCall.setupFailed = false;
         expectedCall.srvccCancellationCount = 1L;
         expectedCall.srvccFailureCount = 1L;
@@ -1599,14 +1703,17 @@
         expectedCall.bearerAtEnd = VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
         expectedCall.ratSwitchCount = 1L;
         expectedCall.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall.bandAtEnd = 0;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 7000L, 1L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 7000L, 12000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         // IMS call created
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -1656,7 +1763,7 @@
         verifyNoMoreInteractions(mPersistAtomsStorage);
         assertProtoEquals(expectedCall, callCaptor.getValue());
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
                 ratUsage.get());
     }
 
@@ -1682,13 +1789,17 @@
                         DisconnectCause.NORMAL);
         expectedCall0.disconnectExtraCode = PreciseDisconnectCause.NORMAL;
         expectedCall0.setupDuration =
-                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_ULTRA_FAST;
+                VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall0.setupDurationMillis = 80;
         expectedCall0.setupFailed = false;
         expectedCall0.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall0.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall0.concurrentCallCountAtStart = 0;
         expectedCall0.concurrentCallCountAtEnd = 1;
         expectedCall0.ratSwitchCount = 1L;
         expectedCall0.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall0.bandAtEnd = 0;
         expectedCall0.srvccCompleted = true;
         expectedCall0.bearerAtEnd = VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
         // call 1 starts later, MT
@@ -1707,21 +1818,25 @@
         expectedCall1.disconnectExtraCode = PreciseDisconnectCause.NORMAL;
         expectedCall1.setupDuration =
                 VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_EXTREMELY_FAST;
+        expectedCall1.setupDurationMillis = 20;
         expectedCall1.setupFailed = false;
         expectedCall1.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
+        expectedCall1.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
         expectedCall1.concurrentCallCountAtStart = 1;
         expectedCall1.concurrentCallCountAtEnd = 0;
         expectedCall1.ratSwitchCount = 1L;
         expectedCall1.ratAtEnd = TelephonyManager.NETWORK_TYPE_UMTS;
+        expectedCall1.bandAtEnd = 0;
         expectedCall1.srvccCompleted = true;
         expectedCall1.bearerAtEnd = VOICE_CALL_SESSION__BEARER_AT_END__CALL_BEARER_CS;
-        RawVoiceCallRatUsage expectedRatUsageLte =
+        VoiceCallRatUsage expectedRatUsageLte =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_LTE, 2000L, 80000L, 2L);
-        RawVoiceCallRatUsage expectedRatUsageUmts =
+        VoiceCallRatUsage expectedRatUsageUmts =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_UMTS, 80000L, 120000L, 2L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         // call 0 dial
         mVoiceCallSessionStats0.setTimeMillis(2000L);
@@ -1784,7 +1899,7 @@
                 new VoiceCallSession[] {expectedCall0, expectedCall1},
                 callCaptor.getAllValues().stream().toArray(VoiceCallSession[]::new));
         assertSortedProtoArrayEquals(
-                new RawVoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
+                new VoiceCallRatUsage[] {expectedRatUsageLte, expectedRatUsageUmts},
                 ratUsage.get());
     }
 
@@ -1806,11 +1921,15 @@
                         TelephonyManager.NETWORK_TYPE_IWLAN,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        expectedCall.bandAtEnd = 0;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_IWLAN, 2000L, 8000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -1851,11 +1970,15 @@
                         TelephonyManager.NETWORK_TYPE_IWLAN,
                         ImsReasonInfo.CODE_LOCAL_CALL_DECLINE);
         expectedCall.setupFailed = true;
+        expectedCall.ratAtConnected = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        expectedCall.bandAtEnd = 0;
         expectedCall.codecBitmask = 1L << AudioCodec.AUDIO_CODEC_AMR;
-        RawVoiceCallRatUsage expectedRatUsage =
+        expectedCall.mainCodecQuality =
+                VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_NARROWBAND;
+        VoiceCallRatUsage expectedRatUsage =
                 makeRatUsageProto(
                         CARRIER_ID_SLOT_0, TelephonyManager.NETWORK_TYPE_IWLAN, 2000L, 8000L, 1L);
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = setupRatUsageCapture();
 
         mVoiceCallSessionStats0.setTimeMillis(2000L);
         doReturn(Call.State.INCOMING).when(mImsCall0).getState();
@@ -1878,8 +2001,8 @@
         assertProtoEquals(expectedRatUsage, ratUsage.get()[0]);
     }
 
-    private AtomicReference<RawVoiceCallRatUsage[]> setupRatUsageCapture() {
-        final AtomicReference<RawVoiceCallRatUsage[]> ratUsage = new AtomicReference<>(null);
+    private AtomicReference<VoiceCallRatUsage[]> setupRatUsageCapture() {
+        final AtomicReference<VoiceCallRatUsage[]> ratUsage = new AtomicReference<>(null);
         doAnswer(invocation -> {
             VoiceCallRatTracker tracker = (VoiceCallRatTracker) invocation.getArguments()[0];
             ratUsage.set(tracker.toProto());
@@ -1895,14 +2018,18 @@
         call.bearerAtEnd = bearer;
         call.direction = direction;
         call.setupDuration = VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
+        call.setupDurationMillis = 0;
         call.setupFailed = true;
         call.disconnectReasonCode = reason;
         call.disconnectExtraCode = 0;
         call.disconnectExtraMessage = "";
         call.ratAtStart = rat;
+        call.ratAtConnected = rat;
         call.ratAtEnd = rat;
+        call.bandAtEnd = 1;
         call.ratSwitchCount = 0L;
         call.codecBitmask = 0L;
+        call.mainCodecQuality = VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
         call.simSlotIndex = 0;
         call.isMultiSim = false;
         call.isEsim = false;
@@ -1914,6 +2041,7 @@
         call.isEmergency = false;
         call.isRoaming = false;
         call.setupBeginMillis = 0L;
+        call.signalStrengthAtEnd = 2;
         return call;
     }
 
@@ -1924,14 +2052,18 @@
         call.bearerAtEnd = bearer;
         call.direction = direction;
         call.setupDuration = VOICE_CALL_SESSION__SETUP_DURATION__CALL_SETUP_DURATION_UNKNOWN;
+        call.setupDurationMillis = 0;
         call.setupFailed = true;
         call.disconnectReasonCode = reason;
         call.disconnectExtraCode = 0;
         call.disconnectExtraMessage = "";
         call.ratAtStart = rat;
+        call.ratAtConnected = rat;
         call.ratAtEnd = rat;
+        call.bandAtEnd = 1;
         call.ratSwitchCount = 0L;
         call.codecBitmask = 0L;
+        call.mainCodecQuality = VOICE_CALL_SESSION__MAIN_CODEC_QUALITY__CODEC_QUALITY_UNKNOWN;
         call.simSlotIndex = 1;
         call.isMultiSim = true;
         call.isEsim = true;
@@ -1943,12 +2075,13 @@
         call.isEmergency = false;
         call.isRoaming = false;
         call.setupBeginMillis = 0L;
+        call.signalStrengthAtEnd = 2;
         return call;
     }
 
-    private static RawVoiceCallRatUsage makeRatUsageProto(
+    private static VoiceCallRatUsage makeRatUsageProto(
             int carrierId, int rat, long beginMillis, long endMillis, long callCount) {
-        RawVoiceCallRatUsage usage = new RawVoiceCallRatUsage();
+        VoiceCallRatUsage usage = new VoiceCallRatUsage();
         usage.carrierId = carrierId;
         usage.rat = rat;
         usage.totalDurationMillis = endMillis - beginMillis;
diff --git a/tests/telephonytests/src/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRulesTest.java b/tests/telephonytests/src/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRulesTest.java
index b383bfa..39ec59d 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRulesTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRulesTest.java
@@ -26,6 +26,7 @@
 import android.content.pm.Signature;
 import android.os.AsyncResult;
 import android.os.Message;
+import android.telephony.UiccAccessRule;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -41,11 +42,17 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.util.List;
+
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class UiccCarrierPrivilegeRulesTest extends TelephonyTest {
     private UiccCarrierPrivilegeRules mUiccCarrierPrivilegeRules;
 
+    private static final String ARAM = "A00000015141434C00";
+    private static final String ARAD = "A00000015144414300";
+    private static final String PKCS15_AID = "A000000063504B43532D3135";
+
     @Mock
     private UiccProfile mUiccProfile;
 
@@ -289,10 +296,6 @@
         assertTrue(!mUiccCarrierPrivilegeRules.shouldRetry(ar, 0));
     }
 
-    private static final String ARAM = "A00000015141434C00";
-    private static final String ARAD = "A00000015144414300";
-    private static final String PKCS15_AID = "A000000063504B43532D3135";
-
     @Test
     @SmallTest
     public void testAID_OnlyARAM() {
@@ -476,7 +479,7 @@
 
     @Test
     @SmallTest
-    public void testAID_NeitherARAMorARAD() {
+    public void testAID_ARFFailed() {
         final String hexString =
                 "FF4045E243E135C114ABCD92CBB156B280FA4E1429A6ECEEB6E5C1BFE4CA1D636F6D2E676F6F676"
                         + "C652E616E64726F69642E617070732E6D79617070E30ADB080000000000000001";
@@ -515,8 +518,285 @@
         assertTrue(!mUiccCarrierPrivilegeRules.hasCarrierPrivilegeRules());
     }
 
+    @Test
+    @SmallTest
+    public void testAID_ARFSucceed() {
+        /**
+         * PKCS#15 application (AID: A0 00 00 00 63 50 4B 43 53 2D 31 35)
+         *   -ODF (5031)
+         *       A7 06 30 04 04 02 52 07
+         *   -DODF (5207)
+         *       A1 29 30 00 30 0F 0C 0D 47 50 20 53 45 20 41 63 63 20 43 74 6C A1 14 30 12
+         *       06 0A 2A 86 48 86 FC 6B 81 48 01 01 30 04 04 02 42 00
+         *   -EF ACMain (4200)
+         *       30 10 04 08 01 02 03 04 05 06 07 08 30 04 04 02 43 00
+         *   -EF ACRules (4300)
+         *       30 10 A0 08 04 06 A0 00 00 01 51 01 30 04 04 02 43 10
+         *   -EF ACConditions1 (4310)
+         *       30 22
+         *          04 20
+         *             B9CFCE1C47A6AC713442718F15EF55B00B3A6D1A6D48CB46249FA8EB51465350
+         *       30 22
+         *          04 20
+         *             4C36AF4A5BDAD97C1F3D8B283416D244496C2AC5EAFE8226079EF6F676FD1859
+         */
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                String aid = (String) invocation.getArguments()[0];
+                Message message = (Message) invocation.getArguments()[2];
+                AsyncResult ar = new AsyncResult(null, null, null);
+                if (aid.equals(ARAM)) {
+                    message.arg2 = 1;
+                } else if (aid.equals(ARAD)) {
+                    message.arg2 = 0;
+                } else {
+                    // PKCS15
+                    ar = new AsyncResult(null, new int[]{2}, null);
+                }
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccOpenLogicalChannel(anyString(), anyInt(), any(Message.class));
+
+        // Select files
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                AsyncResult ar = new AsyncResult(null, new int[]{2}, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xA4), eq(0x00),
+                eq(0x04), eq(0x02), anyString(), any(Message.class));
+
+        // Read binary - ODF
+        String odf = "A706300404025207";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(odf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("5031"), any(Message.class));
+
+        // Read binary - DODF
+        String dodf =
+                "A1293000300F0C0D4750205345204163632043746CA11"
+                        + "43012060A2A864886FC6B81480101300404024200";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(dodf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("5207"), any(Message.class));
+
+        // Read binary - ACMF
+        String acmf = "301004080102030405060708300404024300";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(acmf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("4200"), any(Message.class));
+
+        // Read binary - ACRF
+        String acrf = "3010A0080406FFFFFFFFFFFF300404024310";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(acrf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("4300"), any(Message.class));
+
+        // Read binary - ACCF
+        String accf =
+                "30220420B9CFCE1C47A6AC713442718F15EF55B00B3A6D1A6D48CB46249FA8EB514653503022042"
+                        + "04C36AF4A5BDAD97C1F3D8B283416D244496C2AC5EAFE8226079EF6F676FD1859";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(accf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("4310"), any(Message.class));
+
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[1];
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccCloseLogicalChannel(anyInt(), any(Message.class));
+
+        mUiccCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(mUiccProfile, null);
+        processAllMessages();
+
+        assertTrue(mUiccCarrierPrivilegeRules.hasCarrierPrivilegeRules());
+        assertEquals(2, mUiccCarrierPrivilegeRules.getAccessRules().size());
+        List<UiccAccessRule> accessRules = mUiccCarrierPrivilegeRules.getAccessRules();
+        UiccAccessRule accessRule1 = new UiccAccessRule(
+                IccUtils.hexStringToBytes(
+                        "B9CFCE1C47A6AC713442718F15EF55B00B3A6D1A6D48CB46249FA8EB51465350"),
+                "",
+                0x00);
+        assertTrue(accessRules.contains(accessRule1));
+        UiccAccessRule accessRule2 = new UiccAccessRule(
+                IccUtils.hexStringToBytes(
+                        "4C36AF4A5BDAD97C1F3D8B283416D244496C2AC5EAFE8226079EF6F676FD1859"),
+                "",
+                0x00);
+        assertTrue(accessRules.contains(accessRule2));
+    }
+
+    @Test
+    @SmallTest
+    public void testAID_ARFFallbackToACRF() {
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                String aid = (String) invocation.getArguments()[0];
+                Message message = (Message) invocation.getArguments()[2];
+                AsyncResult ar = new AsyncResult(null, null, null);
+                if (aid.equals(ARAM)) {
+                    message.arg2 = 1;
+                } else if (aid.equals(ARAD)) {
+                    message.arg2 = 0;
+                } else {
+                    // PKCS15
+                    ar = new AsyncResult(null, new int[]{2}, null);
+                }
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccOpenLogicalChannel(anyString(), anyInt(), any(Message.class));
+
+        // Select files
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                AsyncResult ar = new AsyncResult(null, new int[]{2}, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xA4), eq(0x00),
+                eq(0x04), eq(0x02), anyString(), any(Message.class));
+
+        // Read binary ODF failed
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, new byte[]{});
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("5031"), any(Message.class));
+
+        // Read binary - ACRF
+        String acrf = "3010A0080406FFFFFFFFFFFF300404024310";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(acrf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("4300"), any(Message.class));
+
+        // Read binary - ACCF
+        String accf =
+                "30220420B9CFCE1C47A6AC713442718F15EF55B00B3A6D1A6D48CB46249FA8EB514653503022042"
+                        + "04C36AF4A5BDAD97C1F3D8B283416D244496C2AC5EAFE8226079EF6F676FD1859";
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[7];
+                IccIoResult iir = new IccIoResult(0x90, 0x00, IccUtils.hexStringToBytes(accf));
+                AsyncResult ar = new AsyncResult(null, iir, null);
+                message.obj = ar;
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccTransmitApduLogicalChannel(anyInt(), eq(0x00), eq(0xB0), eq(0x00),
+                eq(0x00), eq(0x00), eq("4310"), any(Message.class));
+
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Message message = (Message) invocation.getArguments()[1];
+                message.sendToTarget();
+                return null;
+            }
+        }).when(mUiccProfile).iccCloseLogicalChannel(anyInt(), any(Message.class));
+
+        mUiccCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(mUiccProfile, null);
+        processAllMessages();
+
+        assertTrue(mUiccCarrierPrivilegeRules.hasCarrierPrivilegeRules());
+        assertEquals(2, mUiccCarrierPrivilegeRules.getAccessRules().size());
+        List<UiccAccessRule> accessRules = mUiccCarrierPrivilegeRules.getAccessRules();
+        UiccAccessRule accessRule1 = new UiccAccessRule(
+                IccUtils.hexStringToBytes(
+                        "B9CFCE1C47A6AC713442718F15EF55B00B3A6D1A6D48CB46249FA8EB51465350"),
+                "",
+                0x00);
+        assertTrue(accessRules.contains(accessRule1));
+        UiccAccessRule accessRule2 = new UiccAccessRule(
+                IccUtils.hexStringToBytes(
+                        "4C36AF4A5BDAD97C1F3D8B283416D244496C2AC5EAFE8226079EF6F676FD1859"),
+                "",
+                0x00);
+        assertTrue(accessRules.contains(accessRule2));
+    }
+
     private static final int P2 = 0x40;
     private static final int P2_EXTENDED_DATA = 0x60;
+
     @Test
     @SmallTest
     public void testAID_RetransmitLogicalChannel() {