Merge "eSIM deprecated API Refactoring"
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index a5c5e86..7738bcd 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -310,7 +310,7 @@
     <string name="sim_change_data_title" msgid="9142726786345906606">"Promijeniti SIM za prijenos podataka?"</string>
     <string name="sim_change_data_message" msgid="3567358694255933280">"Koristiti SIM karticu <xliff:g id="NEW_SIM">%1$s</xliff:g> umjesto SIM kartice <xliff:g id="OLD_SIM">%2$s</xliff:g> za prijenos podataka na mobilnoj mreži?"</string>
     <string name="wifi_calling_settings_title" msgid="5800018845662016507">"Pozivanje putem WiFi-ja"</string>
-    <string name="video_calling_settings_title" msgid="342829454913266078">"Operater video pozivanja"</string>
+    <string name="video_calling_settings_title" msgid="342829454913266078">"Video pozivi putem operatera"</string>
     <string name="gsm_umts_options" msgid="4968446771519376808">"GSM/UMTS opcije"</string>
     <string name="cdma_options" msgid="3669592472226145665">"CDMA opcije"</string>
     <string name="throttle_data_usage" msgid="1944145350660420711">"Korištenje podataka"</string>
@@ -839,7 +839,7 @@
     <string name="radio_info_data_connection_enable" msgid="6183729739783252840">"Omogućite vezu za prijenos podataka"</string>
     <string name="radio_info_data_connection_disable" msgid="6404751291511368706">"Onemogući vezu za prijenos podataka"</string>
     <string name="volte_provisioned_switch_string" msgid="4812874990480336178">"VoLTE omogućen"</string>
-    <string name="vt_provisioned_switch_string" msgid="8295542122512195979">"Video poziv obezbijeđen"</string>
+    <string name="vt_provisioned_switch_string" msgid="8295542122512195979">"Video pozivi su omogućeni"</string>
     <string name="wfc_provisioned_switch_string" msgid="3835004640321078988">"WiFi poziv obezbijeđen"</string>
     <string name="eab_provisioned_switch_string" msgid="4449676720736033035">"EAB/Omogućeno prisustvo"</string>
     <string name="cbrs_data_switch_string" msgid="6060356430838077653">"Cbrs podaci"</string>
@@ -858,7 +858,7 @@
     <string name="radio_info_ims_reg_status_not_registered" msgid="8045821447288876085">"Nije registrirano"</string>
     <string name="radio_info_ims_feature_status_available" msgid="6493200914756969292">"Dostupno"</string>
     <string name="radio_info_ims_feature_status_unavailable" msgid="8930391136839759778">"Nedostupno"</string>
-    <string name="radio_info_ims_reg_status" msgid="25582845222446390">"IMS registracija: <xliff:g id="STATUS">%1$s</xliff:g>\nGovor putem LTE mreže: <xliff:g id="AVAILABILITY_0">%2$s</xliff:g>\nGovor putem WiFi mreže: <xliff:g id="AVAILABILITY_1">%3$s</xliff:g>\nVideo pozivanje: <xliff:g id="AVAILABILITY_2">%4$s</xliff:g>\nUT interfejs: <xliff:g id="AVAILABILITY_3">%5$s</xliff:g>"</string>
+    <string name="radio_info_ims_reg_status" msgid="25582845222446390">"IMS registracija: <xliff:g id="STATUS">%1$s</xliff:g>\nGovor putem LTE mreže: <xliff:g id="AVAILABILITY_0">%2$s</xliff:g>\nGovor putem WiFi mreže: <xliff:g id="AVAILABILITY_1">%3$s</xliff:g>\nVideo pozivi: <xliff:g id="AVAILABILITY_2">%4$s</xliff:g>\nUT interfejs: <xliff:g id="AVAILABILITY_3">%5$s</xliff:g>"</string>
     <string name="radioInfo_service_in" msgid="45753418231446400">"Aktivno"</string>
     <string name="radioInfo_service_out" msgid="287972405416142312">"Ne radi"</string>
     <string name="radioInfo_service_emergency" msgid="4763879891415016848">"Samo hitni pozivi"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index c941be2..22aa185 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -890,14 +890,14 @@
     <string name="radio_info_ppp_resets_label" msgid="9131901102339077661">"चालू करने के बाद से पीपीपी रीसेट की संख्या:"</string>
     <string name="radio_info_current_network_label" msgid="3052098695239642450">"मौजूदा नेटवर्क:"</string>
     <string name="radio_info_ppp_received_label" msgid="5753592451640644889">"मिलने वाला डेटा :"</string>
-    <string name="radio_info_gsm_service_label" msgid="6443348321714241328">"Voice की सेवा:"</string>
+    <string name="radio_info_gsm_service_label" msgid="6443348321714241328">"वॉइस सेवा:"</string>
     <string name="radio_info_signal_strength_label" msgid="5545444702102543260">"सिग्नल कितना अच्छा है:"</string>
-    <string name="radio_info_call_status_label" msgid="7693575431923095487">"Voice कॉल की स्थिति:"</string>
+    <string name="radio_info_call_status_label" msgid="7693575431923095487">"वॉइस कॉल की स्थिति:"</string>
     <string name="radio_info_ppp_sent_label" msgid="6542208429356199695">"भेजा गया डेटा :"</string>
     <string name="radio_info_message_waiting_label" msgid="1886549432566952078">"मैसेज वेटिंग:"</string>
     <string name="radio_info_phone_number_label" msgid="2533852539562512203">"फ़ोन नंबर:"</string>
     <string name="radio_info_band_mode_label" msgid="23480556225515290">"रेडियो का बैंड चुनें"</string>
-    <string name="radio_info_voice_network_type_label" msgid="2395347336419593265">"Voice के नेटवर्क प्रकार:"</string>
+    <string name="radio_info_voice_network_type_label" msgid="2395347336419593265">"वॉइस नेटवर्क टाइप:"</string>
     <string name="radio_info_data_network_type_label" msgid="8886597029237501929">"डेटा नेटवर्क प्रकार:"</string>
     <string name="phone_index_label" msgid="6222406512768964268">"फ़ोन इंडेक्स चुनें"</string>
     <string name="radio_info_set_perferred_label" msgid="7408131389363136210">"पसंदीदा नेटवर्क प्रकार सेट करें:"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 18ee718..06af4bc 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -297,7 +297,7 @@
     <string name="sip_accounts_removed_notification_title" msgid="3528076957535736095">"Account SIP deprecati trovati e rimossi"</string>
     <string name="sip_accounts_removed_notification_message" msgid="1916856744869791592">"Le chiamate SIP non sono più supportate dalla piattaforma Android.\nI tuoi account SIP esistenti (<xliff:g id="REMOVED_SIP_ACCOUNTS">%s</xliff:g>) sono stati rimossi.\nConferma l\'impostazione del tuo account predefinito per le chiamate."</string>
     <string name="sip_accounts_removed_notification_action" msgid="3772778402370555562">"Vai alle impostazioni"</string>
-    <string name="data_usage_title" msgid="8438592133893837464">"Utilizzo dei dati delle app"</string>
+    <string name="data_usage_title" msgid="8438592133893837464">"Utilizzo dati delle app"</string>
     <string name="data_usage_template" msgid="6287906680674061783">"Dati mobili usati: <xliff:g id="ID_1">%1$s</xliff:g> nel periodo <xliff:g id="ID_2">%2$s</xliff:g>"</string>
     <string name="advanced_options_title" msgid="9208195294513520934">"Avanzate"</string>
     <string name="carrier_settings_euicc" msgid="1190237227261337749">"Operatore"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 142d1e1..e16c4fa 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -596,7 +596,7 @@
     <string name="hac_mode_summary" msgid="7774989500136009881">"Slå på kompatibilitet med høreapparater"</string>
     <string name="rtt_mode_title" msgid="3075948111362818043">"Sanntidstekst-anrop (STT)"</string>
     <string name="rtt_mode_summary" msgid="8631541375609989562">"Tillat meldingsutveksling i talesamtaler"</string>
-    <string name="rtt_mode_more_information" msgid="587500128658756318">"STT er til hjelp for brukere som er døve, tunghørte, har talefunksjonshemning eller trenger mer enn bare tale.&lt;br&gt; &lt;a href=<xliff:g id="URL">http://support.google.com/mobile?p=telephony_rtt</xliff:g>&gt;Finn ut mer&lt;/a&gt;\n       &lt;br&gt;&lt;br&gt; – STT-anrop lagres som meldingstranskripsjoner\n       &lt;br&gt; – STT er ikke tilgjengelig for videoanrop"</string>
+    <string name="rtt_mode_more_information" msgid="587500128658756318">"STT er til hjelp for brukere som er døve, hørselshemmede, har taleproblemer eller trenger mer enn bare tale.&lt;br&gt; &lt;a href=<xliff:g id="URL">http://support.google.com/mobile?p=telephony_rtt</xliff:g>&gt;Finn ut mer&lt;/a&gt;\n       &lt;br&gt;&lt;br&gt; – STT-anrop lagres som meldingstranskripsjoner\n       &lt;br&gt; – STT er ikke tilgjengelig for videoanrop"</string>
     <string name="no_rtt_when_roaming" msgid="5268008247378355389">"Merk: RTT er ikke tilgjengelig ved roaming"</string>
   <string-array name="tty_mode_entries">
     <item msgid="3238070884803849303">"TTY av"</item>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 6ff98c6..226dc72 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -125,9 +125,9 @@
     <string name="sum_cfnrc_disabled" msgid="739289696796917683">"Uit"</string>
     <string name="disable_cfnrc_forbidden" msgid="775348748084726890">"Je provider biedt geen ondersteuning voor het uitschakelen van oproepdoorschakelingen wanneer je telefoon niet bereikbaar is."</string>
     <string name="registration_cf_forbidden" msgid="4386482610771190420">"Je provider ondersteunt het doorschakelen van gesprekken niet."</string>
-    <string name="cdma_call_waiting" msgid="4565070960879673216">"Wisselgesprek inschakelen?"</string>
+    <string name="cdma_call_waiting" msgid="4565070960879673216">"Wisselgesprek aanzetten?"</string>
     <string name="enable_cdma_call_waiting_setting" msgid="5906811747921744307">"Tijdens een gesprek krijg je een melding over inkomende gesprekken"</string>
-    <string name="enable_cdma_cw" msgid="811047045863422232">"Inschakelen"</string>
+    <string name="enable_cdma_cw" msgid="811047045863422232">"Aanzetten"</string>
     <string name="disable_cdma_cw" msgid="7119290446496301734">"Annuleren"</string>
     <string name="cdma_call_waiting_in_ims_on" msgid="6390979414188659218">"CDMA-wisselgesprek onder IMS aan"</string>
     <string name="cdma_call_waiting_in_ims_off" msgid="1099246114368636334">"CDMA-wisselgesprek onder IMS uit"</string>
@@ -147,7 +147,7 @@
     <string name="fdn_check_failure" msgid="1833769746374185247">"De instelling \'Vaste nummers\' in je app Telefoon is ingeschakeld. Hierdoor werken sommige oproepgerelateerde functies niet."</string>
     <string name="radio_off_error" msgid="8321564164914232181">"Schakel de radio in voordat je deze instellingen bekijkt."</string>
     <string name="close_dialog" msgid="1074977476136119408">"OK"</string>
-    <string name="enable" msgid="2636552299455477603">"Inschakelen"</string>
+    <string name="enable" msgid="2636552299455477603">"Aanzetten"</string>
     <string name="disable" msgid="1122698860799462116">"Uit"</string>
     <string name="change_num" msgid="6982164494063109334">"Updaten"</string>
   <string-array name="clir_display_values">
@@ -443,20 +443,20 @@
     <string name="fdn_activation" msgid="2178637004710435895">"FDN-activering"</string>
     <string name="fdn_enabled" msgid="7017355494808056447">"Vaste nummers zijn ingeschakeld"</string>
     <string name="fdn_disabled" msgid="6696468878037736600">"Vaste nummers zijn uitgeschakeld"</string>
-    <string name="enable_fdn" msgid="4830555730418033723">"FDN inschakelen"</string>
+    <string name="enable_fdn" msgid="4830555730418033723">"FDN aanzetten"</string>
     <string name="disable_fdn" msgid="3918794950264647541">"FDN uitschakelen"</string>
     <string name="change_pin2" msgid="3110844547237754871">"PIN2-code wijzigen"</string>
     <string name="enable_fdn_ok" msgid="5080925177369329827">"FDN uitschakelen"</string>
-    <string name="disable_fdn_ok" msgid="3745475926874838676">"FDN inschakelen"</string>
+    <string name="disable_fdn_ok" msgid="3745475926874838676">"FDN aanzetten"</string>
     <string name="sum_fdn" msgid="6152246141642323582">"Vaste nummers beheren"</string>
     <string name="sum_fdn_change_pin" msgid="3510994280557335727">"Pincode voor FDN-toegang wijzigen"</string>
     <string name="sum_fdn_manage_list" msgid="3311397063233992907">"Lijst met telefoonnummers beheren"</string>
     <string name="voice_privacy" msgid="7346935172372181951">"Spraakprivacy"</string>
-    <string name="voice_privacy_summary" msgid="3556460926168473346">"Geavanceerde privacymodus inschakelen"</string>
+    <string name="voice_privacy_summary" msgid="3556460926168473346">"Geavanceerde privacymodus aanzetten"</string>
     <string name="tty_mode_option_title" msgid="3843817710032641703">"TTY-modus"</string>
     <string name="tty_mode_option_summary" msgid="4770510287236494371">"TTY-modus instellen"</string>
     <string name="auto_retry_mode_title" msgid="2985801935424422340">"Automatisch opnieuw proberen"</string>
-    <string name="auto_retry_mode_summary" msgid="2863919925349511402">"Modus voor automatisch opnieuw proberen inschakelen"</string>
+    <string name="auto_retry_mode_summary" msgid="2863919925349511402">"Modus voor automatisch opnieuw proberen aanzetten"</string>
     <string name="tty_mode_not_allowed_video_call" msgid="6551976083652752815">"De TTY-modus mag niet worden gewijzigd tijdens een videogesprek"</string>
     <string name="menu_add" msgid="5616487894975773141">"Contact toevoegen"</string>
     <string name="menu_edit" msgid="3593856941552460706">"Contact bewerken"</string>
@@ -593,7 +593,7 @@
     <string name="singleContactImportedMsg" msgid="3619804066300998934">"Contact geïmporteerd"</string>
     <string name="failedToImportSingleContactMsg" msgid="228095510489830266">"Kan contact niet importeren"</string>
     <string name="hac_mode_title" msgid="4127986689621125468">"Hoortoestellen"</string>
-    <string name="hac_mode_summary" msgid="7774989500136009881">"Compatibiliteit voor hoortoestel inschakelen"</string>
+    <string name="hac_mode_summary" msgid="7774989500136009881">"Compatibiliteit voor hoortoestel aanzetten"</string>
     <string name="rtt_mode_title" msgid="3075948111362818043">"Realtime tekstoproep (RTT)"</string>
     <string name="rtt_mode_summary" msgid="8631541375609989562">"Berichten in een audiogesprek toestaan"</string>
     <string name="rtt_mode_more_information" msgid="587500128658756318">"RTT helpt bellers die doof of slechthorend zijn, een spraakbeperking hebben of meer dan alleen een stem nodig hebben.&lt;br&gt; &lt;a href=<xliff:g id="URL">http://support.google.com/mobile?p=telephony_rtt</xliff:g>&gt;Meer informatie&lt;/a&gt;\n       &lt;br&gt;&lt;br&gt; - RTT-gesprekken worden opgeslagen als berichttranscript.\n       &lt;br&gt; - RTT is niet beschikbaar voor videogesprekken"</string>
@@ -673,8 +673,8 @@
     <string name="voicemail_change_pin_dialog_title" msgid="4633077715231764435">"Pincode wijzigen"</string>
     <string name="preference_category_ringtone" msgid="8787281191375434976">"Ringtone en trillen"</string>
     <string name="pstn_connection_service_label" msgid="9200102709997537069">"Ingebouwde simkaarten"</string>
-    <string name="enable_video_calling_title" msgid="7246600931634161830">"Videogesprekken inschakelen"</string>
-    <string name="enable_video_calling_dialog_msg" msgid="7141478720386203540">"Als je videogesprekken wilt inschakelen, moet je de geoptimaliseerde 4G LTE-modus inschakelen in de netwerkinstellingen."</string>
+    <string name="enable_video_calling_title" msgid="7246600931634161830">"Videogesprekken aanzetten"</string>
+    <string name="enable_video_calling_dialog_msg" msgid="7141478720386203540">"Als je videogesprekken wilt aanzetten, moet je de geoptimaliseerde 4G LTE-modus aanzetten in de netwerkinstellingen."</string>
     <string name="enable_video_calling_dialog_settings" msgid="8697890611305307110">"Netwerkinstellingen"</string>
     <string name="enable_video_calling_dialog_close" msgid="4298929725917045270">"Sluiten"</string>
     <string name="sim_label_emergency_calls" msgid="9078241989421522310">"Noodoproepen"</string>
@@ -833,22 +833,22 @@
     <string name="supp_service_over_ut_precautions_dual_sim" msgid="5166866975550910474">"Als je <xliff:g id="SUPP_SERVICE">%1$s</xliff:g> wilt gebruiken, zorg je dat je mobiele data hebt ingeschakeld voor sim <xliff:g id="SIM_NUMBER">%2$d</xliff:g>. Je kunt dit wijzigen via de mobiele netwerkinstellingen."</string>
     <string name="supp_service_over_ut_precautions_roaming_dual_sim" msgid="6627654855191817965">"Als je <xliff:g id="SUPP_SERVICE">%1$s</xliff:g> wilt gebruiken, zorg je dat je mobiele data en dataroaming hebt ingeschakeld voor sim <xliff:g id="SIM_NUMBER">%2$d</xliff:g>. Je kunt dit wijzigen via de mobiele netwerkinstellingen."</string>
     <string name="supp_service_over_ut_precautions_dialog_dismiss" msgid="5934541487903081652">"Sluiten"</string>
-    <string name="radio_info_data_connection_enable" msgid="6183729739783252840">"Dataverbinding inschakelen"</string>
+    <string name="radio_info_data_connection_enable" msgid="6183729739783252840">"Dataverbinding aanzetten"</string>
     <string name="radio_info_data_connection_disable" msgid="6404751291511368706">"Dataverbinding uitschakelen"</string>
     <string name="volte_provisioned_switch_string" msgid="4812874990480336178">"VoLTE-registratie"</string>
     <string name="vt_provisioned_switch_string" msgid="8295542122512195979">"Videogesprekken geregistreerd"</string>
     <string name="wfc_provisioned_switch_string" msgid="3835004640321078988">"Wifi-gesprekken geregistreerd"</string>
     <string name="eab_provisioned_switch_string" msgid="4449676720736033035">"EAB/aanwezigheid geregistreerd"</string>
     <string name="cbrs_data_switch_string" msgid="6060356430838077653">"CBRS-gegevens"</string>
