Merge "Renaming PATH_SEGMENT constants in SimPhonebookProvider"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a6c1ec5..014fc17 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -94,13 +94,18 @@
     <protected-broadcast android:name= "android.telephony.action.NETWORK_COUNTRY_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.PRIMARY_SUBSCRIPTION_LIST_CHANGED" />
     <protected-broadcast android:name= "android.telephony.action.MULTI_SIM_CONFIG_CHANGED" />
+    <protected-broadcast android:name= "android.telephony.action.CARRIER_SIGNAL_RESET" />
+    <protected-broadcast android:name= "android.telephony.action.CARRIER_SIGNAL_PCO_VALUE" />
+    <protected-broadcast android:name= "android.telephony.action.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE" />
+    <protected-broadcast android:name= "android.telephony.action.CARRIER_SIGNAL_REDIRECTED" />
+    <protected-broadcast android:name= "android.telephony.action.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED" />
 
     <!-- For Vendor Debugging in Telephony -->
     <protected-broadcast android:name="android.telephony.action.ANOMALY_REPORTED" />
 
     <protected-broadcast android:name= "android.intent.action.SUBSCRIPTION_INFO_RECORD_ADDED" />
     <protected-broadcast android:name= "android.intent.action.ACTION_MANAGED_ROAMING_IND" />
-    <protected-broadcast android:name= "android.intent.action.RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE" />
+    <protected-broadcast android:name= "android.telephony.ims.action.RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE" />
 
     <!-- Allows granting runtime permissions to telephony related components. -->
     <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS_TO_TELEPHONY_DEFAULTS" />
@@ -575,16 +580,6 @@
             </intent-filter>
         </receiver>
 
-        <activity android:name="com.android.services.telephony.sip.SipPhoneAccountSettingsActivity"
-                android:theme="@android:style/Theme.NoDisplay"
-                android:exported="true"
-                android:excludeFromRecents="true">
-            <intent-filter>
-                <action android:name="android.telecom.action.CONFIGURE_PHONE_ACCOUNT" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-
         <activity android:label="Sip Settings"
                   android:name="com.android.services.telephony.sip.SipSettings"
                   android:theme="@style/DialerSettingsLight"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4df6a52..46eaaaf 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -644,6 +644,13 @@
     <string name="limited_sim_function_with_phone_num_notification_message"><xliff:g id="carrier_name">%1$s</xliff:g> calls and data services may be blocked while using <xliff:g id="phone_number">%2$s</xliff:g>.</string>
     <!-- Notification message for limited sim function during dual sim [CHAR LIMIT=80]-->
     <string name="limited_sim_function_notification_message"><xliff:g id="carrier_name">%1$s</xliff:g> calls and data services may be blocked while using another SIM.</string>
+    <!-- Notification title for SIP accounts removed -->
+    <string name="sip_accounts_removed_notification_title">Deprecated SIP accounts found and removed</string>
+    <!-- Notification message for SIP accoutns removed -->
+    <string name="sip_accounts_removed_notification_message">
+        SIP calling is no longer supported by Android platform.\nYour existing SIP accounts <xliff:g id="removed_sip_accounts">%s</xliff:g> have been removed.\nPlease confirm your default calling account setting.
+    </string>
+    <string name="sip_accounts_removed_notification_action">Go to settings</string>
     <!-- Mobile network settings screen, data usage setting check box name -->
     <string name="data_usage_title">App data usage</string>
     <!-- Summary about how much data has been used in a date range [CHAR LIMIT=100] -->
@@ -2180,4 +2187,7 @@
     <!-- name of the notification that pops up during
     a phone call when there is bad call quality -->
     <string name="call_quality_notification_name">Call Quality Notification</string>
+    <!-- Telephony notification channel name for a channel containing SIP accounts removed
+     notificatios -->
+    <string name="notification_channel_sip_account">Deprecated SIP accounts</string>
 </resources>
diff --git a/res/xml/phone_account_settings.xml b/res/xml/phone_account_settings.xml
index a243a65..8722761 100644
--- a/res/xml/phone_account_settings.xml
+++ b/res/xml/phone_account_settings.xml
@@ -55,34 +55,4 @@
 
     </PreferenceCategory>
 
-    <PreferenceCategory
-        android:key="phone_accounts_sip_settings_category_key"
-        android:title="@string/sip_settings"
-        android:persistent="false">
-
-        <PreferenceScreen
-            android:title="@string/sip_accounts"
-            android:persistent="false">
-
-            <intent android:action="android.intent.action.MAIN"
-                android:targetPackage="com.android.phone"
-                android:targetClass="com.android.services.telephony.sip.SipSettings" />
-
-        </PreferenceScreen>
-
-        <ListPreference
-            android:key="use_sip_calling_options_key"
-            android:title="@string/sip_call_options_title"
-            android:persistent="true"
-            android:entries="@array/sip_call_options_entries"
-            android:entryValues="@array/sip_call_options_values"/>
-
-        <SwitchPreference
-            android:key="sip_receive_calls_key"
-            android:title="@string/sip_receive_calls"
-            android:summary="@string/sip_receive_calls_summary"
-            android:persistent="true"/>
-
-    </PreferenceCategory>
-
 </PreferenceScreen>
diff --git a/sip/src/com/android/services/telephony/sip/SipAccountRegistry.java b/sip/src/com/android/services/telephony/sip/SipAccountRegistry.java
index 1cf7f4b..2845dac 100644
--- a/sip/src/com/android/services/telephony/sip/SipAccountRegistry.java
+++ b/sip/src/com/android/services/telephony/sip/SipAccountRegistry.java
@@ -16,8 +16,12 @@
 
 package com.android.services.telephony.sip;
 
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
-import android.net.sip.SipException;
+import android.content.Intent;
 import android.net.sip.SipManager;
 import android.net.sip.SipProfile;
 import android.telecom.PhoneAccount;
@@ -25,9 +29,13 @@
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.phone.R;
+
+import java.io.IOException;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
 
 /**
  * Manages the {@link PhoneAccount} entries for SIP calling.
@@ -45,41 +53,6 @@
         }
 
         /**
-         * Starts the SIP service associated with the SIP profile.
-         *
-         * @param sipManager The SIP manager.
-         * @param context The context.
-         * @param isReceivingCalls {@code True} if the sip service is being started to make and
-         *          receive calls.  {@code False} if the sip service is being started only for
-         *          outgoing calls.
-         * @return {@code True} if the service started successfully.
-         */
-        boolean startSipService(SipManager sipManager, Context context, boolean isReceivingCalls) {
-            if (VERBOSE) log("startSipService, profile: " + mProfile);
-            try {
-                // Stop the Sip service for the profile if it is already running.  This is important
-                // if we are changing the state of the "receive calls" option.
-                sipManager.close(mProfile.getUriString());
-
-                // Start the sip service for the profile.
-                if (isReceivingCalls) {
-                    sipManager.open(
-                            mProfile,
-                            SipUtil.createIncomingCallPendingIntent(context,
-                                    mProfile.getProfileName()),
-                            null);
-                } else {
-                    sipManager.open(mProfile);
-                }
-                return true;
-            } catch (SipException e) {
-                log("startSipService, profile: " + mProfile.getProfileName() +
-                        ", exception: " + e);
-            }
-            return false;
-        }
-
-        /**
          * Stops the SIP service associated with the SIP profile.  The {@code SipAccountRegistry} is
          * informed when the service has been stopped via an intent which triggers
          * {@link SipAccountRegistry#removeSipProfile(String)}.
@@ -102,9 +75,16 @@
     private static final String PREFIX = "[SipAccountRegistry] ";
     private static final boolean VERBOSE = false; /* STOP SHIP if true */
     private static final SipAccountRegistry INSTANCE = new SipAccountRegistry();
+    private static final String NOTIFICATION_TAG = SipAccountRegistry.class.getSimpleName();
+    private static final int SIP_ACCOUNTS_REMOVED_NOTIFICATION_ID = 1;
+
+    private static final String CHANNEL_ID_SIP_ACCOUNTS_REMOVED = "sipAccountsRemoved";
 
     private final List<AccountEntry> mAccounts = new CopyOnWriteArrayList<>();
 
