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;
+}