-    <string name="dsds_switch_string" msgid="7564769822086764796">"DSDS inschakelen"</string>
+    <string name="dsds_switch_string" msgid="7564769822086764796">"DSDS aanzetten"</string>
     <string name="dsds_dialog_title" msgid="8494569893941847575">"Apparaat opnieuw opstarten?"</string>
     <string name="dsds_dialog_message" msgid="4047480385678538850">"Start het apparaat opnieuw op om de instelling te wijzigen."</string>
     <string name="dsds_dialog_confirm" msgid="9032004888134129885">"Opnieuw opstarten"</string>
     <string name="dsds_dialog_cancel" msgid="3245958947099586655">"Annuleren"</string>
     <string name="radio_info_radio_power" msgid="8805595022160471587">"Mobiel radiovermogen"</string>
-    <string name="radioInfo_menu_viewADN" msgid="4533179730908559846">"Adresboek op simkaart weergeven"</string>
-    <string name="radioInfo_menu_viewFDN" msgid="1847236480527032061">"Vaste nummers weergeven"</string>
-    <string name="radioInfo_menu_viewSDN" msgid="2613431584522392842">"Servicenummers weergeven"</string>
+    <string name="radioInfo_menu_viewADN" msgid="4533179730908559846">"Adresboek op simkaart bekijken"</string>
+    <string name="radioInfo_menu_viewFDN" msgid="1847236480527032061">"Vaste nummers bekijken"</string>
+    <string name="radioInfo_menu_viewSDN" msgid="2613431584522392842">"Servicenummers bekijken"</string>
     <string name="radioInfo_menu_getIMS" msgid="1950869267853198232">"IMS-servicestatus"</string>
     <string name="radio_info_ims_reg_status_title" msgid="6875885401313992007">"IMS-status"</string>
     <string name="radio_info_ims_reg_status_registered" msgid="7095182114078864326">"Geregistreerd"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 65376c9..f0da34a 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -451,8 +451,8 @@
     <string name="sum_fdn" msgid="6152246141642323582">"Hantera fasta nummer"</string>
     <string name="sum_fdn_change_pin" msgid="3510994280557335727">"Ändra PIN-kod för FDN-åtkomst"</string>
     <string name="sum_fdn_manage_list" msgid="3311397063233992907">"Hantera nummerlistan"</string>
-    <string name="voice_privacy" msgid="7346935172372181951">"Sekretess för Voice"</string>
-    <string name="voice_privacy_summary" msgid="3556460926168473346">"Aktivera avancerat sekretessläge"</string>
+    <string name="voice_privacy" msgid="7346935172372181951">"Integritet för Voice"</string>
+    <string name="voice_privacy_summary" msgid="3556460926168473346">"Aktivera avancerat integritetsläge"</string>
     <string name="tty_mode_option_title" msgid="3843817710032641703">"TTY-läge"</string>
     <string name="tty_mode_option_summary" msgid="4770510287236494371">"Ange TTY-läge"</string>
     <string name="auto_retry_mode_title" msgid="2985801935424422340">"Försök igen automatiskt"</string>
diff --git a/src/com/android/phone/CarrierConfigLoader.java b/src/com/android/phone/CarrierConfigLoader.java
index 8df411b..45ca974 100644
--- a/src/com/android/phone/CarrierConfigLoader.java
+++ b/src/com/android/phone/CarrierConfigLoader.java
@@ -108,6 +108,10 @@
     private CarrierServiceConnection[] mServiceConnection;
     // Service connection for binding to carrier config app for no SIM config.
     private CarrierServiceConnection[] mServiceConnectionForNoSimConfig;
+    // Whether we are bound to a service for each phone
+    private boolean[] mServiceBound;
+    // Whether we are bound to a service for no SIM config
+    private boolean[] mServiceBoundForNoSimConfig;
     // Whether we have sent config change broadcast for each phone id.
     private boolean[] mHasSentConfigChange;
     // Whether the broadcast was sent from EVENT_SYSTEM_UNLOCKED, to track rebroadcasts
@@ -308,7 +312,7 @@
                     final CarrierServiceConnection conn = (CarrierServiceConnection) msg.obj;
                     // If new service connection has been created, unbind.
                     if (mServiceConnection[phoneId] != conn || conn.service == null) {
-                        unbindIfBound(mContext, conn);
+                        unbindIfBound(mContext, conn, phoneId);
                         break;
                     }
                     final CarrierIdentifier carrierId = getCarrierIdentifierForPhoneId(phoneId);