+    private NotificationChannel mNotificationChannel;
+    private NotificationManager mNm;
+
     private SipAccountRegistry() {}
 
     public static SipAccountRegistry getInstance() {
@@ -115,8 +95,20 @@
      * Sets up the Account registry and performs any upgrade operations before it is used.
      */
     public void setup(Context context) {
+        setupNotificationChannel(context);
         verifyAndPurgeInvalidPhoneAccounts(context);
-        startSipProfilesAsync(context, (String) null, false);
+        startSipProfilesAsync(context);
+    }
+
+    private void setupNotificationChannel(Context context) {
+        mNotificationChannel = new NotificationChannel(
+                CHANNEL_ID_SIP_ACCOUNTS_REMOVED,
+                context.getText(R.string.notification_channel_sip_account),
+                NotificationManager.IMPORTANCE_HIGH);
+        mNm = context.getSystemService(NotificationManager.class);
+        if (mNm != null) {
+            mNm.createNotificationChannel(mNotificationChannel);
+        }
     }
 
     /**
@@ -149,8 +141,8 @@
      * @param sipProfileName The name of the {@link SipProfile} to start, or {@code null} for all.
      * @param enableProfile Sip account should be enabled
      */
-    void startSipService(Context context, String sipProfileName, boolean enableProfile) {
-        startSipProfilesAsync(context, sipProfileName, enableProfile);
+    void startSipService(Context context, String sipProfileName, boolean enabledProfile) {
+        startSipProfilesAsync(context);
     }
 
     /**
@@ -193,33 +185,20 @@
     }
 
     /**
-     * Causes the SIP service to be restarted for all {@link SipProfile}s.  For example, if the user
-     * toggles the "receive calls" option for SIP, this method handles restarting the SIP services
-     * in the new mode.
-     *
-     * @param context The context.
-     */
-    public void restartSipService(Context context) {
-        startSipProfiles(context, null, false);
-    }
-
-    /**
      * Performs an asynchronous call to
      * {@link SipAccountRegistry#startSipProfiles(android.content.Context, String)}, starting the
      * specified SIP profile and registering its {@link android.telecom.PhoneAccount}.
      *
      * @param context The context.
-     * @param sipProfileName Name of the SIP profile.
-     * @param enableProfile Sip account should be enabled.
      */
     private void startSipProfilesAsync(
-            final Context context, final String sipProfileName, final boolean enableProfile) {
+            final Context context) {
         if (VERBOSE) log("startSipProfiles, start auto registration");
 
         new Thread(new Runnable() {
             @Override
             public void run() {
-                startSipProfiles(context, sipProfileName, enableProfile);
+                startSipProfiles(context);
             }}
         ).start();
     }
@@ -230,48 +209,54 @@
      * register the associated SIP account.
      *
      * @param context The context.
-     * @param sipProfileName A specific SIP profile Name to start, or {@code null} to start all.
-     * @param enableProfile Sip account should be enabled.
      */
-    private void startSipProfiles(Context context, String sipProfileName, boolean enableProfile) {
-        final SipPreferences sipPreferences = new SipPreferences(context);
-        boolean isReceivingCalls = sipPreferences.isReceivingCallsEnabled();
-        TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
-        SipManager sipManager = SipManager.newInstance(context);
+    private void startSipProfiles(Context context) {
         SipProfileDb profileDb = new SipProfileDb(context);
         List<SipProfile> sipProfileList = profileDb.retrieveSipProfileList();
 
-        for (SipProfile profile : sipProfileList) {
-            // Register a PhoneAccount for the profile and optionally enable the primary
-            // profile.
-            if (sipProfileName == null || sipProfileName.equals(profile.getProfileName())) {
-                PhoneAccount phoneAccount = SipUtil.createPhoneAccount(context, profile);
-                telecomManager.registerPhoneAccount(phoneAccount);
-                if (enableProfile) {
-                    telecomManager.enablePhoneAccount(phoneAccount.getAccountHandle(), true);
+        // If there're SIP profiles existing in DB, display a notification and delete all these
+        // profiles.
+        if (!sipProfileList.isEmpty()) {
+            for (SipProfile profile : sipProfileList) {
+                stopSipService(context, profile.getProfileName());
+                removeSipProfile(profile.getProfileName());
+                try {
+                    profileDb.deleteProfile(profile);
+                } catch (IOException e) {
+                    // Ignore
                 }
-                startSipServiceForProfile(profile, sipManager, context, isReceivingCalls);
             }
+            sendSipAccountsRemovedNotification(context, sipProfileList);
         }
     }
 
-    /**
-     * Starts the SIP service for a sip profile and saves a new {@code AccountEntry} in the
-     * registry.
-     *
-     * @param profile The {@link SipProfile} to start.
-     * @param sipManager The SIP manager.
-     * @param context The context.
-     * @param isReceivingCalls {@code True} if the profile should be started such that it can
-     *      receive incoming calls.
-     */
-    private void startSipServiceForProfile(SipProfile profile, SipManager sipManager,
-            Context context, boolean isReceivingCalls) {
-        removeSipProfile(profile.getUriString());
+    private void sendSipAccountsRemovedNotification(Context context, List<SipProfile> profiles) {
+        String sipAccounts = profiles.stream().map(p -> p.getProfileName())
+                .collect(Collectors.joining(","));
 
-        AccountEntry entry = new AccountEntry(profile);
-        if (entry.startSipService(sipManager, context, isReceivingCalls)) {
-            mAccounts.add(entry);
+        Intent intent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
+        intent.setFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+
+        Notification.Action action = new Notification.Action.Builder(R.drawable.ic_sim_card,
+                context.getString(R.string.sip_accounts_removed_notification_action),
+                pendingIntent).build();
+        Notification.Builder builder = new Notification.Builder(context)
+                .setSmallIcon(R.drawable.ic_sim_card)
+                .setChannelId(CHANNEL_ID_SIP_ACCOUNTS_REMOVED)
+                .setContentTitle(context.getText(R.string.sip_accounts_removed_notification_title))
+                .setStyle(new Notification.BigTextStyle()
+                .bigText(context.getString(
+                        R.string.sip_accounts_removed_notification_message,
+                        sipAccounts)))
+                .setAutoCancel(true)
+                .addAction(action);
+        Notification notification = builder.build();
+        if (mNm != null) {
+            mNm.notify(NOTIFICATION_TAG, SIP_ACCOUNTS_REMOVED_NOTIFICATION_ID,
+                    notification);
+        } else {
+            log("NotificationManager is null when send the notification of removed SIP accounts");
         }
     }
 
diff --git a/sip/src/com/android/services/telephony/sip/SipPhoneAccountSettingsActivity.java b/sip/src/com/android/services/telephony/sip/SipPhoneAccountSettingsActivity.java
deleted file mode 100644
index a6f6381..0000000
--- a/sip/src/com/android/services/telephony/sip/SipPhoneAccountSettingsActivity.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.services.telephony.sip;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.sip.SipProfile;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-import android.util.Log;
-
-/**
- * This activity receives the standard telecom intent to open settings for a PhoneAccount. It
- * translates the incoming phone account to a SIP profile and opens the corresponding
- * PreferenceActivity for said profile.
- */
-public final class SipPhoneAccountSettingsActivity extends Activity {
-    private static final String TAG = "SipSettingsActivity";
-
-    /** ${inheritDoc} */
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        Intent intent = getIntent();
-        Log.i(TAG, "" + intent);
-        if (intent != null) {
-            PhoneAccountHandle accountHandle = (PhoneAccountHandle)
-                    intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
-            Log.i(TAG, "" + accountHandle);
-
-            if (accountHandle != null) {
-                SipProfileDb profileDb = new SipProfileDb(this);
-                String profileName = SipUtil.getSipProfileNameFromPhoneAccount(accountHandle);
-                SipProfile profile = profileDb.retrieveSipProfileFromName(profileName);
-                if (profile != null) {
-                    Intent settingsIntent = new Intent(this, SipEditor.class);
-                    settingsIntent.putExtra(SipSettings.KEY_SIP_PROFILE, (Parcelable) profile);
-                    startActivity(settingsIntent);
-                }
-            }
-        }
-
-        finish();
-    }
-}
diff --git a/src/com/android/phone/CarrierConfigLoader.java b/src/com/android/phone/CarrierConfigLoader.java
index 5196c59..e729d28 100644
--- a/src/com/android/phone/CarrierConfigLoader.java
+++ b/src/com/android/phone/CarrierConfigLoader.java
@@ -36,6 +36,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.Message;
 import android.os.PersistableBundle;
 import android.os.Process;
@@ -55,6 +56,7 @@
 import android.util.LocalLog;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.ICarrierConfigLoader;
 import com.android.internal.telephony.IccCardConstants;
 import com.android.internal.telephony.Phone;
@@ -201,6 +203,10 @@
     // 3. clearing config (e.g. due to sim removal)
     // 4. encountering bind or IPC error
     private class ConfigHandler extends Handler {
+        ConfigHandler(@NonNull Looper looper) {
+            super(looper);
+        }
+
         @Override
         public void handleMessage(Message msg) {
             final int phoneId = msg.arg1;
@@ -287,7 +293,7 @@
                         } else {
                             // Put a stub bundle in place so that the rest of the logic continues
                             // smoothly.
-                            mConfigFromDefaultApp[phoneId] = new PersistableBundle();
+                            mConfigFromDefaultApp[phoneId] = PersistableBundle.EMPTY;
                             // Send broadcast if bind fails.
                             notifySubscriptionInfoUpdater(phoneId);
                             // TODO: We *must* call unbindService even if bindService returns false.
@@ -372,7 +378,7 @@
                         broadcastConfigChangedIntent(phoneId);
                     }
                     // Put a stub bundle in place so that the rest of the logic continues smoothly.
-                    mConfigFromDefaultApp[phoneId] = new PersistableBundle();
+                    mConfigFromDefaultApp[phoneId] = PersistableBundle.EMPTY;
                     notifySubscriptionInfoUpdater(phoneId);
                     break;
                 }
@@ -419,7 +425,7 @@
                         } else {
                             // Put a stub bundle in place so that the rest of the logic continues
                             // smoothly.
-                            mConfigFromCarrierApp[phoneId] = new PersistableBundle();
+                            mConfigFromCarrierApp[phoneId] = PersistableBundle.EMPTY;
                             // Send broadcast if bind fails.
                             broadcastConfigChangedIntent(phoneId);
                             loge("Bind to carrier app: " + carrierPackageName + " fails");
@@ -504,7 +510,7 @@
                         broadcastConfigChangedIntent(phoneId);
                     }
                     // Put a stub bundle in place so that the rest of the logic continues smoothly.
-                    mConfigFromCarrierApp[phoneId] = new PersistableBundle();
+                    mConfigFromCarrierApp[phoneId] = PersistableBundle.EMPTY;
                     notifySubscriptionInfoUpdater(phoneId);
                     break;
                 }
@@ -666,11 +672,13 @@
      * Constructs a CarrierConfigLoader, registers it as a service, and registers a broadcast
      * receiver for relevant events.
      */
-    private CarrierConfigLoader(Context context) {
+    @VisibleForTesting
+    /* package */ CarrierConfigLoader(Context context,
+            SubscriptionInfoUpdater subscriptionInfoUpdater, @NonNull Looper looper) {
         mContext = context;
         mPlatformCarrierConfigPackage =
                 mContext.getString(R.string.platform_carrier_config_package);
-        mHandler = new ConfigHandler();
+        mHandler = new ConfigHandler(looper);
 
         IntentFilter bootFilter = new IntentFilter();
         bootFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
@@ -690,7 +698,7 @@
         mConfigFromCarrierApp = new PersistableBundle[numPhones];
         mPersistentOverrideConfigs = new PersistableBundle[numPhones];
         mOverrideConfigs = new PersistableBundle[numPhones];
-        mNoSimConfig = new PersistableBundle();
+        mNoSimConfig = PersistableBundle.EMPTY;
         mServiceConnection = new CarrierServiceConnection[numPhones];
         mServiceBound = new boolean[numPhones];
         mHasSentConfigChange = new boolean[numPhones];
@@ -701,7 +709,7 @@
         TelephonyFrameworkInitializer
                 .getTelephonyServiceManager().getCarrierConfigServiceRegisterer().register(this);
         logd("CarrierConfigLoader has started");
-        mSubscriptionInfoUpdater = PhoneFactory.getSubscriptionInfoUpdater();
+        mSubscriptionInfoUpdater = subscriptionInfoUpdater;
         mHandler.sendEmptyMessage(EVENT_CHECK_SYSTEM_UPDATE);
     }
 
@@ -710,11 +718,11 @@
      *
      * This is only done once, at startup, from {@link com.android.phone.PhoneApp#onCreate}.
      */
-    /* package */
-    static CarrierConfigLoader init(Context context) {
+    /* package */ static CarrierConfigLoader init(Context context) {
         synchronized (CarrierConfigLoader.class) {
             if (sInstance == null) {
-                sInstance = new CarrierConfigLoader(context);
+                sInstance = new CarrierConfigLoader(context,
+                        PhoneFactory.getSubscriptionInfoUpdater(), Looper.myLooper());
             } else {
                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
             }
@@ -722,7 +730,8 @@
         }
     }
 
-    private void clearConfigForPhone(int phoneId, boolean fetchNoSimConfig) {
+    @VisibleForTesting
+    /* package */ void clearConfigForPhone(int phoneId, boolean fetchNoSimConfig) {
         /* Ignore clear configuration request if device is being shutdown. */
         Phone phone = PhoneFactory.getPhone(phoneId);
         if (phone != null) {
@@ -759,7 +768,7 @@
         }
 
         if (configToSend == null) {
-            configToSend = new PersistableBundle();
+            configToSend = PersistableBundle.EMPTY;
         }
 
         // mOverrideConfigs is for testing. And it will override current configs.
@@ -844,7 +853,8 @@
         }
     }
 
-    private CarrierIdentifier getCarrierIdentifierForPhoneId(int phoneId) {
+    @VisibleForTesting
+    /* package */ CarrierIdentifier getCarrierIdentifierForPhoneId(int phoneId) {
         String mcc = "";
         String mnc = "";
         String imsi = "";
@@ -1000,12 +1010,14 @@
         }
     }
 
-    private void saveConfigToXml(String packageName, @NonNull String extraString, int phoneId,
+    @VisibleForTesting
+    /* package */ void saveConfigToXml(String packageName, @NonNull String extraString, int phoneId,
             CarrierIdentifier carrierId, PersistableBundle config) {
         saveConfigToXml(packageName, extraString, phoneId, carrierId, config, false);
     }
 
-    private void saveNoSimConfigToXml(String packageName, PersistableBundle config) {
+    @VisibleForTesting
+    /* package */ void saveNoSimConfigToXml(String packageName, PersistableBundle config) {
         saveConfigToXml(packageName, "", -1, null, config, true);
     }
 
@@ -1176,7 +1188,7 @@
             String callingFeatureId) {
         if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext, subId, callingPackage,
                 callingFeatureId, "getCarrierConfig")) {
-            return new PersistableBundle();
+            return PersistableBundle.EMPTY;
         }
 
         int phoneId = SubscriptionManager.getPhoneId(subId);
@@ -1225,31 +1237,35 @@
             logd("Ignore invalid phoneId: " + phoneId + " for subId: " + subscriptionId);
             return;
         }
-        overrideConfig(mOverrideConfigs, phoneId, overrides);
+        // Post to run on handler thread on which all states should be confined.
+        mHandler.post(() -> {
+            overrideConfig(mOverrideConfigs, phoneId, overrides);
 
-        if (persistent) {
-            overrideConfig(mPersistentOverrideConfigs, phoneId, overrides);
+            if (persistent) {
+                overrideConfig(mPersistentOverrideConfigs, phoneId, overrides);
 
-            if (overrides != null) {
-                final CarrierIdentifier carrierId = getCarrierIdentifierForPhoneId(phoneId);
-                saveConfigToXml(mPlatformCarrierConfigPackage, OVERRIDE_PACKAGE_ADDITION, phoneId,
-                        carrierId, mPersistentOverrideConfigs[phoneId]);
-            } else {
-                final String iccid = getIccIdForPhoneId(phoneId);
-                final int cid = getSpecificCarrierIdForPhoneId(phoneId);
-                String fileName = getFilenameForConfig(mPlatformCarrierConfigPackage,
-                        OVERRIDE_PACKAGE_ADDITION, iccid, cid);
-                File fileToDelete = new File(mContext.getFilesDir(), fileName);
-                fileToDelete.delete();
+                if (overrides != null) {
+                    final CarrierIdentifier carrierId = getCarrierIdentifierForPhoneId(phoneId);
+                    saveConfigToXml(mPlatformCarrierConfigPackage, OVERRIDE_PACKAGE_ADDITION,
+                            phoneId,
+                            carrierId, mPersistentOverrideConfigs[phoneId]);
+                } else {
+                    final String iccid = getIccIdForPhoneId(phoneId);
+                    final int cid = getSpecificCarrierIdForPhoneId(phoneId);
+                    String fileName = getFilenameForConfig(mPlatformCarrierConfigPackage,
+                            OVERRIDE_PACKAGE_ADDITION, iccid, cid);
+                    File fileToDelete = new File(mContext.getFilesDir(), fileName);
+                    fileToDelete.delete();
+                }
             }
-        }
-        notifySubscriptionInfoUpdater(phoneId);
+            notifySubscriptionInfoUpdater(phoneId);
+        });
     }
 
     private void overrideConfig(@NonNull PersistableBundle[] currentOverrides, int phoneId,
             @Nullable PersistableBundle overrides) {
         if (overrides == null) {
-            currentOverrides[phoneId] = new PersistableBundle();
+            currentOverrides[phoneId] = PersistableBundle.EMPTY;
         } else if (currentOverrides[phoneId] == null) {
             currentOverrides[phoneId] = overrides;
         } else {
@@ -1311,6 +1327,31 @@
         return mPlatformCarrierConfigPackage;
     }
 
+    @VisibleForTesting
+    /* package */ Handler getHandler() {
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    /* package */ PersistableBundle getConfigFromDefaultApp(int phoneId) {
+        return mConfigFromDefaultApp[phoneId];
+    }
+
+    @VisibleForTesting
+    /* package */ PersistableBundle getConfigFromCarrierApp(int phoneId) {
+        return mConfigFromCarrierApp[phoneId];
+    }
+
+    @VisibleForTesting
+     /* package */ PersistableBundle getNoSimConfig() {
+        return mNoSimConfig;
+    }
+
+    @VisibleForTesting
+    /* package */ PersistableBundle getOverrideConfig(int phoneId) {
+        return mOverrideConfigs[phoneId];
+    }
+
     private void unbindIfBound(Context context, CarrierServiceConnection conn,
             int phoneId) {
         if (mServiceBound[phoneId]) {
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index ebe6c4d..9a1e275 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -9884,6 +9884,30 @@
     }
 
     /**
+     * Overrides the ims feature validation result
+     */
+    @Override
+    public boolean setImsFeatureValidationOverride(int subId, String enabledStr) {
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "setImsFeatureValidationOverride");
+
+        Boolean enabled = "NULL".equalsIgnoreCase(enabledStr) ? null
+                : Boolean.parseBoolean(enabledStr);
+        return RcsProvisioningMonitor.getInstance().overrideImsFeatureValidation(
+                subId, enabled);
+    }
+
+    /**
+     * Gets the ims feature validation override value
+     */
+    @Override
+    public boolean getImsFeatureValidationOverride(int subId) {
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "getImsFeatureValidationOverride");
+        return RcsProvisioningMonitor.getInstance().getImsFeatureValidationOverride(subId);
+    }
+
+    /**
      * Get the mobile provisioning url that is used to launch a browser to allow users to manage
      * their mobile plan.
      */
diff --git a/src/com/android/phone/RcsProvisioningMonitor.java b/src/com/android/phone/RcsProvisioningMonitor.java
index bcf1491..445ff84 100644
--- a/src/com/android/phone/RcsProvisioningMonitor.java
+++ b/src/com/android/phone/RcsProvisioningMonitor.java
@@ -73,6 +73,7 @@
     private static final int EVENT_DEVICE_CONFIG_OVERRIDE = 6;
     private static final int EVENT_CARRIER_CONFIG_OVERRIDE = 7;
     private static final int EVENT_RESET = 8;
+    private static final int EVENT_FEATURE_ENABLED_OVERRIDE = 9;
 
     private final PhoneGlobals mPhone;
     private final Handler mHandler;
@@ -82,6 +83,8 @@
     private Boolean mDeviceSingleRegistrationEnabledOverride;
     private final HashMap<Integer, Boolean> mCarrierSingleRegistrationEnabledOverride =
             new HashMap<>();
+    private final ConcurrentHashMap<Integer, Boolean> mImsFeatureValidationOverride =
+            new ConcurrentHashMap<>();
     private String mDmaPackageName;
     private final SparseArray<RcsFeatureListener> mRcsFeatureListeners = new SparseArray<>();
     private volatile boolean mTestModeEnabled;
@@ -536,8 +539,15 @@
      */
     public boolean isRcsVolteSingleRegistrationEnabled(int subId) {
         if (mRcsProvisioningInfos.containsKey(subId)) {
-            return mRcsProvisioningInfos.get(subId).getSingleRegistrationCapability()
-                    == ProvisioningManager.STATUS_CAPABLE;
+            if (mRcsProvisioningInfos.get(subId).getSingleRegistrationCapability()
+                    == ProvisioningManager.STATUS_CAPABLE) {
+                try {
+                    RcsConfig rcsConfig = new RcsConfig(getConfig(subId));
+                    return rcsConfig.isRcsVolteSingleRegistrationSupported();
+                } catch (IllegalArgumentException e) {
+                    logd("fail to get rcs config for sub:" + subId);
+                }
+            }
         }
         return false;
     }
@@ -626,6 +636,18 @@
     }
 
     /**
+     * override the rcs feature validation result for a subscription
+     */
+    public boolean overrideImsFeatureValidation(int subId, Boolean enabled) {
+        if (enabled == null) {
+            mImsFeatureValidationOverride.remove(subId);
+        } else {
+            mImsFeatureValidationOverride.put(subId, enabled);
+        }
+        return true;
+    }
+
+    /**
      * Returns the device config whether single registration is enabled
      */
     public boolean getDeviceSingleRegistrationEnabled() {
@@ -647,6 +669,13 @@
         return false;
     }
 
+    /**
+     * Returns the rcs feature validation override value, null if it is not set.
+     */
+    public Boolean getImsFeatureValidationOverride(int subId) {
+        return mImsFeatureValidationOverride.get(subId);
+    }
+
     private void onDefaultMessagingApplicationChanged() {
         final String packageName = getDmaPackageName();
         if (!TextUtils.equals(mDmaPackageName, packageName)) {
diff --git a/src/com/android/phone/ServiceStateProvider.java b/src/com/android/phone/ServiceStateProvider.java
index 63bc600..32562fa 100644
--- a/src/com/android/phone/ServiceStateProvider.java
+++ b/src/com/android/phone/ServiceStateProvider.java
@@ -19,6 +19,8 @@
 import static android.provider.Telephony.ServiceStateTable;
 import static android.provider.Telephony.ServiceStateTable.CONTENT_URI;
 import static android.provider.Telephony.ServiceStateTable.DATA_NETWORK_TYPE;
+import static android.provider.Telephony.ServiceStateTable.DATA_REG_STATE;
+import static android.provider.Telephony.ServiceStateTable.DUPLEX_MODE;
 import static android.provider.Telephony.ServiceStateTable.IS_MANUAL_NETWORK_SELECTION;
 import static android.provider.Telephony.ServiceStateTable.VOICE_REG_STATE;
 import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionId;
@@ -62,18 +64,6 @@
     public static final String SERVICE_STATE = "service_state";
 
     /**
-     * An integer value indicating the current data service state.
-     * <p>
-     * Valid values: {@link ServiceState#STATE_IN_SERVICE},
-     * {@link ServiceState#STATE_OUT_OF_SERVICE}, {@link ServiceState#STATE_EMERGENCY_ONLY},
-     * {@link ServiceState#STATE_POWER_OFF}.
-     * <p>
-     * This is the same as {@link ServiceState#getDataRegState()}.
-     * @hide
-     */
-    public static final String DATA_REG_STATE = "data_reg_state";
-
-    /**
      * An integer value indicating the current voice roaming type.
      * <p>
      * This is the same as {@link ServiceState#getVoiceRoamingType()}.
@@ -259,6 +249,7 @@
         OPERATOR_ALPHA_LONG_RAW,
         OPERATOR_ALPHA_SHORT_RAW,
         DATA_NETWORK_TYPE,
+        DUPLEX_MODE,
     };
 
     @Override
@@ -395,6 +386,7 @@
             final String operator_alpha_long_raw = ss.getOperatorAlphaLongRaw();
             final String operator_alpha_short_raw = ss.getOperatorAlphaShortRaw();
             final int data_network_type = ss.getDataNetworkType();
+            final int duplex_mode = ss.getDuplexMode();
 
             return buildSingleRowResult(projection, sColumns, new Object[] {
                     voice_reg_state,
@@ -422,6 +414,7 @@
                     operator_alpha_long_raw,
                     operator_alpha_short_raw,
                     data_network_type,
+                    duplex_mode,
             });
         }
     }
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index b2e1a99..36d539a 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -113,6 +113,8 @@
     private static final String SRC_GET_CARRIER_ENABLED = "get-carrier-enabled";
     private static final String SRC_SET_TEST_ENABLED = "set-test-enabled";
     private static final String SRC_GET_TEST_ENABLED = "get-test-enabled";
+    private static final String SRC_SET_FEATURE_ENABLED = "set-feature-validation";
+    private static final String SRC_GET_FEATURE_ENABLED = "get-feature-validation";
 
     private static final String D2D_SUBCOMMAND = "d2d";
     private static final String D2D_SEND = "send";
@@ -532,6 +534,17 @@
         pw.println("    Options are:");
         pw.println("      -s: The SIM slot ID to read the config value for. If no option");
         pw.println("          is specified, it will choose the default voice SIM slot.");
+        pw.println("  src set-feature-validation [-s SLOT_ID] true|false|null");
+        pw.println("    Sets ims feature validation result.");
+        pw.println("    The value could be true, false, or null(undefined).");
+        pw.println("    Options are:");
+        pw.println("      -s: The SIM slot ID to set the config value for. If no option");
+        pw.println("          is specified, it will choose the default voice SIM slot.");
+        pw.println("  src get-feature-validation [-s SLOT_ID]");
+        pw.println("    Gets ims feature validation override value.");
+        pw.println("    Options are:");
+        pw.println("      -s: The SIM slot ID to read the config value for. If no option");
+        pw.println("          is specified, it will choose the default voice SIM slot.");
     }
 
     private int handleImsCommand() {
@@ -1765,6 +1778,12 @@
             case SRC_GET_CARRIER_ENABLED: {
                 return handleSrcGetCarrierEnabledCommand();
             }
+            case SRC_SET_FEATURE_ENABLED: {
+                return handleSrcSetFeatureValidationCommand();
+            }
+            case SRC_GET_FEATURE_ENABLED: {
+                return handleSrcGetFeatureValidationCommand();
+            }
         }
 
         return -1;
@@ -2088,6 +2107,55 @@
         return 0;
     }
 
+    private int handleSrcSetFeatureValidationCommand() {
+        //the release time value could be -1
+        int subId = getRemainingArgsCount() > 1 ? getSubId("src set-feature-validation")
+                : SubscriptionManager.getDefaultSubscriptionId();
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            return -1;
+        }
+
+        String enabledStr = getNextArg();
+        if (enabledStr == null) {
+            return -1;
+        }
+
+        try {
+            boolean result =
+                    mInterface.setImsFeatureValidationOverride(subId, enabledStr);
+            if (VDBG) {
+                Log.v(LOG_TAG, "src set-feature-validation -s " + subId + " "
+                        + enabledStr + ", result=" + result);
+            }
+            getOutPrintWriter().println(result);
+        } catch (NumberFormatException | RemoteException e) {
+            Log.w(LOG_TAG, "src set-feature-validation -s " + subId + " "
+                    + enabledStr + ", error" + e.getMessage());
+            getErrPrintWriter().println("Exception: " + e.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
+    private int handleSrcGetFeatureValidationCommand() {
+        int subId = getSubId("src get-feature-validation");
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            return -1;
+        }
+
+        Boolean result = false;
+        try {
+            result = mInterface.getImsFeatureValidationOverride(subId);
+        } catch (RemoteException e) {
+            return -1;
+        }
+        if (VDBG) {
+            Log.v(LOG_TAG, "src get-feature-validation -s " + subId + ", returned: " + result);
+        }
+        getOutPrintWriter().println(result);
+        return 0;
+    }
+
     private int handleHasCarrierPrivilegesCommand() {
         String packageName = getNextArgRequired();
 
diff --git a/src/com/android/phone/settings/PhoneAccountSettingsFragment.java b/src/com/android/phone/settings/PhoneAccountSettingsFragment.java
index 3811a77..224a1f9 100644
--- a/src/com/android/phone/settings/PhoneAccountSettingsFragment.java
+++ b/src/com/android/phone/settings/PhoneAccountSettingsFragment.java
@@ -6,14 +6,11 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Icon;
-import android.net.sip.SipManager;
 import android.os.Bundle;
 import android.os.UserManager;
-import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceFragment;
-import android.preference.SwitchPreference;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
@@ -28,9 +25,6 @@
 import com.android.phone.PhoneUtils;
 import com.android.phone.R;
 import com.android.phone.SubscriptionInfoHelper;
-import com.android.services.telephony.sip.SipAccountRegistry;
-import com.android.services.telephony.sip.SipPreferences;
-import com.android.services.telephony.sip.SipUtil;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -48,11 +42,6 @@
 
     private static final String ALL_CALLING_ACCOUNTS_KEY = "phone_accounts_all_calling_accounts";
 
-    private static final String SIP_SETTINGS_CATEGORY_PREF_KEY =
-            "phone_accounts_sip_settings_category_key";
-    private static final String USE_SIP_PREF_KEY = "use_sip_calling_options_key";
-    private static final String SIP_RECEIVE_CALLS_PREF_KEY = "sip_receive_calls_key";
-
     private static final String MAKE_AND_RECEIVE_CALLS_CATEGORY_KEY =
             "make_and_receive_calls_settings_category_key";
     private static final String DEFAULT_OUTGOING_ACCOUNT_KEY = "default_outgoing_account";
@@ -84,10 +73,6 @@
     private PreferenceCategory mMakeAndReceiveCallsCategory;
     private boolean mMakeAndReceiveCallsCategoryPresent;
 
-    private ListPreference mUseSipCalling;
-    private SwitchPreference mSipReceiveCallsPreference;
-    private SipPreferences mSipPreferences;
-
     private final SubscriptionManager.OnSubscriptionsChangedListener
             mOnSubscriptionsChangeListener =
             new SubscriptionManager.OnSubscriptionsChangedListener() {
@@ -154,39 +139,6 @@
         updateAccounts();
         updateMakeCallsOptions();
 
-        if (isPrimaryUser() && SipUtil.isVoipSupported(getActivity())) {
-            mSipPreferences = new SipPreferences(getActivity());
-
-            mUseSipCalling = (ListPreference)
-                    getPreferenceScreen().findPreference(USE_SIP_PREF_KEY);
-            mUseSipCalling.setEntries(!SipManager.isSipWifiOnly(getActivity())
-                    ? R.array.sip_call_options_wifi_only_entries
-                    : R.array.sip_call_options_entries);
-            mUseSipCalling.setOnPreferenceChangeListener(this);
-
-            int optionsValueIndex =
-                    mUseSipCalling.findIndexOfValue(mSipPreferences.getSipCallOption());
-            if (optionsValueIndex == -1) {
-                // If the option is invalid (eg. deprecated value), default to SIP_ADDRESS_ONLY.
-                mSipPreferences.setSipCallOption(
-                        getResources().getString(R.string.sip_address_only));
-                optionsValueIndex =
-                        mUseSipCalling.findIndexOfValue(mSipPreferences.getSipCallOption());
-            }
-            mUseSipCalling.setValueIndex(optionsValueIndex);
-            mUseSipCalling.setSummary(mUseSipCalling.getEntry());
-
-            mSipReceiveCallsPreference = (SwitchPreference)
-                    getPreferenceScreen().findPreference(SIP_RECEIVE_CALLS_PREF_KEY);
-            mSipReceiveCallsPreference.setEnabled(SipUtil.isPhoneIdle(getActivity()));
-            mSipReceiveCallsPreference.setChecked(
-                    mSipPreferences.isReceivingCallsEnabled());
-            mSipReceiveCallsPreference.setOnPreferenceChangeListener(this);
-        } else {
-            getPreferenceScreen().removePreference(
-                    getPreferenceScreen().findPreference(SIP_SETTINGS_CATEGORY_PREF_KEY));
-        }
-
         SubscriptionManager.from(getActivity()).addOnSubscriptionsChangedListener(
                 mOnSubscriptionsChangeListener);
     }
@@ -207,21 +159,6 @@
      */
     @Override
     public boolean onPreferenceChange(Preference pref, Object objValue) {
-        if (pref == mUseSipCalling) {
-            String option = objValue.toString();
-            mSipPreferences.setSipCallOption(option);
-            mUseSipCalling.setValueIndex(mUseSipCalling.findIndexOfValue(option));
-            mUseSipCalling.setSummary(mUseSipCalling.getEntry());
-            return true;
-        } else if (pref == mSipReceiveCallsPreference) {
-            final boolean isEnabled = !mSipReceiveCallsPreference.isChecked();
-            new Thread(new Runnable() {
-                public void run() {
-                    handleSipReceiveCallsOption(isEnabled);
-                }
-            }).start();
-            return true;
-        }
         return false;
     }
 
@@ -256,22 +193,6 @@
     @Override
     public void onAccountChanged(AccountSelectionPreference pref) {}
 
-    private synchronized void handleSipReceiveCallsOption(boolean isEnabled) {
-        Context context = getActivity();
-        if (context == null) {
-            // Return if the fragment is detached from parent activity before executed by thread.
-            return;
-        }
-
-        mSipPreferences.setReceivingCallsEnabled(isEnabled);
-
-        SipUtil.useSipToReceiveIncomingCalls(context, isEnabled);
-
-        // Restart all Sip services to ensure we reflect whether we are receiving calls.
-        SipAccountRegistry sipAccountRegistry = SipAccountRegistry.getInstance();
-        sipAccountRegistry.restartSipService(context);
-    }
-
     /**
      * Queries the telcomm manager to update the default outgoing account selection preference
      * with the list of outgoing accounts and the current default outgoing account.
@@ -409,32 +330,24 @@
             mAccountList.removeAll();
             List<PhoneAccountHandle> allNonSimAccounts =
                     getCallingAccounts(false /* includeSims */, true /* includeDisabled */);
-            // Check to see if we should show the entire section at all.
-            if (shouldShowConnectionServiceList(allNonSimAccounts)) {
-                List<PhoneAccountHandle> enabledAccounts =
-                        getCallingAccounts(true /* includeSims */, false /* includeDisabled */);
-                // Initialize the account list with the set of enabled & SIM accounts.
-                initAccountList(enabledAccounts);
 
-                // Only show the 'Make Calls With..." option if there are multiple accounts.
-                if (enabledAccounts.size() > 1) {
-                    mMakeAndReceiveCallsCategory.addPreference(mDefaultOutgoingAccount);
-                    mMakeAndReceiveCallsCategoryPresent = true;
-                    mDefaultOutgoingAccount.setListener(this);
-                    updateDefaultOutgoingAccountsModel();
-                } else {
-                    mMakeAndReceiveCallsCategory.removePreference(mDefaultOutgoingAccount);
-                }
+            List<PhoneAccountHandle> enabledAccounts =
+                    getCallingAccounts(true /* includeSims */, false /* includeDisabled */);
+            // Initialize the account list with the set of enabled & SIM accounts.
+            initAccountList(enabledAccounts);
 
-                // If there are no third party (nonSim) accounts,
-                // then don't show enable/disable dialog.
-                if (!allNonSimAccounts.isEmpty()) {
-                    mAccountList.addPreference(mAllCallingAccounts);
-                } else {
-                    mAccountList.removePreference(mAllCallingAccounts);
-                }
+            // Always show the 'Make Calls With..." option
+            mMakeAndReceiveCallsCategory.addPreference(mDefaultOutgoingAccount);
+            mMakeAndReceiveCallsCategoryPresent = true;
+            mDefaultOutgoingAccount.setListener(this);
+            updateDefaultOutgoingAccountsModel();
+
+            // If there are no third party (nonSim) accounts,
+            // then don't show enable/disable dialog.
+            if (!allNonSimAccounts.isEmpty()) {
+                mAccountList.addPreference(mAllCallingAccounts);
             } else {
-                getPreferenceScreen().removePreference(mAccountList);
+                mAccountList.removePreference(mAllCallingAccounts);
             }
         }
     }
diff --git a/src/com/android/services/telephony/TelephonyConferenceController.java b/src/com/android/services/telephony/TelephonyConferenceController.java
index fd16d4b..228541a 100644
--- a/src/com/android/services/telephony/TelephonyConferenceController.java
+++ b/src/com/android/services/telephony/TelephonyConferenceController.java
@@ -177,7 +177,7 @@
     }
 
     private void recalculateConference() {
-        Set<Connection> conferencedConnections = new HashSet<>();
+        Set<TelephonyConnection> conferencedConnections = new HashSet<>();
         int numGsmConnections = 0;
 
         for (TelephonyConnection connection : mTelephonyConnections) {
@@ -254,7 +254,7 @@
                     PhoneAccountHandle phoneAccountHandle = null;
                     if (!conferencedConnections.isEmpty()) {
                         TelephonyConnection telephonyConnection =
-                                (TelephonyConnection) conferencedConnections.iterator().next();
+                                conferencedConnections.iterator().next();
                         phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle(
                                 telephonyConnection.getPhone());
                     }
@@ -262,11 +262,31 @@
                     mTelephonyConference = new TelephonyConference(phoneAccountHandle);
                     Log.i(this, "Creating new TelephonyConference to hold conferenced connections."
                             + " conference=" + mTelephonyConference);
-                    for (Connection connection : conferencedConnections) {
+                    boolean isDowngradedConference = false;
+                    for (TelephonyConnection connection : conferencedConnections) {
                         Log.d(this, "Adding a connection to a conference call: %s %s",
                                 mTelephonyConference, connection);
+                        if ((connection.getConnectionProperties()
+                                & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0) {
+                            // 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);
+                            isDowngradedConference = true;
+                        }
                         mTelephonyConference.addTelephonyConnection(connection);
                     }
+                    // Reapply the downgraded-conference flag to the parent conference if it was on
+                    // one of the children.
+                    if (isDowngradedConference) {
+                        mTelephonyConference.setConnectionProperties(
+                                mTelephonyConference.getConnectionProperties()
+                                        | Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE);
+                    }
                     mTelephonyConference.updateCallRadioTechAfterCreation();
                     mConnectionService.addConference(mTelephonyConference);
                 } else {
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 3706d3b..d745aab 100755
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -1596,6 +1596,7 @@
                 Connection.AUDIO_CODEC_NONE);
         if (newCodecType != oldCodecType) {
             newExtras.putInt(Connection.EXTRA_AUDIO_CODEC, newCodecType);
+            Log.i(this, "put audio codec:" + newCodecType);
             changed = true;
         }
         if (isImsConnection()) {
@@ -1603,6 +1604,7 @@
             float oldBitrate = newExtras.getFloat(Connection.EXTRA_AUDIO_CODEC_BITRATE_KBPS, 0.0f);
             if (Math.abs(newBitrate - oldBitrate) > THRESHOLD) {
                 newExtras.putFloat(Connection.EXTRA_AUDIO_CODEC_BITRATE_KBPS, newBitrate);
+                Log.i(this, "put audio bitrate:" + newBitrate);
                 changed = true;
             }
 
@@ -1611,6 +1613,7 @@
                     0.0f);
             if (Math.abs(newBandwidth - oldBandwidth) > THRESHOLD) {
                 newExtras.putFloat(Connection.EXTRA_AUDIO_CODEC_BANDWIDTH_KHZ, newBandwidth);
+                Log.i(this, "put audio bandwidth:" + newBandwidth);
                 changed = true;
             }
         } else {
@@ -1621,6 +1624,12 @@
         }
 
         if (changed) {
+            Log.i(this, "Audio attribute, Codec:"
+                    + newExtras.getInt(Connection.EXTRA_AUDIO_CODEC, Connection.AUDIO_CODEC_NONE)
+                    + ", Bitrate:"
+                    + newExtras.getFloat(Connection.EXTRA_AUDIO_CODEC_BITRATE_KBPS, 0.0f)
+                    + ", Bandwidth:"
+                    + newExtras.getFloat(Connection.EXTRA_AUDIO_CODEC_BANDWIDTH_KHZ, 0.0f));
             putTelephonyExtras(newExtras);
         }
     }
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
index a948cdb..cecc8a2 100644
--- a/src/com/android/services/telephony/rcs/SipTransportController.java
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -19,8 +19,10 @@
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
 import android.content.Context;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.telephony.CarrierConfigManager;
 import android.telephony.ims.DelegateRequest;
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.ImsException;
@@ -44,12 +46,14 @@
 import com.android.ims.RcsFeatureManager;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.phone.RcsProvisioningMonitor;
 
 import com.google.common.base.Objects;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -247,6 +251,10 @@
     private RcsFeatureManager mRcsManager;
     // Cached package name of the app that is considered the default SMS app.
     private String mCachedSmsRolePackageName = "";
+    // Callback to monitor rcs provisioning change
+    private CarrierConfigManager mCarrierConfigManager;
+    // Cached allowed feature tags from carrier config
+    ArraySet<String> mFeatureTagsAllowed = new ArraySet<>();
 
     /**
      * Create an instance of SipTransportController.
@@ -261,6 +269,7 @@
         mRoleManagerAdapter = new RoleManagerAdapterImpl(context);
         mTimerAdapter = new TimerAdapterImpl();
         mExecutorService = Executors.newSingleThreadScheduledExecutor();
+        mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class);
     }
 
     /**
@@ -277,6 +286,7 @@
         mTimerAdapter = timerAdapter;
         mDelegateControllerFactory = delegateFactory;
         mExecutorService = executor;
+        mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class);
         logi("created");
     }
 
@@ -813,21 +823,14 @@
         }
 
         ArraySet<String> previouslyGrantedTags = new ArraySet<>(alreadyRequestedTags);
-        // deny tags already used by other delegates
-        Set<FeatureTagState> deniedTags = new ArraySet<>();
-        for (String s : requestedFeatureTags) {
-            if (previouslyGrantedTags.contains(s)) {
-                deniedTags.add(new FeatureTagState(s,
-                        SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
-            }
-        }
-        Set<String> nonDeniedTags = requestedFeatureTags.stream()
-                .filter(r -> !previouslyGrantedTags.contains(r))
-                .collect(Collectors.toSet());
+        ArraySet<String> candidateFeatureTags = new ArraySet<>(requestedFeatureTags);
+        Set<FeatureTagState> deniedTags =
+                updateSupportedTags(candidateFeatureTags, previouslyGrantedTags);
+
         // Add newly granted tags to the already requested tags list.
-        previouslyGrantedTags.addAll(nonDeniedTags);
+        previouslyGrantedTags.addAll(candidateFeatureTags);
         CompletableFuture<Boolean> pendingChange = controller.changeSupportedFeatureTags(
-                nonDeniedTags, deniedTags);
+                candidateFeatureTags, deniedTags);
         logi("changeSupportedFeatureTags pendingChange=" + pendingChange);
         // do not worry about executor used here, this stage used to interpret result + add log.
         return pendingChange.thenApply((completedSuccessfully) ->  {
@@ -838,6 +841,51 @@
     }
 
     /**
+     * Update candidate feature tags according to feature tags allowed by carrier config,
+     * and previously granted by other SipDelegates.
+     *
+     * @param candidateFeatureTags The candidate feature tags to be updated. It will be
+     * updated as needed per the carrier config and previously granted feature tags.
+     * @param previouslyGrantedTags The feature tags already granted by other SipDelegates.
+     * @return The set of denied feature tags.
+     */
+    private Set<FeatureTagState> updateSupportedTags(Set<String> candidateFeatureTags,
+            Set<String> previouslyGrantedTags) {
+        Boolean overrideRes = RcsProvisioningMonitor.getInstance()
+                .getImsFeatureValidationOverride(mSubId);
+        // deny tags already used by other delegates
+        Set<FeatureTagState> deniedTags = new ArraySet<>();
+
+        // match config if feature validation is not overridden
+        if (overrideRes == null) {
+            Iterator<String> it = candidateFeatureTags.iterator();
+            while (it.hasNext()) {
+                String tag = it.next();
+                if (previouslyGrantedTags.contains(tag)) {
+                    logi(tag + " has already been granted previously.");
+                    it.remove();
+                    deniedTags.add(new FeatureTagState(tag,
+                            SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+                } else if (!mFeatureTagsAllowed.contains(tag.trim().toLowerCase())) {
+                    logi(tag + " is not allowed per config.");
+                    it.remove();
+                    deniedTags.add(new FeatureTagState(tag,
+                              SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+                }
+            }
+        } else if (Boolean.FALSE.equals(overrideRes)) {
+            logi("all features are denied for test purpose.");
+            for (String s : candidateFeatureTags) {
+                deniedTags.add(new FeatureTagState(s,
+                        SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+            }
+            candidateFeatureTags.clear();
+        }
+
+        return deniedTags;
+    }
+
+    /**
      * Run a Callable on the ExecutorService Thread and wait for the result.
      * If an ImsException is thrown, catch it and rethrow it to caller.
      */
@@ -918,6 +966,8 @@
             scheduleDestroyDelegates(
                     SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
         }
+
+        onCarrierConfigChangedInternal();
     }
 
     /**
@@ -925,6 +975,15 @@
      */
     private void onCarrierConfigChangedInternal() {
         logi("Carrier Config changed for subId: " + mSubId);
+        mFeatureTagsAllowed.clear();
+        PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId);
+        String[] tagConfigs = carrierConfig.getStringArray(
+                CarrierConfigManager.Ims.KEY_RCS_FEATURE_TAG_ALLOWED_STRING_ARRAY);
+        if (tagConfigs != null && tagConfigs.length > 0) {
+            for (String tag : tagConfigs) {
+                mFeatureTagsAllowed.add(tag.trim().toLowerCase());
+            }
+        }
     }
 
     /**
diff --git a/testapps/TestRcsApp/TestApp/AndroidManifest.xml b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
index 4e40120..460a1ce 100644
--- a/testapps/TestRcsApp/TestApp/AndroidManifest.xml
+++ b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
@@ -19,8 +19,8 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.google.android.sample.rcsclient"
-    android:versionCode="9"
-    android:versionName="1.0.8">
+    android:versionCode="12"
+    android:versionName="1.0.11">
 
     <uses-sdk
         android:minSdkVersion="30"
@@ -55,6 +55,7 @@
         <activity android:name=".ChatActivity" />
         <activity android:name=".ContactListActivity" />
         <activity android:name=".ProvisioningActivity" />
+        <activity android:name=".FileUploadActivity" />
 
         <provider
             android:name=".util.ChatProvider"
diff --git a/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml b/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml
index db7ea33..939feb0 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
@@ -50,6 +66,14 @@
             android:textAlignment="center"
             android:textAllCaps="false" />
 
+        <Button
+            android:id="@+id/uploadFile"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/upload_file_gba"
+            android:textAlignment="center"
+            android:textAllCaps="false" />
+
         <TextView
             android:id="@+id/version_info"
             android:layout_width="match_parent"
diff --git a/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml
index df80e54..e184b04 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
diff --git a/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml b/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml
index eb4d1fa..0117549 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
diff --git a/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml
index 106a024..94d6efa 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
@@ -115,4 +131,4 @@
             android:textStyle="bold" />
 
     </LinearLayout>
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/file_upload_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/file_upload_layout.xml
new file mode 100644
index 0000000..a41376b
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/file_upload_layout.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".FileUploadActivity">
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/server"
+                android:textSize="15dp"
+                android:textStyle="bold" />
+
+            <EditText
+                android:id="@+id/ft_uri"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="15dp" />
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/browse_btn"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="@string/browse"
+            android:textAllCaps="false" />
+
+        <Button
+            android:id="@+id/upload_btn"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="@string/upload"
+            android:textAllCaps="false" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <TextView
+                android:text="@string/file_name"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="15dp"
+                android:textStyle="bold"/>
+            <TextView
+                android:id="@+id/file_name"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="15dp"
+                android:textStyle="bold"/>
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/upload_file_result"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/result"
+            android:scrollbars="vertical"
+            android:layout_marginTop="20dp"
+            android:textSize="15dp"
+            android:textStyle="bold" />
+
+    </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml
index 5ccbc8d..f9866e8 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
@@ -103,8 +119,6 @@
             android:id="@+id/naf_url"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:inputType="number"
-            android:text="https://3GPP-bootstrapping@ue.fcs.mstore.msg.t-mobile.com"
             android:textSize="15dp" />
 
         <Button
@@ -126,4 +140,4 @@
             android:textStyle="bold" />
     </LinearLayout>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml b/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml
index 0390d51..7e31581 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -31,4 +47,4 @@
         android:layout_height="wrap_content"
         android:text="@string/ok" />
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml
index a70cd4a..47f534a 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
diff --git a/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml
index 5cf2da2..a4e6ff2 100644
--- a/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml
+++ b/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml
@@ -1,4 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
diff --git a/testapps/TestRcsApp/TestApp/res/values/donottranslate_strings.xml b/testapps/TestRcsApp/TestApp/res/values/donottranslate_strings.xml
index 502874f..f52b70d 100644
--- a/testapps/TestRcsApp/TestApp/res/values/donottranslate_strings.xml
+++ b/testapps/TestRcsApp/TestApp/res/values/donottranslate_strings.xml
@@ -1,3 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
 <resources>
     <string name="app_name">RcsClient</string>
     <string name="provisioning_test">Provisioning Test</string>
@@ -51,6 +68,15 @@
     <string name="registration_timeout">Registration timeout</string>
     <string name="registration_done">Registration done. Enjoy chat!</string>
     <string name="registration_failed">Registration failed</string>
+    <string name="attach">+</string>
+    <string name="browse">Browse</string>
+    <string name="upload">Upload</string>
+    <string name="upload_file_gba">Upload File with GBA</string>
+    <string name="invalid_parameters">Invalid Parameters</string>
+    <string name="server">Server:</string>
+    <string name="file_name">File Name:</string>
+    <string name="server_empty">Server is empty</string>
+    <string name="file_empty">File is empty</string>
     <string name="version_info">Version: %s</string>
 
     <string-array name="rcs_profile">
@@ -85,5 +111,9 @@
         <item>CSIM</item>
         <item>ISIM</item>
     </string-array>
+    <string-array name="server">
+        <item>STAGING</item>
+        <item>PRODUCTION</item>
+    </string-array>
 
 </resources>
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
index 8f7e6a8..40a108d 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
@@ -21,6 +21,7 @@
 import android.database.Cursor;
 import android.graphics.Color;
 import android.graphics.Typeface;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
@@ -41,6 +42,9 @@
 import com.google.android.sample.rcsclient.util.ChatManager;
 import com.google.android.sample.rcsclient.util.ChatProvider;
 import com.google.android.sample.rcsclient.util.NumberUtils;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -176,11 +180,8 @@
                     if (TextUtils.isEmpty(mDestNumber)) {
                         Log.i(TAG, "Destination number is empty");
                     } else {
-                        ChatManager.getInstance(getApplicationContext(), mSubId).addNewMessage(
-                                mNewMessage.getText().toString(), ChatManager.SELF, mDestNumber);
-                        ChatManager.getInstance(getApplicationContext(), mSubId).sendMessage(
-                                mDestNumber, mNewMessage.getText().toString());
-                        mHandler.sendMessage(mHandler.obtainMessage(EMPTY_MSG));
+                        Log.i(TAG, "send message");
+                        sendChatMessage();
                     }
                 });
             });
@@ -192,6 +193,34 @@
         }
     }
 
+    private void sendChatMessage() {
+        Uri result = ChatManager.getInstance(getApplicationContext(), mSubId).addNewMessage(
+                mNewMessage.getText().toString(), ChatManager.SELF, mDestNumber);
+        String chatId = result.getPathSegments().get(1);
+        Futures.addCallback(
+                ChatManager.getInstance(getApplicationContext(),
+                        mSubId).sendMessage(
+                        mDestNumber,
+                        mNewMessage.getText().toString()),
+                new FutureCallback<Void>() {
+                    @Override
+                    public void onSuccess(Void param) {
+                        Log.i(TAG, "send chat msg successfully");
+                        ChatManager.getInstance(getApplicationContext(), mSubId).updateMsgResult(
+                                chatId, true);
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        Log.i(TAG, "fail to send chat message:" + t);
+                        ChatManager.getInstance(getApplicationContext(), mSubId).updateMsgResult(
+                                chatId, false);
+                    }
+                },
+                MoreExecutors.directExecutor());
+        mHandler.sendMessage(mHandler.obtainMessage(EMPTY_MSG));
+    }
+
     private void initChatMessageLayout(Cursor cursor) {
         Log.i(TAG, "initChatMessageLayout");
         RelativeLayout rl = findViewById(R.id.relative_layout);
@@ -221,7 +250,8 @@
         lp.setMargins(0, MARGIN_SIZE, 0, 0);
         if (messageFromSelf(cursor)) {
             lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
-            chatMsg.setBackgroundColor(Color.YELLOW);
+            int result = cursor.getInt(cursor.getColumnIndex(ChatProvider.RcsColumns.RESULT));
+            chatMsg.setBackgroundColor(result == 1 ? Color.GREEN : Color.RED);
         } else {
             lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
             chatMsg.setBackgroundColor(Color.LTGRAY);
@@ -243,7 +273,8 @@
             Cursor cursor = getContentResolver().query(ChatProvider.CHAT_URI,
                     new String[]{ChatProvider.RcsColumns.SRC_PHONE_NUMBER,
                             ChatProvider.RcsColumns.DEST_PHONE_NUMBER,
-                            ChatProvider.RcsColumns.CHAT_MESSAGE},
+                            ChatProvider.RcsColumns.CHAT_MESSAGE,
+                            ChatProvider.RcsColumns.RESULT},
                     ChatProvider.RcsColumns.SRC_PHONE_NUMBER + "=? OR "
                             + ChatProvider.RcsColumns.DEST_PHONE_NUMBER + "=?",
                     new String[]{mDestNumber, mDestNumber},
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/FileUploadActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/FileUploadActivity.java
new file mode 100644
index 0000000..3bc1c24
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/FileUploadActivity.java
@@ -0,0 +1,357 @@
+/*
+ * 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.google.android.sample.rcsclient;
+
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.provider.OpenableColumns;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.ProvisioningManager.RcsProvisioningCallback;
+import android.telephony.ims.RcsClientConfiguration;
+import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.util.Xml;
+import android.view.MenuItem;
+import android.webkit.MimeTypeMap;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.libraries.rcs.simpleclient.filetransfer.FileTransferController;
+import com.android.libraries.rcs.simpleclient.filetransfer.FileTransferControllerImpl;
+import com.android.libraries.rcs.simpleclient.filetransfer.requestexecutor.GbaAuthenticationProvider;
+import com.android.libraries.rcs.simpleclient.filetransfer.requestexecutor.GbaRequestExecutor;
+import com.android.libraries.rcs.simpleclient.filetransfer.requestexecutor.HttpRequestExecutor;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+/** An activity to verify file upload with GBA authentication. */
+public class FileUploadActivity extends AppCompatActivity {
+
+    private static final String TAG = "TestRcsApp.FileUploadActivity";
+    private static final String NAF_PREFIX = "https://3GPP-bootstrapping@";
+    private static final int PICKFILE_RESULT = 1;
+    private static final String HTTP_URI = "ftHTTPCSURI";
+    private static final String PARM = "parm";
+    private static final String NAME = "name";
+    private static final String VALUE = "value";
+
+
+    private ProvisioningManager mProvisioningManager;
+    private int mDefaultSmsSubId;
+    private File mFile;
+    private Button mUpload, mBrowse;
+    private TextView mUploadResult;
+    private TextView mFileName;
+    private EditText mServerUri;
+    private RcsProvisioningCallback mCallback =
+            new RcsProvisioningCallback() {
+                @Override
+                public void onConfigurationChanged(@NonNull byte[] configXml) {
+                    String configResult = new String(configXml);
+                    String server = getFtServerUri(configXml);
+                    Log.i(TAG, "RcsProvisioningCallback.onConfigurationChanged called with xml:");
+                    Log.i(TAG, configResult);
+                    Log.i(TAG, "serverUri:" + server);
+                    mServerUri.setText(server);
+                }
+
+                @Override
+                public void onConfigurationReset() {
+                    Log.i(TAG, "RcsProvisioningCallback.onConfigurationReset called.");
+                }
+
+                @Override
+                public void onRemoved() {
+                    Log.i(TAG, "RcsProvisioningCallback.onRemoved called.");
+                }
+            };
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.file_upload_layout);
+
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+        initLayout();
+        registerProvisioning();
+    }
+
+    private void initLayout() {
+        mServerUri = findViewById(R.id.ft_uri);
+        mUpload = findViewById(R.id.upload_btn);
+        mBrowse = findViewById(R.id.browse_btn);
+        mFileName = findViewById(R.id.file_name);
+        mUploadResult = findViewById(R.id.upload_file_result);
+        mUploadResult.setMovementMethod(new ScrollingMovementMethod());
+
+        mBrowse.setOnClickListener(view -> {
+            Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
+            chooseFile.setType("*/*");
+            chooseFile = Intent.createChooser(chooseFile, "Choose a file");
+            startActivityForResult(chooseFile, PICKFILE_RESULT);
+        });
+
+        mUpload.setOnClickListener(view -> {
+            if (TextUtils.isEmpty(mServerUri.getText())) {
+                Toast.makeText(FileUploadActivity.this,
+                        getResources().getString(R.string.server_empty),
+                        Toast.LENGTH_SHORT).show();
+                return;
+            }
+            if (mFile == null) {
+                Toast.makeText(FileUploadActivity.this,
+                        getResources().getString(R.string.file_empty),
+                        Toast.LENGTH_SHORT).show();
+                return;
+            }
+
+            Log.i(TAG, "upload file");
+            try {
+                FileTransferController fileTransferController = initFileTransferController();
+                if (fileTransferController == null) {
+                    Log.i(TAG, "FileTransferController null");
+                    return;
+                }
+                Futures.addCallback(
+                        fileTransferController.uploadFile(UUID.randomUUID().toString(),
+                                mFile),
+                        new FutureCallback<String>() {
+                            @Override
+                            public void onSuccess(String xml) {
+                                String text;
+                                if (TextUtils.isEmpty(xml)) {
+                                    text = "onFailure: Empty Xml";
+                                    Log.i(TAG, text);
+                                    mUploadResult.setText(text);
+                                    return;
+                                }
+                                text = "onSuccess\r\n" + xml;
+                                Log.i(TAG, text);
+                                mUploadResult.setText(text);
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                String text = "onFailure:" + t;
+                                Log.i(TAG, text);
+                                mUploadResult.setText(text);
+                            }
+                        },
+                        getMainExecutor());
+            } catch (IOException e) {
+                Log.e(TAG, e.getMessage());
+            }
+        });
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case PICKFILE_RESULT:
+                if (resultCode == RESULT_OK) {
+                    Uri fileUri = data.getData();
+                    String fileName = getFileName(fileUri);
+                    mFileName.setText(fileName);
+                    try {
+                        mFile = uriToFile(fileUri);
+                        Log.i(TAG, "mFile:" + mFile);
+                    } catch (Exception e) {
+                        Log.e(TAG, e.getMessage());
+                        e.printStackTrace();
+                    }
+                }
+                break;
+        }
+    }
+
+    private void registerProvisioning() {
+        mDefaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
+        Log.i(TAG, "mDefaultSmsSubId:" + mDefaultSmsSubId);
+        if (SubscriptionManager.isValidSubscriptionId(mDefaultSmsSubId)) {
+            try {
+                mProvisioningManager = ProvisioningManager.createForSubscriptionId(
+                        mDefaultSmsSubId);
+                mProvisioningManager.setRcsClientConfiguration(getDefaultClientConfiguration());
+                mProvisioningManager.registerRcsProvisioningCallback(getMainExecutor(), mCallback);
+            } catch (ImsException e) {
+                Log.e(TAG, e.getMessage());
+            }
+        }
+    }
+
+    private RcsClientConfiguration getDefaultClientConfiguration() {
+        SharedPreferences pref = getSharedPreferences("CONFIG", MODE_PRIVATE);
+
+        return new RcsClientConfiguration(
+                /*rcsVersion=*/ pref.getString("RCS_VERSION", "6.0"),
+                /*rcsProfile=*/ pref.getString("RCS_PROFILE", "UP_1.0"),
+                /*clientVendor=*/ "Goog",
+                /*clientVersion=*/ "RCSAndrd-1.0");
+    }
+
+    private FileTransferController initFileTransferController() {
+        mDefaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
+        if (SubscriptionManager.isValidSubscriptionId(mDefaultSmsSubId)) {
+            TelephonyManager telephonyManager = getSystemService(
+                    TelephonyManager.class).createForSubscriptionId(mDefaultSmsSubId);
+            PersistableBundle carrierConfig = telephonyManager.getCarrierConfig();
+            String uploadUrl = carrierConfig.getString(
+                    CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING);
+            String carrierName = telephonyManager.getSimOperatorName();
+
+            HttpRequestExecutor executor = new GbaRequestExecutor(
+                    new GbaAuthenticationProvider(getSystemService(TelephonyManager.class),
+                            NAF_PREFIX + uploadUrl, getMainExecutor()));
+            return new FileTransferControllerImpl(executor, mServerUri.getText().toString(),
+                    carrierName);
+        } else {
+            Log.i(TAG, "Invalid subId:" + mDefaultSmsSubId);
+            return null;
+        }
+    }
+
+    private String getFileName(Uri uri) throws IllegalArgumentException {
+        Cursor cursor = getContentResolver().query(uri, null, null, null, null);
+
+        if (cursor.getCount() <= 0) {
+            cursor.close();
+            throw new IllegalArgumentException("Can't obtain file name, cursor is empty");
+        }
+        cursor.moveToFirst();
+        String fileName = cursor.getString(
+                cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+        cursor.close();
+
+        return fileName;
+    }
+
+    private File uriToFile(Uri uri) {
+        File file = null;
+        if (uri == null) return file;
+        if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
+            file = new File(uri.getPath());
+        } else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
+            ContentResolver contentResolver = getContentResolver();
+            String cachedName = System.currentTimeMillis() + Math.round((Math.random() + 1) * 1000)
+                    + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(
+                    contentResolver.getType(uri));
+
+            try {
+                InputStream is = contentResolver.openInputStream(uri);
+                File cache = new File(getExternalCacheDir().getAbsolutePath(), cachedName);
+                FileOutputStream fos = new FileOutputStream(cache);
+                ByteStreams.copy(is, fos);
+                file = cache;
+                fos.close();
+                is.close();
+            } catch (IOException e) {
+                Log.i(TAG, e.getMessage());
+            }
+        }
+        return file;
+    }
+
+    private String getContentType(Uri uri) {
+        MimeTypeMap mime = MimeTypeMap.getSingleton();
+        return mime.getExtensionFromMimeType(getContentResolver().getType(uri));
+    }
+
+
+    /**
+     * According GSMA RCC.72, get FileTransfer URI from the config xml whose content includes the
+     * following parameter.
+     * <parm name="ftHTTPCSURI"
+     * value="https://ftcontentserver.rcs.mnc008.mcc123.pub.3gppnetwork.org/content/"/>
+     */
+    private String getFtServerUri(byte[] xml) {
+        try {
+            InputStream inputStream = new ByteArrayInputStream(xml);
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(inputStream, "utf-8");
+
+            int eventType = parser.getEventType();
+            while (eventType != XmlPullParser.END_DOCUMENT) {
+                switch (eventType) {
+                    case XmlPullParser.START_TAG:
+                        if (parser.getName().equals(PARM)) {
+                            String name = parser.getAttributeValue(null, NAME);
+                            if (HTTP_URI.equalsIgnoreCase(name)) {
+                                return parser.getAttributeValue(null, VALUE);
+                            }
+                        }
+                }
+                eventType = parser.next();
+            }
+        } catch (Exception e) {
+            Log.e(TAG, e.getMessage());
+            return "";
+        }
+        return "";
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        //delete cache files
+        File cache = new File(getExternalCacheDir().getAbsolutePath());
+        File[] files = cache.listFiles();
+        for (File file : files) {
+            file.delete();
+        }
+        if (mProvisioningManager != null) {
+            mProvisioningManager.unregisterRcsProvisioningCallback(mCallback);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/GbaActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/GbaActivity.java
index 5b889fb..9ee2a35 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/GbaActivity.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/GbaActivity.java
@@ -20,6 +20,10 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.TelephonyManager.BootstrapAuthenticationCallback;
 import android.telephony.gba.UaSecurityProtocolIdentifier;
@@ -46,6 +50,8 @@
 public class GbaActivity extends AppCompatActivity {
 
     private static final String TAG = "TestRcsApp.GbaActivity";
+    private static final String NAF_PREFIX = "https://3GPP-bootstrapping@";
+
     private static final int MSG_RESULT = 1;
     private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
     private Button mGbaButton;
@@ -96,6 +102,18 @@
         initProtocol();
         initUicctype();
 
+        int defaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
+        if (!SubscriptionManager.isValidSubscriptionId(defaultSmsSubId)) {
+            Log.i(TAG, "invalid subId:" + defaultSmsSubId);
+            return;
+        }
+        TelephonyManager telephonyManager = getSystemService(
+                TelephonyManager.class).createForSubscriptionId(defaultSmsSubId);
+        PersistableBundle carrierConfig = telephonyManager.getCarrierConfig();
+        String uploadUrl = carrierConfig.getString(
+                CarrierConfigManager.KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING);
+        mNaf.setText(NAF_PREFIX + uploadUrl);
+
         mGbaButton.setOnClickListener(view -> {
             Log.i(TAG, "trigger bootstrapAuthenticationRequest");
             UaSecurityProtocolIdentifier.Builder builder =
@@ -109,7 +127,6 @@
                 return;
             }
             UaSecurityProtocolIdentifier spId = builder.build();
-            TelephonyManager telephonyManager = this.getSystemService(TelephonyManager.class);
             telephonyManager.bootstrapAuthenticationRequest(mUiccType,
                     Uri.parse(mNaf.getText().toString()),
                     spId,
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/MainActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/MainActivity.java
index 62302fe..89c5268 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/MainActivity.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/MainActivity.java
@@ -37,6 +37,7 @@
     private Button mUceButton;
     private Button mGbaButton;
     private Button mMessageClientButton;
+    private Button mFileUploadButton;
     private TextView mVersionInfo;
 
     @Override
@@ -53,6 +54,7 @@
         mMessageClientButton = (Button) this.findViewById(R.id.msgClient);
         mUceButton = (Button) this.findViewById(R.id.uce);
         mGbaButton = (Button) this.findViewById(R.id.gba);
+        mFileUploadButton = findViewById(R.id.uploadFile);
         mVersionInfo = this.findViewById(R.id.version_info);
         mProvisionButton.setOnClickListener(view -> {
             Intent intent = new Intent(this, ProvisioningActivity.class);
@@ -77,6 +79,10 @@
             Intent intent = new Intent(this, ContactListActivity.class);
             MainActivity.this.startActivity(intent);
         });
+        mFileUploadButton.setOnClickListener(view -> {
+            Intent intent = new Intent(this, FileUploadActivity.class);
+            MainActivity.this.startActivity(intent);
+        });
 
         String appVersionName = getVersionCode(getPackageName());
         if (!TextUtils.isEmpty(appVersionName)) {
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ProvisioningActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ProvisioningActivity.java
index 0c2996c..dae2835 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ProvisioningActivity.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ProvisioningActivity.java
@@ -138,7 +138,7 @@
         super.onStart();
         mDefaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
         Log.i(TAG, "defaultSmsSubId:" + mDefaultSmsSubId);
-        if (isValidSubscriptionId(mDefaultSmsSubId)) {
+        if (SubscriptionManager.isValidSubscriptionId(mDefaultSmsSubId)) {
             mProvisioningManager = ProvisioningManager.createForSubscriptionId(mDefaultSmsSubId);
             init();
         }
@@ -221,10 +221,6 @@
         }
     }
 
-    private boolean isValidSubscriptionId(int subId) {
-        return SubscriptionManager.isValidSubscriptionId(mDefaultSmsSubId);
-    }
-
     private void initRcsProfile() {
         mRcsProfileSpinner = findViewById(R.id.rcs_profile_list);
         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
index 399a860..ed22f03 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
@@ -19,6 +19,7 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.net.Uri;
 import android.telephony.ims.ImsManager;
 import android.text.TextUtils;
 import android.util.Log;
@@ -36,6 +37,7 @@
 import com.google.android.sample.rcsclient.SessionStateCallback;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 
 import gov.nist.javax.sip.address.AddressFactoryImpl;
@@ -227,13 +229,14 @@
      * @param contact destination phone number.
      * @param message chat message.
      */
-    public void sendMessage(String contact, String message) {
+    public ListenableFuture<Void> sendMessage(String contact, String message) {
         SimpleChatSession chatSession = mContactSessionMap.get(contact);
         if (chatSession == null) {
             Log.i(TAG, "session is unavailable for contact = " + contact);
-            return;
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("Chat session does not exist"));
         }
-        chatSession.sendMessage(message);
+        return chatSession.sendMessage(message);
     }
 
     public boolean isRegistered() {
@@ -263,7 +266,7 @@
      * @param src source phone number.
      * @param dest destination phone number.
      */
-    public void addNewMessage(String message, String src, String dest) {
+    public Uri addNewMessage(String message, String src, String dest) {
         long currentTime = Instant.now().getEpochSecond();
         ContentValues contentValues = new ContentValues();
         contentValues.put(ChatProvider.RcsColumns.SRC_PHONE_NUMBER, src);
@@ -272,7 +275,7 @@
         contentValues.put(ChatProvider.RcsColumns.MSG_TIMESTAMP, currentTime);
         contentValues.put(ChatProvider.RcsColumns.IS_READ, Boolean.TRUE);
         // insert chat table
-        mContext.getContentResolver().insert(ChatProvider.CHAT_URI, contentValues);
+        Uri result = mContext.getContentResolver().insert(ChatProvider.CHAT_URI, contentValues);
 
         ContentValues summary = new ContentValues();
         summary.put(ChatProvider.SummaryColumns.LATEST_MESSAGE, message);
@@ -288,6 +291,17 @@
             summary.put(ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER, remoteNumber);
             mContext.getContentResolver().insert(ChatProvider.SUMMARY_URI, summary);
         }
+        return result;
+    }
+
+    /**
+     * Update MSRP chat message sent result.
+     */
+    public void updateMsgResult(String id, boolean success) {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(ChatProvider.RcsColumns.RESULT, success);
+        mContext.getContentResolver().update(ChatProvider.CHAT_URI, contentValues,
+                ChatProvider.RcsColumns._ID + "=?", new String[]{id});
     }
 
     /**
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java
index 050da1f..98f3ceb 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java
@@ -23,6 +23,7 @@
 import android.database.Cursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
@@ -36,7 +37,7 @@
     public static final Uri SUMMARY_URI = Uri.parse("content://rcsprovider/summary");
     public static final String AUTHORITY = "rcsprovider";
     private static final String TAG = "TestRcsApp.ChatProvider";
-    private static final int DATABASE_VERSION = 1;
+    private static final int DATABASE_VERSION = 2;
     private static final String CHAT_TABLE_NAME = "chat";
     private static final String SUMMARY_TABLE_NAME = "summary";
 
@@ -146,9 +147,9 @@
         public static final String SRC_PHONE_NUMBER = "source_phone_number";
         public static final String DEST_PHONE_NUMBER = "destination_phone_number";
         public static final String CHAT_MESSAGE = "chat_message";
-        public static final String SUBSCRIPTION_ID = "subscription_id";
         public static final String MSG_TIMESTAMP = "msg_timestamp";
         public static final String IS_READ = "is_read";
+        public static final String RESULT = "result";
     }
 
     /** Define columns for the summary table. */
@@ -169,7 +170,9 @@
                 + RcsColumns.DEST_PHONE_NUMBER + " Text DEFAULT NULL, "
                 + RcsColumns.CHAT_MESSAGE + " Text DEFAULT NULL, "
                 + RcsColumns.MSG_TIMESTAMP + " LONG DEFAULT NULL, "
-                + RcsColumns.IS_READ + " BOOLEAN DEFAULT false);";
+                + RcsColumns.IS_READ + " BOOLEAN DEFAULT false, "
+                + RcsColumns.RESULT + " BOOLEAN DEFAULT true);";
+
         public static final String SQL_CREATE_SUMMARY_TABLE = "CREATE TABLE "
                 + SUMMARY_TABLE_NAME
                 + " ("
@@ -191,8 +194,32 @@
         }
 
         @Override
-        public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             Log.d(TAG, "DB upgrade from " + oldVersion + " to " + newVersion);
+            db.beginTransaction();
+            try {
+                switch (oldVersion) {
+                    case 1:
+                        upgradeDatabaseToVersion2(db);
+                        break;
+                    default: // fall out
+                }
+                db.setTransactionSuccessful();
+            } catch (Exception ex) {
+                Log.e(TAG, ex.getMessage(), ex);
+            } finally {
+                db.endTransaction();
+            }
+        }
+
+        private static void upgradeDatabaseToVersion2(SQLiteDatabase db) {
+            try {
+                Log.d(TAG, "upgradeDatabaseToVersion2");
+                String alterTable = "ALTER TABLE " + CHAT_TABLE_NAME + " ADD COLUMN ";
+                db.execSQL(alterTable + RcsColumns.RESULT + " BOOLEAN DEFAULT true");
+            } catch (SQLiteException e) {
+                Log.w(TAG, "[upgradeDatabaseToVersion10] Exception adding column: " + e);
+            }
         }
     }
 }
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp b/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp
index 413b5e8..215c692 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp
@@ -33,6 +33,7 @@
 
     libs: [
         "auto_value_annotations",
+        "org.apache.http.legacy",
     ],
 
     plugins: [
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileTransferController.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileTransferController.java
new file mode 100644
index 0000000..f6548d8
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileTransferController.java
@@ -0,0 +1,47 @@
+/*
+ * 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.libraries.rcs.simpleclient.filetransfer;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** File transfer functionality. */
+public interface FileTransferController {
+
+    /**
+     * Downloads a file from the content server.
+     *
+     * @param fileUrl http URL to the file content on the server.
+     * @return the response for the file download.
+     */
+    ListenableFuture<InputStream> downloadFile(String fileUrl);
+
+    /**
+     * Uploads a file to the content server.
+     *
+     * @param transactionId the transaction id of the file upload.
+     * @param file          the file to be uploaded.
+     * @return the XML response for the file upload, as defined in RCC.07.0-v19.0. This can then be
+     * parsed by the FileInfoParse to get the URL to be used for the download.
+     */
+    ListenableFuture<String> uploadFile(
+            String transactionId, File file)
+            throws IOException;
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileTransferControllerImpl.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileTransferControllerImpl.java
new file mode 100644
index 0000000..dde340c
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileTransferControllerImpl.java
@@ -0,0 +1,49 @@
+/*
+ * 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.libraries.rcs.simpleclient.filetransfer;
+
+import com.android.libraries.rcs.simpleclient.filetransfer.requestexecutor.HttpRequestExecutor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** FileTransferController implementation. */
+public class FileTransferControllerImpl implements FileTransferController {
+
+    private final FileUploadController fileUploadController;
+
+    public FileTransferControllerImpl(HttpRequestExecutor requestExecutor,
+            String contentServerUri, String carrierName) {
+        this.fileUploadController = new FileUploadController(requestExecutor, contentServerUri,
+                carrierName);
+    }
+
+    @Override
+    public ListenableFuture<InputStream> downloadFile(String fileUrl) {
+        throw new UnsupportedOperationException("File download not supported");
+    }
+
+    @Override
+    public ListenableFuture<String> uploadFile(
+            String transactionId, File file)
+            throws IOException {
+        return fileUploadController.uploadFile(transactionId, file);
+    }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileUploadController.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileUploadController.java
new file mode 100644
index 0000000..d8e38e0
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/FileUploadController.java
@@ -0,0 +1,287 @@
+/*
+ * 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.libraries.rcs.simpleclient.filetransfer;
+
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.internal.http.multipart.FilePart;
+import com.android.internal.http.multipart.MultipartEntity;
+import com.android.internal.http.multipart.Part;
+import com.android.internal.http.multipart.StringPart;
+import com.android.libraries.rcs.simpleclient.filetransfer.requestexecutor.HttpRequestExecutor;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AUTH;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.params.AuthPolicy;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.auth.DigestScheme;
+import org.apache.http.impl.auth.RFC2617Scheme;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.Executors;
+
+/** File upload functionality. */
+final class FileUploadController {
+
+    private static final String TAG = "FileUploadController";
+    private static final String ATTRIBUTE_PREEMPTIVE_AUTH = "preemptive-auth";
+    private static final String PARAM_NONCE = "nonce";
+    private static final String PARAM_REALM = "realm";
+    private static final String FILE_PART_NAME = "File";
+    private static final String TRANSFER_ID_PART_NAME = "tid";
+    private static final String CONTENT_TYPE = "text/plain";
+    private static final String THREE_GPP_GBA = "3gpp-gba";
+    private static final int HTTPS_PORT = 443;
+
+    private final HttpRequestExecutor requestExecutor;
+    private final String contentServerUri;
+    private final ListeningExecutorService executor =
+            MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4));
+    private String mCarrierName;
+
+    FileUploadController(HttpRequestExecutor requestExecutor, String contentServerUri,
+            String carrierName) {
+        this.requestExecutor = requestExecutor;
+        this.contentServerUri = contentServerUri;
+        this.mCarrierName = carrierName;
+    }
+
+    public ListenableFuture<String> uploadFile(
+            String transactionId, File file) {
+        DefaultHttpClient httpClient = getSecureHttpClient();
+
+        Log.i(TAG, "sendEmptyPost");
+        // Send an empty post.
+        ListenableFuture<HttpResponse> initialResponseFuture = sendEmptyPost(httpClient);
+
+        BasicHttpContext httpContext = new BasicHttpContext();
+        ListenableFuture<Void> prepareAuthFuture =
+                Futures.transform(
+                        initialResponseFuture,
+                        initialResponse -> {
+                            Log.i(TAG, "Response for the empty post: "
+                                    + initialResponse.getStatusLine());
+                            if (initialResponse.getStatusLine().getStatusCode()
+                                    != HttpURLConnection.HTTP_UNAUTHORIZED) {
+                                throw new IllegalArgumentException(
+                                        "Expected HTTP_UNAUTHORIZED, but got "
+                                                + initialResponse.getStatusLine());
+                            }
+                            try {
+                                initialResponse.getEntity().consumeContent();
+                            } catch (IOException e) {
+                                throw new IllegalArgumentException(e);
+                            }
+
+                            // Override nonce and realm in the HTTP context.
+                            RFC2617Scheme authScheme = createAuthScheme(initialResponse);
+                            httpContext.setAttribute(ATTRIBUTE_PREEMPTIVE_AUTH, authScheme);
+
+                            return null;
+                        },
+                        executor);
+
+        // Executing the post with credentials.
+        return Futures.transformAsync(
+                prepareAuthFuture,
+                unused ->
+                        executeAuthenticatedPost(
+                                httpClient, httpContext, transactionId, file),
+                executor);
+    }
+
+    private RFC2617Scheme createAuthScheme(HttpResponse initialResponse) {
+        if (!initialResponse.containsHeader(AUTH.WWW_AUTH)) {
+            throw new IllegalArgumentException(
+                    AUTH.WWW_AUTH + " header not found in the original response.");
+        }
+
+        Header authHeader = initialResponse.getFirstHeader(AUTH.WWW_AUTH);
+        String scheme = authHeader.getValue();
+
+        if (scheme.contains(AuthPolicy.DIGEST)) {
+            HeaderElement[] elements = authHeader.getElements();
+
+            if (elements == null || elements.length == 0) {
+                throw new IllegalArgumentException(
+                        "Unable to find header elements. Cannot perform Digest authentication.");
+            }
+
+            DigestScheme digestScheme = new DigestScheme();
+            for (HeaderElement element : elements) {
+                // TODO(b/180601658): Add checks for the realm, which should start with
+                //  3GPP-bootstrapping@.
+                if (element.getName().contains(PARAM_REALM)) {
+                    digestScheme.overrideParamter(PARAM_REALM, element.getValue());
+                    Log.i(TAG, "Realm: " + element.getValue());
+                }
+                if (element.getName().contains(PARAM_NONCE)) {
+                    digestScheme.overrideParamter(PARAM_NONCE, element.getValue());
+                    Log.i(TAG, "Nonce: " + element.getValue());
+                }
+            }
+
+            return digestScheme;
+        } else {
+            throw new IllegalArgumentException("Unable to create authentication scheme " + scheme);
+        }
+    }
+
+    private DefaultHttpClient getSecureHttpClient() {
+        SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
+        Uri uri = Uri.parse(contentServerUri);
+        int port = uri.getPort();
+        if (port <= 0) {
+            port = HTTPS_PORT;
+        }
+
+        Scheme scheme = new Scheme("https", socketFactory, port);
+        DefaultHttpClient httpClient = new DefaultHttpClient();
+        ClientConnectionManager manager = httpClient.getConnectionManager();
+        SchemeRegistry registry = manager.getSchemeRegistry();
+        registry.register(scheme);
+
+        return httpClient;
+    }
+
+    private ListenableFuture<HttpResponse> sendEmptyPost(HttpClient httpClient) {
+        Log.i(TAG, "Sending an empty post: ");
+        HttpPost emptyPost = new HttpPost(contentServerUri);
+        emptyPost.setHeader("User-Agent", getUserAgent());
+        return executor.submit(() -> httpClient.execute(emptyPost));
+    }
+
+    private ListenableFuture<String> executeAuthenticatedPost(
+            DefaultHttpClient httpClient,
+            HttpContext context,
+            String transactionId,
+            File file)
+            throws IOException {
+
+        Part[] parts = {
+                new StringPart(TRANSFER_ID_PART_NAME, transactionId),
+                new FilePart(file.getName(), file)
+        };
+        MultipartEntity entity = new MultipartEntity(parts);
+
+        HttpPost postRequest = new HttpPost(contentServerUri);
+        postRequest.setHeader("User-Agent", getUserAgent());
+        postRequest.setEntity(entity);
+        Log.i(TAG, "Created file upload POST:" + contentServerUri);
+
+        ListenableFuture<HttpResponse> responseFuture =
+                requestExecutor.executeAuthenticatedRequest(httpClient, context, postRequest);
+
+        Futures.addCallback(
+                responseFuture,
+                new FutureCallback<HttpResponse>() {
+                    @Override
+                    public void onSuccess(HttpResponse response) {
+                        Log.i(TAG, "onSuccess:" + response.toString());
+                        Log.i(TAG, "statusLine:" + response.getStatusLine());
+                        Log.i(TAG, "statusCode:" + response.getStatusLine().getStatusCode());
+                        Log.i(TAG, "contentLentgh:" + response.getEntity().getContentLength());
+                        Log.i(TAG, "contentType:" + response.getEntity().getContentType());
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        Log.i(TAG, "onFailure");
+                        throw new IllegalArgumentException(t);
+                    }
+                },
+                executor);
+
+        return Futures.transform(
+                responseFuture,
+                response -> {
+                    try {
+                        return consumeResponse(response);
+                    } catch (IOException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+                },
+                executor);
+    }
+
+    public String consumeResponse(HttpResponse response) throws IOException {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != HttpURLConnection.HTTP_OK) {
+            throw new IllegalArgumentException(
+                    "Server responded with error code " + statusCode + "!");
+        }
+        HttpEntity responseEntity = response.getEntity();
+
+        if (responseEntity == null) {
+            throw new IOException("Did not receive a response body.");
+        }
+
+        return readResponseData(responseEntity.getContent());
+    }
+
+    public String readResponseData(InputStream inputStream) throws IOException {
+        Log.i(TAG, "readResponseData");
+        ByteArrayOutputStream data = new ByteArrayOutputStream();
+        ByteStreams.copy(inputStream, data);
+
+        data.flush();
+        Log.i(TAG, "Parsed HTTP POST response: " + data.toString());
+
+        return data.toString();
+    }
+
+    private String getUserAgent() {
+        String buildId = Build.ID;
+        String buildDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
+                .withZone(ZoneId.systemDefault())
+                .format(Instant.ofEpochMilli(Build.TIME));
+        String buildVersion = Build.VERSION.RELEASE_OR_CODENAME;
+        String deviceName = Build.DEVICE;
+        String userAgent = String.format("%s %s %s %s %s %s %s",
+                mCarrierName, buildId, buildDate, "Android", buildVersion,
+                deviceName, THREE_GPP_GBA);
+        Log.i(TAG, "UserAgent:" + userAgent);
+        return userAgent;
+    }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaAuthenticationProvider.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaAuthenticationProvider.java
new file mode 100644
index 0000000..0bfa4cb
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaAuthenticationProvider.java
@@ -0,0 +1,139 @@
+/*
+ * 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.libraries.rcs.simpleclient.filetransfer.requestexecutor;
+
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.gba.TlsParams;
+import android.telephony.gba.UaSecurityProtocolIdentifier;
+import android.util.Log;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.io.BaseEncoding;
+import com.google.common.util.concurrent.SettableFuture;
+
+import org.apache.http.auth.Credentials;
+
+import java.security.Principal;
+import java.util.concurrent.Executor;
+
+/** Provides GBA authentication credentials. */
+public class GbaAuthenticationProvider {
+
+    private static final String TAG = "GbaAuthenticationProvider";
+    private final TelephonyManager telephonyManager;
+    private final String contentServerUrl;
+    private final Executor executor;
+
+    public GbaAuthenticationProvider(
+            TelephonyManager telephonyManager, String contentServerUrl, Executor executor) {
+        this.telephonyManager = telephonyManager;
+        this.contentServerUrl = contentServerUrl;
+        this.executor = executor;
+    }
+
+    public SettableFuture<Credentials> provideCredentials(boolean forceBootstrapping) {
+        SettableFuture<Credentials> credentialsFuture = SettableFuture.create();
+
+        UaSecurityProtocolIdentifier.Builder builder =
+                new UaSecurityProtocolIdentifier.Builder();
+        try {
+            PersistableBundle carrierConfig = telephonyManager.getCarrierConfig();
+            int organization = carrierConfig.getInt(
+                    CarrierConfigManager.KEY_GBA_UA_SECURITY_ORGANIZATION_INT);
+            int protocol = carrierConfig.getInt(
+                    CarrierConfigManager.KEY_GBA_UA_SECURITY_PROTOCOL_INT);
+            int cipherSuite = carrierConfig.getInt(
+                    CarrierConfigManager.KEY_GBA_UA_TLS_CIPHER_SUITE_INT);
+            Log.i(TAG, "organization:" + organization + ", protocol:" + protocol + ", cipherSuite:"
+                    + cipherSuite + ", contentServerUrl:" + contentServerUrl);
+
+            builder.setOrg(UaSecurityProtocolIdentifier.ORG_3GPP)
+                    .setProtocol(
+                            UaSecurityProtocolIdentifier.UA_SECURITY_PROTOCOL_3GPP_TLS_DEFAULT);
+            if (cipherSuite == TlsParams.TLS_NULL_WITH_NULL_NULL) {
+                builder.setTlsCipherSuite(TlsParams.TLS_RSA_WITH_AES_128_CBC_SHA);
+            }
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, e.getMessage());
+            credentialsFuture.setException(e);
+            return credentialsFuture;
+        }
+        UaSecurityProtocolIdentifier spId = builder.build();
+        TelephonyManager.BootstrapAuthenticationCallback callback =
+                new TelephonyManager.BootstrapAuthenticationCallback() {
+                    @Override
+                    public void onKeysAvailable(byte[] gbaKey, String btId) {
+                        Log.i(TAG, "onKeysAvailable: String key:[" + new String(gbaKey) + "] btid:["
+                                + btId + "]" + "Base64 key:[" + BaseEncoding.base64().encode(gbaKey)
+                                + "]");
+                        credentialsFuture.set(GbaCredentials.create(btId, gbaKey));
+                    }
+
+                    @Override
+                    public void onAuthenticationFailure(int reason) {
+                        Log.i(TAG, "onAuthenticationFailure:" + reason);
+                        credentialsFuture.setException(
+                                new BootstrapAuthenticationException(reason));
+                    }
+                };
+        telephonyManager.bootstrapAuthenticationRequest(
+                TelephonyManager.APPTYPE_ISIM,
+                Uri.parse(contentServerUrl),
+                spId,
+                forceBootstrapping,
+                executor,
+                callback);
+
+        return credentialsFuture;
+    }
+
+    @SuppressWarnings("AndroidJdkLibsChecker")
+    @AutoValue
+    abstract static class GbaCredentials implements Credentials {
+
+        public static GbaCredentials create(String btId, byte[] gbaKey) {
+            return new AutoValue_GbaAuthenticationProvider_GbaCredentials(
+                    GbaPrincipal.create(btId), BaseEncoding.base64().encode(gbaKey));
+        }
+
+        @Override
+        public abstract Principal getUserPrincipal();
+
+        @Override
+        public abstract String getPassword();
+    }
+
+    @AutoValue
+    abstract static class GbaPrincipal implements Principal {
+
+        public static GbaPrincipal create(String name) {
+            return new AutoValue_GbaAuthenticationProvider_GbaPrincipal(name);
+        }
+
+        @Override
+        public abstract String getName();
+    }
+
+    static class BootstrapAuthenticationException extends Exception {
+        BootstrapAuthenticationException(int reason) {
+            super("Bootstrap authentication request failure: " + reason);
+        }
+    }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaRequestExecutor.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaRequestExecutor.java
new file mode 100644
index 0000000..856fec1
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaRequestExecutor.java
@@ -0,0 +1,106 @@
+/*
+ * 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.libraries.rcs.simpleclient.filetransfer.requestexecutor;
+
+import android.util.Log;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.protocol.HttpContext;
+
+import java.net.HttpURLConnection;
+import java.util.concurrent.Executors;
+
+/** Executes GBA authenticated HTTP requests. */
+public class GbaRequestExecutor implements HttpRequestExecutor {
+
+    private static final String TAG = "GbaRequestExecutor";
+    private final GbaAuthenticationProvider gbaAuthenticationProvider;
+    private final ListeningExecutorService executor =
+            MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4));
+
+    public GbaRequestExecutor(GbaAuthenticationProvider gbaAuthenticationProvider) {
+        this.gbaAuthenticationProvider = gbaAuthenticationProvider;
+    }
+
+    @Override
+    @SuppressWarnings("CheckReturnValue")
+    public ListenableFuture<HttpResponse> executeAuthenticatedRequest(
+            DefaultHttpClient httpClient, HttpContext context, HttpRequestBase request) {
+
+        // Set authentication for the client.
+        ListenableFuture<Credentials> credentialsFuture =
+                gbaAuthenticationProvider.provideCredentials(/*forceBootrapping*/ false);
+
+        ListenableFuture<HttpResponse> responseFuture =
+                Futures.transformAsync(
+                        credentialsFuture,
+                        credentials -> {
+                            Log.i(TAG,
+                                    "Obtained credentialsFuture, making the POST with credentials"
+                                            + ".");
+                            httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY,
+                                    credentials);
+
+                            // Make the first request.
+                            return executor.submit(() -> httpClient.execute(request, context));
+                        },
+                        executor);
+
+        return Futures.transformAsync(
+                responseFuture,
+                response -> {
+
+                    // If the response code is 401, the keys might be invalid so force boostrapping.
+                    if (response.getStatusLine().getStatusCode()
+                            != HttpURLConnection.HTTP_UNAUTHORIZED) {
+                        return Futures.immediateFuture(response);
+                    }
+                    Log.i(TAG, "Obtained 401 for the authneticated request. Forcing boostrapping.");
+
+                    ListenableFuture<Credentials> forceBootstrappedCredentialsFuture =
+                            gbaAuthenticationProvider.provideCredentials(/*forceBoostrapping*/
+                                    true);
+
+                    return Futures.transformAsync(
+                            forceBootstrappedCredentialsFuture,
+                            forceBootstrappedCredentials -> {
+                                httpClient
+                                        .getCredentialsProvider()
+                                        .setCredentials(AuthScope.ANY,
+                                                forceBootstrappedCredentials);
+
+                                // Make a second request.
+                                Log.i(TAG,
+                                        "Obtained new credentialsFuture, making POST with the new"
+                                                + " credentials.");
+                                return Futures.submit(() -> httpClient.execute(request, context),
+                                        executor);
+                            },
+                            executor);
+                },
+                executor);
+    }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/HttpRequestExecutor.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/HttpRequestExecutor.java
new file mode 100644
index 0000000..59a3aa9
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/HttpRequestExecutor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.libraries.rcs.simpleclient.filetransfer.requestexecutor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+
+/** Executes authenticated HTTP requests. */
+public interface HttpRequestExecutor {
+
+    ListenableFuture<HttpResponse> executeAuthenticatedRequest(
+            DefaultHttpClient httpClient, HttpContext context, HttpRequestBase request)
+            throws IOException;
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java
index 75eb48d..e3a091d 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java
@@ -88,7 +88,7 @@
             return (Message)
                     method.invoke(
                             new StringMsgParser(),
-                            message.getEncodedMessage(),
+                            message.toEncodedMessage(),
                             true,
                             false,
                             (ParseExceptionListener)
@@ -98,7 +98,7 @@
         } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
             try {
                 method = StringMsgParser.class.getDeclaredMethod("parseSIPMessage", byte[].class);
-                return (Message) method.invoke(new StringMsgParser(), message.getEncodedMessage());
+                return (Message) method.invoke(new StringMsgParser(), message.toEncodedMessage());
             } catch (IllegalAccessException | InvocationTargetException
                     | NoSuchMethodException ex) {
                 ex.printStackTrace();
diff --git a/tests/src/com/android/TestContext.java b/tests/src/com/android/TestContext.java
index 9d712d3..fc5ee4c 100644
--- a/tests/src/com/android/TestContext.java
+++ b/tests/src/com/android/TestContext.java
@@ -24,6 +24,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.PersistableBundle;
 import android.telecom.TelecomManager;
@@ -32,16 +34,22 @@
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsManager;
 import android.test.mock.MockContext;
+import android.util.Log;
 import android.util.SparseArray;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
 
+import java.util.HashSet;
 import java.util.concurrent.Executor;
 
 public class TestContext extends MockContext {
 
+    private static final String TAG = "TestContext";
+    // Stub used to grant all permissions
+    public static final String STUB_PERMISSION_ENABLE_ALL = "stub_permission_enable_all";
+
     @Mock CarrierConfigManager mMockCarrierConfigManager;
     @Mock TelecomManager mMockTelecomManager;
     @Mock TelephonyManager mMockTelephonyManager;
@@ -50,6 +58,8 @@
 
     private SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
 
+    private final HashSet<String> mPermissionTable = new HashSet<>();
+
     public TestContext() {
         MockitoAnnotations.initMocks(this);
         doAnswer((Answer<PersistableBundle>) invocation -> {
@@ -161,4 +171,67 @@
         }
         return b;
     }
+
+    @Override
+    public void enforceCallingOrSelfPermission(String permission, String message) {
+        if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(permission + " denied: " + message);
+        }
+    }
+
+    @Override
+    public void enforcePermission(String permission, int pid, int uid, String message) {
+        enforceCallingOrSelfPermission(permission, message);
+    }
+
+    @Override
+    public void enforceCallingPermission(String permission, String message) {
+        enforceCallingOrSelfPermission(permission, message);
+    }
+
+    @Override
+    public int checkCallingOrSelfPermission(String permission) {
+        return checkPermission(permission, Binder.getCallingPid(), Binder.getCallingUid());
+    }
+
+    @Override
+    public int checkPermission(String permission, int pid, int uid) {
+        synchronized (mPermissionTable) {
+            if (mPermissionTable.contains(permission)
+                    || mPermissionTable.contains(STUB_PERMISSION_ENABLE_ALL)) {
+                logd("checkCallingOrSelfPermission: " + permission + " return GRANTED");
+                return PackageManager.PERMISSION_GRANTED;
+            } else {
+                logd("checkCallingOrSelfPermission: " + permission + " return DENIED");
+                return PackageManager.PERMISSION_DENIED;
+            }
+        }
+    }
+
+    public void grantPermission(String permission) {
+        synchronized (mPermissionTable) {
+            if (mPermissionTable != null && permission != null) {
+                mPermissionTable.remove(STUB_PERMISSION_ENABLE_ALL);
+                mPermissionTable.add(permission);
+            }
+        }
+    }
+
+    public void revokePermission(String permission) {
+        synchronized (mPermissionTable) {
+            if (mPermissionTable != null && permission != null) {
+                mPermissionTable.remove(permission);
+            }
+        }
+    }
+
+    public void revokeAllPermissions() {
+        synchronized (mPermissionTable) {
+            mPermissionTable.clear();
+        }
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
 }
diff --git a/tests/src/com/android/phone/CarrierConfigLoaderTest.java b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
new file mode 100644
index 0000000..7fd9069
--- /dev/null
+++ b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
@@ -0,0 +1,381 @@
+/*
+ * 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 com.android.TestContext.STUB_PERMISSION_ENABLE_ALL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.SubscriptionInfoUpdater;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+/**
+ * Unit Test for CarrierConfigLoader.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CarrierConfigLoaderTest extends TelephonyTestBase {
+
+    private static final int DEFAULT_PHONE_ID = 0;
+    private static final int DEFAULT_SUB_ID = SubscriptionManager.getDefaultSubscriptionId();
+    private static final String PLATFORM_CARRIER_CONFIG_PACKAGE = "com.android.carrierconfig";
+    private static final long PLATFORM_CARRIER_CONFIG_PACKAGE_VERSION_CODE = 1;
+    private static final String CARRIER_CONFIG_EXAMPLE_KEY =
+            CarrierConfigManager.KEY_CARRIER_USSD_METHOD_INT;
+    private static final int CARRIER_CONFIG_EXAMPLE_VALUE =
+            CarrierConfigManager.USSD_OVER_CS_PREFERRED;
+
+    @Mock Resources mResources;
+    @Mock PackageManager mPackageManager;
+    @Mock PackageInfo mPackageInfo;
+    @Mock SubscriptionInfoUpdater mSubscriptionInfoUpdater;
+    @Mock SharedPreferences mSharedPreferences;
+
+    private TelephonyManager mTelephonyManager;
+    private CarrierConfigLoader mCarrierConfigLoader;
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+    private TestableLooper mTestableLooper;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+        doReturn(Build.FINGERPRINT).when(mSharedPreferences).getString(eq("build_fingerprint"),
+                any());
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        doReturn(mResources).when(mContext).getResources();
+        doReturn(InstrumentationRegistry.getTargetContext().getFilesDir()).when(
+                mContext).getFilesDir();
+        doReturn(PLATFORM_CARRIER_CONFIG_PACKAGE).when(mResources).getString(
+                eq(R.string.platform_carrier_config_package));
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        doReturn(1).when(mTelephonyManager).getSupportedModemCount();
+        doReturn(1).when(mTelephonyManager).getActiveModemCount();
+        doReturn("spn").when(mTelephonyManager).getSimOperatorNameForPhone(anyInt());
+        doReturn("310260").when(mTelephonyManager).getSimOperatorNumericForPhone(anyInt());
+        doReturn(mPackageInfo).when(mPackageManager).getPackageInfo(
+                eq(PLATFORM_CARRIER_CONFIG_PACKAGE), eq(0) /*flags*/);
+        doReturn(PLATFORM_CARRIER_CONFIG_PACKAGE_VERSION_CODE).when(
+                mPackageInfo).getLongVersionCode();
+
+        mHandlerThread = new HandlerThread("CarrierConfigLoaderTest");
+        mHandlerThread.start();
+
+        mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+        mCarrierConfigLoader = new CarrierConfigLoader(mContext, mSubscriptionInfoUpdater,
+                mTestableLooper.getLooper());
+        mHandler = mCarrierConfigLoader.getHandler();
+
+        // Clear all configs to have the same starting point.
+        mCarrierConfigLoader.clearConfigForPhone(DEFAULT_PHONE_ID, false);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mContext.revokeAllPermissions();
+        mTestableLooper.destroy();
+        super.tearDown();
+    }
+
+    /**
+     * Verifies that SecurityException should throw when call #updateConfigForPhoneId() without
+     * MODIFY_PHONE_STATE permission.
+     */
+    @Test
+    public void testUpdateConfigForPhoneId_noPermission() throws Exception {
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
+                        IccCardConstants.INTENT_VALUE_ICC_ABSENT));
+    }
+
+    /**
+     * Verifies that when call #updateConfigForPhoneId() with SIM absence, both carrier config from
+     * default app and carrier should be cleared but no-sim config should be loaded.
+     */
+    @Test
+    public void testUpdateConfigForPhoneId_simAbsent() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+        doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+
+        // Prepare a cached config to fetch from xml
+        PersistableBundle config = getTestConfig();
+        mCarrierConfigLoader.saveNoSimConfigToXml(PLATFORM_CARRIER_CONFIG_PACKAGE, config);
+        mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
+                IccCardConstants.INTENT_VALUE_ICC_ABSENT);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID)).isNull();
+        assertThat(mCarrierConfigLoader.getConfigFromCarrierApp(DEFAULT_PHONE_ID)).isNull();
+        assertThat(mCarrierConfigLoader.getNoSimConfig().getInt(CARRIER_CONFIG_EXAMPLE_KEY))
+                .isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
+        verify(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+    }
+
+    /**
+     * Verifies that with cached config in XML, calling #updateConfigForPhoneId() with SIM loaded
+     * will return the right config in the XML.
+     */
+    @Test
+    public void testUpdateConfigForPhoneId_simLoaded_withCachedConfigInXml() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        // Prepare to make sure we can save the config into the XML file which used as cache
+        List<String> carrierPackages = List.of(PLATFORM_CARRIER_CONFIG_PACKAGE);
+        doReturn(carrierPackages).when(mTelephonyManager).getCarrierPackageNamesForIntentAndPhone(
+                nullable(Intent.class), anyInt());
+
+        // Save the sample config into the XML file
+        PersistableBundle config = getTestConfig();
+        CarrierIdentifier carrierId = mCarrierConfigLoader.getCarrierIdentifierForPhoneId(
+                DEFAULT_PHONE_ID);
+        mCarrierConfigLoader.saveConfigToXml(PLATFORM_CARRIER_CONFIG_PACKAGE, "",
+                DEFAULT_PHONE_ID, carrierId, config);
+        mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
+                IccCardConstants.INTENT_VALUE_ICC_LOADED);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID).getInt(
+                CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
+
+    }
+
+    /**
+     * Verifies that SecurityException should throw if call #overrideConfig() without
+     * MODIFY_PHONE_STATE permission.
+     */
+    @Test
+    public void testOverrideConfig_noPermission() throws Exception {
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, PersistableBundle.EMPTY,
+                        false));
+    }
+
+    /**
+     * Verifies that override config is not null when calling #overrideConfig with null bundle.
+     */
+    @Test
+    public void testOverrideConfig_withNullBundle() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, null /*overrides*/,
+                false/*persistent*/);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID)).isEqualTo(
+                PersistableBundle.EMPTY);
+        verify(mSubscriptionInfoUpdater).updateSubscriptionByCarrierConfigAndNotifyComplete(
+                eq(DEFAULT_PHONE_ID), eq(PLATFORM_CARRIER_CONFIG_PACKAGE),
+                any(PersistableBundle.class), any(Message.class));
+    }
+
+    /**
+     * Verifies that override config is not null when calling #overrideConfig with non-null bundle.
+     */
+    @Test
+    public void testOverrideConfig_withNonNullBundle() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        PersistableBundle config = getTestConfig();
+        mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, config /*overrides*/,
+                false/*persistent*/);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID).getInt(
+                CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
+        verify(mSubscriptionInfoUpdater).updateSubscriptionByCarrierConfigAndNotifyComplete(
+                eq(DEFAULT_PHONE_ID), eq(PLATFORM_CARRIER_CONFIG_PACKAGE),
+                any(PersistableBundle.class), any(Message.class));
+    }
+
+    /**
+     * Verifies that calling #notifyConfigChangedForSubId() with invalid subId will be ignored.
+     */
+    @Test
+    public void testNotifyConfigChangedForSubId_invalidSubId() throws Exception {
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        mCarrierConfigLoader.notifyConfigChangedForSubId(
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+        // verifying the behavior following the permission check is not actually performed.
+        // It against "verify value instead of behavior", but we don't have other alternatives.
+        verify(mPackageManager, never()).getNameForUid(anyInt());
+        verify(mContext, never()).checkCallingOrSelfPermission(
+                eq(android.Manifest.permission.MODIFY_PHONE_STATE));
+    }
+
+    // TODO(b/184040111): Enable test case when support disabling carrier privilege
+    // Phone/System UID always has carrier privilege (TelephonyPermission#getCarrierPrivilegeStatus)
+    // when running the test here.
+    /**
+     * Verifies that SecurityException should throw when calling notifyConfigChangedForSubId without
+     * MODIFY_PHONE_STATE permission.
+     */
+    @Ignore
+    public void testNotifyConfigChangedForSubId_noPermission() throws Exception {
+        setCarrierPrivilegesForSubId(false, DEFAULT_SUB_ID);
+
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.notifyConfigChangedForSubId(DEFAULT_SUB_ID));
+    }
+
+    /**
+     * Verifies that SecurityException should throw when calling getDefaultCarrierServicePackageName
+     * without READ_PRIVILEGED_PHONE_STATE permission.
+     */
+    @Test
+    public void testGetDefaultCarrierServicePackageName_noPermission() {
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.getDefaultCarrierServicePackageName());
+    }
+
+    /**
+     * Verifies that the right default carrier service package name is return when calling
+     * getDefaultCarrierServicePackageName with permission.
+     */
+    @Test
+    public void testGetDefaultCarrierServicePackageName_withPermission() {
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        assertThat(mCarrierConfigLoader.getDefaultCarrierServicePackageName())
+                .isEqualTo(PLATFORM_CARRIER_CONFIG_PACKAGE);
+    }
+
+    // TODO(b/184040111): Enable test case when support disabling carrier privilege
+    // Phone/System UID always has carrier privilege (TelephonyPermission#getCarrierPrivilegeStatus)
+    // when running the test here.
+    /**
+     * Verifies that without permission, #getConfigForSubId will return an empty PersistableBundle.
+     */
+    @Ignore
+    public void testGetConfigForSubId_noPermission() {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        setCarrierPrivilegesForSubId(false, DEFAULT_SUB_ID);
+
+        assertThat(mCarrierConfigLoader.getConfigForSubId(DEFAULT_SUB_ID,
+                PLATFORM_CARRIER_CONFIG_PACKAGE)).isEqualTo(PersistableBundle.EMPTY);
+    }
+
+    /**
+     * Verifies that when have no DUMP permission, the #dump() method shows permission denial.
+     */
+    @Test
+    public void testDump_noPermission() {
+        StringWriter stringWriter = new StringWriter();
+        mCarrierConfigLoader.dump(new FileDescriptor(), new PrintWriter(stringWriter),
+                new String[0]);
+        stringWriter.flush();
+
+        assertThat(stringWriter.toString()).contains("Permission Denial:");
+    }
+
+    /**
+     * Verifies that when have DUMP permission, the #dump() method can dump the CarrierConfigLoader.
+     */
+    @Test
+    public void testDump_withPermission() {
+        mContext.grantPermission(android.Manifest.permission.DUMP);
+
+        StringWriter stringWriter = new StringWriter();
+        mCarrierConfigLoader.dump(new FileDescriptor(), new PrintWriter(stringWriter),
+                new String[0]);
+        stringWriter.flush();
+
+        String dumpContent = stringWriter.toString();
+        assertThat(dumpContent).contains("CarrierConfigLoader:");
+        assertThat(dumpContent).doesNotContain("Permission Denial:");
+    }
+
+    private static PersistableBundle getTestConfig() {
+        PersistableBundle config = new PersistableBundle();
+        config.putInt(CARRIER_CONFIG_EXAMPLE_KEY, CARRIER_CONFIG_EXAMPLE_VALUE);
+        return config;
+    }
+
+    private void setCarrierPrivilegesForSubId(boolean hasCarrierPrivileges, int subId) {
+        TelephonyManager mockTelephonyManager = Mockito.mock(TelephonyManager.class);
+        doReturn(mockTelephonyManager).when(mTelephonyManager).createForSubscriptionId(subId);
+        doReturn(hasCarrierPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
+                : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS).when(
+                mockTelephonyManager).getCarrierPrivilegeStatus(anyInt());
+    }
+}
diff --git a/tests/src/com/android/phone/RcsProvisioningMonitorTest.java b/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
index 28c4390..c7d0c8f 100644
--- a/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
+++ b/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
@@ -85,7 +85,7 @@
  */
 public class RcsProvisioningMonitorTest {
     private static final String TAG = "RcsProvisioningMonitorTest";
-    private static final String SAMPLE_CONFIG = "<RCSConfig>\n"
+    private static final String CONFIG_DEFAULT = "<RCSConfig>\n"
             + "\t<rcsVolteSingleRegistration>1</rcsVolteSingleRegistration>\n"
             + "\t<SERVICES>\n"
             + "\t\t<SupportedRCSProfileVersions>UP_2.0</SupportedRCSProfileVersions>\n"
@@ -104,6 +104,9 @@
             + "\t\t</Ext>\n"
             + "\t</SERVICES>\n"
             + "</RCSConfig>";
+    private static final String CONFIG_SINGLE_REGISTRATION_DISABLED = "<RCSConfig>\n"
+            + "\t<rcsVolteSingleRegistration>0</rcsVolteSingleRegistration>\n"
+            + "</RCSConfig>";
     private static final int FAKE_SUB_ID_BASE = 0x0FFFFFF0;
     private static final String DEFAULT_MESSAGING_APP1 = "DMA1";
     private static final String DEFAULT_MESSAGING_APP2 = "DMA2";
@@ -252,7 +255,7 @@
         when(mCursor.moveToFirst()).thenReturn(true);
         when(mCursor.getColumnIndexOrThrow(any())).thenReturn(1);
         when(mCursor.getBlob(anyInt())).thenReturn(
-                RcsConfig.compressGzip(SAMPLE_CONFIG.getBytes()));
+                RcsConfig.compressGzip(CONFIG_DEFAULT.getBytes()));
 
         mHandlerThread = new HandlerThread("RcsProvisioningMonitorTest");
         mHandlerThread.start();
