diff options
| author | Allen Su <allenwtsu@google.com> | 2021-03-30 02:24:19 +0000 |
|---|---|---|
| committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-03-30 02:24:19 +0000 |
| commit | 0778a07e9e484cefe1e2c77c3f36a100553a8694 (patch) | |
| tree | e1a51ebedda8df1633d3311268890e4b770c41b8 /testapps | |
| parent | 68fce192a40c206df6f196eecb03f0869b557a01 (diff) | |
| parent | ae2e72527ca7c16a0eb353da74feb15359d78133 (diff) | |
| download | platform_packages_services_Telephony-0778a07e9e484cefe1e2c77c3f36a100553a8694.tar.gz platform_packages_services_Telephony-0778a07e9e484cefe1e2c77c3f36a100553a8694.tar.bz2 platform_packages_services_Telephony-0778a07e9e484cefe1e2c77c3f36a100553a8694.zip | |
Merge "[RCS]Implement File Upload" am: f8f0e5d0b9 am: 06809ae0e5 am: ae2e72527c
Original change: https://android-review.googlesource.com/c/platform/packages/services/Telephony/+/1647006
Change-Id: Iaf6fa2b3047aa738f52e41fed874d6293a1c177e
Diffstat (limited to 'testapps')
22 files changed, 1310 insertions, 13 deletions
diff --git a/testapps/TestRcsApp/TestApp/AndroidManifest.xml b/testapps/TestRcsApp/TestApp/AndroidManifest.xml index 4e401209e..485d65eaa 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 db7ea332d..939feb035 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 df80e547a..e184b04f5 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 eb4d1faa4..0117549be 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 106a024df..94d6efa49 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 000000000..a41376bee --- /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 5ccbc8daa..f9866e842 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 0390d5102..7e31581cb 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 a70cd4a9f..47f534a77 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 5cf2da205..a4e6ff21a 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 502874f78..f52b70d62 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 000000000..3bc1c24cb --- /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 5b889fb85..9ee2a35a8 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.net.Uri; 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 @@ import java.util.concurrent.Executors; 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 @@ public class GbaActivity extends AppCompatActivity { 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 @@ public class GbaActivity extends AppCompatActivity { 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 62302fe73..89c5268d6 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 @@ public class MainActivity extends AppCompatActivity { private Button mUceButton; private Button mGbaButton; private Button mMessageClientButton; + private Button mFileUploadButton; private TextView mVersionInfo; @Override @@ -53,6 +54,7 @@ public class MainActivity extends AppCompatActivity { 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 @@ public class MainActivity extends AppCompatActivity { 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 0c2996c0d..dae283547 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 @@ public class ProvisioningActivity extends AppCompatActivity { 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 @@ public class ProvisioningActivity extends AppCompatActivity { } } - 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 413b5e82b..215c69292 100644 --- a/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp +++ b/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp @@ -33,6 +33,7 @@ android_library { 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 000000000..f6548d873 --- /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 000000000..dde340c48 --- /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 000000000..d8e38e003 --- /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 000000000..55608e0ee --- /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 000000000..856fec15c --- /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 000000000..59a3aa94e --- /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; +} |