@@ -317,7 +321,7 @@
                             new ResultReceiver(this) {
                                 @Override
                                 public void onReceiveResult(int resultCode, Bundle resultData) {
-                                    unbindIfBound(mContext, conn);
+                                    unbindIfBound(mContext, conn, phoneId);
                                     removeMessages(EVENT_FETCH_DEFAULT_TIMEOUT,
                                             getMessageToken(phoneId));
                                     // If new service connection has been created, this is stale.
@@ -352,7 +356,7 @@
                     } catch (RemoteException e) {
                         loge("Failed to get carrier config from default app: " +
                                 mPlatformCarrierConfigPackage + " err: " + e.toString());
-                        unbindIfBound(mContext, conn);
+                        unbindIfBound(mContext, conn, phoneId);
                         break; // So we don't set a timeout.
                     }
                     sendMessageDelayed(
@@ -372,7 +376,7 @@
                     if (mServiceConnection[phoneId] != null) {
                         // If a ResponseReceiver callback is in the queue when this happens, we will
                         // unbind twice and throw an exception.
-                        unbindIfBound(mContext, mServiceConnection[phoneId]);
+                        unbindIfBound(mContext, mServiceConnection[phoneId], phoneId);
                         broadcastConfigChangedIntent(phoneId);
                     }
                     // Put a stub bundle in place so that the rest of the logic continues smoothly.
@@ -438,7 +442,7 @@
                     final CarrierServiceConnection conn = (CarrierServiceConnection) msg.obj;
                     // If new service connection has been created, unbind.
                     if (mServiceConnection[phoneId] != conn || conn.service == null) {
-                        unbindIfBound(mContext, conn);
+                        unbindIfBound(mContext, conn, phoneId);
                         break;
                     }
                     final CarrierIdentifier carrierId = getCarrierIdentifierForPhoneId(phoneId);
@@ -447,7 +451,7 @@
                             new ResultReceiver(this) {
                                 @Override
                                 public void onReceiveResult(int resultCode, Bundle resultData) {
-                                    unbindIfBound(mContext, conn);
+                                    unbindIfBound(mContext, conn, phoneId);
                                     removeMessages(EVENT_FETCH_CARRIER_TIMEOUT,
                                             getMessageToken(phoneId));
                                     // If new service connection has been created, this is stale.
@@ -491,7 +495,7 @@
                                 + " carrierid: " + carrierId.toString());
                     } catch (RemoteException e) {
                         loge("Failed to get carrier config: " + e.toString());
-                        unbindIfBound(mContext, conn);
+                        unbindIfBound(mContext, conn, phoneId);
                         break; // So we don't set a timeout.
                     }
                     sendMessageDelayed(
@@ -512,7 +516,7 @@
                     if (mServiceConnection[phoneId] != null) {
                         // If a ResponseReceiver callback is in the queue when this happens, we will
                         // unbind twice and throw an exception.
-                        unbindIfBound(mContext, mServiceConnection[phoneId]);
+                        unbindIfBound(mContext, mServiceConnection[phoneId], phoneId);
                         broadcastConfigChangedIntent(phoneId);
                     }
                     // Put a stub bundle in place so that the rest of the logic continues smoothly.
@@ -606,7 +610,8 @@
                     if (mServiceConnectionForNoSimConfig[phoneId] != null) {
                         // If a ResponseReceiver callback is in the queue when this happens, we will
                         // unbind twice and throw an exception.
-                        unbindIfBound(mContext, mServiceConnectionForNoSimConfig[phoneId]);
+                        unbindIfBoundForNoSimConfig(mContext,
+                                mServiceConnectionForNoSimConfig[phoneId], phoneId);
                     }
                     broadcastConfigChangedIntent(phoneId, false);
                     break;
@@ -617,7 +622,7 @@
                     final CarrierServiceConnection conn = (CarrierServiceConnection) msg.obj;
                     // If new service connection has been created, unbind.
                     if (mServiceConnectionForNoSimConfig[phoneId] != conn || conn.service == null) {
-                        unbindIfBound(mContext, conn);
+                        unbindIfBoundForNoSimConfig(mContext, conn, phoneId);
                         break;
                     }
 
@@ -626,7 +631,7 @@
                             new ResultReceiver(this) {
                                 @Override
                                 public void onReceiveResult(int resultCode, Bundle resultData) {
-                                    unbindIfBound(mContext, conn);
+                                    unbindIfBoundForNoSimConfig(mContext, conn, phoneId);
                                     // If new service connection has been created, this is stale.
                                     if (mServiceConnectionForNoSimConfig[phoneId] != conn) {
                                         loge("Received response for stale request.");
@@ -658,7 +663,7 @@
                     } catch (RemoteException e) {
                         loge("Failed to get no sim carrier config from default app: " +
                                 mPlatformCarrierConfigPackage + " err: " + e.toString());
-                        unbindIfBound(mContext, conn);
+                        unbindIfBoundForNoSimConfig(mContext, conn, phoneId);
                         break; // So we don't set a timeout.
                     }
                     sendMessageDelayed(
@@ -705,9 +710,11 @@
         mOverrideConfigs = new PersistableBundle[numPhones];
         mNoSimConfig = new PersistableBundle();
         mServiceConnection = new CarrierServiceConnection[numPhones];
+        mServiceBound = new boolean[numPhones];
         mHasSentConfigChange = new boolean[numPhones];
         mFromSystemUnlocked = new boolean[numPhones];
         mServiceConnectionForNoSimConfig = new CarrierServiceConnection[numPhones];
+        mServiceBoundForNoSimConfig = new boolean[numPhones];
         logd("CarrierConfigLoader has started");
         mSubscriptionInfoUpdater = subscriptionInfoUpdater;
         mHandler.sendEmptyMessage(EVENT_CHECK_SYSTEM_UPDATE);
@@ -842,7 +849,11 @@
         try {
             if (mContext.bindService(carrierService, serviceConnection,
                     Context.BIND_AUTO_CREATE)) {
-                serviceConnection.isBound = true;
+                if (eventId == EVENT_CONNECTED_TO_DEFAULT_FOR_NO_SIM_CONFIG) {
+                    mServiceBoundForNoSimConfig[phoneId] = true;
+                } else {
+                    mServiceBound[phoneId] = true;
+                }
                 return true;
             } else {
                 return false;
@@ -1384,9 +1395,18 @@
         return mOverrideConfigs[phoneId];
     }
 
-    private void unbindIfBound(Context context, CarrierServiceConnection conn) {
-        if (conn.isBound) {
-            conn.isBound = false;
+    private void unbindIfBound(Context context, CarrierServiceConnection conn,
+            int phoneId) {
+        if (mServiceBound[phoneId]) {
+            mServiceBound[phoneId] = false;
+            context.unbindService(conn);
+        }
+    }
+
+    private void unbindIfBoundForNoSimConfig(Context context, CarrierServiceConnection conn,
+            int phoneId) {
+        if (mServiceBoundForNoSimConfig[phoneId]) {
+            mServiceBoundForNoSimConfig[phoneId] = false;
             context.unbindService(conn);
         }
     }
@@ -1611,15 +1631,11 @@
         final String pkgName;
         final int eventId;
         IBinder service;
-        // If bindService was called and return true which means unbindService
-        // must be called later to release the connection
-        boolean isBound;
 
         CarrierServiceConnection(int phoneId, String pkgName, int eventId) {
             this.phoneId = phoneId;
             this.pkgName = pkgName;
             this.eventId = eventId;
-            this.isBound = false;
         }
 
         @Override
diff --git a/src/com/android/phone/ImsStateCallbackController.java b/src/com/android/phone/ImsStateCallbackController.java
new file mode 100644
index 0000000..28fca59
--- /dev/null
+++ b/src/com/android/phone/ImsStateCallbackController.java
@@ -0,0 +1,1184 @@
+/*
+ * 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.phone;
+
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED;
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_NOT_READY;
+import static android.telephony.ims.ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED;
+import static android.telephony.ims.ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE;
+import static android.telephony.ims.ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR;
+import static android.telephony.ims.ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_MMTEL;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_RCS;
+import static android.telephony.ims.feature.ImsFeature.STATE_READY;
+import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
+
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_NOT_READY;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyRegistryManager;
+import android.telephony.ims.feature.ImsFeature;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.ims.FeatureConnector;
+import com.android.ims.ImsManager;
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IImsStateCallback;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConfigurationManager;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.ims.ImsResolver;
+import com.android.internal.telephony.util.HandlerExecutor;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.services.telephony.rcs.RcsFeatureController;
+import com.android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of the controller managing {@link ImsStateCallback}s
+ */
+public class ImsStateCallbackController {
+    private static final String TAG = "ImsStateCallbackController";
+    private static final boolean VDBG = false;
+    private static final int LOG_SIZE = 50;
+
+    /**
+     * Create a FeatureConnector for this class to use to connect to an ImsManager.
+     */
+    @VisibleForTesting
+    public interface MmTelFeatureConnectorFactory {
+        /**
+         * Create a FeatureConnector for this class to use to connect to an ImsManager.
+         * @param listener will receive ImsManager instance.
+         * @param executor that the Listener callbacks will be called on.
+         * @return A FeatureConnector
+         */
+        FeatureConnector<ImsManager> create(Context context, int slotId,
+                String logPrefix, FeatureConnector.Listener<ImsManager> listener,
+                Executor executor);
+    }
+
+    /**
+     * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager.
+     */
+    @VisibleForTesting
+    public interface RcsFeatureConnectorFactory {
+        /**
+         * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager.
+         * @param listener will receive RcsFeatureManager instance.
+         * @param executor that the Listener callbacks will be called on.
+         * @return A FeatureConnector
+         */
+        FeatureConnector<RcsFeatureManager> create(Context context, int slotId,
+                FeatureConnector.Listener<RcsFeatureManager> listener,
+                Executor executor, String logPrefix);
+    }
+
+    /** Indicates that the state is not valid, used in ExternalRcsFeatureState only */
+    private static final int STATE_UNKNOWN = -1;
+
+    /** The unavailable reason of ImsFeature is not initialized */
+    private static final int NOT_INITIALIZED = -1;
+    /** The ImsFeature is available. */
+    private static final int AVAILABLE = 0;
+
+    private static final int EVENT_SUB_CHANGED = 1;
+    private static final int EVENT_REGISTER_CALLBACK = 2;
+    private static final int EVENT_UNREGISTER_CALLBACK = 3;
+    private static final int EVENT_CARRIER_CONFIG_CHANGED = 4;
+    private static final int EVENT_EXTERNAL_RCS_STATE_CHANGED = 5;
+    private static final int EVENT_MSIM_CONFIGURATION_CHANGE = 6;
+
+    private static ImsStateCallbackController sInstance;
+    private static final LocalLog sLocalLog = new LocalLog(LOG_SIZE);
+
+    /**
+     * get the instance
+     */
+    public static ImsStateCallbackController getInstance() {
+        synchronized (ImsStateCallbackController.class) {
+            return sInstance;
+        }
+    }
+
+    private final PhoneGlobals mApp;
+    private final Handler mHandler;
+    private final ImsResolver mImsResolver;
+    private final SparseArray<MmTelFeatureListener> mMmTelFeatureListeners = new SparseArray<>();
+    private final SparseArray<RcsFeatureListener> mRcsFeatureListeners = new SparseArray<>();
+
+    private final SubscriptionManager mSubscriptionManager;
+    private final TelephonyRegistryManager mTelephonyRegistryManager;
+    private MmTelFeatureConnectorFactory mMmTelFeatureFactory;
+    private RcsFeatureConnectorFactory mRcsFeatureFactory;
+
+    private HashMap<IBinder, CallbackWrapper> mWrappers = new HashMap<>();
+
+    private final Object mDumpLock = new Object();
+
+    private int mNumSlots;
+
+    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent == null) {
+                return;
+            }
+            if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+                Bundle bundle = intent.getExtras();
+                if (bundle == null) {
+                    return;
+                }
+                int slotId = bundle.getInt(CarrierConfigManager.EXTRA_SLOT_INDEX,
+                        SubscriptionManager.INVALID_PHONE_INDEX);
+                int subId = bundle.getInt(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX,
+                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+                if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+                    loge("onReceive ACTION_CARRIER_CONFIG_CHANGED invalid slotId");
+                    return;
+                }
+
+                if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                    loge("onReceive ACTION_CARRIER_CONFIG_CHANGED invalid subId");
+                    //subscription changed will be notified by mSubChangedListener
+                    return;
+                }
+
+                notifyCarrierConfigChanged(slotId);
+            }
+        }
+    };
+
+    private final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener =
+            new SubscriptionManager.OnSubscriptionsChangedListener() {
+        @Override
+        public void onSubscriptionsChanged() {
+            if (!mHandler.hasMessages(EVENT_SUB_CHANGED)) {
+                mHandler.sendEmptyMessage(EVENT_SUB_CHANGED);
+            }
+        }
+    };
+
+    private final class MyHandler extends Handler {
+        MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (VDBG) logv("handleMessage: " + msg);
+            synchronized (mDumpLock) {
+                switch (msg.what) {
+                    case EVENT_SUB_CHANGED:
+                        onSubChanged();
+                        break;
+
+                    case EVENT_REGISTER_CALLBACK:
+                        onRegisterCallback((ImsStateCallbackController.CallbackWrapper) msg.obj);
+                        break;
+
+                    case EVENT_UNREGISTER_CALLBACK:
+                        onUnregisterCallback((IImsStateCallback) msg.obj);
+                        break;
+
+                    case EVENT_CARRIER_CONFIG_CHANGED:
+                        onCarrierConfigChanged(msg.arg1);
+                        break;
+
+                    case EVENT_EXTERNAL_RCS_STATE_CHANGED:
+                        if (msg.obj == null) break;
+                        onExternalRcsStateChanged((ExternalRcsFeatureState) msg.obj);
+                        break;
+
+                    case EVENT_MSIM_CONFIGURATION_CHANGE:
+                        AsyncResult result = (AsyncResult) msg.obj;
+                        Integer numSlots = (Integer) result.result;
+                        if (numSlots == null) {
+                            Log.w(TAG, "msim config change with null num slots");
+                            break;
+                        }
+                        updateFeatureControllerSize(numSlots);
+                        break;
+
+                    default:
+                        loge("Unhandled event " + msg.what);
+                }
+            }
+        }
+    }
+
+    private final class MmTelFeatureListener implements FeatureConnector.Listener<ImsManager> {
+        private FeatureConnector<ImsManager> mConnector;
+        private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        private int mState = STATE_UNAVAILABLE;
+        private int mReason = REASON_IMS_SERVICE_DISCONNECTED;
+
+        /*
+         * Remember the last return of verifyImsMmTelConfigured().
+         * true means ImsResolver found an IMS package for FEATURE_MMTEL.
+         *
+         * mReason is updated through connectionUnavailable triggered by ImsResolver.
+         * mHasConfig is update through notifyConfigChanged triggered by mReceiver.
+         * mHasConfig can be a redundancy of (mReason == REASON_NO_IMS_SERVICE_CONFIGURED).
+         * However, when a carrier config changes, we are not sure the order
+         * of execution of connectionUnavailable and notifyConfigChanged.
+         * So, it's safe to use a separated state to retain it.
+         * We assume mHasConfig is true, until it's determined explicitly.
+         */
+        private boolean mHasConfig = true;
+
+        private int mSlotId = -1;
+        private String mLogPrefix = "";
+
+        MmTelFeatureListener(int slotId) {
+            mSlotId = slotId;
+            mLogPrefix = "[" + slotId + ", MMTEL] ";
+            if (VDBG) logv(mLogPrefix + "created");
+
+            mConnector = mMmTelFeatureFactory.create(
+                    mApp, slotId, TAG, this, new HandlerExecutor(mHandler));
+            mConnector.connect();
+        }
+
+        void setSubId(int subId) {
+            if (VDBG) logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId);
+            if (mSubId == subId) return;
+            logd(mLogPrefix + "setSubId changed subId=" + subId);
+
+            mSubId = subId;
+        }
+
+        void destroy() {
+            if (VDBG) logv(mLogPrefix + "destroy");
+            mConnector.disconnect();
+            mConnector = null;
+        }
+
+        @Override
+        public void connectionReady(ImsManager manager) {
+            logd(mLogPrefix + "connectionReady");
+
+            mState = STATE_READY;
+            mReason = AVAILABLE;
+            mHasConfig = true;
+            onFeatureStateChange(mSubId, FEATURE_MMTEL, mState, mReason);
+        }
+
+        @Override
+        public void connectionUnavailable(int reason) {
+            logd(mLogPrefix + "connectionUnavailable reason=" + connectorReasonToString(reason));
+
+            reason = convertReasonType(reason);
+            if (mReason == reason) return;
+
+            connectionUnavailableInternal(reason);
+        }
+
+        private void connectionUnavailableInternal(int reason) {
+            mState = STATE_UNAVAILABLE;
+            mReason = reason;
+
+            /* If having no IMS package for MMTEL,
+             * dicard the reason except REASON_NO_IMS_SERVICE_CONFIGURED. */
+            if (!mHasConfig && reason != REASON_NO_IMS_SERVICE_CONFIGURED) return;
+
+            onFeatureStateChange(mSubId, FEATURE_MMTEL, mState, mReason);
+        }
+
+        void notifyConfigChanged(boolean hasConfig) {
+            if (mHasConfig == hasConfig) return;
+
+            logd(mLogPrefix + "notifyConfigChanged " + hasConfig);
+
+            mHasConfig = hasConfig;
+            if (hasConfig) {
+                // REASON_NO_IMS_SERVICE_CONFIGURED is already reported to the clients,
+                // since there is no configuration of IMS package for MMTEL.
+                // Now, a carrier configuration change is notified and
+                // the response from ImsResolver is changed from false to true.
+                if (mState != STATE_READY) {
+                    if (mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+                        // In this case, notify clients the reason, REASON_DISCONNCTED,
+                        // to update the state.
+                        connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+                    } else {
+                        // ImsResolver and ImsStateCallbackController run with different Looper.
+                        // In this case, FeatureConnectorListener is updated ahead of this.
+                        // But, connectionUnavailable didn't notify clients since mHasConfig is
+                        // false. So, notify clients here.
+                        connectionUnavailableInternal(mReason);
+                    }
+                }
+            } else {
+                // FeatureConnector doesn't report UNAVAILABLE_REASON_IMS_UNSUPPORTED,
+                // so report the reason here.
+                connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+            }
+        }
+
+        // called from onRegisterCallback
+        boolean notifyState(CallbackWrapper wrapper) {
+            if (VDBG) logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId);
+
+            return wrapper.notifyState(mSubId, FEATURE_MMTEL, mState, mReason);
+        }
+
+        void dump(IndentingPrintWriter pw) {
+            pw.println("Listener={slotId=" + mSlotId
+                    + ", subId=" + mSubId
+                    + ", state=" + ImsFeature.STATE_LOG_MAP.get(mState)
+                    + ", reason=" + imsStateReasonToString(mReason)
+                    + ", hasConfig=" + mHasConfig
+                    + "}");
+        }
+    }
+
+    private final class RcsFeatureListener implements FeatureConnector.Listener<RcsFeatureManager> {
+        private FeatureConnector<RcsFeatureManager> mConnector;
+        private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        private int mState = STATE_UNAVAILABLE;
+        private int mReason = REASON_IMS_SERVICE_DISCONNECTED;
+
+        /*
+         * Remember the last return of verifyImsMmTelConfigured().
+         * true means ImsResolver found an IMS package for FEATURE_RCS.
+         *
+         * mReason is updated through connectionUnavailable triggered by ImsResolver.
+         * mHasConfig is update through notifyConfigChanged triggered by mReceiver,
+         * and notifyExternalRcsState which triggered by TelephonyRcsService refers it.
+         * mHasConfig can be a redundancy of (mReason == REASON_NO_IMS_SERVICE_CONFIGURED).
+         * However, when a carrier config changes, we are not sure the order
+         * of execution of connectionUnavailable, notifyConfigChanged and notifyExternalRcsState.
+         * So, it's safe to use a separated state to retain it.
+         * We assume mHasConfig is true, until it's determined explicitly.
+         */
+        private boolean mHasConfig = true;
+
+        /*
+         * TelephonyRcsService doesn’t try to connect to RcsFeature if there is no active feature
+         * for a given subscription. The active features are declared by carrier configs and
+         * configuration resources. The APIs of ImsRcsManager and SipDelegateManager are available
+         * only when the RcsFeatureController has a STATE_READY state connection.
+         * This configuration is different from the configuration of IMS package for RCS.
+         * ImsStateCallbackController's FeatureConnectorListener can be STATE_READY state,
+         * even in case there is no active RCS feature. But Manager's APIs throws exception.
+         *
+         * For RCS, in addition to mHasConfig, the sate of TelephonyRcsService and
+         * RcsFeatureConnector will be traced to determine the state to be notified to clients.
+         */
+        private ExternalRcsFeatureState mExternalState = null;
+
+        private int mSlotId = -1;
+        private String mLogPrefix = "";
+
+        RcsFeatureListener(int slotId) {
+            mSlotId = slotId;
+            mLogPrefix = "[" + slotId + ", RCS] ";
+            if (VDBG) logv(mLogPrefix + "created");
+
+            mConnector = mRcsFeatureFactory.create(
+                    mApp, slotId, this, new HandlerExecutor(mHandler), TAG);
+            mConnector.connect();
+        }
+
+        void setSubId(int subId) {
+            if (VDBG) logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId);
+            if (mSubId == subId) return;
+            logd(mLogPrefix + "setSubId changed subId=" + subId);
+
+            mSubId = subId;
+        }
+
+        void destroy() {
+            if (VDBG) logv(mLogPrefix + "destroy");
+
+            mConnector.disconnect();
+            mConnector = null;
+        }
+
+        @Override
+        public void connectionReady(RcsFeatureManager manager) {
+            logd(mLogPrefix + "connectionReady");
+
+            mState = STATE_READY;
+            mReason = AVAILABLE;
+            mHasConfig = true;
+
+            if (mExternalState != null && mExternalState.isReady()) {
+                onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason);
+            }
+        }
+
+        @Override
+        public void connectionUnavailable(int reason) {
+            logd(mLogPrefix + "connectionUnavailable reason=" + connectorReasonToString(reason));
+
+            reason = convertReasonType(reason);
+            if (mReason == reason) return;
+
+            connectionUnavailableInternal(reason);
+        }
+
+        private void connectionUnavailableInternal(int reason) {
+            mState = STATE_UNAVAILABLE;
+            mReason = reason;
+
+            /* If having no IMS package for RCS,
+             * dicard the reason except REASON_NO_IMS_SERVICE_CONFIGURED. */
+            if (!mHasConfig && reason != REASON_NO_IMS_SERVICE_CONFIGURED) return;
+
+            if (mExternalState == null && reason != REASON_NO_IMS_SERVICE_CONFIGURED) {
+                // Wait until TelephonyRcsService notifies its state.
+                return;
+            }
+
+            if (mExternalState != null && !mExternalState.hasActiveFeatures()) {
+                // notifyExternalRcsState has notified REASON_NO_IMS_SERVICE_CONFIGURED already
+                // ignore it
+                return;
+            }
+
+            if ((mExternalState != null && mExternalState.hasActiveFeatures())
+                    || mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+                onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason);
+            }
+        }
+
+        void notifyConfigChanged(boolean hasConfig) {
+            if (mHasConfig == hasConfig) return;
+
+            logd(mLogPrefix + "notifyConfigChanged " + hasConfig);
+
+            mHasConfig = hasConfig;
+            if (hasConfig) {
+                // REASON_NO_IMS_SERVICE_CONFIGURED is already reported to the clients,
+                // since there is no configuration of IMS package for RCS.
+                // Now, a carrier configuration change is notified and
+                // the response from ImsResolver is changed from false to true.
+                if (mState != STATE_READY) {
+                    if (mReason == REASON_NO_IMS_SERVICE_CONFIGURED) {
+                        // In this case, notify clients the reason, REASON_DISCONNCTED,
+                        // to update the state.
+                        connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+                    } else {
+                        // ImsResolver and ImsStateCallbackController run with different Looper.
+                        // In this case, FeatureConnectorListener is updated ahead of this.
+                        // But, connectionUnavailable didn't notify clients since mHasConfig is
+                        // false. So, notify clients here.
+                        connectionUnavailableInternal(mReason);
+                    }
+                }
+            } else {
+                // FeatureConnector doesn't report UNAVAILABLE_REASON_IMS_UNSUPPORTED,
+                // so report the reason here.
+                connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+            }
+        }
+
+        void notifyExternalRcsState(ExternalRcsFeatureState fs) {
+            if (VDBG) {
+                logv(mLogPrefix + "notifyExternalRcsState"
+                        + " state=" + (fs.mState == STATE_UNKNOWN
+                                ? "" : ImsFeature.STATE_LOG_MAP.get(fs.mState))
+                        + ", reason=" + imsStateReasonToString(fs.mReason));
+            }
+
+            ExternalRcsFeatureState oldFs = mExternalState;
+            // External state is from TelephonyRcsService while a feature is added or removed.
+            if (fs.mState == STATE_UNKNOWN) {
+                if (oldFs != null) fs.mState = oldFs.mState;
+                else fs.mState = STATE_UNAVAILABLE;
+            }
+
+            mExternalState = fs;
+
+            // No IMS package found.
+            // REASON_NO_IMS_SERVICE_CONFIGURED is notified to clients already.
+            if (!mHasConfig) return;
+
+            if (fs.hasActiveFeatures()) {
+                if (mState == STATE_READY) {
+                    if ((oldFs == null || !oldFs.isReady()) && fs.isReady()) {
+                        // it is waiting RcsFeatureConnector's notification.
+                        // notify clients here.
+                        onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason);
+                    } else if (!fs.isReady()) {
+                        // Wait RcsFeatureConnector's notification
+                    } else {
+                        // ignore duplicated notification
+                    }
+                }
+            } else {
+                // notify only once
+                if (oldFs == null || oldFs.hasActiveFeatures()) {
+                    if (mReason != REASON_NO_IMS_SERVICE_CONFIGURED) {
+                        onFeatureStateChange(
+                                mSubId, FEATURE_RCS, STATE_UNAVAILABLE,
+                                REASON_NO_IMS_SERVICE_CONFIGURED);
+                    }
+                } else {
+                    // ignore duplicated notification
+                }
+            }
+        }
+
+        // called from onRegisterCallback
+        boolean notifyState(CallbackWrapper wrapper) {
+            if (VDBG) logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId);
+
+            if (mHasConfig) {
+                if (mExternalState == null) {
+                    // Wait until TelephonyRcsService notifies its state.
+                    return wrapper.notifyState(mSubId, FEATURE_RCS, STATE_UNAVAILABLE,
+                            REASON_IMS_SERVICE_DISCONNECTED);
+                } else if (!mExternalState.hasActiveFeatures()) {
+                    return wrapper.notifyState(mSubId, FEATURE_RCS, STATE_UNAVAILABLE,
+                            REASON_NO_IMS_SERVICE_CONFIGURED);
+                }
+            }
+
+            return wrapper.notifyState(mSubId, FEATURE_RCS, mState, mReason);
+        }
+
+        void dump(IndentingPrintWriter pw) {
+            pw.println("Listener={slotId=" + mSlotId
+                    + ", subId=" + mSubId
+                    + ", state=" + ImsFeature.STATE_LOG_MAP.get(mState)
+                    + ", reason=" + imsStateReasonToString(mReason)
+                    + ", hasConfig=" + mHasConfig
+                    + ", isReady=" + (mExternalState == null ? false : mExternalState.isReady())
+                    + ", hasFeatures=" + (mExternalState == null ? false
+                            : mExternalState.hasActiveFeatures())
+                    + "}");
+        }
+    }
+
+    /**
+     * A wrapper class for the callback registered
+     */
+    private static class CallbackWrapper {
+        private final int mSubId;
+        private final int mRequiredFeature;
+        private final IImsStateCallback mCallback;
+        private final IBinder mBinder;
+        private final String mCallingPackage;
+        private int mLastReason = NOT_INITIALIZED;
+
+        CallbackWrapper(int subId, int feature, IImsStateCallback callback,
+                String callingPackage) {
+            mSubId = subId;
+            mRequiredFeature = feature;
+            mCallback = callback;
+            mBinder = callback.asBinder();
+            mCallingPackage = callingPackage;
+        }
+
+        /**
+         * @return false when accessing callback binder throws an Exception.
+         * That means the callback binder is not valid any longer.
+         * The death of remote process can cause this.
+         * This instance shall be removed from the list.
+         */
+        boolean notifyState(int subId, int feature, int state, int reason) {
+            if (VDBG) {
+                logv("CallbackWrapper notifyState subId=" + subId
+                        + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature)
+                        + ", state=" + ImsFeature.STATE_LOG_MAP.get(state)
+                        + ", reason=" + imsStateReasonToString(reason));
+            }
+
+            try {
+                if (state == STATE_READY) {
+                    mCallback.onAvailable();
+                } else {
+                    mCallback.onUnavailable(reason);
+                }
+                mLastReason = reason;
+            } catch (Exception e) {
+                loge("CallbackWrapper notifyState e=" + e);
+                return false;
+            }
+
+            return true;
+        }
+
+        void notifyInactive() {
+            if (VDBG) logv("CallbackWrapper notifyInactive subId=" + mSubId);
+
+            try {
+                mCallback.onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+            } catch (Exception e) {
+                // ignored
+            }
+        }
+
+        void dump(IndentingPrintWriter pw) {
+            pw.println("CallbackWrapper={subId=" + mSubId
+                    + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(mRequiredFeature)
+                    + ", reason=" + imsStateReasonToString(mLastReason)
+                    + ", pkg=" + mCallingPackage
+                    + "}");
+        }
+    }
+
+    private static class ExternalRcsFeatureState {
+        private int mSlotId;
+        private int mState = STATE_UNAVAILABLE;
+        private int mReason = NOT_INITIALIZED;
+
+        ExternalRcsFeatureState(int slotId, int state, int reason) {
+            mSlotId = slotId;
+            mState = state;
+            mReason = reason;
+        }
+
+        boolean hasActiveFeatures() {
+            return mReason != REASON_NO_IMS_SERVICE_CONFIGURED;
+        }
+
+        boolean isReady() {
+            return mState == STATE_READY;
+        }
+    }
+
+    /**
+     * create an instance
+     */
+    public static ImsStateCallbackController make(PhoneGlobals app, int numSlots) {
+        synchronized (ImsStateCallbackController.class) {
+            if (sInstance == null) {
+                logd("ImsStateCallbackController created");
+
+                HandlerThread handlerThread = new HandlerThread(TAG);
+                handlerThread.start();
+                sInstance = new ImsStateCallbackController(app, handlerThread.getLooper(), numSlots,
+                        ImsManager::getConnector, RcsFeatureManager::getConnector,
+                        ImsResolver.getInstance());
+            }
+        }
+        return sInstance;
+    }
+
+    @VisibleForTesting
+    public ImsStateCallbackController(PhoneGlobals app, Looper looper, int numSlots,
+            MmTelFeatureConnectorFactory mmTelFactory, RcsFeatureConnectorFactory rcsFactory,
+            ImsResolver imsResolver) {
+        mApp = app;
+        mHandler = new MyHandler(looper);
+        mImsResolver = imsResolver;
+        mSubscriptionManager = mApp.getSystemService(SubscriptionManager.class);
+        mTelephonyRegistryManager = mApp.getSystemService(TelephonyRegistryManager.class);
+        mMmTelFeatureFactory = mmTelFactory;
+        mRcsFeatureFactory = rcsFactory;
+
+        updateFeatureControllerSize(numSlots);
+
+        mTelephonyRegistryManager.addOnSubscriptionsChangedListener(
+                mSubChangedListener, mSubChangedListener.getHandlerExecutor());
+
+        PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler,
+                EVENT_MSIM_CONFIGURATION_CHANGE, null);
+
+        mApp.registerReceiver(mReceiver, new IntentFilter(
+                CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
+
+        onSubChanged();
+    }
+
+    /**
+     * Update the number of {@link RcsFeatureController}s that are created based on the number of
+     * active slots on the device.
+     */
+    @VisibleForTesting
+    public void updateFeatureControllerSize(int newNumSlots) {
+        if (mNumSlots != newNumSlots) {
+            logd("updateFeatures: oldSlots=" + mNumSlots
+                    + ", newNumSlots=" + newNumSlots);
+            if (mNumSlots < newNumSlots) {
+                for (int i = mNumSlots; i < newNumSlots; i++) {
+                    MmTelFeatureListener m = new MmTelFeatureListener(i);
+                    mMmTelFeatureListeners.put(i, m);
+                    RcsFeatureListener r = new RcsFeatureListener(i);
+                    mRcsFeatureListeners.put(i, r);
+                }
+            } else {
+                for (int i = (mNumSlots - 1); i > (newNumSlots - 1); i--) {
+                    MmTelFeatureListener m = mMmTelFeatureListeners.get(i);
+                    if (m != null) {
+                        mMmTelFeatureListeners.remove(i);
+                        m.destroy();
+                    }
+                    RcsFeatureListener r = mRcsFeatureListeners.get(i);
+                    if (r != null) {
+                        mRcsFeatureListeners.remove(i);
+                        r.destroy();
+                    }
+                }
+            }
+        }
+        mNumSlots = newNumSlots;
+    }
+
+    /**
+     * Dependencies for testing.
+     */
+    @VisibleForTesting
+    public void onSubChanged() {
+        for (int i = 0; i < mMmTelFeatureListeners.size(); i++) {
+            MmTelFeatureListener l = mMmTelFeatureListeners.valueAt(i);
+            l.setSubId(getSubId(i));
+        }
+
+        for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+            RcsFeatureListener l = mRcsFeatureListeners.valueAt(i);
+            l.setSubId(getSubId(i));
+        }
+
+        if (mWrappers.size() == 0) return;
+
+        ArrayList<IBinder> inactiveCallbacks = new ArrayList<>();
+        final int[] activeSubs = mSubscriptionManager.getActiveSubscriptionIdList();
+
+        if (VDBG) logv("onSubChanged activeSubs=" + Arrays.toString(activeSubs));
+
+        // Remove callbacks for inactive subscriptions
+        for (IBinder binder : mWrappers.keySet()) {
+            CallbackWrapper wrapper = mWrappers.get(binder);
+            if (wrapper != null) {
+                if (!isActive(activeSubs, wrapper.mSubId)) {
+                    // inactive subscription
+                    inactiveCallbacks.add(binder);
+                }
+            } else {
+                // unexpected, remove it
+                inactiveCallbacks.add(binder);
+            }
+        }
+        removeInactiveCallbacks(inactiveCallbacks, "onSubChanged");
+    }
+
+    private void onFeatureStateChange(int subId, int feature, int state, int reason) {
+        if (VDBG) {
+            logv("onFeatureStateChange subId=" + subId
+                    + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature)
+                    + ", state=" + ImsFeature.STATE_LOG_MAP.get(state)
+                    + ", reason=" + imsStateReasonToString(reason));
+        }
+
+        ArrayList<IBinder> inactiveCallbacks = new ArrayList<>();
+        mWrappers.values().forEach(wrapper -> {
+            if (subId == wrapper.mSubId
+                    && feature == wrapper.mRequiredFeature
+                    && !wrapper.notifyState(subId, feature, state, reason)) {
+                // callback has exception, remove it
+                inactiveCallbacks.add(wrapper.mBinder);
+            }
+        });
+        removeInactiveCallbacks(inactiveCallbacks, "onFeatureStateChange");
+    }
+
+    private void onRegisterCallback(CallbackWrapper wrapper) {
+        if (wrapper == null) return;
+
+        if (VDBG) logv("onRegisterCallback before size=" + mWrappers.size());
+        if (VDBG) {
+            logv("onRegisterCallback subId=" + wrapper.mSubId
+                    + ", feature=" + wrapper.mRequiredFeature);
+        }
+
+        // Not sure the following case can happen or not:
+        // step1) Subscription changed
+        // step2) ImsStateCallbackController not processed onSubChanged yet
+        // step3) Client registers with a strange subId
+        // The validity of the subId is checked PhoneInterfaceManager#registerImsStateCallback.
+        // So, register the wrapper here before trying to notifyState.
+        // TODO: implement the recovery for this case, notifying the current reson, in onSubChanged
+        mWrappers.put(wrapper.mBinder, wrapper);
+
+        if (wrapper.mRequiredFeature == FEATURE_MMTEL) {
+            for (int i = 0; i < mMmTelFeatureListeners.size(); i++) {
+                MmTelFeatureListener l = mMmTelFeatureListeners.valueAt(i);
+                if (l.mSubId == wrapper.mSubId
+                        && !l.notifyState(wrapper)) {
+                    mWrappers.remove(wrapper.mBinder);
+                    break;
+                }
+            }
+        } else if (wrapper.mRequiredFeature == FEATURE_RCS) {
+            for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+                RcsFeatureListener l = mRcsFeatureListeners.valueAt(i);
+                if (l.mSubId == wrapper.mSubId
+                        && !l.notifyState(wrapper)) {
+                    mWrappers.remove(wrapper.mBinder);
+                    break;
+                }
+            }
+        }
+
+        if (VDBG) logv("onRegisterCallback after size=" + mWrappers.size());
+    }
+
+    private void onUnregisterCallback(IImsStateCallback cb) {
+        if (cb == null) return;
+        mWrappers.remove(cb.asBinder());
+    }
+
+    private void onCarrierConfigChanged(int slotId) {
+        if (slotId >= mNumSlots) {
+            logd("onCarrierConfigChanged invalid slotId "
+                    + slotId + ", mNumSlots=" + mNumSlots);
+            return;
+        }
+
+        logv("onCarrierConfigChanged slotId=" + slotId);
+
+        boolean hasConfig = verifyImsMmTelConfigured(slotId);
+        if (slotId < mMmTelFeatureListeners.size()) {
+            MmTelFeatureListener listener = mMmTelFeatureListeners.valueAt(slotId);
+            listener.notifyConfigChanged(hasConfig);
+        }
+
+        hasConfig = verifyImsRcsConfigured(slotId);
+        if (slotId < mRcsFeatureListeners.size()) {
+            RcsFeatureListener listener = mRcsFeatureListeners.valueAt(slotId);
+            listener.notifyConfigChanged(hasConfig);
+        }
+    }
+
+    private void onExternalRcsStateChanged(ExternalRcsFeatureState fs) {
+        logv("onExternalRcsStateChanged slotId=" + fs.mSlotId
+                + ", state=" + (fs.mState == STATE_UNKNOWN
+                        ? "" : ImsFeature.STATE_LOG_MAP.get(fs.mState))
+                + ", reason=" + imsStateReasonToString(fs.mReason));
+
+        RcsFeatureListener listener = mRcsFeatureListeners.get(fs.mSlotId);
+        if (listener != null) {
+            listener.notifyExternalRcsState(fs);
+        } else {
+            // unexpected state
+            loge("onExternalRcsStateChanged slotId=" + fs.mSlotId + ", no listener.");
+        }
+    }
+
+    /**
+     * Interface to be notified from TelephonyRcsSerice and RcsFeatureController
+     *
+     * @param ready true if feature's state is STATE_READY. Valid only when it is true.
+     * @param hasActiveFeatures true if the RcsFeatureController has active features.
+     */
+    public void notifyExternalRcsStateChanged(
+            int slotId, boolean ready, boolean hasActiveFeatures) {
+        int state = STATE_UNKNOWN;
+        int reason = REASON_IMS_SERVICE_DISCONNECTED;
+
+        if (ready) {
+            // From RcsFeatureController
+            state = STATE_READY;
+            reason = AVAILABLE;
+        } else if (!hasActiveFeatures) {
+            // From TelephonyRcsService
+            reason = REASON_NO_IMS_SERVICE_CONFIGURED;
+            state = STATE_UNAVAILABLE;
+        } else {
+            // From TelephonyRcsService
+            // TelephonyRcsService doesn't know the exact state of FeatureConnection.
+            // Only when there is no feature, we can assume the state.
+        }
+
+        if (VDBG) {
+            logv("notifyExternalRcsStateChanged slotId=" + slotId
+                    + ", ready=" + ready
+                    + ", hasActiveFeatures=" + hasActiveFeatures);
+        }
+
+        ExternalRcsFeatureState fs = new ExternalRcsFeatureState(slotId, state, reason);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_EXTERNAL_RCS_STATE_CHANGED, fs));
+    }
+
+    /**
+     * Notifies carrier configuration has changed.
+     */
+    @VisibleForTesting
+    public void notifyCarrierConfigChanged(int slotId) {
+        if (VDBG) logv("notifyCarrierConfigChanged slotId=" + slotId);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARRIER_CONFIG_CHANGED, slotId, 0));
+    }
+    /**
+     * Register IImsStateCallback
+     *
+     * @param feature for which state is changed, ImsFeature.FEATURE_*
+     */
+    public void registerImsStateCallback(int subId, int feature, IImsStateCallback cb,
+            String callingPackage) {
+        if (VDBG) {
+            logv("registerImsStateCallback subId=" + subId
+                    + ", feature=" + feature + ", pkg=" + callingPackage);
+        }
+
+        CallbackWrapper wrapper = new CallbackWrapper(subId, feature, cb, callingPackage);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_CALLBACK, wrapper));
+    }
+
+    /**
+     * Unegister previously registered callback
+     */
+    public void unregisterImsStateCallback(IImsStateCallback cb) {
+        if (VDBG) logv("unregisterImsStateCallback");
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_CALLBACK, cb));
+    }
+
+    private void removeInactiveCallbacks(
+            ArrayList<IBinder> inactiveCallbacks, String message) {
+        if (inactiveCallbacks == null || inactiveCallbacks.size() == 0) return;
+
+        if (VDBG) {
+            logv("removeInactiveCallbacks size="
+                    + inactiveCallbacks.size() + " from " + message);
+        }
+
+        for (IBinder binder : inactiveCallbacks) {
+            CallbackWrapper wrapper = mWrappers.get(binder);
+            if (wrapper != null) {
+                // Send the reason REASON_SUBSCRIPTION_INACTIVE to the client
+                wrapper.notifyInactive();
+                mWrappers.remove(binder);
+            }
+        }
+        inactiveCallbacks.clear();
+    }
+
+    private int getSubId(int slotId) {
+        Phone phone = mPhoneFactoryProxy.getPhone(slotId);
+        if (phone != null) return phone.getSubId();
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
+    private static boolean isActive(final int[] activeSubs, int subId) {
+        for (int i : activeSubs) {
+            if (i == subId) return true;
+        }
+        return false;
+    }
+
+    private static int convertReasonType(int reason) {
+        switch(reason) {
+            case UNAVAILABLE_REASON_NOT_READY:
+                return REASON_IMS_SERVICE_NOT_READY;
+            case UNAVAILABLE_REASON_IMS_UNSUPPORTED:
+                return REASON_NO_IMS_SERVICE_CONFIGURED;
+            default:
+                break;
+        }
+
+        return REASON_IMS_SERVICE_DISCONNECTED;
+    }
+
+    private boolean verifyImsMmTelConfigured(int slotId) {
+        boolean ret = false;
+        if (mImsResolver == null) {
+            loge("verifyImsMmTelConfigured mImsResolver is null");
+        } else {
+            ret = mImsResolver.isImsServiceConfiguredForFeature(slotId, FEATURE_MMTEL);
+        }
+        if (VDBG) logv("verifyImsMmTelConfigured slotId=" + slotId + ", ret=" + ret);
+        return ret;
+    }
+
+    private boolean verifyImsRcsConfigured(int slotId) {
+        boolean ret = false;
+        if (mImsResolver == null) {
+            loge("verifyImsRcsConfigured mImsResolver is null");
+        } else {
+            ret = mImsResolver.isImsServiceConfiguredForFeature(slotId, FEATURE_RCS);
+        }
+        if (VDBG) logv("verifyImsRcsConfigured slotId=" + slotId + ", ret=" + ret);
+        return ret;
+    }
+
+    private static String connectorReasonToString(int reason) {
+        switch(reason) {
+            case UNAVAILABLE_REASON_DISCONNECTED:
+                return "DISCONNECTED";
+            case UNAVAILABLE_REASON_NOT_READY:
+                return "NOT_READY";
+            case UNAVAILABLE_REASON_IMS_UNSUPPORTED:
+                return "IMS_UNSUPPORTED";
+            case UNAVAILABLE_REASON_SERVER_UNAVAILABLE:
+                return "SERVER_UNAVAILABLE";
+            default:
+                break;
+        }
+        return "";
+    }
+
+    private static String imsStateReasonToString(int reason) {
+        switch(reason) {
+            case AVAILABLE:
+                return "READY";
+            case REASON_UNKNOWN_TEMPORARY_ERROR:
+                return "UNKNOWN_TEMPORARY_ERROR";
+            case REASON_UNKNOWN_PERMANENT_ERROR:
+                return "UNKNOWN_PERMANENT_ERROR";
+            case REASON_IMS_SERVICE_DISCONNECTED:
+                return "IMS_SERVICE_DISCONNECTED";
+            case REASON_NO_IMS_SERVICE_CONFIGURED:
+                return "NO_IMS_SERVICE_CONFIGURED";
+            case REASON_SUBSCRIPTION_INACTIVE:
+                return "SUBSCRIPTION_INACTIVE";
+            case REASON_IMS_SERVICE_NOT_READY:
+                return "IMS_SERVICE_NOT_READY";
+            default:
+                break;
+        }
+        return "";
+    }
+
+    /**
+     * PhoneFactory Dependencies for testing.
+     */
+    @VisibleForTesting
+    public interface PhoneFactoryProxy {
+        /**
+         * Override getPhone for testing.
+         */
+        Phone getPhone(int index);
+    }
+
+    private PhoneFactoryProxy mPhoneFactoryProxy = new PhoneFactoryProxy() {
+        @Override
+        public Phone getPhone(int index) {
+            return PhoneFactory.getPhone(index);
+        }
+    };
+
+    private void release() {
+        if (VDBG) logv("release");
+
+        mTelephonyRegistryManager.removeOnSubscriptionsChangedListener(mSubChangedListener);
+        mApp.unregisterReceiver(mReceiver);
+
+        for (int i = 0; i < mMmTelFeatureListeners.size(); i++) {
+            mMmTelFeatureListeners.valueAt(i).destroy();
+        }
+        mMmTelFeatureListeners.clear();
+
+        for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+            mRcsFeatureListeners.valueAt(i).destroy();
+        }
+        mRcsFeatureListeners.clear();
+    }
+
+    /**
+     * destroy the instance
+     */
+    @VisibleForTesting
+    public void destroy() {
+        if (VDBG) logv("destroy it");
+
+        release();
+        mHandler.getLooper().quit();
+    }
+
+    /**
+     * get the handler
+     */
+    @VisibleForTesting
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * Determine whether the callback is registered or not
+     */
+    @VisibleForTesting
+    public boolean isRegistered(IImsStateCallback cb) {
+        if (cb == null) return false;
+        return mWrappers.containsKey(cb.asBinder());
+    }
+
+    /**
+     * Dump this instance into a readable format for dumpsys usage.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        pw.increaseIndent();
+        synchronized (mDumpLock) {
+            pw.println("CallbackWrappers:");
+            pw.increaseIndent();
+            mWrappers.values().forEach(wrapper -> wrapper.dump(pw));
+            pw.decreaseIndent();
+            pw.println("MmTelFeatureListeners:");
+            pw.increaseIndent();
+            for (int i = 0; i < mNumSlots; i++) {
+                MmTelFeatureListener l = mMmTelFeatureListeners.get(i);
+                if (l == null) continue;
+                l.dump(pw);
+            }
+            pw.decreaseIndent();
+            pw.println("RcsFeatureListeners:");
+            pw.increaseIndent();
+            for (int i = 0; i < mNumSlots; i++) {
+                RcsFeatureListener l = mRcsFeatureListeners.get(i);
+                if (l == null) continue;
+                l.dump(pw);
+            }
+            pw.decreaseIndent();
+            pw.println("Most recent logs:");
+            pw.increaseIndent();
+            sLocalLog.dump(pw);
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+
+    private static void logv(String msg) {
+        Rlog.d(TAG, msg);
+    }
+
+    private static void logd(String msg) {
+        Rlog.d(TAG, msg);
+        sLocalLog.log(msg);
+    }
+
+    private static void loge(String msg) {
+        Rlog.e(TAG, msg);
+        sLocalLog.log(msg);
+    }
+}
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 23cbaac..e7cb28c 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -160,6 +160,7 @@
     TelephonyRcsService mTelephonyRcsService;
     public PhoneInterfaceManager phoneMgr;
     public ImsRcsController imsRcsController;
+    public ImsStateCallbackController mImsStateCallbackController;
     CarrierConfigLoader configLoader;
 
     private Phone phoneInEcm;
@@ -463,6 +464,8 @@
             imsRcsController = ImsRcsController.init(this);
 
             if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS)) {
+                mImsStateCallbackController =
+                        ImsStateCallbackController.make(this, PhoneFactory.getPhones().length);
                 mTelephonyRcsService = new TelephonyRcsService(this,
                         PhoneFactory.getPhones().length);
                 mTelephonyRcsService.initialize();
@@ -1035,6 +1038,12 @@
         } catch (Exception e) {
             e.printStackTrace();
         }
+        pw.println("ImsStateCallbackController:");
+        try {
+            if (mImsStateCallbackController != null) mImsStateCallbackController.dump(pw);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
         pw.decreaseIndent();
         pw.println("------- End PhoneGlobals -------");
     }
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index f09b338..3721c40 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -22,6 +22,7 @@
 import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_GSM;
 import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_IMS;
 import static com.android.internal.telephony.PhoneConstants.SUBSCRIPTION_KEY;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__CLIENT_PARAMS_SENT;
 
 import android.Manifest;
 import android.Manifest.permission;
@@ -157,6 +158,7 @@
 import com.android.internal.telephony.HalVersion;
 import com.android.internal.telephony.IBooleanConsumer;
 import com.android.internal.telephony.ICallForwardingInfoCallback;
+import com.android.internal.telephony.IImsStateCallback;
 import com.android.internal.telephony.IIntegerConsumer;
 import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.ITelephony;
@@ -185,6 +187,7 @@
 import com.android.internal.telephony.ims.ImsResolver;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
 import com.android.internal.telephony.uicc.IccIoResult;
@@ -1113,7 +1116,9 @@
                             // any service for voice call.
                             if ((callForwardInfo.serviceClass
                                     & CommandsInterface.SERVICE_CLASS_VOICE) > 0) {
-                                callForwardingInfo = new CallForwardingInfo(true,
+                                callForwardingInfo = new CallForwardingInfo(
+                                        callForwardInfo.status
+                                                == CommandsInterface.CF_ACTION_ENABLE,
                                         callForwardInfo.reason,
                                         callForwardInfo.number,
                                         callForwardInfo.timeSeconds);
@@ -3411,17 +3416,6 @@
         mApp.enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
     }
 
-    /**
-     * Make sure the caller is system.
-     *
-     * @throws SecurityException if the caller is not system.
-     */
-    private static void enforceSystemCaller() {
-        if (Binder.getCallingUid() != Process.SYSTEM_UID) {
-            throw new SecurityException("Caller must be system");
-        }
-    }
-
     private void enforceActiveEmergencySessionPermission() {
         mApp.enforceCallingOrSelfPermission(
                 android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION, null);
@@ -6603,34 +6597,6 @@
     }
 
     /**
-     * Enable or disable always reporting signal strength changes from radio.
-     *
-     * @param isEnable {@code true} for enabling; {@code false} for disabling.
-     */
-    @Override
-    public void setAlwaysReportSignalStrength(int subId, boolean isEnable) {
-        enforceModifyPermission();
-        enforceSystemCaller();
-
-        final long identity = Binder.clearCallingIdentity();
-        final Phone phone = getPhone(subId);
-        try {
-            if (phone != null) {
-                if (DBG) {
-                    log("setAlwaysReportSignalStrength: subId=" + subId
-                            + " isEnable=" + isEnable);
-                }
-                phone.setAlwaysReportSignalStrength(isEnable);
-            } else {
-                loge("setAlwaysReportSignalStrength: no phone found for subId="
-                        + subId);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    /**
      * Get the user enabled state of Mobile Data.
      *
      * TODO: remove and use isUserDataEnabled.
@@ -8340,6 +8306,16 @@
             int result = (int) sendRequest(CMD_ENABLE_VONR, enabled, subId,
                     workSource);
             if (DBG) log("setVoNrEnabled result: " + result);
+
+            if (result == TelephonyManager.ENABLE_VONR_SUCCESS) {
+                if (DBG) {
+                    log("Set VoNR settings in siminfo db; subId=" + subId + ", value:" + enabled);
+                }
+                SubscriptionManager.setSubscriptionProperty(
+                        subId, SubscriptionManager.NR_ADVANCED_CALLING_ENABLED,
+                        (enabled ? "1" : "0"));
+            }
+
             return result;
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -10527,6 +10503,9 @@
             } else {
                 configBinder.setRcsClientConfiguration(rcc);
             }
+
+            RcsStats.getInstance().onRcsClientProvisioningStats(subId,
+                    RCS_CLIENT_PROVISIONING_STATS__EVENT__CLIENT_PARAMS_SENT);
         } catch (RemoteException e) {
             Rlog.e(LOG_TAG, "fail to setRcsClientConfiguration " + e.getMessage());
             throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
@@ -10929,7 +10908,7 @@
         mApp.getSystemService(AppOpsManager.class)
                 .checkPackage(callingUid, callingPackage);
 
-        validateSignalStrengthUpdateRequest(request, callingUid);
+        validateSignalStrengthUpdateRequest(mApp, request, callingUid);
 
         final long identity = Binder.clearCallingIdentity();
         try {
@@ -10968,19 +10947,19 @@
         }
     }
 
-    private static void validateSignalStrengthUpdateRequest(SignalStrengthUpdateRequest request,
-            int callingUid) {
+    private static void validateSignalStrengthUpdateRequest(Context context,
+            SignalStrengthUpdateRequest request, int callingUid) {
         if (callingUid == Process.PHONE_UID || callingUid == Process.SYSTEM_UID) {
             // phone/system process do not have further restriction on request
             return;
         }
 
         // Applications has restrictions on how to use the request:
-        // Only system caller can set mIsSystemThresholdReportingRequestedWhileIdle
+        // Non-system callers need permission to set mIsSystemThresholdReportingRequestedWhileIdle
         if (request.isSystemThresholdReportingRequestedWhileIdle()) {
-            // This is not system caller which has been checked above
-            throw new IllegalArgumentException(
-                    "Only system can set isSystemThresholdReportingRequestedWhileIdle");
+            context.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH,
+                    "validateSignalStrengthUpdateRequest");
         }
 
         for (SignalThresholdInfo info : request.getSignalThresholdInfos()) {
@@ -11061,4 +11040,73 @@
             Binder.restoreCallingIdentity(identity);
         }
     }
+
+    /**
+     * Register an IMS connection state callback
+     */
+    @Override
+    public void registerImsStateCallback(int subId, int feature, IImsStateCallback cb,
+            String callingPackage) {
+        if (feature == ImsFeature.FEATURE_MMTEL) {
+            // ImsMmTelManager
+            // The following also checks READ_PRIVILEGED_PHONE_STATE.
+            TelephonyPermissions
+                    .enforceCallingOrSelfReadPrecisePhoneStatePermissionOrCarrierPrivilege(
+                            mApp, subId, "registerImsStateCallback");
+        } else if (feature == ImsFeature.FEATURE_RCS) {
+            // ImsRcsManager or SipDelegateManager
+            TelephonyPermissions.enforceAnyPermissionGrantedOrCarrierPrivileges(mApp, subId,
+                    Binder.getCallingUid(), "registerImsStateCallback",
+                    Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                    Manifest.permission.READ_PRECISE_PHONE_STATE,
+                    Manifest.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE,
+                    Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION);
+        }
+
+        if (!ImsManager.isImsSupportedOnDevice(mApp)) {
+            throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                    "IMS not available on device.");
+        }
+
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+            throw new ServiceSpecificException(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION);
+        }
+
+        ImsStateCallbackController controller = ImsStateCallbackController.getInstance();
+        if (controller == null) {
+            throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+                    "IMS not available on device.");
+        }
+
+        if (callingPackage == null) {
+            callingPackage = getCurrentPackageName();
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            int slotId = getSlotIndexOrException(subId);
+            controller.registerImsStateCallback(subId, feature, cb, callingPackage);
+        } catch (ImsException e) {
+            throw new ServiceSpecificException(e.getCode());
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Unregister an IMS connection state callback
+     */
+    @Override
+    public void unregisterImsStateCallback(IImsStateCallback cb) {
+        final long token = Binder.clearCallingIdentity();
+        ImsStateCallbackController controller = ImsStateCallbackController.getInstance();
+        if (controller == null) {
+            return;
+        }
+        try {
+            controller.unregisterImsStateCallback(cb);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
 }
diff --git a/src/com/android/phone/RcsProvisioningMonitor.java b/src/com/android/phone/RcsProvisioningMonitor.java
index 6d2bd6f..e819afc 100644
--- a/src/com/android/phone/RcsProvisioningMonitor.java
+++ b/src/com/android/phone/RcsProvisioningMonitor.java
@@ -16,6 +16,10 @@
 
 package com.android.phone;
 
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__DMA_CHANGED;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION;
+
 import android.Manifest;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
@@ -47,6 +51,8 @@
 import com.android.ims.FeatureUpdates;
 import com.android.ims.RcsFeatureManager;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.metrics.RcsStats;
+import com.android.internal.telephony.metrics.RcsStats.RcsProvisioningCallback;
 import com.android.internal.telephony.util.HandlerExecutor;
 import com.android.internal.util.CollectionUtils;
 import com.android.telephony.Rlog;
@@ -100,6 +106,8 @@
     private final RoleManagerAdapter mRoleManager;
     private FeatureConnectorFactory<RcsFeatureManager> mFeatureFactory;
 
+    private RcsStats mRcsStats;
+
     private static RcsProvisioningMonitor sInstance;
 
     private final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener =
@@ -227,6 +235,10 @@
             if (mSingleRegistrationCapability != singleRegistrationCapability) {
                 mSingleRegistrationCapability = singleRegistrationCapability;
                 notifyDma();
+
+                // update whether single registration supported.
+                mRcsStats.setEnableSingleRegistration(mSubId,
+                        mSingleRegistrationCapability == ProvisioningManager.STATUS_CAPABLE);
             }
         }
 
@@ -338,6 +350,9 @@
                     } else {
                         notifyRcsAutoConfigurationReceived();
                     }
+
+                    // check callback for metrics if not registered, register callback
+                    registerMetricsCallback();
                 } else {
                     // clear callbacks if rcs disconnected
                     clearCallbacks();
@@ -397,6 +412,18 @@
                 }
             }
         }