@@ -278,7 +281,7 @@
         createMonitor(3);
 
         for (int i = 0; i < 3; i++) {
-            assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
+            assertTrue(Arrays.equals(CONFIG_DEFAULT.getBytes(),
                     mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE + i)));
         }
 
@@ -312,7 +315,7 @@
         ArgumentCaptor<Intent> captorIntent = ArgumentCaptor.forClass(Intent.class);
 
         for (int i = 0; i < 3; i++) {
-            assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
+            assertTrue(Arrays.equals(CONFIG_DEFAULT.getBytes(),
                     mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE + i)));
         }
         verify(mPhone, times(3)).sendBroadcast(captorIntent.capture(), any());
@@ -358,7 +361,7 @@
         processAllMessages();
         byte[] configCached = mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE);
 
-        assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(), configCached));
+        assertTrue(Arrays.equals(CONFIG_DEFAULT.getBytes(), configCached));
         verify(mIImsConfig, times(1)).notifyRcsAutoConfigurationRemoved();
         // The api should be called 2 times, one happens when monitor is initilized,
         // Another happens when DMS is changed.
@@ -421,12 +424,12 @@
         createMonitor(1);
         final ArgumentCaptor<byte[]> argumentBytes = ArgumentCaptor.forClass(byte[].class);
 
