Merge "[RCS]Implement File Upload"
diff --git a/testapps/TestRcsApp/TestApp/AndroidManifest.xml b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
index 4e40120..485d65e 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="10"
+    android:versionName="1.0.9">
 
     <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/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/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..55608e0
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/filetransfer/requestexecutor/GbaAuthenticationProvider.java
@@ -0,0 +1,137 @@
+/*
+ * 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.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);
+
+            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: key:[" + new String(gbaKey) + "] btid:[" + btId
+                                + "]");
+                        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), new String(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;
+}