+
+        private void registerMetricsCallback() {
+            RcsProvisioningCallback rcsProvisioningCallback = mRcsStats.getRcsProvisioningCallback(
+                    mSubId, mSingleRegistrationCapability == ProvisioningManager.STATUS_CAPABLE);
+
+            // if not yet registered, register callback and set registered value
+            if (rcsProvisioningCallback != null && !rcsProvisioningCallback.getRegistered()) {
+                if (addRcsConfigCallback(rcsProvisioningCallback)) {
+                    rcsProvisioningCallback.setRegistered(true);
+                }
+            }
+        }
     }
 
     @VisibleForTesting
@@ -454,7 +481,7 @@
 
     @VisibleForTesting
     public RcsProvisioningMonitor(PhoneGlobals app, Looper looper, RoleManagerAdapter roleManager,
-            FeatureConnectorFactory<RcsFeatureManager> factory) {
+            FeatureConnectorFactory<RcsFeatureManager> factory, RcsStats rcsStats) {
         mPhone = app;
         mHandler = new MyHandler(looper);
         mCarrierConfigManager = mPhone.getSystemService(CarrierConfigManager.class);
@@ -465,6 +492,7 @@
         logv("DMA is " + mDmaPackageName);
         mDmaChangedListener = new DmaChangedListener();
         mFeatureFactory = factory;
+        mRcsStats = rcsStats;
         init();
     }
 