-        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, SAMPLE_CONFIG.getBytes(), false);
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, CONFIG_DEFAULT.getBytes(), false);
         processAllMessages();
 
         verify(mIImsConfig, atLeastOnce()).notifyRcsAutoConfigurationReceived(
                 argumentBytes.capture(), eq(false));
-        assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(), argumentBytes.getValue()));
+        assertTrue(Arrays.equals(CONFIG_DEFAULT.getBytes(), argumentBytes.getValue()));
     }
 
     @Test
@@ -447,13 +450,15 @@
         createMonitor(1);
 
         when(mPackageManager.hasSystemFeature(
-                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(true);
+                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(false);
         mBundle.putBoolean(
-                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, true);
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, false);
         broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
         processAllMessages();
-        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
 
+        when(mPackageManager.hasSystemFeature(
+                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(true);
         mBundle.putBoolean(
                 CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, false);
         broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
@@ -468,6 +473,27 @@
         broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
         processAllMessages();
         assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        when(mPackageManager.hasSystemFeature(
+                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(true);
+        mBundle.putBoolean(
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, true);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, null, false);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, CONFIG_DEFAULT.getBytes(), false);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE,
+                CONFIG_SINGLE_REGISTRATION_DISABLED.getBytes(), false);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
     }
 
     @Test
@@ -537,6 +563,50 @@
 
     @Test
     @SmallTest
+    public void testOverrideDeviceSingleRegistrationEnabled() throws Exception {
+        createMonitor(1);
+
+        when(mPackageManager.hasSystemFeature(
+                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(true);
+        mBundle.putBoolean(
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, true);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.overrideDeviceSingleRegistrationEnabled(false);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.getDeviceSingleRegistrationEnabled());
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.overrideDeviceSingleRegistrationEnabled(null);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.getDeviceSingleRegistrationEnabled());
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        when(mPackageManager.hasSystemFeature(
+                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(false);
+        //use carrier config change to refresh the value as system feature is static
+        mBundle.putBoolean(
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, true);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.overrideDeviceSingleRegistrationEnabled(true);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.getDeviceSingleRegistrationEnabled());
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.overrideDeviceSingleRegistrationEnabled(null);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.getDeviceSingleRegistrationEnabled());
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+    }
+
+    @Test
+    @SmallTest
     public void testTestModeEnabledAndDisabled() throws Exception {
         when(mCursor.getBlob(anyInt())).thenReturn(null);
         createMonitor(1);
@@ -550,12 +620,12 @@
         verify(mCursor, times(1)).getBlob(anyInt());
         assertNull(mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE));
 
-        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, SAMPLE_CONFIG.getBytes(), false);
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, CONFIG_DEFAULT.getBytes(), false);
         processAllMessages();
 
         //config cahced in monitor should be updated, but db should not
         assertNull(mProvider.getContentValues());