@@ -477,7 +505,8 @@
             HandlerThread handlerThread = new HandlerThread(TAG);
             handlerThread.start();
             sInstance = new RcsProvisioningMonitor(app, handlerThread.getLooper(),
-                    new RoleManagerAdapterImpl(app), RcsFeatureManager::getConnector);
+                    new RoleManagerAdapterImpl(app), RcsFeatureManager::getConnector,
+                    RcsStats.getInstance());
         }
         return sInstance;
     }
@@ -704,6 +733,10 @@
                     logv("acs not used, set cached config and notify.");
                     v.setConfig(cachedConfig);
                 }
+
+                // store RCS metrics - DMA changed event
+                mRcsStats.onRcsClientProvisioningStats(k,
+                        RCS_CLIENT_PROVISIONING_STATS__EVENT__DMA_CHANGED);
             });
         }
     }
@@ -803,6 +836,14 @@
         }
         info.setConfig(isCompressed ? RcsConfig.decompressGzip(config) : config);
         updateConfigForSub(subId, config, isCompressed);
+
+        // Supporting ACS means config data comes from ACS
+        // store RCS metrics - received provisioning event
+        if (isAcsUsed(subId)) {
+            mRcsStats.onRcsAcsProvisioningStats(subId, 200,
+                    RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML,
+                    isRcsVolteSingleRegistrationEnabled(subId));
+        }
     }
 
     private void onReconfigRequest(int subId) {
@@ -814,6 +855,10 @@
             updateConfigForSub(subId, null, true);
             info.triggerRcsReconfiguration();
         }