-        assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
+        assertTrue(Arrays.equals(CONFIG_DEFAULT.getBytes(),
                 mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE)));
 
         //verify if monitor goes back to normal mode
@@ -565,15 +635,74 @@
         verify(mCursor, times(2)).getBlob(anyInt());
         assertNull(mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE));
 
-        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, SAMPLE_CONFIG.getBytes(), false);
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, CONFIG_DEFAULT.getBytes(), false);
         processAllMessages();
 
-        assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
+        assertTrue(Arrays.equals(CONFIG_DEFAULT.getBytes(),
                 mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE)));
-        assertTrue(Arrays.equals(RcsConfig.compressGzip(SAMPLE_CONFIG.getBytes()),
+        assertTrue(Arrays.equals(RcsConfig.compressGzip(CONFIG_DEFAULT.getBytes()),
                 (byte[]) mProvider.getContentValues().get(SimInfo.COLUMN_RCS_CONFIG)));
     }
 
+    @Test
+    @SmallTest
+    public void testOverrideCarrierSingleRegistrationEnabled() throws Exception {
+        createMonitor(1);
+
+        when(mPackageManager.hasSystemFeature(
+                eq(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION))).thenReturn(true);
+        mBundle.putBoolean(
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, true);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor
+                .overrideCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE, false);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.getCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor
+                .overrideCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE, null);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.getCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mBundle.putBoolean(
+                CarrierConfigManager.Ims.KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, false);
+        broadcastCarrierConfigChange(FAKE_SUB_ID_BASE);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor
+                .overrideCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE, true);
+        processAllMessages();
+        assertTrue(mRcsProvisioningMonitor.getCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+        assertTrue(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor
+                .overrideCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE, null);
+        processAllMessages();
+        assertFalse(mRcsProvisioningMonitor.getCarrierSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+        assertFalse(mRcsProvisioningMonitor.isRcsVolteSingleRegistrationEnabled(FAKE_SUB_ID_BASE));
+    }
+
+    @Test
+    @SmallTest
+    public void testOverrideImsFeatureValidation() throws Exception {
+        createMonitor(1);
+
+        mRcsProvisioningMonitor.overrideImsFeatureValidation(FAKE_SUB_ID_BASE, false);
+        assertFalse(mRcsProvisioningMonitor.getImsFeatureValidationOverride(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.overrideImsFeatureValidation(FAKE_SUB_ID_BASE, true);
+        assertTrue(mRcsProvisioningMonitor.getImsFeatureValidationOverride(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.overrideImsFeatureValidation(FAKE_SUB_ID_BASE, null);
+        assertNull(mRcsProvisioningMonitor.getImsFeatureValidationOverride(FAKE_SUB_ID_BASE));
+    }
+
     private void createMonitor(int subCount) throws Exception {
         if (Looper.myLooper() == null) {
             Looper.prepare();
diff --git a/tests/src/com/android/phone/ServiceStateProviderTest.java b/tests/src/com/android/phone/ServiceStateProviderTest.java
index 55fdada..d85976a 100644
--- a/tests/src/com/android/phone/ServiceStateProviderTest.java
+++ b/tests/src/com/android/phone/ServiceStateProviderTest.java
@@ -64,7 +64,7 @@
     private final String[] mTestProjection =
     {
         ServiceStateTable.VOICE_REG_STATE,
-        ServiceStateProvider.DATA_REG_STATE,
+        ServiceStateTable.DATA_REG_STATE,
         ServiceStateProvider.VOICE_OPERATOR_ALPHA_LONG,
         ServiceStateProvider.VOICE_OPERATOR_ALPHA_SHORT,
         ServiceStateTable.VOICE_OPERATOR_NUMERIC,
@@ -86,6 +86,7 @@
         ServiceStateProvider.OPERATOR_ALPHA_LONG_RAW,
         ServiceStateProvider.OPERATOR_ALPHA_SHORT_RAW,
         ServiceStateTable.DATA_NETWORK_TYPE,
+        ServiceStateTable.DUPLEX_MODE,
     };
 
     // Exception used internally to verify if the Resolver#notifyChange has been called.
@@ -111,6 +112,15 @@
         mTestServiceStateForSubId1 = new ServiceState();
         mTestServiceStateForSubId1.setStateOff();
 
+        // Add NRI to trigger SS with non-default values (e.g. duplex mode)
+        NetworkRegistrationInfo nriWwan = new NetworkRegistrationInfo.Builder()
+                .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
+                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
+                .setDomain(NetworkRegistrationInfo.DOMAIN_PS)
+                .build();
+        mTestServiceStateForSubId1.addNetworkRegistrationInfo(nriWwan);
+        mTestServiceStateForSubId1.setChannelNumber(65536); // EutranBand.BAND_65, DUPLEX_MODE_FDD
+
         // Mock out the actual phone state
         ServiceStateProvider provider = new ServiceStateProvider() {
             @Override
@@ -196,6 +206,7 @@
         final String operatorAlphaLongRaw = ss.getOperatorAlphaLongRaw();
         final String operatorAlphaShortRaw = ss.getOperatorAlphaShortRaw();
         final int dataNetworkType = ss.getDataNetworkType();
+        final int duplexMode = ss.getDuplexMode();
 
         assertEquals(voiceRegState, cursor.getInt(0));
         assertEquals(dataRegState, cursor.getInt(1));
@@ -220,6 +231,7 @@
         assertEquals(operatorAlphaLongRaw, cursor.getString(20));
         assertEquals(operatorAlphaShortRaw, cursor.getString(21));
         assertEquals(dataNetworkType, cursor.getInt(22));
+        assertEquals(duplexMode, cursor.getInt(23));
     }
 
     /**
@@ -315,6 +327,28 @@
         assertTrue(notifyChangeCalledForSubIdAndField(oldSS, newSS, subId));
     }
 
+    @Test
+    @SmallTest
+    public void testNotifyChanged_dataRegStateUpdated() {
+        int subId = 0;
+
+        ServiceState oldSS = new ServiceState();
+        oldSS.setStateOutOfService();
+        oldSS.setDataRegState(ServiceState.STATE_OUT_OF_SERVICE);
+
+        ServiceState newSS = new ServiceState();
+        newSS.setStateOutOfService();
+        newSS.setDataRegState(ServiceState.STATE_POWER_OFF);
+
+        // Test that notifyChange is called by notifyChangeForSubId
+        // when the data_reg_state changes
+        assertTrue(notifyChangeCalledForSubId(oldSS, newSS, subId));
+
+        // Test that notifyChange is called by notifyChangeForSubIdAndField
+        // when the data_reg_state changes
+        assertTrue(notifyChangeCalledForSubIdAndField(oldSS, newSS, subId));
+    }
+
     // Check if notifyChange was called by notifyChangeForSubId
     private boolean notifyChangeCalledForSubId(ServiceState oldSS,
             ServiceState newSS, int subId) {
diff --git a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
index fa27775..d364fe4 100644
--- a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
@@ -35,7 +35,9 @@
 
 import android.app.role.RoleManager;
 import android.os.IBinder;
+import android.os.PersistableBundle;
 import android.os.UserHandle;
+import android.telephony.CarrierConfigManager;
 import android.telephony.ims.DelegateRequest;
 import android.telephony.ims.FeatureTagState;
 import android.telephony.ims.ImsException;
@@ -54,6 +56,7 @@
 import com.android.TelephonyTestBase;
 import com.android.TestExecutorService;
 import com.android.ims.RcsFeatureManager;
+import com.android.phone.RcsProvisioningMonitor;
 
 import org.junit.After;
 import org.junit.Before;
@@ -129,6 +132,9 @@
             return c;
         }).when(mMockDelegateControllerFactory).create(anyInt(), any(), anyString(), any(), any(),
                 any(), any(), any());
+        setFeatureAllowedConfig(TEST_SUB_ID, new String[]{ImsSignallingUtils.MMTEL_TAG,
+                ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG, ImsSignallingUtils.GROUP_CHAT_TAG,
+                ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG});
     }
 
     @After
@@ -138,6 +144,7 @@
         if (!isShutdown) {
             mExecutorService.shutdownNow();
         }
+        RcsProvisioningMonitor.getInstance().overrideImsFeatureValidation(TEST_SUB_ID, null);
     }
 
     @SmallTest
@@ -607,6 +614,9 @@
     @Test
     public void testTimingSubIdChangedAndCreateNewSubId() throws Exception {
         SipTransportController controller = setupLiveTransportController(THROTTLE_MS, 0);
+        setFeatureAllowedConfig(TEST_SUB_ID + 1, new String[]{ImsSignallingUtils.MMTEL_TAG,
+                ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG, ImsSignallingUtils.GROUP_CHAT_TAG,
+                ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG});
 
         ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
         DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
@@ -643,6 +653,67 @@
         verifyDelegateChanged(c2, pendingC2Change, secondDelegate, Collections.emptySet(), 0);
     }
 
+    @SmallTest
+    @Test
+    public void testFeatureTagsDeniedByConfig() throws Exception {
+        setFeatureAllowedConfig(TEST_SUB_ID, new String[]{ImsSignallingUtils.GROUP_CHAT_TAG,
+                ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG});
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS, 0);
+
+        ArraySet<String> requestTags = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        ArraySet<String> allowedTags = new ArraySet<>(requestTags);
+        ArraySet<String> deniedTags = new ArraySet<>();
+        DelegateRequest delegateRequest = new DelegateRequest(requestTags);
+        SipDelegateController sc = injectMockDelegateController(TEST_PACKAGE_NAME,
+                delegateRequest);
+        CompletableFuture<Boolean> pendingScChange = createDelegate(controller, sc,
+                delegateRequest, requestTags, Collections.emptySet(), TEST_PACKAGE_NAME);
+        allowedTags.remove(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+        deniedTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDelegateChanged(sc, pendingScChange, allowedTags, getDeniedTagsForReason(
+                deniedTags, SipDelegateManager.DENIED_REASON_NOT_ALLOWED), 0);
+    }
+
+    @SmallTest
+    @Test
+    public void testFeatureTagsDeniedByOverride() throws Exception {
+        RcsProvisioningMonitor.getInstance().overrideImsFeatureValidation(TEST_SUB_ID, false);
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS, 0);
+
+        ArraySet<String> requestTags = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        ArraySet<String> deniedTags = new ArraySet<>(requestTags);
+        DelegateRequest delegateRequest = new DelegateRequest(requestTags);
+        SipDelegateController sc = injectMockDelegateController(TEST_PACKAGE_NAME,
+                delegateRequest);
+        CompletableFuture<Boolean> pendingScChange = createDelegate(controller, sc,
+                delegateRequest, requestTags, Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDelegateChanged(sc, pendingScChange, Collections.emptySet(), getDeniedTagsForReason(
+                deniedTags, SipDelegateManager.DENIED_REASON_NOT_ALLOWED), 0);
+    }
+
+    @SmallTest
+    @Test
+    public void testFeatureTagsDeniedByConfigAllowedByOverride() throws Exception {
+        setFeatureAllowedConfig(TEST_SUB_ID, new String[]{});
+        RcsProvisioningMonitor.getInstance().overrideImsFeatureValidation(TEST_SUB_ID, true);
+        SipTransportController controller = setupLiveTransportController(THROTTLE_MS, 0);
+
+        ArraySet<String> requestTags = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+        ArraySet<String> allowedTags = new ArraySet<>(requestTags);
+        DelegateRequest delegateRequest = new DelegateRequest(requestTags);
+        SipDelegateController sc = injectMockDelegateController(TEST_PACKAGE_NAME,
+                delegateRequest);
+        CompletableFuture<Boolean> pendingScChange = createDelegate(controller, sc,
+                delegateRequest, requestTags, Collections.emptySet(), TEST_PACKAGE_NAME);
+
+        assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+        verifyDelegateChanged(sc, pendingScChange, allowedTags, Collections.emptySet(), 0);
+    }
+
     @SafeVarargs
     private final Pair<Set<String>, Set<FeatureTagState>> getAllowedAndDeniedTagsForConfig(
             DelegateRequest r, int denyReason, Set<String>... previousRequestedTagSets) {
@@ -910,4 +981,10 @@
         }
         return true;
     }
+
+    private void setFeatureAllowedConfig(int subId, String[] tags) {
+        PersistableBundle bundle = mContext.getCarrierConfig(subId);
+        bundle.putStringArray(
+                CarrierConfigManager.Ims.KEY_RCS_FEATURE_TAG_ALLOWED_STRING_ARRAY, tags);
+    }
 }