+
+        // store RCS metrics - reconfig event
+        mRcsStats.onRcsClientProvisioningStats(subId,
+                RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION);
     }
 
     private void notifyDmaForSub(int subId, int capability) {
diff --git a/src/com/android/phone/settings/RadioInfo.java b/src/com/android/phone/settings/RadioInfo.java
index d4c926e..7681a02 100644
--- a/src/com/android/phone/settings/RadioInfo.java
+++ b/src/com/android/phone/settings/RadioInfo.java
@@ -17,6 +17,7 @@
 package com.android.phone.settings;
 
 import static android.net.ConnectivityManager.NetworkCallback;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.content.ComponentName;
@@ -57,7 +58,6 @@
 import android.telephony.CellSignalStrengthLte;
 import android.telephony.CellSignalStrengthWcdma;
 import android.telephony.DataSpecificRegistrationInfo;
-import android.telephony.data.NetworkSlicingConfig;
 import android.telephony.NetworkRegistrationInfo;
 import android.telephony.PhysicalChannelConfig;
 import android.telephony.RadioAccessFamily;
@@ -66,6 +66,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
+import android.telephony.data.NetworkSlicingConfig;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Menu;
@@ -102,8 +103,8 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Radio Information Class
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index 0be927a..c62b4fa 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -257,7 +257,7 @@
 
                 @Override
                 public void onConnectionPropertiesChanged(Connection c, int connectionProperties) {
-                    Log.d(this, "onConnectionPropertiesChanged: Connection: %s,"
+                    Log.i(ImsConference.this, "onConnectionPropertiesChanged: Connection: %s,"
                             + " connectionProperties: %s", c, connectionProperties);
                     updateConnectionProperties(connectionProperties);
                 }
@@ -383,6 +383,11 @@
     private boolean mIsUsingSimCallManager = false;
 
     /**
+     * See {@link #isRemotelyHosted()} for details.
+     */
+    private boolean mWasRemotelyHosted = false;
+
+    /**
      * Where {@link #isMultiparty()} is {@code false}, contains the
      * {@link ConferenceParticipantConnection#getUserEntity()} and
      * {@link ConferenceParticipantConnection#getEndpoint()} of the single participant which this
@@ -522,11 +527,16 @@
                 (properties & Connection.PROPERTY_IS_EXTERNAL_CALL) != 0);
 
         conferenceProperties = changeBitmask(conferenceProperties,
-                Connection.PROPERTY_REMOTELY_HOSTED, !isConferenceHost());
+                Connection.PROPERTY_REMOTELY_HOSTED, isRemotelyHosted());
 
         conferenceProperties = changeBitmask(conferenceProperties,
                 Connection.PROPERTY_IS_ADHOC_CONFERENCE,
                 (properties & Connection.PROPERTY_IS_ADHOC_CONFERENCE) != 0);
+        Log.i(this, "applyHostProperties: confProp=%s", conferenceProperties);
+
+        conferenceProperties = changeBitmask(conferenceProperties,
+                Connection.PROPERTY_CROSS_SIM,
+                (properties & Connection.PROPERTY_CROSS_SIM) != 0);
 
         return conferenceProperties;
     }
@@ -774,6 +784,26 @@
     }
 
     /**
+     * Returns whether the conference is remotely hosted or not.
+     * This method will cache the current remotely hosted state when the conference host or
+     * original connection becomes null.  This is important for scenarios where the conference host
+     * or original connection changes midway through a conference such as in an SRVCC scenario.
+     * @return {@code true} if the conference was remotely hosted based on the conference host and
+     * its original connection, or based on the last known remotely hosted state.  {@code false}
+     * otherwise.
+     */
+    public boolean isRemotelyHosted() {
+        if (mConferenceHost == null || mConferenceHost.getOriginalConnection() == null) {
+            return mWasRemotelyHosted;
+        }
+        com.android.internal.telephony.Connection originalConnection =
+                mConferenceHost.getOriginalConnection();
+        mWasRemotelyHosted = originalConnection.isMultiparty()
+                && !originalConnection.isConferenceHost();
+        return mWasRemotelyHosted;
+    }
+
+    /**
      * Determines if this conference is hosted on the current device or the peer device.
      *
      * @return {@code true} if this conference is hosted on the current device, {@code false} if it
@@ -1209,6 +1239,7 @@
         ConferenceParticipantConnection connection = new ConferenceParticipantConnection(
                 parent.getOriginalConnection(), participant,
                 !isConferenceHost() /* isRemotelyHosted */);
+
         if (participant.getConnectTime() == 0) {
             connection.setConnectTimeMillis(parent.getConnectTimeMillis());
             connection.setConnectionStartElapsedRealtimeMillis(
diff --git a/src/com/android/services/telephony/TelephonyConferenceController.java b/src/com/android/services/telephony/TelephonyConferenceController.java
index 228541a..9aa3dbe 100644
--- a/src/com/android/services/telephony/TelephonyConferenceController.java
+++ b/src/com/android/services/telephony/TelephonyConferenceController.java
@@ -40,6 +40,7 @@
  */
 final class TelephonyConferenceController {
     private static final int TELEPHONY_CONFERENCE_MAX_SIZE = 5;
+    private static final String RIL_REPORTED_CONFERENCE_CALL_STRING = "Conference Call";
 
     private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
             new TelephonyConnection.TelephonyConnectionListener() {
@@ -179,7 +180,6 @@
     private void recalculateConference() {
         Set<TelephonyConnection> conferencedConnections = new HashSet<>();
         int numGsmConnections = 0;
-
         for (TelephonyConnection connection : mTelephonyConnections) {
             com.android.internal.telephony.Connection radioConnection =
                 connection.getOriginalConnection();
@@ -271,11 +271,19 @@
                             // Remove all instances of PROPERTY_IS_DOWNGRADED_CONFERENCE. This
                             // property should only be set on the parent call (i.e. the newly
                             // created TelephonyConference.
-                            Log.d(this, "Removing PROPERTY_IS_DOWNGRADED_CONFERENCE from connection"
-                                    + " %s", connection);
-                            int newProperties = connection.getConnectionProperties()
-                                    & ~Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE;
-                            connection.setTelephonyConnectionProperties(newProperties);
+                            // This doesn't apply to a connection whose address is "Conference
+                            // Call", which may be updated by some modem to create a connection
+                            // to represent a merged conference connection in SRVCC.
+                            if (connection.getAddress() == null
+                                    || !connection.getAddress().getSchemeSpecificPart()
+                                            .equalsIgnoreCase(
+                                                    RIL_REPORTED_CONFERENCE_CALL_STRING)) {
+                                Log.d(this, "Removing PROPERTY_IS_DOWNGRADED_CONFERENCE"
+                                        + " from connection %s", connection);
+                                int newProperties = connection.getConnectionProperties()
+                                        & ~Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE;
+                                connection.setTelephonyConnectionProperties(newProperties);
+                            }
                             isDowngradedConference = true;
                         }
                         mTelephonyConference.addTelephonyConnection(connection);
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 18a40cf..ed07726 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -3814,4 +3814,13 @@
             mCommunicator.sendMessages(set);
         }
     }
+
+    /**
+     * Returns the current telephony connection listeners for test purposes.
+     * @return list of telephony connection listeners.
+     */
+    @VisibleForTesting
+    public List<TelephonyConnectionListener> getTelephonyConnectionListeners() {
+        return new ArrayList<>(mTelephonyListeners);
+    }
 }
diff --git a/src/com/android/services/telephony/rcs/DelegateStateTracker.java b/src/com/android/services/telephony/rcs/DelegateStateTracker.java
index 321c7ba..18aab88 100644
--- a/src/com/android/services/telephony/rcs/DelegateStateTracker.java
+++ b/src/com/android/services/telephony/rcs/DelegateStateTracker.java
@@ -24,9 +24,12 @@
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
 import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.util.ArraySet;
 import android.util.LocalLog;
 import android.util.Log;
 
+import com.android.internal.telephony.metrics.RcsStats;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -50,11 +53,15 @@
     private boolean mCreatedCalled = false;
     private int mRegistrationStateOverride = -1;
 
+    private Set<String> mDelegateSupportedTags;
+    private final RcsStats mRcsStats;
+
     public DelegateStateTracker(int subId, ISipDelegateConnectionStateCallback appStateCallback,
-            ISipDelegate localDelegateImpl) {
+            ISipDelegate localDelegateImpl, RcsStats rcsStats) {
         mSubId = subId;
         mAppStateCallback = appStateCallback;
         mLocalDelegateImpl = localDelegateImpl;
+        mRcsStats = rcsStats;
     }
 
     /**
@@ -63,10 +70,13 @@
      * Registration and state updates will be send via the
      * {@link SipDelegateBinderConnection.StateCallback} callback implemented by this class as they
      * arrive.
+     * @param supportedTags the tags supported by the SipTransportController and ImsService creating
+     *                      the SipDelegate. These tags will be used as a key for SipDelegate
+     *                      metrics.
      * @param deniedTags The tags denied by the SipTransportController and ImsService creating the
      *         SipDelegate. These tags will need to be notified back to the IMS application.
      */
-    public void sipDelegateConnected(Set<FeatureTagState> deniedTags) {
+    public void sipDelegateConnected(Set<String> supportedTags, Set<FeatureTagState> deniedTags) {
         logi("SipDelegate connected with denied tags:" + deniedTags);
         // From the IMS application perspective, we only call onCreated/onDestroyed once and
         // provide the local implementation of ISipDelegate, which doesn't change, even though
@@ -74,6 +84,8 @@
         if (!mCreatedCalled) {
             mCreatedCalled = true;
             notifySipDelegateCreated();
+            mDelegateSupportedTags = supportedTags;
+            mRcsStats.createSipDelegateStats(mSubId, mDelegateSupportedTags);
         }
         mRegistrationStateOverride = -1;
         mDelegateDeniedTags = new ArrayList<>(deniedTags);
@@ -84,8 +96,8 @@
      *
      * This will trigger an override of the IMS application's registration state. All feature tags
      * in the REGISTERED state will be overridden to move to the deregistering state specified until
-     * a new SipDelegate was successfully created and {@link #sipDelegateConnected(Set)} was called
-     * or it was destroyed and {@link #sipDelegateDestroyed(int)} was called.
+     * a new SipDelegate was successfully created and {@link #sipDelegateConnected(Set, Set)} was
+     * called or it was destroyed and {@link #sipDelegateDestroyed(int)} was called.
      * @param deregisteringReason The new deregistering reason that all feature tags in the
      *         registered state should now report.
      */
@@ -115,6 +127,7 @@
         mRegistrationStateOverride = -1;
         try {
             mAppStateCallback.onDestroyed(reason);
+            mRcsStats.onSipDelegateStats(mSubId, mDelegateSupportedTags, reason);
         } catch (RemoteException e) {
             logw("sipDelegateDestroyed: IMS application is dead: " + e);
         }
@@ -141,6 +154,11 @@
         logi("onRegistrationStateChanged: sending reg state " + registrationState);
         try {
             mAppStateCallback.onFeatureTagStatusChanged(registrationState, mDelegateDeniedTags);
+            Set<String> registeredFeatureTags = registrationState.getRegisteredFeatureTags();
+            mRcsStats.onSipTransportFeatureTagStats(mSubId,
+                    new ArraySet<FeatureTagState>(mDelegateDeniedTags),
+                    registrationState.getDeregisteredFeatureTags(),
+                    registeredFeatureTags);
         } catch (RemoteException e) {
             logw("onRegistrationStateChanged: IMS application is dead: " + e);
         }
diff --git a/src/com/android/services/telephony/rcs/RcsFeatureController.java b/src/com/android/services/telephony/rcs/RcsFeatureController.java
index 7834903..cc1a2cc 100644
--- a/src/com/android/services/telephony/rcs/RcsFeatureController.java
+++ b/src/com/android/services/telephony/rcs/RcsFeatureController.java
@@ -33,6 +33,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.imsphone.ImsRegistrationCallbackHelper;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.phone.ImsStateCallbackController;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -139,6 +140,8 @@
                         // ImsService is gone.
                         updateConnectionStatus(manager);
                         setupConnectionToService(manager);
+                        ImsStateCallbackController.getInstance()
+                                .notifyExternalRcsStateChanged(mSlotId, true, true);
                     } catch (ImsException e) {
                         updateConnectionStatus(null /*manager*/);
                         // Use deprecated Exception for compatibility.
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
index 8cc70a4..c728141 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateController.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -34,6 +34,7 @@
 import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.PrintWriter;
@@ -102,7 +103,7 @@
         mMessageTransportWrapper = new MessageTransportWrapper(mSubId, executorService,
                 messageCallback);
         mDelegateStateTracker = new DelegateStateTracker(mSubId, stateCallback,
-                mMessageTransportWrapper.getDelegateConnection());
+                mMessageTransportWrapper.getDelegateConnection(), RcsStats.getInstance());
     }
 
     /**
@@ -191,7 +192,7 @@
                     .collect(Collectors.toSet()));
             mMessageTransportWrapper.openTransport(resultPair.first, allowedTags,
                     resultPair.second);
-            mDelegateStateTracker.sipDelegateConnected(resultPair.second);
+            mDelegateStateTracker.sipDelegateConnected(allowedTags, resultPair.second);
             return true;
         });
     }
diff --git a/src/com/android/services/telephony/rcs/SipSessionTracker.java b/src/com/android/services/telephony/rcs/SipSessionTracker.java
index 5ab482f..68e3065 100644
--- a/src/com/android/services/telephony/rcs/SipSessionTracker.java
+++ b/src/com/android/services/telephony/rcs/SipSessionTracker.java
@@ -24,6 +24,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.SipMessageParsingUtils;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.PrintWriter;
@@ -66,6 +67,14 @@
     // associated pending operation.
     private final ArrayMap<String, Runnable> mPendingAck = new ArrayMap<>();
 
+    private final RcsStats mRcsStats;
+    int mSubId;
+
+    public SipSessionTracker(int subId, RcsStats rcsStats) {
+        mSubId = subId;
+        mRcsStats = rcsStats;
+    }
+
     /**
      * Filter a SIP message to determine if it will result in a new SIP dialog. This will need to be
      * successfully acknowledged by the remote IMS stack using
@@ -73,10 +82,10 @@
      *
      * @param message The Incoming SIP message.
      */
-    public void filterSipMessage(SipMessage message) {
+    public void filterSipMessage(int direction, SipMessage message) {
         final Runnable r;
         if (startsEarlyDialog(message)) {
-            r = getCreateDialogRunnable(message);
+            r = getCreateDialogRunnable(direction, message);
         } else if (closesDialog(message)) {
             r = getCloseDialogRunnable(message);
         } else if (SipMessageParsingUtils.isSipResponse(message.getStartLine())) {
@@ -137,6 +146,8 @@
         if (dialogsToCleanup.isEmpty()) return;
         logi("Cleanup dialogs associated with call id: " + callId);
         for (SipDialog d : dialogsToCleanup) {
+            mRcsStats.onSipTransportSessionClosed(mSubId, callId, 0,
+                    d.getState() == d.STATE_CLOSED);
             d.close();
             logi("Dialog closed: " + d);
         }
@@ -197,6 +208,9 @@
      * Clears all tracked sessions.
      */
     public void clearAllSessions() {
+        for (SipDialog d : mTrackedDialogs) {
+            mRcsStats.onSipTransportSessionClosed(mSubId, d.getCallId(), 0, false);
+        }
         mTrackedDialogs.clear();
         mPendingAck.clear();
     }
@@ -262,7 +276,7 @@
         return SIP_CLOSE_DIALOG_REQUEST_METHOD.equalsIgnoreCase(startLineSegments[0]);
     }
 
-    private Runnable getCreateDialogRunnable(SipMessage m) {
+    private Runnable getCreateDialogRunnable(int direction, SipMessage m) {
         return () -> {
             List<SipDialog> duplicateDialogs = mTrackedDialogs.stream()
                     .filter(d -> d.getCallId().equals(m.getCallIdParameter()))
@@ -273,6 +287,10 @@
                 return;
             }
             SipDialog dialog = SipDialog.fromSipMessage(m);
+            String[] startLineSegments =
+                    SipMessageParsingUtils.splitStartLineAndVerify(m.getStartLine());
+            mRcsStats.earlySipTransportSession(startLineSegments[0], dialog.getCallId(),
+                    direction);
             logi("Starting new SipDialog: " + dialog);
             mTrackedDialogs.add(dialog);
         };
@@ -285,6 +303,7 @@
                     .collect(Collectors.toList());
             if (dialogsToClose.isEmpty()) return;
             logi("Closing dialogs associated with: " + m);
+            mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), 0, true);
             for (SipDialog d : dialogsToClose) {
                 d.close();
                 logi("Dialog closed: " + d);
@@ -344,11 +363,13 @@
         if (statusCode <= 100) return;
         // If 300+, then this dialog has received an error response and should move to closed state.
         if (statusCode >= 300) {
+            mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), statusCode, true);
             d.close();
             return;
         }
         if (toTag == null) logw("updateSipDialogState: No to tag for message: " + m);
         if (statusCode >= 200) {
+            mRcsStats.confirmedSipTransportSession(m.getCallIdParameter(), statusCode);
             d.confirm(toTag);
             return;
         }
diff --git a/src/com/android/services/telephony/rcs/TelephonyRcsService.java b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
index 034382c..dfcea74 100644
--- a/src/com/android/services/telephony/rcs/TelephonyRcsService.java
+++ b/src/com/android/services/telephony/rcs/TelephonyRcsService.java
@@ -32,7 +32,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.PhoneConfigurationManager;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.phone.ImsStateCallbackController;
 import com.android.phone.R;
 
 import java.io.FileDescriptor;
@@ -163,6 +165,7 @@
         mFeatureControllers = new SparseArray<>(numSlots);
         mSlotToAssociatedSubIds = new SparseArray<>(numSlots);
         mRcsUceEnabled = sResourceProxy.getDeviceUceEnabled(mContext);
+        RcsStats.getInstance().registerUceCallback();
     }
 
     @VisibleForTesting
@@ -173,6 +176,7 @@
         mSlotToAssociatedSubIds = new SparseArray<>(numSlots);
         sResourceProxy = resourceProxy;
         mRcsUceEnabled = sResourceProxy.getDeviceUceEnabled(mContext);
+        RcsStats.getInstance().registerUceCallback();
     }
 
     /**
@@ -310,6 +314,9 @@
         }
         // Only start the connection procedure if we have active features.
         if (c.hasActiveFeatures()) c.connect();
+
+        ImsStateCallbackController.getInstance()
+                .notifyExternalRcsStateChanged(slotId, false, c.hasActiveFeatures());
     }
 
     /**
diff --git a/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
index 777026c..3b1b26d 100644
--- a/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
+++ b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
@@ -16,6 +16,9 @@
 
 package com.android.services.telephony.rcs;
 
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING;
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING;
+
 import android.telephony.ims.DelegateRegistrationState;
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.SipDelegateConfiguration;
@@ -26,6 +29,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.SipMessageParsingUtils;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.services.telephony.rcs.validator.IncomingTransportStateValidator;
 import com.android.services.telephony.rcs.validator.MalformedSipMessageValidator;
@@ -145,11 +150,13 @@
     private PendingTask mPendingClose;
     private PendingRegCleanupTask mPendingRegCleanup;
     private Consumer<Set<String>> mRegistrationAppliedConsumer;
+    private final RcsStats mRcsStats;
 
     public TransportSipMessageValidator(int subId, ScheduledExecutorService executor) {
         mSubId = subId;
         mExecutor = executor;
-        mSipSessionTracker = new SipSessionTracker();
+        mRcsStats = RcsStats.getInstance();
+        mSipSessionTracker = new SipSessionTracker(subId, mRcsStats);
         mOutgoingTransportStateValidator = new OutgoingTransportStateValidator(mSipSessionTracker);
         mIncomingTransportStateValidator = new IncomingTransportStateValidator();
         mOutgoingMessageValidator = new MalformedSipMessageValidator().andThen(
@@ -163,7 +170,7 @@
     public TransportSipMessageValidator(int subId, ScheduledExecutorService executor,
             SipSessionTracker sipSessionTracker,
             OutgoingTransportStateValidator outgoingStateValidator,
-            IncomingTransportStateValidator incomingStateValidator) {
+            IncomingTransportStateValidator incomingStateValidator, RcsStats rcsStats) {
         mSubId = subId;
         mExecutor = executor;
         mSipSessionTracker = sipSessionTracker;
@@ -171,6 +178,7 @@
         mIncomingTransportStateValidator = incomingStateValidator;
         mOutgoingMessageValidator = mOutgoingTransportStateValidator;
         mIncomingMessageValidator = mIncomingTransportStateValidator;
+        mRcsStats = rcsStats;
     }
 
     /**
@@ -365,7 +373,11 @@
         }
         ValidationResult result = mOutgoingMessageValidator.validate(message);
         logi("verifyOutgoingMessage: " + result + ", message=" + message);
-        if (result.isValidated) mSipSessionTracker.filterSipMessage(message);
+        if (result.isValidated) {
+            mSipSessionTracker.filterSipMessage(
+                    SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, message);
+        }
+        updateForMetrics(SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, message, result);
         return result;
     }
 
@@ -378,7 +390,11 @@
     public ValidationResult verifyIncomingMessage(SipMessage message) {
         ValidationResult result = mIncomingMessageValidator.validate(message);
         logi("verifyIncomingMessage: " + result + ", message=" + message);
-        if (result.isValidated) mSipSessionTracker.filterSipMessage(message);
+        if (result.isValidated) {
+            mSipSessionTracker.filterSipMessage(
+                    SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING, message);
+        }
+        updateForMetrics(SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING, message, result);
         return result;
     }
 
@@ -539,6 +555,28 @@
                 .collect(Collectors.toSet());
     }
 
+    private void updateForMetrics(int direction, SipMessage m, ValidationResult result) {
+        String[] startLineSegments = SipMessageParsingUtils
+                .splitStartLineAndVerify(m.getStartLine());
+        if (SipMessageParsingUtils.isSipRequest(m.getStartLine())) {
+            if (result.isValidated) {
+                // SipMessage add to list for Metrics stats
+                mRcsStats.onSipMessageRequest(m.getCallIdParameter(), startLineSegments[0],
+                        direction);
+            } else {
+                //Message sending fail and there is no response.
+                mRcsStats.invalidatedMessageResult(mSubId, startLineSegments[0], direction,
+                        result.restrictedReason);
+            }
+        } else if (SipMessageParsingUtils.isSipResponse(m.getStartLine())) {
+            int statusCode = Integer.parseInt(startLineSegments[1]);
+            mRcsStats.onSipMessageResponse(mSubId, m.getCallIdParameter(), statusCode,
+                    result.restrictedReason);
+        } else {
+            logw("Message is Restricted");
+        }
+    }
+
     private void logi(String log) {
         Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
         mLocalLog.log("[I] " + log);
diff --git a/tests/src/com/android/phone/ImsStateCallbackControllerTest.java b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
new file mode 100644
index 0000000..c493f6b
--- /dev/null
+++ b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
@@ -0,0 +1,895 @@
+/*
+ * 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.phone;
+
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED;
+import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_NOT_READY;
+import static android.telephony.ims.ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED;
+import static android.telephony.ims.ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_MMTEL;
+import static android.telephony.ims.feature.ImsFeature.FEATURE_RCS;
+
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED;
+import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_NOT_READY;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyRegistryManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.TestableLooper;
+import android.util.Log;
+
+import com.android.ims.FeatureConnector;
+import com.android.ims.ImsManager;
+import com.android.ims.RcsFeatureManager;
+import com.android.internal.telephony.IImsStateCallback;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.ims.ImsResolver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for RcsProvisioningMonitor
+ */
+public class ImsStateCallbackControllerTest {
+    private static final String TAG = "ImsStateCallbackControllerTest";
+    private static final int FAKE_SUB_ID_BASE = 0x0FFFFFF0;
+
+    private static final int SLOT_0 = 0;
+    private static final int SLOT_1 = 1;
+
+    private static final int SLOT_0_SUB_ID = 1;
+    private static final int SLOT_1_SUB_ID = 2;
+    private static final int SLOT_2_SUB_ID = 3;
+
+    private ImsStateCallbackController mImsStateCallbackController;
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+    private TestableLooper mLooper;
+    @Mock private SubscriptionManager mSubscriptionManager;
+    private SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener;
+    @Mock private TelephonyRegistryManager mTelephonyRegistryManager;
+    @Mock private ITelephony.Stub mITelephony;
+    @Mock private RcsFeatureManager mRcsFeatureManager;
+    @Mock private ImsManager mMmTelFeatureManager;
+    @Mock private ImsStateCallbackController.MmTelFeatureConnectorFactory mMmTelFeatureFactory;
+    @Mock private ImsStateCallbackController.RcsFeatureConnectorFactory mRcsFeatureFactory;
+    @Mock private FeatureConnector<ImsManager> mMmTelFeatureConnectorSlot0;
+    @Mock private FeatureConnector<ImsManager> mMmTelFeatureConnectorSlot1;
+    @Mock private FeatureConnector<RcsFeatureManager> mRcsFeatureConnectorSlot0;
+    @Mock private FeatureConnector<RcsFeatureManager> mRcsFeatureConnectorSlot1;
+    @Captor ArgumentCaptor<FeatureConnector.Listener<ImsManager>> mMmTelConnectorListenerSlot0;
+    @Captor ArgumentCaptor<FeatureConnector.Listener<ImsManager>> mMmTelConnectorListenerSlot1;
+    @Captor ArgumentCaptor<FeatureConnector.Listener<RcsFeatureManager>> mRcsConnectorListenerSlot0;
+    @Captor ArgumentCaptor<FeatureConnector.Listener<RcsFeatureManager>> mRcsConnectorListenerSlot1;
+    @Mock private PhoneGlobals mPhone;
+    @Mock ImsStateCallbackController.PhoneFactoryProxy mPhoneFactoryProxy;
+    @Mock Phone mPhoneSlot0;
+    @Mock Phone mPhoneSlot1;
+    @Mock private IBinder mBinder0;
+    @Mock private IBinder mBinder1;
+    @Mock private IBinder mBinder2;
+    @Mock private IBinder mBinder3;
+    @Mock private IImsStateCallback mCallback0;
+    @Mock private IImsStateCallback mCallback1;
+    @Mock private IImsStateCallback mCallback2;
+    @Mock private IImsStateCallback mCallback3;
+    @Mock private ImsResolver mImsResolver;
+
+    private Executor mExecutor = new Executor() {
+        @Override
+        public void execute(Runnable r) {
+            r.run();
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mPhone.getMainExecutor()).thenReturn(mExecutor);
+        when(mPhone.getSystemServiceName(eq(SubscriptionManager.class)))
+                .thenReturn(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        when(mPhone.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE)))
+                .thenReturn(mSubscriptionManager);
+        when(mPhone.getSystemServiceName(eq(TelephonyRegistryManager.class)))
+                .thenReturn(Context.TELEPHONY_REGISTRY_SERVICE);
+        when(mPhone.getSystemService(eq(Context.TELEPHONY_REGISTRY_SERVICE)))
+                .thenReturn(mTelephonyRegistryManager);
+        when(mPhoneFactoryProxy.getPhone(eq(0))).thenReturn(mPhoneSlot0);
+        when(mPhoneFactoryProxy.getPhone(eq(1))).thenReturn(mPhoneSlot1);
+        when(mPhoneSlot0.getSubId()).thenReturn(SLOT_0_SUB_ID);
+        when(mPhoneSlot1.getSubId()).thenReturn(SLOT_1_SUB_ID);
+
+        when(mCallback0.asBinder()).thenReturn(mBinder0);
+        when(mCallback1.asBinder()).thenReturn(mBinder1);
+        when(mCallback2.asBinder()).thenReturn(mBinder2);
+        when(mCallback3.asBinder()).thenReturn(mBinder3);
+
+        // slot 0
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(0), eq(FEATURE_MMTEL)))
+                .thenReturn(true);
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(0), eq(FEATURE_RCS)))
+                .thenReturn(true);
+
+        // slot 1
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+                .thenReturn(true);
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_RCS)))
+                .thenReturn(true);
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                mSubChangedListener = (SubscriptionManager.OnSubscriptionsChangedListener)
+                        invocation.getArguments()[0];
+                return null;
+            }
+        }).when(mTelephonyRegistryManager).addOnSubscriptionsChangedListener(
+                any(SubscriptionManager.OnSubscriptionsChangedListener.class),
+                any());
+
+        mHandlerThread = new HandlerThread("ImsStateCallbackControllerTest");
+        mHandlerThread.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mImsStateCallbackController != null) {
+            mImsStateCallbackController.destroy();
+            mImsStateCallbackController = null;
+        }
+
+        if (mLooper != null) {
+            mLooper.destroy();
+            mLooper = null;
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testMmTelRegisterThenUnregisterCallback() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testMmTelConnectionUnavailable() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        mMmTelConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        mMmTelConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testMmTelConnectionReady() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback0, times(0)).onAvailable();
+
+        mMmTelConnectorListenerSlot0.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, atLeastOnce()).onAvailable();
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testMmTelIgnoreDuplicatedConsecutiveReason() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        mMmTelConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        mMmTelConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsRegisterThenUnregisterCallback() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsConnectionUnavailable() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // TelephonyRcsService notifying active features
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+        processAllMessages();
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsConnectionReady() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // TelephonyRcsService notifying active features
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+        processAllMessages();
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(0)).onAvailable();
+
+        // RcsFeatureController notifying STATE_READY
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, true, true);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+        processAllMessages();
+        verify(mCallback0, times(2)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(2)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        // RcsFeatureController notifying STATE_READY
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, true, true);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+
+        mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(2)).onAvailable();
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsHasNoActiveFeature() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // TelephonyRcsService notifying NO active feature
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, false);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(0)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(0)).onAvailable();
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testRcsIgnoreDuplicatedConsecutiveReason() throws Exception {
+        createController(1);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback0, "callback0");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // TelephonyRcsService notifying active features
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+        processAllMessages();
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+    }
+
+    @Test
+    @SmallTest
+    public void testCallbackRemovedWhenSubInfoChanged() throws Exception {
+        createController(2);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_RCS, mCallback1, "callback1");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        makeFakeActiveSubIds(0);
+        mExecutor.execute(() -> mSubChangedListener.onSubscriptionsChanged());
+        processAllMessages();
+
+        verify(mCallback0, times(1)).onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+        verify(mCallback1, times(1)).onUnavailable(REASON_SUBSCRIPTION_INACTIVE);
+
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+    }
+
+    @Test
+    @SmallTest
+    public void testCarrierConfigurationChanged() throws Exception {
+        createController(2);
+
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+                .thenReturn(false);
+
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_MMTEL, mCallback1, "callback1");
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_RCS, mCallback2, "callback2");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+
+        // check initial reason
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        verify(mCallback0, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+        verify(mCallback1, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+        verify(mCallback2, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        // ensure only one reason reported until now
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        // state change in RCS for slot 0
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+
+        // ensure there is no change, since callbacks are not interested RCS on slot 0
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        // carrier config changed, no MMTEL package for slot 1
+        mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+        processAllMessages();
+
+        // only the callback for MMTEL of slot 1 received the reason
+        verify(mCallback0, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+        verify(mCallback1, times(1)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+        verify(mCallback2, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        // ensure no other callbacks
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        mMmTelConnectorListenerSlot1.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        mMmTelConnectorListenerSlot1.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED);
+
+        // resons except REASON_NO_IMS_SERVICE_CONFIGURED are discared
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        // IMS package for MMTEL of slot 1 is added
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+                .thenReturn(true);
+        mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+        processAllMessages();
+
+        // ensure the callback to MMTEL of slot 1 only received REASON_IMS_SERVICE_DISCONNECTED
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback1, times(2)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // ensure no other reason repored
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(3)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        // carrier config changed, no MMTEL package for slot 1
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+                .thenReturn(false);
+        mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+        mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+        processAllMessages();
+        // only the callback for MMTEL of slot 1 received the reason
+        verify(mCallback0, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+        verify(mCallback1, times(2)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+        verify(mCallback2, times(0)).onUnavailable(REASON_NO_IMS_SERVICE_CONFIGURED);
+
+        // ensure no other reason repored
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(4)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        mMmTelConnectorListenerSlot1.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+
+        // resons except REASON_NO_IMS_SERVICE_CONFIGURED are discared
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(4)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        // IMS package for MMTEL of slot 1 is added
+        when(mImsResolver.isImsServiceConfiguredForFeature(eq(1), eq(FEATURE_MMTEL)))
+                .thenReturn(true);
+        mImsStateCallbackController.notifyCarrierConfigChanged(SLOT_1);
+        processAllMessages();
+
+        // ensure the callback to MMTEL of slot 1
+        // there is a pending reason UNAVAILABLE_REASON_NOT_READY
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback1, times(2)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // ensure no other reason repored
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(5)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback1);
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback2);
+        processAllMessages();
+
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback2));
+    }
+
+    @Test
+    @SmallTest
+    public void testMultiSubscriptions() throws Exception {
+        createController(2);
+
+        // registration
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_MMTEL, mCallback0, "callback0");
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_0_SUB_ID, FEATURE_RCS, mCallback1, "callback1");
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_MMTEL, mCallback2, "callback2");
+        mImsStateCallbackController
+                .registerImsStateCallback(SLOT_1_SUB_ID, FEATURE_RCS, mCallback3, "callback3");
+        processAllMessages();
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback0));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+        verify(mCallback3, times(1)).onUnavailable(REASON_IMS_SERVICE_DISCONNECTED);
+
+        // TelephonyRcsService notifying active features
+        // slot 0
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, false, true);
+        // slot 1
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_1, false, true);
+        processAllMessages();
+
+        verify(mCallback0, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+        verify(mCallback0, times(0)).onAvailable();
+        verify(mCallback1, times(0)).onAvailable();
+        verify(mCallback2, times(0)).onAvailable();
+        verify(mCallback3, times(0)).onAvailable();
+
+        // connectionUnavailable
+        mMmTelConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+        mRcsConnectorListenerSlot0.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+        mMmTelConnectorListenerSlot1.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(anyInt());
+
+        mRcsConnectorListenerSlot1.getValue()
+                .connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
+        processAllMessages();
+        verify(mCallback0, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(1)).onUnavailable(REASON_IMS_SERVICE_NOT_READY);
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        // connectionReady
+        mMmTelConnectorListenerSlot0.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+        verify(mCallback1, times(0)).onAvailable();
+        verify(mCallback2, times(0)).onAvailable();
+        verify(mCallback3, times(0)).onAvailable();
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        mRcsConnectorListenerSlot0.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+        verify(mCallback1, times(0)).onAvailable();
+        verify(mCallback2, times(0)).onAvailable();
+        verify(mCallback3, times(0)).onAvailable();
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_0, true, true);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+        verify(mCallback1, times(1)).onAvailable();
+        verify(mCallback2, times(0)).onAvailable();
+        verify(mCallback3, times(0)).onAvailable();
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        mMmTelConnectorListenerSlot1.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+        verify(mCallback1, times(1)).onAvailable();
+        verify(mCallback2, times(1)).onAvailable();
+        verify(mCallback3, times(0)).onAvailable();
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        mRcsConnectorListenerSlot1.getValue().connectionReady(null);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+        verify(mCallback1, times(1)).onAvailable();
+        verify(mCallback2, times(1)).onAvailable();
+        verify(mCallback3, times(0)).onAvailable();
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        mImsStateCallbackController.notifyExternalRcsStateChanged(SLOT_1, true, true);
+        processAllMessages();
+        verify(mCallback0, times(1)).onAvailable();
+        verify(mCallback1, times(1)).onAvailable();
+        verify(mCallback2, times(1)).onAvailable();
+        verify(mCallback3, times(1)).onAvailable();
+        verify(mCallback0, times(2)).onUnavailable(anyInt());
+        verify(mCallback1, times(2)).onUnavailable(anyInt());
+        verify(mCallback2, times(2)).onUnavailable(anyInt());
+        verify(mCallback3, times(2)).onUnavailable(anyInt());
+
+        // unregistration
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback0);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback1));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback1);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback2));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback2);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback2));
+        assertTrue(mImsStateCallbackController.isRegistered(mCallback3));
+
+        mImsStateCallbackController.unregisterImsStateCallback(mCallback3);
+        processAllMessages();
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback0));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback1));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback2));
+        assertFalse(mImsStateCallbackController.isRegistered(mCallback3));
+    }
+
+    @Test
+    @SmallTest
+    public void testSlotUpdates() throws Exception {
+        createController(1);
+
+        verify(mMmTelFeatureConnectorSlot0, times(1)).connect();
+        verify(mRcsFeatureConnectorSlot0, times(1)).connect();
+        verify(mMmTelFeatureConnectorSlot0, times(0)).disconnect();
+        verify(mRcsFeatureConnectorSlot0, times(0)).disconnect();
+
+        // Add a new slot.
+        mImsStateCallbackController.updateFeatureControllerSize(2);
+
+        // connect in slot 1
+        verify(mMmTelFeatureConnectorSlot1, times(1)).connect();
+        verify(mRcsFeatureConnectorSlot1, times(1)).connect();
+
+        // no change in slot 0
+        verify(mMmTelFeatureConnectorSlot0, times(1)).connect();
+        verify(mRcsFeatureConnectorSlot0, times(1)).connect();
+
+        // Remove a slot.
+        mImsStateCallbackController.updateFeatureControllerSize(1);
+
+        // destroy in slot 1
+        verify(mMmTelFeatureConnectorSlot1, times(1)).disconnect();
+        verify(mRcsFeatureConnectorSlot1, times(1)).disconnect();
+
+        // no change in slot 0
+        verify(mMmTelFeatureConnectorSlot0, times(0)).disconnect();
+        verify(mRcsFeatureConnectorSlot0, times(0)).disconnect();
+    }
+
+    private void createController(int slotCount) throws Exception {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        makeFakeActiveSubIds(slotCount);
+
+        when(mMmTelFeatureFactory
+                .create(any(), eq(0), any(), mMmTelConnectorListenerSlot0.capture(), any()))
+                .thenReturn(mMmTelFeatureConnectorSlot0);
+        when(mMmTelFeatureFactory
+                .create(any(), eq(1), any(), mMmTelConnectorListenerSlot1.capture(), any()))
+                .thenReturn(mMmTelFeatureConnectorSlot1);
+        when(mRcsFeatureFactory
+                .create(any(), eq(0), mRcsConnectorListenerSlot0.capture(), any(), any()))
+                .thenReturn(mRcsFeatureConnectorSlot0);
+        when(mRcsFeatureFactory
+                .create(any(), eq(1), mRcsConnectorListenerSlot1.capture(), any(), any()))
+                .thenReturn(mRcsFeatureConnectorSlot1);
+
+        mImsStateCallbackController =
+                new ImsStateCallbackController(mPhone, mHandlerThread.getLooper(),
+                        slotCount, mMmTelFeatureFactory, mRcsFeatureFactory, mImsResolver);
+
+        replaceInstance(ImsStateCallbackController.class,
+                "mPhoneFactoryProxy", mImsStateCallbackController, mPhoneFactoryProxy);
+        mImsStateCallbackController.onSubChanged();
+
+        mHandler = mImsStateCallbackController.getHandler();
+        try {
+            mLooper = new TestableLooper(mHandler.getLooper());
+        } catch (Exception e) {
+            logd("Unable to create looper from handler.");
+        }
+
+        verify(mRcsFeatureConnectorSlot0, atLeastOnce()).connect();
+        verify(mMmTelFeatureConnectorSlot0, atLeastOnce()).connect();
+
+        if (slotCount == 1) {
+            verify(mRcsFeatureConnectorSlot1, times(0)).connect();
+            verify(mMmTelFeatureConnectorSlot1, times(0)).connect();
+        } else {
+            verify(mRcsFeatureConnectorSlot1, atLeastOnce()).connect();
+            verify(mMmTelFeatureConnectorSlot1, atLeastOnce()).connect();
+        }
+    }
+
+    private static void replaceInstance(final Class c,
+            final String instanceName, final Object obj, final Object newValue) throws Exception {
+        Field field = c.getDeclaredField(instanceName);
+        field.setAccessible(true);
+        field.set(obj, newValue);
+    }
+
+    private void makeFakeActiveSubIds(int count) {
+        final int[] subIds = new int[count];
+        for (int i = 0; i < count; i++) {
+            subIds[i] = FAKE_SUB_ID_BASE + i;
+        }
+        when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(subIds);
+    }
+
+    private void processAllMessages() {
+        while (!mLooper.getLooper().getQueue().isIdle()) {
+            mLooper.processAllMessages();
+        }
+    }
+
+    private static void logd(String str) {
+        Log.d(TAG, str);
+    }
+}
diff --git a/tests/src/com/android/phone/RcsProvisioningMonitorTest.java b/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
index 8e5e073..8873402 100644
--- a/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
+++ b/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
@@ -16,6 +16,10 @@
 
 package com.android.phone;
 
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__DMA_CHANGED;
+import static com.android.internal.telephony.TelephonyStatsLog.RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
@@ -64,6 +68,7 @@
 import com.android.ims.FeatureConnector;
 import com.android.ims.RcsFeatureManager;
 import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.metrics.RcsStats;
 
 import org.junit.After;
 import org.junit.Before;
@@ -179,6 +184,10 @@
     private IRcsConfigCallback mCallback;
     @Mock
     private PackageManager mPackageManager;
+    @Mock
+    private RcsStats mRcsStats;
+    @Mock
+    private RcsStats.RcsProvisioningCallback mRcsProvisioningCallback;
 
     private Executor mExecutor = new Executor() {
         @Override
@@ -768,6 +777,66 @@
         assertNull(mRcsProvisioningMonitor.getImsFeatureValidationOverride(FAKE_SUB_ID_BASE));
     }
 
+    @Test
+    @SmallTest
+    public void testMetricsAcsNotUsed() throws Exception {
+        createMonitor(1);
+
+        // Not used ACS
+        mBundle.putBoolean(CarrierConfigManager.KEY_USE_ACS_FOR_RCS_BOOL, false);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, CONFIG_DEFAULT.getBytes(), false);
+        processAllMessages();
+        verify(mRcsStats, never()).onRcsAcsProvisioningStats(anyInt(), anyInt(),
+                anyInt(), anyBoolean());
+    }
+
+    @Test
+    @SmallTest
+    public void testMetricsAcsUsed() throws Exception {
+        when(mRcsStats.getRcsProvisioningCallback(anyInt(), anyBoolean()))
+                .thenReturn(mRcsProvisioningCallback);
+        createMonitor(1);
+
+        verify(mIImsConfig, times(1))
+                .notifyRcsAutoConfigurationReceived(any(), anyBoolean());
+        // verify RcsStats.getRcsProvisioningCallback() is called
+        verify(mRcsStats, times(1)).getRcsProvisioningCallback(
+                eq(FAKE_SUB_ID_BASE), anyBoolean());
+        // verify registered callback obj which comes from RcsStats.getRcsProvisioningCallback()
+        verify(mIImsConfig, times(1))
+                .addRcsConfigCallback(eq(mRcsProvisioningCallback));
+
+        // Config data received and ACS used
+        int errorCode = 200;
+        mBundle.putBoolean(CarrierConfigManager.KEY_USE_ACS_FOR_RCS_BOOL, true);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, CONFIG_DEFAULT.getBytes(), false);
+        processAllMessages();
+        verify(mRcsStats, times(1)).onRcsAcsProvisioningStats(eq(FAKE_SUB_ID_BASE), eq(errorCode),
+                eq(RCS_ACS_PROVISIONING_STATS__RESPONSE_TYPE__PROVISIONING_XML), anyBoolean());
+    }
+
+    @Test
+    @SmallTest
+    public void testMetricsClientProvisioningStats() throws Exception {
+        createMonitor(1);
+
+        // reconfig trigger
+        mRcsProvisioningMonitor.requestReconfig(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        verify(mRcsStats, times(1)).onRcsClientProvisioningStats(eq(FAKE_SUB_ID_BASE),
+                eq(RCS_CLIENT_PROVISIONING_STATS__EVENT__TRIGGER_RCS_RECONFIGURATION));
+
+        // DMA changed
+        updateDefaultMessageApplication(DEFAULT_MESSAGING_APP2);
+        processAllMessages();
+        verify(mRcsStats, times(1)).onRcsClientProvisioningStats(eq(FAKE_SUB_ID_BASE),
+                eq(RCS_CLIENT_PROVISIONING_STATS__EVENT__DMA_CHANGED));
+    }
+
     private void createMonitor(int subCount) throws Exception {
         if (Looper.myLooper() == null) {
             Looper.prepare();
@@ -777,7 +846,7 @@
                 .thenReturn(mFeatureConnector);
         when(mFeatureManager.getConfig()).thenReturn(mIImsConfig);
         mRcsProvisioningMonitor = new RcsProvisioningMonitor(mPhone, mHandlerThread.getLooper(),
-                mRoleManager, mFeatureFactory);
+                mRoleManager, mFeatureFactory, mRcsStats);
         mHandler = mRcsProvisioningMonitor.getHandler();
         try {
             mLooper = new TestableLooper(mHandler.getLooper());
diff --git a/tests/src/com/android/services/telephony/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
index 3bc5ee8..9d2f5ac 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -622,6 +622,46 @@
     }
 
     /**
+     * Tests a scenario where a handover connection arrives via
+     * {@link TelephonyConnection#onOriginalConnectionRedialed(
+     * com.android.internal.telephony.Connection)}.  During this process, the conference properties
+     * get updated.  Since the original connection is null at this point, we need to verify that
+     * the remotely hosted property is retained from before the original connection was nulled.
+     */
+    @Test
+    public void testIsConferenceRemotelyHostedCachingOnSRVCC() {
+        mConferenceHost.setIsImsConnection(true);
+        when(mConferenceHost.getMockImsPhoneConnection().isMultiparty()).thenReturn(true);
+        when(mConferenceHost.getMockImsPhoneConnection().isConferenceHost()).thenReturn(true);
+
+        // Start out with a valid conference host.
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> false /* featureFlagProxy */,
+                new ImsConference.CarrierConfiguration.Builder().build());
+
+        // By default it is not remotely hosted.
+        assertFalse(imsConference.isRemotelyHosted());
+        assertEquals(0,
+                imsConference.getConnectionProperties() & Connection.PROPERTY_REMOTELY_HOSTED);
+
+        // Simulate a change to the original connection due to srvcc
+        com.android.internal.telephony.Connection previousOriginalConnection =
+                mConferenceHost.getMockImsPhoneConnection();
+        mConferenceHost.setMockImsPhoneConnection(null);
+
+        // Trigger the property update which takes place when the original connection changes.
+        mConferenceHost.getTelephonyConnectionListeners().forEach(
+                l -> l.onConnectionPropertiesChanged(mConferenceHost,
+                        mConferenceHost.getConnectionProperties()));
+
+        // Should still NOT be remotely hosted based on cached value.
+        assertFalse(imsConference.isRemotelyHosted());
+        assertEquals(0,
+                imsConference.getConnectionProperties() & Connection.PROPERTY_REMOTELY_HOSTED);
+    }
+
+    /**
      * Verifies that an ImsConference can handle SIP and TEL URIs for both the P-Associated-Uri and
      * conference event package identities.
      */
diff --git a/tests/src/com/android/services/telephony/TelephonyConferenceControllerTest.java b/tests/src/com/android/services/telephony/TelephonyConferenceControllerTest.java
index cfdc2fd..b7fe988 100644
--- a/tests/src/com/android/services/telephony/TelephonyConferenceControllerTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConferenceControllerTest.java
@@ -23,9 +23,11 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.net.Uri;
 import android.os.Looper;
 import android.telecom.Conference;
 import android.telecom.Connection;
+import android.telecom.PhoneAccount;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import org.junit.Before;
@@ -35,7 +37,9 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 
 /**
  * Tests the functionality in TelephonyConferenceController.java
@@ -111,6 +115,46 @@
     }
 
     /**
+     * Verify the connection with "Conference Call" with PROPERTY_IS_DOWNGRADED_CONFERENCE
+     * during SRVCC
+     */
+    @Test
+    @SmallTest
+    public void testSrvccConferenceConnection() {
+        when(mTestTelephonyConnectionA.mMockRadioConnection.getCall()
+                .isMultiparty()).thenReturn(true);
+        when(mTestTelephonyConnectionB.mMockRadioConnection.getCall()
+                .isMultiparty()).thenReturn(true);
+
+        List<Connection> listConnections = Arrays.asList(
+                mTestTelephonyConnectionA, mTestTelephonyConnectionB);
+        when(mMockTelephonyConnectionServiceProxy.getAllConnections()).thenReturn(listConnections);
+
+        mTestTelephonyConnectionA.setAddress(
+                Uri.fromParts(PhoneAccount.SCHEME_TEL, "Conference Call", null), 0);
+        mTestTelephonyConnectionA.setTelephonyConnectionProperties(
+                Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE);
+        mTestTelephonyConnectionB.setAddress(
+                Uri.fromParts(PhoneAccount.SCHEME_TEL, "5551213", null), 0);
+        mTestTelephonyConnectionB.setTelephonyConnectionProperties(
+                Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE);
+
+        // add telephony connection B
+        mControllerTest.add(mTestTelephonyConnectionB);
+
+        // add telephony connection A
+        mControllerTest.add(mTestTelephonyConnectionA);
+
+        // verify the connection with "Conference Call" has PROPERTY_IS_DOWNGRADED_CONFERENCE
+        assertTrue((mTestTelephonyConnectionA.getConnectionProperties()
+                & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0);
+
+        // verify the connection with "5551213" hasn't PROPERTY_IS_DOWNGRADED_CONFERENCE
+        assertFalse((mTestTelephonyConnectionB.getConnectionProperties()
+                & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0);
+    }
+
+    /**
      * Behavior: add telephony connection B and A to conference controller,
      *           set status for connections and merged calls, remove one call
      * Assumption: after performing the behaviours, the status of Connection A is STATE_ACTIVE;
diff --git a/tests/src/com/android/services/telephony/TestTelephonyConnection.java b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
index 817e9a7..e149d3b 100644
--- a/tests/src/com/android/services/telephony/TestTelephonyConnection.java
+++ b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
@@ -305,4 +305,12 @@
     public PersistableBundle getCarrierConfigBundle() {
         return mCarrierConfig;
     }
+
+    public ImsPhoneConnection getMockImsPhoneConnection() {
+        return mImsPhoneConnection;
+    }
+
+    public void setMockImsPhoneConnection(ImsPhoneConnection connection) {
+        mImsPhoneConnection = connection;
+    }
 }
diff --git a/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java b/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
index 8236f44..25b5339 100644
--- a/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
@@ -37,6 +37,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.TelephonyTestBase;
+import com.android.internal.telephony.metrics.RcsStats;
 
 import org.junit.After;
 import org.junit.Before;
@@ -56,6 +57,7 @@
 
     @Mock private ISipDelegate mSipDelegate;
     @Mock private ISipDelegateConnectionStateCallback mAppCallback;
+    @Mock private RcsStats mRcsStats;
 
     @Before
     public void setUp() throws Exception {
@@ -76,12 +78,14 @@
     @Test
     public void testDelegateCreated() throws Exception {
         DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
-                mSipDelegate);
+                mSipDelegate, mRcsStats);
         Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
-        stateTracker.sipDelegateConnected(deniedTags);
+        Set<String> supportedTags = getSupportedTags();
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         // Calling connected multiple times should not generate multiple onCreated events.
-        stateTracker.sipDelegateConnected(deniedTags);
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         verify(mAppCallback).onCreated(mSipDelegate);
+        verify(mRcsStats).createSipDelegateStats(TEST_SUB_ID, supportedTags);
 
         // Ensure status updates are sent to app as expected.
         DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
@@ -97,8 +101,10 @@
         stateTracker.onConfigurationChanged(c);
         verify(mAppCallback).onFeatureTagStatusChanged(eq(regState),
                 eq(new ArrayList<>(deniedTags)));
+        verify(mRcsStats).onSipTransportFeatureTagStats(TEST_SUB_ID, deniedTags,
+                regState.getDeregisteredFeatureTags(),
+                regState.getRegisteredFeatureTags());
         verify(mAppCallback).onConfigurationChanged(c);
-
         verify(mAppCallback, never()).onDestroyed(anyInt());
     }
 
@@ -109,14 +115,18 @@
     @Test
     public void testDelegateDestroyed() throws Exception {
         DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
-                mSipDelegate);
+                mSipDelegate, mRcsStats);
         Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
-        stateTracker.sipDelegateConnected(deniedTags);
+        Set<String> supportedTags = getSupportedTags();
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
+        verify(mRcsStats).createSipDelegateStats(eq(TEST_SUB_ID), eq(supportedTags));
 
         stateTracker.sipDelegateDestroyed(
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
         verify(mAppCallback).onDestroyed(
                 SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+        verify(mRcsStats).onSipDelegateStats(eq(TEST_SUB_ID), eq(supportedTags),
+                eq(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP));
     }
 
     /**
@@ -130,9 +140,10 @@
     @Test
     public void testDelegateChangingRegisteredTagsOverride() throws Exception {
         DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
-                mSipDelegate);
+                mSipDelegate, mRcsStats);
         Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
-        stateTracker.sipDelegateConnected(deniedTags);
+        Set<String> supportedTags = getSupportedTags();
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         // SipDelegate created
         verify(mAppCallback).onCreated(mSipDelegate);
         DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
@@ -158,7 +169,7 @@
                         DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
                 .build();
         // new underlying SipDelegate created
-        stateTracker.sipDelegateConnected(deniedTags);
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         stateTracker.onRegistrationStateChanged(regState);
 
         // Verify registration state through the process:
@@ -187,9 +198,10 @@
     @Test
     public void testDelegateChangingDeniedTagsChanged() throws Exception {
         DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
-                mSipDelegate);
+                mSipDelegate, mRcsStats);
         Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
-        stateTracker.sipDelegateConnected(deniedTags);
+        Set<String> supportedTags = getSupportedTags();
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         // SipDelegate created
         verify(mAppCallback).onCreated(mSipDelegate);
         DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
@@ -220,7 +232,7 @@
         // new underlying SipDelegate created, but SipDelegate denied one to one chat
         deniedTags.add(new FeatureTagState(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
                 SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
-        stateTracker.sipDelegateConnected(deniedTags);
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         DelegateRegistrationState fullyDeniedRegState = new DelegateRegistrationState.Builder()
                 .build();
         // In this special case, it will be the SipDelegateConnectionBase that will trigger
@@ -244,9 +256,10 @@
     @Test
     public void testDelegateChangingDeniedTagsChangingToDestroy() throws Exception {
         DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
-                mSipDelegate);
+                mSipDelegate, mRcsStats);
         Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
-        stateTracker.sipDelegateConnected(deniedTags);
+        Set<String> supportedTags = getSupportedTags();
+        stateTracker.sipDelegateConnected(supportedTags, deniedTags);
         // SipDelegate created
         verify(mAppCallback).onCreated(mSipDelegate);
         DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
@@ -296,4 +309,11 @@
                 SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
         return deniedTags;
     }
+
+    private Set<String> getSupportedTags() {
+        Set<String> supportedTags = new ArraySet<>();
+        supportedTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        supportedTags.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+        return supportedTags;
+    }
 }
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
index 5b0e7c5..78f6894 100644
--- a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
@@ -107,7 +107,8 @@
         assertTrue(future.get());
         verify(mMockMessageTracker).openTransport(mMockSipDelegate, request.getFeatureTags(),
                 Collections.emptySet());
-        verify(mMockDelegateStateTracker).sipDelegateConnected(Collections.emptySet());
+        verify(mMockDelegateStateTracker).sipDelegateConnected(request.getFeatureTags(),
+                Collections.emptySet());
     }
 
     @SmallTest
@@ -138,7 +139,7 @@
         allowedTags.removeAll(deniedTags.stream().map(FeatureTagState::getFeatureTag)
                 .collect(Collectors.toSet()));
         verify(mMockMessageTracker).openTransport(mMockSipDelegate, allowedTags, deniedTags);
-        verify(mMockDelegateStateTracker).sipDelegateConnected(deniedTags);
+        verify(mMockDelegateStateTracker).sipDelegateConnected(allowedTags, deniedTags);
     }
 
     @SmallTest
@@ -249,7 +250,9 @@
                 Collections.emptySet());
         verify(mMockMessageTracker).openTransport(mMockSipDelegate, newFts,
                 Collections.emptySet());
-        verify(mMockDelegateStateTracker, times(2)).sipDelegateConnected(Collections.emptySet());
+        verify(mMockDelegateStateTracker).sipDelegateConnected(
+                request.getFeatureTags(), Collections.emptySet());
+        verify(mMockDelegateStateTracker).sipDelegateConnected(newFts, Collections.emptySet());
     }
 
     private void createSipDelegate(DelegateRequest request, SipDelegateController controller)
diff --git a/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java b/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java
index 823a8be..37abb83 100644
--- a/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipSessionTrackerTest.java
@@ -16,19 +16,28 @@
 
 package com.android.services.telephony.rcs;
 
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
 import android.net.Uri;
 import android.telephony.ims.SipMessage;
 import android.util.Base64;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.internal.telephony.metrics.RcsStats;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.nio.ByteBuffer;
 import java.util.Arrays;
@@ -90,11 +99,104 @@
     // Keep track of the string entry so we can generate unique strings.
     private int mStringEntryCounter = 0;
     private SipSessionTracker mTrackerUT;
+    private static final int TEST_SUB_ID = 1;
+    private static final String TEST_INVITE_SIP_METHOD = "INVITE";
+    private static final int TEST_SIP_RESPONSE_CODE = 200;
+    private static final int TEST_SIP_CLOSE_RESPONSE_CODE = 0;
+    @Mock
+    private RcsStats mRcsStats;
 
     @Before
     public void setUp() {
         mStringEntryCounter = 0;
-        mTrackerUT = new SipSessionTracker();
+        MockitoAnnotations.initMocks(this);
+        mTrackerUT = new SipSessionTracker(TEST_SUB_ID, mRcsStats);
+    }
+
+    @Test
+    public void testMetricsEndedGracefullyBye() {
+        DialogAttributes attr = new DialogAttributes();
+        // INVITE
+        SipMessage inviteRequest = generateSipRequest(SipMessageUtils.INVITE_SIP_METHOD, attr);
+        filterMessage(inviteRequest, attr);
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getEarlyDialogs(), attr);
+
+        // confirmed dialog
+        attr.setToTag();
+        SipMessage inviteConfirm = generateSipResponse("200", "OK", attr);
+        filterMessage(inviteConfirm, attr);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr);
+
+        // Gracefully Ended
+        SipMessage inviteClose = generateSipRequest(SipMessageUtils.BYE_SIP_METHOD, attr);
+        filterMessage(inviteClose, attr);
+
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getClosedDialogs(), attr);
+
+        // verify Metrics information
+        verify(mRcsStats).onSipTransportSessionClosed(eq(TEST_SUB_ID), eq(attr.callId),
+                eq(TEST_SIP_CLOSE_RESPONSE_CODE), eq(true));
+    }
+
+    @Test
+    public void testMetricsCloseCleanupSession() {
+        //mTrackerUT.setRcsStats(mRcsStats);
+        DialogAttributes attr = new DialogAttributes();
+        // INVITE A -> B
+        SipMessage inviteRequest = generateSipRequest(SipMessageUtils.INVITE_SIP_METHOD, attr);
+        filterMessage(inviteRequest, attr);
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getEarlyDialogs(), attr);
+
+        // confirmed dialog
+        attr.setToTag();
+        SipMessage inviteConfirm = generateSipResponse("200", "OK", attr);
+        filterMessage(inviteConfirm, attr);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr);
+
+        //forcefully close session
+        mTrackerUT.cleanupSession(attr.callId);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        assertTrue(mTrackerUT.getClosedDialogs().isEmpty());
+
+        // verify Metrics information
+        verify(mRcsStats).onSipTransportSessionClosed(eq(TEST_SUB_ID), eq(attr.callId),
+                eq(TEST_SIP_CLOSE_RESPONSE_CODE), eq(false));
+    }
+
+    @Test
+    public void testMetricsCloseClearAllSessions() {
+        //mTrackerUT.setRcsStats(mRcsStats);
+        DialogAttributes attr = new DialogAttributes();
+
+        // INVITE
+        SipMessage inviteRequest = generateSipRequest(SipMessageUtils.INVITE_SIP_METHOD, attr);
+        filterMessage(inviteRequest, attr);
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getEarlyDialogs(), attr);
+
+        // confirmed dialog
+        attr.setToTag();
+        SipMessage inviteConfirm = generateSipResponse("200", "OK", attr);
+        filterMessage(inviteConfirm, attr);
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        verifyContainsCallIds(mTrackerUT.getConfirmedDialogs(), attr);
+
+        //forcefully close session
+        mTrackerUT.clearAllSessions();
+        assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
+        assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
+        assertTrue(mTrackerUT.getClosedDialogs().isEmpty());
+
+        // verify Metrics information
+        verify(mRcsStats).onSipTransportSessionClosed(eq(TEST_SUB_ID), eq(attr.callId),
+                eq(TEST_SIP_CLOSE_RESPONSE_CODE), eq(false));
     }
 
     @Test
@@ -291,7 +393,8 @@
     public void testAcknowledgeMessageFailed() {
         DialogAttributes attr = new DialogAttributes();
         SipMessage inviteRequest = generateSipRequest(SipMessageUtils.INVITE_SIP_METHOD, attr);
-        mTrackerUT.filterSipMessage(inviteRequest);
+        mTrackerUT.filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, inviteRequest);
         // Do not acknowledge the request and ensure that the operation has not been applied yet.
         assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
         assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
@@ -310,8 +413,10 @@
         // We unexpectedly received two filter requests for the same branchId without
         // acknowledgePendingMessage being called in between. Ensure that when it is called, it
         // applies both operations.
-        mTrackerUT.filterSipMessage(inviteRequest);
-        mTrackerUT.filterSipMessage(inviteConfirm);
+        mTrackerUT.filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, inviteRequest);
+        mTrackerUT.filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, inviteConfirm);
         assertTrue(mTrackerUT.getEarlyDialogs().isEmpty());
         assertTrue(mTrackerUT.getConfirmedDialogs().isEmpty());
         // we should skip right to confirmed as both operations run back-to-back
@@ -321,7 +426,8 @@
     }
 
     private void filterMessage(SipMessage m, DialogAttributes attr) {
-        mTrackerUT.filterSipMessage(m);
+        mTrackerUT.filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, m);
         mTrackerUT.acknowledgePendingMessage(attr.branchId);
     }
     private void verifyContainsCallIds(Set<SipDialog> callIdSet, DialogAttributes... attrs) {
diff --git a/tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java b/tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java
index 4d222e3..0fe897f 100644
--- a/tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java
+++ b/tests/src/com/android/services/telephony/rcs/TransportSipMessageValidatorTest.java
@@ -16,6 +16,9 @@
 
 package com.android.services.telephony.rcs;
 
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING;
+import static com.android.internal.telephony.TelephonyStatsLog.SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -23,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -37,6 +41,7 @@
 
 import com.android.TelephonyTestBase;
 import com.android.TestExecutorService;
+import com.android.internal.telephony.metrics.RcsStats;
 import com.android.services.telephony.rcs.validator.IncomingTransportStateValidator;
 import com.android.services.telephony.rcs.validator.OutgoingTransportStateValidator;
 import com.android.services.telephony.rcs.validator.ValidationResult;
@@ -76,6 +81,8 @@
     private IncomingTransportStateValidator mIncomingStateValidator;
     @Mock
     private OutgoingTransportStateValidator mOutgoingStateValidator;
+    @Mock
+    private RcsStats mRcsStats;
 
     @Before
     public void setUp() throws Exception {
@@ -117,12 +124,50 @@
     }
 
     @Test
+    public void testMetricsResponse() {
+        String testSipInviteMethod = "INVITE";
+        String testSipMessageMethod = "MESSAGE";
+        int testSipResponseCode = 200;
+        int testMessageError = 0;
+
+        TestExecutorService executor = new TestExecutorService();
+        TransportSipMessageValidator tracker = openTransport(executor);
+        // Since the incoming/outgoing messages were verified, there should have been two calls
+        // to filter the message.
+        verify(mSipSessionTracker).filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, TEST_MESSAGE);
+        verify(mSipSessionTracker).filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING, TEST_MESSAGE);
+
+        assertTrue(tracker.verifyOutgoingMessage(generateSipRequest("INVITE",
+                "testId1"), TEST_CONFIG_VERSION).isValidated);
+        verify(mRcsStats).onSipMessageRequest(eq("testId1"),
+                eq(testSipInviteMethod),
+                eq(SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING));
+
+        assertTrue(tracker.verifyOutgoingMessage(generateSipRequest("MESSAGE",
+                "testId2"), TEST_CONFIG_VERSION).isValidated);
+        verify(mRcsStats).onSipMessageRequest(eq("testId2"),
+                eq(testSipMessageMethod),
+                eq(SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING));
+
+        assertTrue(tracker.verifyIncomingMessage(
+                 generateSipResponse("200", "OK", "testId2"))
+                 .isValidated);
+        verify(mRcsStats).onSipMessageResponse(eq(TEST_SUB_ID), eq("testId2"),
+                eq(testSipResponseCode), eq(testMessageError));
+    }
+
+    @Test
     public void testSessionTrackerFiltering() {
         TestExecutorService executor = new TestExecutorService();
         TransportSipMessageValidator tracker = openTransport(executor);
         // Since the incoming/outgoing messages were verified, there should have been two calls
         // to filter the message.
-        verify(mSipSessionTracker, times(2)).filterSipMessage(TEST_MESSAGE);
+        verify(mSipSessionTracker).filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, TEST_MESSAGE);
+        verify(mSipSessionTracker).filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING, TEST_MESSAGE);
         // ensure pass through methods are working
         tracker.acknowledgePendingMessage("abc");
         verify(mSipSessionTracker).acknowledgePendingMessage("abc");
@@ -140,7 +185,10 @@
         assertFalse(tracker.verifyOutgoingMessage(TEST_MESSAGE, TEST_CONFIG_VERSION).isValidated);
         // The number of times the filter method was called should still only be two after these
         // messages were not validated.
-        verify(mSipSessionTracker, times(2)).filterSipMessage(TEST_MESSAGE);
+        verify(mSipSessionTracker).filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__OUTGOING, TEST_MESSAGE);
+        verify(mSipSessionTracker).filterSipMessage(
+                SIP_TRANSPORT_SESSION__SIP_MESSAGE_DIRECTION__INCOMING, TEST_MESSAGE);
     }
 
 
@@ -482,6 +530,27 @@
         doReturn(ValidationResult.SUCCESS).when(mIncomingStateValidator).validate(any());
         doReturn(mIncomingStateValidator).when(mIncomingStateValidator).andThen(any());
         return new TransportSipMessageValidator(TEST_SUB_ID, executor, mSipSessionTracker,
-                mOutgoingStateValidator, mIncomingStateValidator);
+                mOutgoingStateValidator, mIncomingStateValidator, mRcsStats);
+    }
+
+    private SipMessage generateSipResponse(String statusCode, String statusString, String callId) {
+        String fromHeader = "Alice <sip:alice@atlanta.com>;tag=1928301774";
+        String toHeader = "Bob <sip:bob@biloxi.com>";
+        String branchId = "AAAA";
+        String fromTag = "tag=1928301774";
+        String toTag = "";
+        return SipMessageUtils.generateSipResponse(statusCode, statusString, fromHeader,
+            toHeader, branchId, callId, fromTag, toTag);
+    }
+
+    private SipMessage generateSipRequest(String requestMethod, String callId) {
+        String fromHeader = "Alice <sip:alice@atlanta.com>;tag=1928301774";
+        String toHeader = "Bob <sip:bob@biloxi.com>";
+        String branchId = "AAAA";
+        String fromTag = "tag=1928301774";
+        String toTag = "";
+        String toUri = "sip:bob@biloxi.com";
+        return SipMessageUtils.generateSipRequest(requestMethod, fromHeader, toHeader,
+                toUri, branchId, callId, fromTag, toTag);
     }
 }