[RCS] Implement TestRcsApp
Bug: 176472177
Test: manual
Change-Id: I740b1be2e8c2acdaf317bff6cdcf8e267226f855
diff --git a/testapps/TestRcsApp/OWNERS b/testapps/TestRcsApp/OWNERS
new file mode 100644
index 0000000..1d0d52b
--- /dev/null
+++ b/testapps/TestRcsApp/OWNERS
@@ -0,0 +1,3 @@
+allenwtsu@google.com
+calvinpan@google.com
+jamescflin@google.com
diff --git a/testapps/TestRcsApp/TestApp/Android.bp b/testapps/TestRcsApp/TestApp/Android.bp
new file mode 100644
index 0000000..dfa1f2e
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/Android.bp
@@ -0,0 +1,19 @@
+android_app {
+ name: "TestRcsApp",
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ static_libs: [
+ "androidx-constraintlayout_constraintlayout",
+ "aosp_test_rcs_client_base",
+ "androidx.appcompat_appcompat",
+ ],
+ certificate: "platform",
+
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+}
+
+
diff --git a/testapps/TestRcsApp/TestApp/AndroidManifest.xml b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
new file mode 100644
index 0000000..9801faf
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //packages/services/Telephony/testapps/TestRcsApp/TestApp/AndroidManifest.xml
+**
+** Copyright 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.
+*/
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.sample.rcsclient">
+
+ <uses-sdk
+ android:minSdkVersion="30"
+ android:targetSdkVersion="30" />
+
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+ <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".DelegateActivity" />
+ <activity android:name=".UceActivity" />
+ <activity android:name=".GbaActivity" />
+ <activity android:name=".PhoneNumberActivity" />
+ <activity android:name=".ChatActivity" />
+ <activity android:name=".ContactListActivity" />
+ <activity android:name=".ProvisioningActivity" />
+
+ <provider
+ android:name=".util.ChatProvider"
+ android:authorities="rcsprovider" />
+
+
+ <!-- In order to make this App eligible to be selected as the default Message App, the
+ following components are required to be declared even if they are not implemented.
+ -->
+
+ <!-- BroadcastReceiver that listens for incoming SMS messages -->
+ <receiver
+ android:name=".SmsReceiver"
+ android:permission="android.permission.BROADCAST_SMS">
+ <intent-filter>
+ <action android:name="android.provider.Telephony.SMS_DELIVER" />
+ </intent-filter>
+ </receiver>
+
+ <!-- BroadcastReceiver that listens for incoming MMS messages -->
+ <receiver
+ android:name=".MmsReceiver"
+ android:permission="android.permission.BROADCAST_WAP_PUSH">
+ <intent-filter>
+ <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
+ <data android:mimeType="application/vnd.wap.mms-message" />
+ </intent-filter>
+ </receiver>
+
+ <!-- Activity that allows the user to send new SMS/MMS messages -->
+ <activity android:name=".ComposeSmsActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <action android:name="android.intent.action.SENDTO" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data android:scheme="sms" />
+ <data android:scheme="smsto" />
+ <data android:scheme="mms" />
+ <data android:scheme="mmsto" />
+ </intent-filter>
+ </activity>
+
+ <!-- Service that delivers messages from the phone "quick response" -->
+ <service
+ android:name=".HeadlessSmsSendService"
+ android:exported="true"
+ android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
+ <intent-filter>
+ <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:scheme="sms" />
+ <data android:scheme="smsto" />
+ <data android:scheme="mms" />
+ <data android:scheme="mmsto" />
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/testapps/TestRcsApp/TestApp/res/drawable-v24/ic_launcher_foreground.xml b/testapps/TestRcsApp/TestApp/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..fc0c6ab
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,42 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillType="evenOdd"
+ android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,
+ 49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="78.5885"
+ android:endY="90.9159"
+ android:startX="48.7653"
+ android:startY="61.0927"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,
+ 50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,
+ 37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,
+ 42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,
+ 40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,
+ 52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,
+ 56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,
+ 52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
diff --git a/testapps/TestRcsApp/TestApp/res/drawable/ic_launcher_background.xml b/testapps/TestRcsApp/TestApp/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml b/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml
new file mode 100644
index 0000000..c406972
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/activity_main.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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=".MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/provision"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/provisioning_test"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/delegate"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/delegate_test"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/uce"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/uce_test"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/gba"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gba_test"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/msgClient"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/test_msg_client"
+ android:textAlignment="center"
+ android:textAllCaps="false" />
+ </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml
new file mode 100644
index 0000000..374db9b
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/chat_layout.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@id/title"
+ 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/to"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <EditText
+ android:id="@+id/destNum"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:text="16504483120" />
+ </LinearLayout>
+
+
+ <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/title">
+
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/relative_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"></RelativeLayout>
+ </ScrollView>
+
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+
+ <EditText
+ android:id="@+id/new_msg"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/chat_btn"
+ android:text="@string/chat_message" />
+
+ <Button
+ android:id="@+id/chat_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:text="@string/send" />
+ </RelativeLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml b/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml
new file mode 100644
index 0000000..44f6d3c
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/contact_list.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/start_chat_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:text="@string/start_chat"
+ android:textAllCaps="false" />
+
+ <ListView
+ android:id="@+id/listview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml
new file mode 100644
index 0000000..106a024
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/delegate_layout.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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=".DelegateActivity">
+
+ <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">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/standalone-pager"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/standalone_pager" />
+
+ <CheckBox
+ android:id="@+id/standalone-large"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/standalone_large" />
+
+ <CheckBox
+ android:id="@+id/standalone-deferred"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/standalone_deferred" />
+
+ <CheckBox
+ android:id="@+id/standalone-pager-large"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/standalone_pager_large" />
+
+ <CheckBox
+ android:id="@+id/chat"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/chat" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/file_transfer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/file_transfer" />
+
+ <CheckBox
+ android:id="@+id/geolocation_sms"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/geolocation_sms" />
+
+ <CheckBox
+ android:id="@+id/chatbot_session"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/chatbot_session" />
+
+ <CheckBox
+ android:id="@+id/chatbot_standalone"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/chatbot_standalone" />
+
+ <CheckBox
+ android:id="@+id/chatbot_version"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/chatbot_version" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/init_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/initialize_delegate"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/destroy_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="@string/destroy_delegate"
+ android:textAllCaps="false" />
+
+ <TextView
+ android:id="@+id/delegate_callback_result"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="30dp"
+ android:scrollbars="vertical"
+ android:text="@string/callback_result"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml
new file mode 100644
index 0000000..5ccbc8d
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/gba_layout.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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=".GbaActivity">
+
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/organization"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <Spinner
+ android:id="@+id/organization_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/uicc_type"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <Spinner
+ android:id="@+id/uicc_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/protocol"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <Spinner
+ android:id="@+id/protocol_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/tls_cs"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <EditText
+ android:id="@+id/tls_id"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:text="47"
+ android:textSize="15dp" />
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/naf"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <EditText
+ 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
+ android:id="@+id/gba_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:text="@string/gba_bootstrap"
+ android:textAllCaps="false" />
+
+ <TextView
+ android:id="@+id/gba_result"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"
+ android:text="@string/result"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+ </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml b/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml
new file mode 100644
index 0000000..5d71cd1
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/number_to_chat.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ 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/to"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <EditText
+ android:id="@+id/destNum"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:text="16504396583" />
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/launch_chat_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/ok" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml
new file mode 100644
index 0000000..d98dde2
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/provision_layout.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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=".ProvisionActivity">
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/provisioning_register_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="register"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/provisioning_unregister_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="unregister"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/provisioning_singlereg_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:text="isRcsVolteSingleRegCapable"
+ android:textAllCaps="false" />
+
+ <TextView
+ android:id="@+id/provisioning_singlereg_result"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/result"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/provisioning_callback_result"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:scrollbars="vertical"
+ android:text="@string/callback_result"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml b/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml
new file mode 100644
index 0000000..0174d71
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/layout/uce_layout.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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=".UceActivity">
+
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/uce_description"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <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/number"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <EditText
+ android:id="@+id/number_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:text="16504483123, 16504489023" />
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/capability_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:text="@string/request_capability"
+ android:textAllCaps="false" />
+
+ <TextView
+ android:id="@+id/capability_result"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/result"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+
+ <Button
+ android:id="@+id/availability_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:text="@string/request_availability"
+ android:textAllCaps="false" />
+
+ <TextView
+ android:id="@+id/availability_result"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/result"
+ android:textSize="15dp"
+ android:textStyle="bold" />
+ </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/mipmap-anydpi-v26/ic_launcher.xml b/testapps/TestRcsApp/TestApp/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/mipmap-anydpi-v26/ic_launcher_round.xml b/testapps/TestRcsApp/TestApp/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/testapps/TestRcsApp/TestApp/res/values/colors.xml b/testapps/TestRcsApp/TestApp/res/values/colors.xml
new file mode 100644
index 0000000..3d5cded
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+</resources>
+
diff --git a/testapps/TestRcsApp/TestApp/res/values/strings.xml b/testapps/TestRcsApp/TestApp/res/values/strings.xml
new file mode 100644
index 0000000..2369449
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/values/strings.xml
@@ -0,0 +1,74 @@
+<resources>
+ <string name="app_name">RcsClient</string>
+ <string name="provisioning_test">Provisioning Test</string>
+ <string name="delegate_test">Delegate Test</string>
+ <string name="uce_test">UCE Test</string>
+ <string name="gba_test">GBA Test</string>
+ <string name="test_msg_client">TestMessageClient</string>
+ <string name="db_client">DBClient</string>
+ <string name="result">Result:</string>
+ <string name="callback_result">Callback Result:</string>
+ <string name="initialize_delegate">initializeSipDelegate</string>
+ <string name="destroy_delegate">destroySipDelegate</string>
+ <string name="uce_description">Enter the number to query capability and separate by \',\' if
+ multiple ones.</string>
+ <string name="number">Number: </string>
+ <string name="request_capability">requestCapability</string>
+ <string name="request_availability">requestNetworkAvailability</string>
+ <string name="gba_bootstrap">bootstrapAuthenticationRequest</string>
+ <string name="start_chat">Start Chat</string>
+ <string name="to">To:</string>
+ <string name="chat_message">Chat Message</string>
+ <string name="send">Send</string>
+ <string name="ok">OK</string>
+ <string name="session_succeeded">Session init succeeded</string>
+ <string name="session_failed">Session init failed</string>
+ <string name="session_not_ready">Session not ready</string>
+ <string name="organization">Organization:</string>
+ <string name="uicc_type">UICC Type:</string>
+ <string name="protocol">Protocol:</string>
+ <string name="tls_cs">TLS Cipher Suite:</string>
+ <string name="naf">NAF URI:</string>
+ <string name="standalone_pager">Standalone Pager</string>
+ <string name="standalone_large">Standalone Large</string>
+ <string name="standalone_deferred">Standalone Deferred</string>
+ <string name="standalone_pager_large">Standalone Large Pager</string>
+ <string name="chat">Chat</string>
+ <string name="file_transfer">File Transfer</string>
+ <string name="geolocation_sms">Geolocation SMS</string>
+ <string name="chatbot_session">Chatbot Session</string>
+ <string name="chatbot_standalone">Chatbot Standalone</string>
+ <string name="chatbot_version">Chatbot Version</string>
+ <string name="provisioning_done">Provisioning Done</string>
+ <string name="registration_done">Registration Done</string>
+
+ <string-array name="organization">
+ <item>NONE</item>
+ <item>3GPP</item>
+ <item>3GPP2</item>
+ <item>OMA</item>
+ <item>GSMA</item>
+ <item>LOCAL</item>
+ </string-array>
+ <string-array name="protocol">
+ <item>SUBSCRIBER_CERTIFICATE</item>
+ <item>MBMS</item>
+ <item>HTTP_DIGEST_AUTH</item>
+ <item>3GPP_HTTP_BASED_MBMS</item>
+ <item>GENERIC_PUSH_LAYER</item>
+ <item>IMS_MEDIA_PLANE</item>
+ <item>GENERATION_TMPI</item>
+ <item>3GPP_HTTP_BASED_MBMS</item>
+ <item>TLS_DEFAULT</item>
+ <item>TLS_BROWSER</item>
+ </string-array>
+ <string-array name="uicc_type">
+ <item>UNKNOWN</item>
+ <item>SIM</item>
+ <item>USIM</item>
+ <item>RSIM</item>
+ <item>CSIM</item>
+ <item>ISIM</item>
+ </string-array>
+
+</resources>
diff --git a/testapps/TestRcsApp/TestApp/res/values/styles.xml b/testapps/TestRcsApp/TestApp/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
new file mode 100644
index 0000000..1619f14
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
@@ -0,0 +1,272 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.SubscriptionManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.sample.rcsclient.util.ChatManager;
+import com.google.android.sample.rcsclient.util.ChatProvider;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** An activity to show chat message with specific number. */
+public class ChatActivity extends AppCompatActivity {
+
+ public static final String EXTRA_REMOTE_PHONE_NUMBER = "REMOTE_PHONE_NUMBER";
+ public static final String TELURI_PREFIX = "tel:";
+ private static final String TAG = "TestRcsApp.ChatActivity";
+ private static final int INIT_LIST = 1;
+ private static final int SHOW_TOAST = 2;
+ private static final float TEXT_SIZE = 20.0f;
+ private static final int MARGIN_SIZE = 20;
+ private final ExecutorService mFixedThreadPool = Executors.newFixedThreadPool(3);
+ private boolean mSessionInitResult = false;
+ private Button mSend;
+ private String mDestNumber;
+ private EditText mNewMessage;
+ private ChatObserver mChatObserver;
+ private Handler mHandler;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i(TAG, "onCreate");
+ setContentView(R.layout.chat_layout);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+ mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ Log.d(TAG, "handleMessage:" + msg.what);
+ switch (msg.what) {
+ case INIT_LIST:
+ initChatMessageLayout((Cursor) msg.obj);
+ break;
+ case SHOW_TOAST:
+ Toast.makeText(ChatActivity.this, msg.obj.toString(),
+ Toast.LENGTH_SHORT).show();
+ break;
+ default:
+ Log.d(TAG, "unknown msg:" + msg.what);
+ break;
+ }
+
+ }
+ };
+ initDestNumber();
+ mChatObserver = new ChatObserver(mHandler);
+ }
+
+ private void initDestNumber() {
+ Intent intent = getIntent();
+ mDestNumber = intent.getStringExtra(EXTRA_REMOTE_PHONE_NUMBER);
+ TextView destNumber = findViewById(R.id.destNum);
+ destNumber.setText(mDestNumber);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ initChatButton();
+ queryChatData();
+ getContentResolver().registerContentObserver(ChatProvider.CHAT_URI, false,
+ mChatObserver);
+ }
+
+ private void initChatButton() {
+ mNewMessage = findViewById(R.id.new_msg);
+ mSend = findViewById(R.id.chat_btn);
+
+ int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ Log.e(TAG, "invalid subId:" + subId);
+ return;
+ }
+ try {
+
+ ChatManager.getInstance(getApplicationContext(), subId).initChatSession(
+ TELURI_PREFIX + mDestNumber, new SessionStateCallback() {
+ @Override
+ public void onSuccess() {
+ Log.i(TAG, "session init succeeded");
+ mHandler.sendMessage(mHandler.obtainMessage(SHOW_TOAST,
+ ChatActivity.this.getResources().getString(
+ R.string.session_succeeded)));
+ mSessionInitResult = true;
+ }
+
+ @Override
+ public void onFailure() {
+ Log.i(TAG, "session init failed");
+ mHandler.sendMessage(mHandler.obtainMessage(SHOW_TOAST,
+ ChatActivity.this.getResources().getString(
+ R.string.session_failed)));
+ mSessionInitResult = false;
+ }
+ });
+
+ mSend.setOnClickListener(view -> {
+ if (!mSessionInitResult) {
+ Toast.makeText(ChatActivity.this,
+ getResources().getString(R.string.session_not_ready),
+ Toast.LENGTH_SHORT).show();
+ Log.i(TAG, "session not ready");
+ return;
+ }
+ mFixedThreadPool.execute(() -> {
+ if (TextUtils.isEmpty(mDestNumber)) {
+ Log.i(TAG, "Destination number is empty");
+ } else {
+ ChatManager.getInstance(getApplicationContext(), subId).addNewMessage(
+ mNewMessage.getText().toString(), ChatManager.SELF, mDestNumber);
+ ChatManager.getInstance(getApplicationContext(), subId).sendMessage(
+ TELURI_PREFIX + mDestNumber, mNewMessage.getText().toString());
+ }
+ });
+ });
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ private void initChatMessageLayout(Cursor cursor) {
+ Log.i(TAG, "initChatMessageLayout");
+ RelativeLayout rl = findViewById(R.id.relative_layout);
+ int id = 1;
+ if (cursor != null && cursor.moveToNext()) {
+ TextView chatMessage = initChatMessageItem(cursor, id++, true);
+ rl.addView(chatMessage);
+ }
+ while (cursor != null && cursor.moveToNext()) {
+ TextView chatMessage = initChatMessageItem(cursor, id++, false);
+ rl.addView(chatMessage);
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ private TextView initChatMessageItem(Cursor cursor, int id, boolean isFirst) {
+ TextView chatMsg = new TextView(this);
+ chatMsg.setId(id);
+ chatMsg.setText(
+ cursor.getString(cursor.getColumnIndex(ChatProvider.RcsColumns.CHAT_MESSAGE)));
+ chatMsg.setTextSize(TEXT_SIZE);
+ chatMsg.setTypeface(null, Typeface.BOLD);
+ RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(0, MARGIN_SIZE, 0, 0);
+ if (messageFromSelf(cursor)) {
+ lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ chatMsg.setBackgroundColor(Color.YELLOW);
+ } else {
+ lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+ chatMsg.setBackgroundColor(Color.LTGRAY);
+ }
+ if (!isFirst) {
+ lp.addRule(RelativeLayout.BELOW, id - 1);
+ }
+ chatMsg.setLayoutParams(lp);
+ return chatMsg;
+ }
+
+ private boolean messageFromSelf(Cursor cursor) {
+ return ChatManager.SELF.equals(
+ cursor.getString(cursor.getColumnIndex(ChatProvider.RcsColumns.SRC_PHONE_NUMBER)));
+ }
+
+ private void queryChatData() {
+ mFixedThreadPool.execute(() -> {
+ Cursor cursor = getContentResolver().query(ChatProvider.CHAT_URI,
+ new String[]{ChatProvider.RcsColumns.SRC_PHONE_NUMBER,
+ ChatProvider.RcsColumns.DEST_PHONE_NUMBER,
+ ChatProvider.RcsColumns.CHAT_MESSAGE},
+ ChatProvider.RcsColumns.SRC_PHONE_NUMBER + "=? OR "
+ + ChatProvider.RcsColumns.DEST_PHONE_NUMBER + "=?",
+ new String[]{mDestNumber, mDestNumber},
+ ChatProvider.RcsColumns.MSG_TIMESTAMP + " ASC");
+ mHandler.sendMessage(mHandler.obtainMessage(INIT_LIST, cursor));
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.i(TAG, "onStop");
+ getContentResolver().unregisterContentObserver(mChatObserver);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "onDestroy");
+ }
+
+ private void dispose() {
+ int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ Log.e(TAG, "invalid subId:" + subId);
+ return;
+ }
+ ChatManager chatManager = ChatManager.getInstance(this, subId);
+ chatManager.deregister();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private class ChatObserver extends ContentObserver {
+ ChatObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Log.i(TAG, "onChange");
+ queryChatData();
+ }
+ }
+
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ContactListActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ContactListActivity.java
new file mode 100644
index 0000000..70715f0
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ContactListActivity.java
@@ -0,0 +1,255 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.SubscriptionManager;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClient.State;
+
+import com.google.android.sample.rcsclient.util.ChatManager;
+import com.google.android.sample.rcsclient.util.ChatProvider;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** An activity to show the contacts which UE ever chatted before. */
+public class ContactListActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestRcsApp.ContactListActivity";
+ private static final int RENDER_LISTVIEW = 1;
+ private static final int SHOW_TOAST = 2;
+ private final ExecutorService mSingleThread = Executors.newSingleThreadExecutor();
+ private Button mStartChatButton;
+ private Handler mHandler;
+ private SummaryObserver mSummaryObserver;
+ private ArrayAdapter mAdapter;
+ private ListView mListview;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i(TAG, "onCreate");
+ setContentView(R.layout.contact_list);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+ mStartChatButton = findViewById(R.id.start_chat_btn);
+ mStartChatButton.setOnClickListener(view -> {
+ Intent intent = new Intent(ContactListActivity.this, PhoneNumberActivity.class);
+ ContactListActivity.this.startActivity(intent);
+ });
+
+ mHandler = new Handler() {
+ public void handleMessage(Message message) {
+ Log.i(TAG, "handleMessage:" + message.what);
+ switch (message.what) {
+ case RENDER_LISTVIEW:
+ renderListView((ArrayList<ContactAttributes>) message.obj);
+ break;
+ case SHOW_TOAST:
+ Toast.makeText(ContactListActivity.this, message.obj.toString(),
+ Toast.LENGTH_SHORT).show();
+ break;
+ default:
+ Log.i(TAG, "unknown msg:" + message.what);
+ }
+ }
+ };
+ initListView();
+ mSummaryObserver = new SummaryObserver(mHandler);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Log.i(TAG, "onStart");
+ initSipDelegate();
+ querySummaryData();
+ getContentResolver().registerContentObserver(ChatProvider.SUMMARY_URI, false,
+ mSummaryObserver);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.i(TAG, "onStop");
+ getContentResolver().unregisterContentObserver(mSummaryObserver);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "onDestroy");
+ dispose();
+ }
+
+ private void dispose() {
+ int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ Log.e(TAG, "invalid subId:" + subId);
+ return;
+ }
+ ChatManager chatManager = ChatManager.getInstance(this, subId);
+ chatManager.deregister();
+ }
+
+ private void initListView() {
+ Log.i(TAG, "initListView");
+ mListview = findViewById(R.id.listview);
+
+ mAdapter = new ArrayAdapter<ContactAttributes>(this,
+ android.R.layout.simple_list_item_2,
+ android.R.id.text1) {
+ @Override
+ public View getView(int pos, View convert, ViewGroup group) {
+ View v = super.getView(pos, convert, group);
+ TextView t1 = (TextView) v.findViewById(android.R.id.text1);
+ TextView t2 = (TextView) v.findViewById(android.R.id.text2);
+ t1.setText(getItem(pos).phoneNumber);
+ t2.setText(getItem(pos).message);
+ if (!getItem(pos).isRead) {
+ t1.setTypeface(null, Typeface.BOLD);
+ t2.setTypeface(null, Typeface.BOLD);
+ }
+ return v;
+ }
+ };
+ mListview.setAdapter(mAdapter);
+ }
+
+ private void querySummaryData() {
+ mSingleThread.execute(() -> {
+ Cursor cursor = getContentResolver().query(ChatProvider.SUMMARY_URI,
+ new String[]{ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER,
+ ChatProvider.SummaryColumns.LATEST_MESSAGE,
+ ChatProvider.SummaryColumns.IS_READ}, null, null, null);
+
+ ArrayList<ContactAttributes> contactList = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ String phoneNumber = getPhoneNumber(cursor);
+ String latestMessage = getLatestMessage(cursor);
+ boolean isRead = getIsRead(cursor);
+ contactList.add(new ContactAttributes(phoneNumber, latestMessage, isRead));
+ }
+ mHandler.sendMessage(mHandler.obtainMessage(RENDER_LISTVIEW, contactList));
+ cursor.close();
+ });
+ }
+
+ private void renderListView(ArrayList<ContactAttributes> contactList) {
+ mAdapter.clear();
+ mAdapter.addAll(contactList);
+ mListview.setOnItemClickListener((parent, view, position, id) -> {
+ Intent intent = new Intent(ContactListActivity.this, ChatActivity.class);
+ intent.putExtra(ChatActivity.EXTRA_REMOTE_PHONE_NUMBER,
+ contactList.get(position).phoneNumber);
+ ContactListActivity.this.startActivity(intent);
+ });
+
+ }
+
+ private void initSipDelegate() {
+ int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ Log.e(TAG, "invalid subId:" + subId);
+ return;
+ }
+ Log.i(TAG, "initSipDelegate");
+ ChatManager chatManager = ChatManager.getInstance(this, subId);
+ chatManager.setRcsStateChangedCallback((oldState, newState) -> {
+ //Show toast when provisioning or registration is done.
+ if (newState == State.REGISTERING) {
+ mHandler.sendMessage(mHandler.obtainMessage(SHOW_TOAST,
+ ContactListActivity.this.getResources().getString(
+ R.string.provisioning_done)));
+ } else if (newState == State.REGISTERED) {
+ mHandler.sendMessage(mHandler.obtainMessage(SHOW_TOAST,
+ ContactListActivity.this.getResources().getString(
+ R.string.registration_done)));
+ }
+
+ });
+ chatManager.register();
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ private String getPhoneNumber(Cursor cursor) {
+ return cursor.getString(
+ cursor.getColumnIndex(ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER));
+ }
+
+ private String getLatestMessage(Cursor cursor) {
+ return cursor.getString(cursor.getColumnIndex(ChatProvider.SummaryColumns.LATEST_MESSAGE));
+ }
+
+ private boolean getIsRead(Cursor cursor) {
+ return 1 == cursor.getInt(cursor.getColumnIndex(ChatProvider.SummaryColumns.IS_READ));
+ }
+
+ class ContactAttributes {
+ public String phoneNumber;
+ public String message;
+ public boolean isRead;
+
+ ContactAttributes(String phoneNumber, String message, boolean isRead) {
+ this.phoneNumber = phoneNumber;
+ this.message = message;
+ this.isRead = isRead;
+ }
+ }
+
+ private class SummaryObserver extends ContentObserver {
+ SummaryObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ querySummaryData();
+ }
+ }
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/DelegateActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/DelegateActivity.java
new file mode 100644
index 0000000..519e610
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/DelegateActivity.java
@@ -0,0 +1,404 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.SipDelegateConnection;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.telephony.ims.stub.DelegateConnectionMessageCallback;
+import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.MenuItem;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** An activity to verify SipDelegate creation and destruction. */
+public class DelegateActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestRcsApp.DelegateActivity";
+ private static final String ICSI = "+g.3gpp.icsi-ref=";
+ private static final String IARI = "+g.3gpp.iari-ref=";
+
+ //https://www.gsma.com/futurenetworks/wp-content/uploads/2019/10/RCC.07-v11.0.pdf
+ private static final String SESSION_TAG =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+ private static final String STANDALONE_PAGER =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.msg\"";
+ private static final String STANDALONE_LARGE =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.largemsg\"";
+ private static final String STANDALONE_DEFERRED =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.deferred\"";
+ private static final String STANDALONE_LARGE_PAGER = "+g.gsma.rcs.cpm.pager-large";
+
+
+ private static final String FILE_TRANSFER =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+ private static final String GEOLOCATION_SMS =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.geosms\"";
+
+ private static final String CHATBOT_SESSION =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot\"";
+ private static final String CHATBOT_STANDALONE =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot.sa\"";
+ private static final String CHATBOT_VERSION = "+g.gsma.rcs.botversion=\"#=1,#=2\"";
+
+
+ private static final int MSG_RESULT = 1;
+ private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+ // Callback for incoming messages on the modem connection
+ private final DelegateConnectionMessageCallback mMessageCallback =
+ new DelegateConnectionMessageCallback() {
+ @Override
+ public void onMessageReceived(@NonNull SipMessage message) {
+ Log.i(TAG, "onMessageReceived:" + message);
+ }
+
+ @Override
+ public void onMessageSendFailure(@NonNull String viaTransactionId, int reason) {
+ Log.i(TAG, "onMessageSendFailure, viaTransactionId:" + viaTransactionId
+ + " reason:" + reason);
+ }
+
+ @Override
+ public void onMessageSent(@NonNull String viaTransactionId) {
+ Log.i(TAG, "onMessageSent, viaTransactionId:" + viaTransactionId);
+ }
+
+ };
+ private String mCallbackResultStr = "";
+ private int mDefaultSmsSubId;
+ private SipDelegateManager mSipDelegateManager;
+ private SipDelegateConnection mSipDelegateConnection;
+ private Button mInitButton;
+ private Button mDestroyButton;
+ private TextView mCallbackResult;
+ private CheckBox mChatCb, mStandalonePagerCb, mStandaloneLargeCb, mStandaloneDeferredCb,
+ mStandaloneLargePagerCb, mFileTransferCb, mGeolocationSmsCb, mChatbotSessionCb,
+ mChatbotStandaloneCb, mChatbotVersionCb;
+ private Handler mHandler;
+ private final DelegateConnectionStateCallback mConnectionCallback =
+ new DelegateConnectionStateCallback() {
+
+ @Override
+ public void onCreated(SipDelegateConnection c) {
+ mSipDelegateConnection = c;
+ mCallbackResultStr += "onCreated\r\n\r\n";
+ Log.i(TAG, mCallbackResultStr);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT));
+ }
+
+ @Override
+ public void onImsConfigurationChanged(
+ SipDelegateImsConfiguration registeredSipConfig) {
+ mCallbackResultStr += "onImsConfigurationChanged SipDelegateImsConfiguration:"
+ + registeredSipConfig + "\r\n\r\n";
+ Log.i(TAG, mCallbackResultStr);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT));
+ dumpConfig(registeredSipConfig);
+ }
+
+ @Override
+ public void onFeatureTagStatusChanged(
+ @NonNull DelegateRegistrationState registrationState,
+ @NonNull Set<FeatureTagState> deniedFeatureTags) {
+ StringBuilder stringBuilder = new StringBuilder(
+ "onFeatureTagStatusChanged ").append(
+ " deniedFeatureTags:[");
+ Iterator<FeatureTagState> iterator = deniedFeatureTags.iterator();
+ while (iterator.hasNext()) {
+ FeatureTagState featureTagState = iterator.next();
+ stringBuilder.append(featureTagState.getFeatureTag()).append(" ").append(
+ featureTagState.getState());
+ }
+ Set<String> registeredFt = registrationState.getRegisteredFeatureTags();
+ Iterator<String> iteratorStr = registeredFt.iterator();
+ stringBuilder.append("] registeredFT:[");
+ while (iteratorStr.hasNext()) {
+ String ft = iteratorStr.next();
+ stringBuilder.append(ft).append(" ");
+ }
+ stringBuilder.append("]\r\n\r\n");
+ mCallbackResultStr += stringBuilder.toString();
+ Log.i(TAG, mCallbackResultStr);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT));
+ }
+
+ @Override
+ public void onDestroyed(int reason) {
+ mCallbackResultStr = "onDestroyed reason:" + reason;
+ Log.i(TAG, mCallbackResultStr);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT));
+ }
+ };
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.delegate_layout);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+ mHandler = new Handler() {
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RESULT:
+ mCallbackResult.setText(mCallbackResultStr);
+ break;
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ init();
+ }
+
+ private void init() {
+ mInitButton = findViewById(R.id.init_btn);
+ mDestroyButton = findViewById(R.id.destroy_btn);
+ mCallbackResult = findViewById(R.id.delegate_callback_result);
+ mChatCb = findViewById(R.id.chat);
+ mStandalonePagerCb = findViewById(R.id.standalone_pager);
+ mStandaloneLargeCb = findViewById(R.id.standalone_large);
+ mStandaloneDeferredCb = findViewById(R.id.standalone_deferred);
+ mStandaloneLargePagerCb = findViewById(R.id.standalone_pager_large);
+
+ mFileTransferCb = findViewById(R.id.file_transfer);
+ mGeolocationSmsCb = findViewById(R.id.geolocation_sms);
+ mChatbotSessionCb = findViewById(R.id.chatbot_session);
+ mChatbotStandaloneCb = findViewById(R.id.chatbot_standalone);
+ mChatbotVersionCb = findViewById(R.id.chatbot_version);
+
+ mChatCb.setChecked(true);
+
+ mDefaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
+ mCallbackResult.setMovementMethod(new ScrollingMovementMethod());
+
+ ImsManager imsManager = this.getSystemService(ImsManager.class);
+ if (SubscriptionManager.isValidSubscriptionId(mDefaultSmsSubId)) {
+ mSipDelegateManager = imsManager.getSipDelegateManager(mDefaultSmsSubId);
+ }
+ setClickable(mDestroyButton, false);
+
+ mInitButton.setOnClickListener(view -> {
+ mCallbackResultStr = "";
+ if (mSipDelegateManager != null) {
+ Set<String> featureTags = getFeatureTags();
+ try {
+ Log.i(TAG, "createSipDelegate");
+ dumpFt(featureTags);
+ mSipDelegateManager.createSipDelegate(new DelegateRequest(featureTags),
+ mExecutorService, mConnectionCallback, mMessageCallback);
+ } catch (ImsException e) {
+ //e.printStackTrace();
+ mCallbackResult.setText(e.toString());
+ Log.e(TAG, e.toString());
+ }
+ setClickable(mInitButton, false);
+ setClickable(mDestroyButton, true);
+ }
+ });
+
+ mDestroyButton.setOnClickListener(view -> {
+ mCallbackResultStr = "";
+ if (mSipDelegateManager != null && mSipDelegateConnection != null) {
+ Log.i(TAG, "destroySipDelegate");
+ mSipDelegateManager.destroySipDelegate(mSipDelegateConnection,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ setClickable(mInitButton, true);
+ setClickable(mDestroyButton, false);
+ }
+ });
+ }
+
+ private Set<String> getFeatureTags() {
+ HashSet<String> fts = new HashSet<>();
+ if (mChatCb.isChecked()) {
+ fts.add(SESSION_TAG);
+ }
+ if (mStandalonePagerCb.isChecked()) {
+ fts.add(STANDALONE_PAGER);
+ }
+ if (mStandaloneLargeCb.isChecked()) {
+ fts.add(STANDALONE_LARGE);
+ }
+ if (mStandaloneDeferredCb.isChecked()) {
+ fts.add(STANDALONE_DEFERRED);
+ }
+ if (mStandaloneLargePagerCb.isChecked()) {
+ fts.add(STANDALONE_LARGE_PAGER);
+ }
+ if (mFileTransferCb.isChecked()) {
+ fts.add(FILE_TRANSFER);
+ }
+ if (mGeolocationSmsCb.isChecked()) {
+ fts.add(GEOLOCATION_SMS);
+ }
+ if (mChatbotSessionCb.isChecked()) {
+ fts.add(CHATBOT_SESSION);
+ }
+ if (mChatbotStandaloneCb.isChecked()) {
+ fts.add(CHATBOT_STANDALONE);
+ }
+ if (mChatbotVersionCb.isChecked()) {
+ fts.add(CHATBOT_VERSION);
+ }
+ return fts;
+ }
+
+ private void dumpFt(Set<String> fts) {
+ Iterator<String> iterator = fts.iterator();
+ StringBuilder res = new StringBuilder();
+ while (iterator.hasNext()) {
+ res.append(iterator.next()).append("\r\n");
+ }
+ Log.i(TAG, "FeatureTag: " + res.toString());
+ }
+
+ private void setClickable(Button button, boolean clickable) {
+ if (clickable) {
+ button.setAlpha(1);
+ button.setClickable(true);
+ } else {
+ button.setAlpha(.5f);
+ button.setClickable(false);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mSipDelegateManager != null && mSipDelegateConnection != null) {
+ Log.i(TAG, "onStop() destroySipDelegate");
+ mSipDelegateManager.destroySipDelegate(mSipDelegateConnection,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ setClickable(mInitButton, true);
+ setClickable(mDestroyButton, false);
+ }
+
+ }
+
+ private void dumpConfig(SipDelegateImsConfiguration config) {
+ Log.i(TAG, "KEY_SIP_CONFIG_TRANSPORT_TYPE_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_TRANSPORT_TYPE_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_USER_ID_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_USER_ID_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PRIVATE_USER_ID_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PRIVATE_USER_ID_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_HOME_DOMAIN_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_HOME_DOMAIN_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_IMEI_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IMEI_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_IPTYPE_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IPTYPE_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_DEFAULT_IPADDRESS_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_DEFAULT_IPADDRESS_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_DEFAULT_IPADDRESS_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_DEFAULT_IPADDRESS_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_IPADDRESS_WITH_NAT_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_IPADDRESS_WITH_NAT_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_GRUU_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_GRUU_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_AUTHENTICATION_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_AUTHENTICATION_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_AUTHENTICATION_NONCE_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_AUTHENTICATION_NONCE_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVICE_ROUTE_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVICE_ROUTE_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_SECURITY_VERIFY_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SECURITY_VERIFY_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_PATH_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_PATH_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_URI_USER_PART_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_URI_USER_PART_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_P_ACCESS_NETWORK_INFO_HEADER_STRING:"
+ + config.getString(SipDelegateImsConfiguration
+ .KEY_SIP_CONFIG_P_ACCESS_NETWORK_INFO_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_P_LAST_ACCESS_NETWORK_INFO_HEADER_STRING:"
+ + config.getString(SipDelegateImsConfiguration
+ .KEY_SIP_CONFIG_P_LAST_ACCESS_NETWORK_INFO_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_P_ASSOCIATED_URI_HEADER_STRING:"
+ + config.getString(SipDelegateImsConfiguration
+ .KEY_SIP_CONFIG_P_ASSOCIATED_URI_HEADER_STRING));
+
+ Log.i(TAG, "KEY_SIP_CONFIG_MAX_PAYLOAD_SIZE_ON_UDP_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_MAX_PAYLOAD_SIZE_ON_UDP_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_DEFAULT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_DEFAULT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_DEFAULT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_DEFAULT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_PORT_WITH_NAT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_PORT_WITH_NAT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_IPSEC_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_IPSEC_CLIENT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_IPSEC_SERVER_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_IPSEC_SERVER_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_IPSEC_OLD_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_IPSEC_OLD_CLIENT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_IPSEC_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_IPSEC_CLIENT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_IPSEC_SERVER_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_IPSEC_SERVER_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_IPSEC_OLD_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_IPSEC_OLD_CLIENT_PORT_INT, -99));
+
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_COMPACT_FORM_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_COMPACT_FORM_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_KEEPALIVE_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_KEEPALIVE_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_NAT_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_NAT_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_GRUU_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_GRUU_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_IPSEC_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_IPSEC_ENABLED_BOOL, false));
+ }
+}
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
new file mode 100644
index 0000000..5b889fb
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/GbaActivity.java
@@ -0,0 +1,289 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.TelephonyManager;
+import android.telephony.TelephonyManager.BootstrapAuthenticationCallback;
+import android.telephony.gba.UaSecurityProtocolIdentifier;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** An activity to verify GBA authentication. */
+public class GbaActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestRcsApp.GbaActivity";
+ private static final int MSG_RESULT = 1;
+ private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+ private Button mGbaButton;
+ private TextView mCallbackResult;
+ private Spinner mOrganizationSpinner, mProtocolSpinner, mUiccSpinner;
+ private EditText mTlsCs;
+ private EditText mNaf;
+ private Handler mHandler;
+ private int mOrganization;
+ private int mProtocol;
+ private int mUiccType;
+
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte aByte : bytes) {
+ result.append(String.format(Locale.US, "%02X", aByte));
+ }
+ return result.toString();
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.gba_layout);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+ initLayout();
+ mHandler = new Handler() {
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RESULT:
+ mCallbackResult.setText(message.obj.toString());
+ break;
+ }
+ }
+ };
+ }
+
+ private void initLayout() {
+ mGbaButton = findViewById(R.id.gba_btn);
+ mCallbackResult = findViewById(R.id.gba_result);
+ mCallbackResult.setMovementMethod(new ScrollingMovementMethod());
+ mTlsCs = findViewById(R.id.tls_id);
+ mNaf = findViewById(R.id.naf_url);
+
+ initOrganization();
+ initProtocol();
+ initUicctype();
+
+ mGbaButton.setOnClickListener(view -> {
+ Log.i(TAG, "trigger bootstrapAuthenticationRequest");
+ UaSecurityProtocolIdentifier.Builder builder =
+ new UaSecurityProtocolIdentifier.Builder();
+ try {
+ builder.setOrg(mOrganization)
+ .setProtocol(mProtocol)
+ .setTlsCipherSuite(Integer.parseInt(mTlsCs.getText().toString()));
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, e.getMessage());
+ return;
+ }
+ UaSecurityProtocolIdentifier spId = builder.build();
+ TelephonyManager telephonyManager = this.getSystemService(TelephonyManager.class);
+ telephonyManager.bootstrapAuthenticationRequest(mUiccType,
+ Uri.parse(mNaf.getText().toString()),
+ spId,
+ true,
+ mExecutorService,
+ new BootstrapAuthenticationCallback() {
+ @Override
+ public void onKeysAvailable(byte[] gbaKey, String btId) {
+ String result = "OnKeysAvailable key:" + bytesToHex(gbaKey)
+ + "\r\n btId:" + btId;
+ Log.i(TAG, result);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT, result));
+ }
+
+ @Override
+ public void onAuthenticationFailure(int reason) {
+ String result = "OnAuthenticationFailure reason: " + reason;
+ Log.i(TAG, result);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT, result));
+ }
+ });
+ });
+ }
+
+ private void initOrganization() {
+ mOrganizationSpinner = findViewById(R.id.organization_list);
+ ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
+ R.array.organization, android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mOrganizationSpinner.setAdapter(adapter);
+ mOrganizationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ Log.i(TAG, "Organization position:" + position);
+ switch (position) {
+ case 0:
+ mOrganization = UaSecurityProtocolIdentifier.ORG_NONE;
+ break;
+ case 1:
+ mOrganization = UaSecurityProtocolIdentifier.ORG_3GPP;
+ break;
+ case 2:
+ mOrganization = UaSecurityProtocolIdentifier.ORG_3GPP2;
+ break;
+ case 3:
+ mOrganization = UaSecurityProtocolIdentifier.ORG_GSMA;
+ break;
+ case 4:
+ mOrganization = UaSecurityProtocolIdentifier.ORG_LOCAL;
+ break;
+ default:
+ Log.e(TAG, "invalid position:" + position);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // TODO Auto-generated method stub
+ }
+ });
+ mOrganizationSpinner.setSelection(1);
+ }
+
+ private void initProtocol() {
+ mProtocolSpinner = findViewById(R.id.protocol_list);
+ ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
+ R.array.protocol, android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mProtocolSpinner.setAdapter(adapter);
+ mProtocolSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ Log.i(TAG, "Protocol position:" + position);
+ switch (position) {
+ case 0:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_SUBSCRIBER_CERTIFICATE;
+ break;
+ case 1:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_MBMS;
+ break;
+ case 2:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_HTTP_DIGEST_AUTHENTICATION;
+ break;
+ case 3:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_HTTP_BASED_MBMS;
+ break;
+ case 4:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_SIP_BASED_MBMS;
+ break;
+ case 5:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_GENERIC_PUSH_LAYER;
+ break;
+ case 6:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_IMS_MEDIA_PLANE;
+ break;
+ case 7:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_GENERATION_TMPI;
+ break;
+ case 8:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_TLS_DEFAULT;
+ break;
+ case 9:
+ mProtocol = UaSecurityProtocolIdentifier
+ .UA_SECURITY_PROTOCOL_3GPP_TLS_BROWSER;
+ break;
+ default:
+ Log.e(TAG, "invalid position:" + position);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // TODO Auto-generated method stub
+ }
+ });
+ mProtocolSpinner.setSelection(8);
+ }
+
+ private void initUicctype() {
+ mUiccSpinner = findViewById(R.id.uicc_list);
+ ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
+ R.array.uicc_type, android.R.layout.simple_spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUiccSpinner.setAdapter(adapter);
+ mUiccSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ Log.i(TAG, "uicc position:" + position);
+ switch (position) {
+ case 0:
+ mUiccType = TelephonyManager.APPTYPE_UNKNOWN;
+ break;
+ case 1:
+ mUiccType = TelephonyManager.APPTYPE_SIM;
+ break;
+ case 2:
+ mUiccType = TelephonyManager.APPTYPE_USIM;
+ break;
+ case 3:
+ mUiccType = TelephonyManager.APPTYPE_RUIM;
+ break;
+ case 4:
+ mUiccType = TelephonyManager.APPTYPE_CSIM;
+ break;
+ case 5:
+ mUiccType = TelephonyManager.APPTYPE_ISIM;
+ break;
+ default:
+ Log.e(TAG, "invalid position:" + position);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // TODO Auto-generated method stub
+ }
+ });
+ mUiccSpinner.setSelection(5);
+ }
+
+ @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/MainActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/MainActivity.java
new file mode 100644
index 0000000..04fdb5b
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/MainActivity.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+
+package com.google.android.sample.rcsclient;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.widget.Button;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/** An activity to show function list. */
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "TestRcsApp.MainActivity";
+ private Button mProvisionButton;
+ private Button mDelegateButton;
+ private Button mUceButton;
+ private Button mGbaButton;
+ private Button mMessageClientButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+ mProvisionButton = (Button) this.findViewById(R.id.provision);
+ mDelegateButton = (Button) this.findViewById(R.id.delegate);
+ mMessageClientButton = (Button) this.findViewById(R.id.msgClient);
+ mUceButton = (Button) this.findViewById(R.id.uce);
+ mGbaButton = (Button) this.findViewById(R.id.gba);
+ mProvisionButton.setOnClickListener(view -> {
+ Intent intent = new Intent(this, ProvisioningActivity.class);
+ MainActivity.this.startActivity(intent);
+ });
+
+ mDelegateButton.setOnClickListener(view -> {
+ Intent intent = new Intent(this, DelegateActivity.class);
+ MainActivity.this.startActivity(intent);
+ });
+
+ mUceButton.setOnClickListener(view -> {
+ Intent intent = new Intent(this, UceActivity.class);
+ MainActivity.this.startActivity(intent);
+ });
+
+ mGbaButton.setOnClickListener(view -> {
+ Intent intent = new Intent(this, GbaActivity.class);
+ MainActivity.this.startActivity(intent);
+ });
+ mMessageClientButton.setOnClickListener(view -> {
+ Intent intent = new Intent(this, ContactListActivity.class);
+ MainActivity.this.startActivity(intent);
+ });
+ }
+
+ @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/PhoneNumberActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/PhoneNumberActivity.java
new file mode 100644
index 0000000..a277994
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/PhoneNumberActivity.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuItem;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/** An activity to let user input phone number to chat. */
+public class PhoneNumberActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestRcsApp.PhoneNumberActivity";
+ private Button mChatButton;
+ private EditText mPhoneNumber;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.number_to_chat);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+ mChatButton = this.findViewById(R.id.launch_chat_btn);
+ mPhoneNumber = findViewById(R.id.destNum);
+ mChatButton.setOnClickListener(view -> {
+ Intent intent = new Intent(PhoneNumberActivity.this, ChatActivity.class);
+ intent.putExtra(ChatActivity.EXTRA_REMOTE_PHONE_NUMBER,
+ mPhoneNumber.getText().toString());
+ PhoneNumberActivity.this.startActivity(intent);
+
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.i(TAG, "onStop");
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onStop();
+ Log.i(TAG, "onDestroy");
+ }
+}
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
new file mode 100644
index 0000000..da0cf39
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ProvisioningActivity.java
@@ -0,0 +1,239 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.ProvisioningManager.RcsProvisioningCallback;
+import android.telephony.ims.RcsClientConfiguration;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.MenuItem;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+/** An activity to verify provisioning. */
+public class ProvisioningActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestRcsApp.ProvisioningActivity";
+ private static final int MSG_RESULT = 1;
+ private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+ private int mDefaultSmsSubId;
+ private ProvisioningManager mProvisioningManager;
+ private Button mRegisterButton;
+ private Button mUnRegisterButton;
+ private Button mIsCapableButton;
+ private TextView mSingleRegResult;
+ private TextView mCallbackResult;
+ private SingleRegCapabilityReceiver mSingleRegCapabilityReceiver;
+ private boolean mIsRegistered = false;
+ private Handler mHandler;
+ private RcsProvisioningCallback mCallback =
+ new RcsProvisioningCallback() {
+ @Override
+ public void onConfigurationChanged(@NonNull byte[] configXml) {
+ String configResult = new String(configXml);
+ Log.i(TAG, "RcsProvisioningCallback.onConfigurationChanged called with xml:");
+ Log.i(TAG, configResult);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT,
+ "onConfigurationChanged \r\n" + configResult));
+ }
+
+ @Override
+ public void onConfigurationReset() {
+ Log.i(TAG, "RcsProvisioningCallback.onConfigurationReset called.");
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_RESULT, "onConfigurationReset"));
+ }
+
+ @Override
+ public void onRemoved() {
+ Log.i(TAG, "RcsProvisioningCallback.onRemoved called.");
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_RESULT, "onRemoved"));
+ }
+ };
+
+ // Static configuration.
+ private static RcsClientConfiguration getDefaultClientConfiguration() {
+ return new RcsClientConfiguration(
+ /*rcsVersion=*/ "6.0",
+ /*rcsProfile=*/ "UP_2.3",
+ /*clientVendor=*/ "Goog",
+ /*clientVersion=*/ "RCSAndrd-1.0");
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.provision_layout);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+ mSingleRegCapabilityReceiver = new SingleRegCapabilityReceiver();
+ this.registerReceiver(mSingleRegCapabilityReceiver, new IntentFilter(
+ ProvisioningManager.ACTION_RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE));
+ mHandler = new Handler() {
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RESULT:
+ mCallbackResult.setText(message.obj.toString());
+ break;
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mDefaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
+ Log.i(TAG, "defaultSmsSubId:" + mDefaultSmsSubId);
+ if (isValidSubscriptionId(mDefaultSmsSubId)) {
+ mProvisioningManager = ProvisioningManager.createForSubscriptionId(mDefaultSmsSubId);
+ init();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ this.unregisterReceiver(mSingleRegCapabilityReceiver);
+ if (mIsRegistered) {
+ mProvisioningManager.unregisterRcsProvisioningChangedCallback(mCallback);
+ }
+ }
+
+ private void init() {
+ mRegisterButton = findViewById(R.id.provisioning_register_btn);
+ mUnRegisterButton = findViewById(R.id.provisioning_unregister_btn);
+ mIsCapableButton = findViewById(R.id.provisioning_singlereg_btn);
+ mSingleRegResult = findViewById(R.id.provisioning_singlereg_result);
+ mCallbackResult = findViewById(R.id.provisioning_callback_result);
+ mCallbackResult.setMovementMethod(new ScrollingMovementMethod());
+
+ boolean isSingleRegCapable = false;
+ try {
+ mProvisioningManager.isRcsVolteSingleRegistrationCapable();
+ } catch (ImsException e) {
+ Log.i(TAG, e.getMessage());
+ }
+ if (isSingleRegCapable && !mIsRegistered) {
+ setClickable(mRegisterButton, true);
+ }
+
+ mRegisterButton.setOnClickListener(view -> {
+ if (mProvisioningManager != null) {
+ Log.i(TAG, "Using configuration: " + getDefaultClientConfiguration());
+ try {
+ Log.i(TAG, "setRcsClientConfiguration()");
+ Log.i(TAG, "registerRcsProvisioningChangedCallback()");
+ mProvisioningManager.setRcsClientConfiguration(getDefaultClientConfiguration());
+ mProvisioningManager.registerRcsProvisioningChangedCallback(mExecutorService,
+ mCallback);
+ mIsRegistered = true;
+ } catch (ImsException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ setClickable(mRegisterButton, false);
+ setClickable(mUnRegisterButton, true);
+ } else {
+ Log.i(TAG, "provisioningManager null");
+ }
+ });
+ mUnRegisterButton.setOnClickListener(view -> {
+ if (mProvisioningManager != null) {
+ mProvisioningManager.unregisterRcsProvisioningChangedCallback(mCallback);
+ setClickable(mRegisterButton, false);
+ setClickable(mRegisterButton, true);
+ mIsRegistered = false;
+ }
+ });
+ mIsCapableButton.setOnClickListener(view -> {
+ if (mProvisioningManager != null) {
+ try {
+ boolean capable = mProvisioningManager.isRcsVolteSingleRegistrationCapable();
+ mSingleRegResult.setText(String.valueOf(capable));
+ Log.i(TAG, "isRcsVolteSingleRegistrationCapable:" + capable);
+ } catch (ImsException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+ });
+ }
+
+ private void setClickable(Button button, boolean clickable) {
+ if (clickable) {
+ button.setAlpha(1);
+ button.setClickable(true);
+ } else {
+ button.setAlpha(.5f);
+ button.setClickable(false);
+ }
+ }
+
+ private boolean isValidSubscriptionId(int subId) {
+ return SubscriptionManager.isValidSubscriptionId(mDefaultSmsSubId);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ class SingleRegCapabilityReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ Log.i(TAG, "onReceive action:" + action);
+ if (ProvisioningManager.ACTION_RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE.equals(
+ action)) {
+ int status = intent.getIntExtra(ProvisioningManager.EXTRA_STATUS,
+ ProvisioningManager.STATUS_DEVICE_NOT_CAPABLE);
+ Log.i(TAG, "singleRegCap status:" + status);
+ if (mRegisterButton != null && !mIsRegistered) {
+ if (status == ProvisioningManager.STATUS_DEVICE_NOT_CAPABLE) {
+ setClickable(mRegisterButton, true);
+ } else {
+ setClickable(mRegisterButton, false);
+ }
+ }
+
+ }
+ }
+ }
+
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/RcsStateChangedCallback.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/RcsStateChangedCallback.java
new file mode 100644
index 0000000..fd36f01
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/RcsStateChangedCallback.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClient.State;
+
+/** A callback used to notify RCS state change. */
+public interface RcsStateChangedCallback {
+ /** callback for RCS state change. */
+ void notifyStateChange(State oldState, State newState);
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/SessionStateCallback.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/SessionStateCallback.java
new file mode 100644
index 0000000..3881775
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/SessionStateCallback.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+/** A callback used to inform chat session creation result. */
+public interface SessionStateCallback {
+ /** callback for successful session creation */
+ void onSuccess();
+
+ /**callback for failed session creation. */
+ void onFailure();
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/UceActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/UceActivity.java
new file mode 100644
index 0000000..04efb6e
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/UceActivity.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.telephony.SmsManager;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+//import android.telephony.ims.RcsUceAdapter.CapabilitiesCallback;
+
+/** An activity to verify UCE. */
+public class UceActivity extends AppCompatActivity {
+
+ private static final String TAG = "TestRcsApp.UceActivity";
+ private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+ private Button mCapabilityButton;
+ private Button mAvailabilityButton;
+ private TextView mCapabilityResult;
+ private TextView mAvailabilityResult;
+ private EditText mNumbers;
+ private int mDefaultSmsSubId;
+ private ImsRcsManager mImsRcsManager;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.uce_layout);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+
+ initLayout();
+ }
+
+ private void initLayout() {
+ mDefaultSmsSubId = SmsManager.getDefaultSmsSubscriptionId();
+
+ mCapabilityButton = findViewById(R.id.capability_btn);
+ mAvailabilityButton = findViewById(R.id.availability_btn);
+ mCapabilityResult = findViewById(R.id.capability_result);
+ mAvailabilityResult = findViewById(R.id.capability_result);
+
+ List<Uri> contactList = getContectList();
+ mImsRcsManager = getImsRcsManager(mDefaultSmsSubId);
+// mCapabilityButton.setOnClickListener(view -> {
+// if(contactList.size() == 0) {
+// Log.i(TAG, "empty contact list");
+// return;
+// }
+// mImsRcsManager.getUceAdapter().requestCapabilities(mExecutorService, contactList,
+// new CapabilitiesCallback() {
+// public void onCapabilitiesReceived(
+// @NonNull List<RcsContactUceCapability> contactCapabilities) {
+// Log.i(TAG, "onCapabilitiesReceived()");
+// mCapabilityResult.setText("onCapabilitiesReceived()");
+// }
+//
+// public void onComplete() {
+// Log.i(TAG, "onComplete()");
+// mCapabilityResult.setText("onComplete()");
+//
+// }
+//
+// public void onError(int errorCode, long retryAfterMilliseconds) {
+// Log.i(TAG, "onError() errorCode:" + errorCode + " retryAfterMs:"
+// + retryAfterMilliseconds);
+// mCapabilityResult.setText("onError() errorCode:" + errorCode
+// + " retryAfterMs:" + retryAfterMilliseconds);
+// }
+// });
+// });
+//
+// mAvailabilityButton.setOnClickListener(view -> {
+// if(contactList.size() == 0) {
+// Log.i(TAG, "empty contact list");
+// return;
+// }
+// mImsRcsManager.getUceAdapter().requestNetworkAvailability(mExecutorService,
+// contactList,
+// new CapabilitiesCallback() {
+// public void onCapabilitiesReceived(
+// @NonNull List<RcsContactUceCapability> contactCapabilities) {
+// Log.i(TAG, "onCapabilitiesReceived()");
+// mAvailabilityResult.setText("onCapabilitiesReceived()");
+// }
+//
+// public void onComplete() {
+// Log.i(TAG, "onComplete()");
+// mAvailabilityResult.setText("onComplete()");
+//
+// }
+//
+// public void onError(int errorCode, long retryAfterMilliseconds) {
+// Log.i(TAG, "onError() errorCode:" + errorCode + " retryAfterMs:"
+// + retryAfterMilliseconds);
+// mAvailabilityResult.setText("onError() errorCode:" + errorCode
+// + " retryAfterMs:" + retryAfterMilliseconds);
+// }
+// });
+// });
+ }
+
+ private List<Uri> getContectList() {
+ mNumbers = findViewById(R.id.number_list);
+ String []numbers;
+ ArrayList<Uri> contactList = new ArrayList<>();
+ if (!TextUtils.isEmpty(mNumbers.getText().toString())) {
+ String numberList = mNumbers.getText().toString().trim();
+ numbers = numberList.split(",");
+ for (String number : numbers) {
+ contactList.add(Uri.parse(ChatActivity.TELURI_PREFIX + number));
+ }
+ }
+
+ return contactList;
+ }
+
+ private ImsRcsManager getImsRcsManager(int subId) {
+ ImsManager imsManager = getSystemService(ImsManager.class);
+ if (imsManager != null) {
+ try {
+ return imsManager.getImsRcsManager(subId);
+ } catch (Exception e) {
+ Log.e(TAG, "fail to getImsRcsManager " + e.getMessage());
+ return null;
+ }
+ }
+ return null;
+ }
+
+ @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/util/ChatManager.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
new file mode 100644
index 0000000..9d27fbc
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient.util;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.telephony.ims.ImsManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClient;
+import com.android.libraries.rcs.simpleclient.SimpleRcsClient.State;
+import com.android.libraries.rcs.simpleclient.provisioning.ProvisioningController;
+import com.android.libraries.rcs.simpleclient.provisioning.StaticConfigProvisioningController;
+import com.android.libraries.rcs.simpleclient.registration.RegistrationController;
+import com.android.libraries.rcs.simpleclient.registration.RegistrationControllerImpl;
+import com.android.libraries.rcs.simpleclient.service.chat.MinimalCpmChatService;
+import com.android.libraries.rcs.simpleclient.service.chat.SimpleChatSession;
+
+import com.google.android.sample.rcsclient.RcsStateChangedCallback;
+import com.google.android.sample.rcsclient.SessionStateCallback;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import gov.nist.javax.sip.address.AddressFactoryImpl;
+
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.sip.address.AddressFactory;
+import javax.sip.address.URI;
+
+/**
+ * This class takes advantage of rcs library to manage chat session and send/receive chat message.
+ */
+public class ChatManager {
+ public static final String SELF = "self";
+ private static final String TAG = "TestRcsApp.ChatManager";
+ private static AddressFactory sAddressFactory = new AddressFactoryImpl();
+ private static HashMap<Integer, ChatManager> sChatManagerInstances = new HashMap<>();
+ private final ExecutorService mFixedThreadPool = Executors.newFixedThreadPool(5);
+ private Context mContext;
+ private ProvisioningController mProvisioningController;
+ private RegistrationController mRegistrationController;
+ private MinimalCpmChatService mImsService;
+ private SimpleRcsClient mSimpleRcsClient;
+ private State mState;
+ private int mSubId;
+ private HashMap<URI, SimpleChatSession> mContactSessionMap = new HashMap<>();
+ private RcsStateChangedCallback mRcsStateChangedCallback;
+
+ private ChatManager(Context context, int subId) {
+ mContext = context;
+ mSubId = subId;
+ mProvisioningController = StaticConfigProvisioningController.createForSubscriptionId(subId);
+ ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+ mRegistrationController = new RegistrationControllerImpl(subId, mFixedThreadPool,
+ imsManager);
+ mImsService = new MinimalCpmChatService(context);
+ mSimpleRcsClient = SimpleRcsClient.newBuilder()
+ .registrationController(mRegistrationController)
+ .provisioningController(mProvisioningController)
+ .imsService(mImsService)
+ .executor(mFixedThreadPool).build();
+ mState = State.NEW;
+ // register callback for state change
+ mSimpleRcsClient.onStateChanged((oldState, newState) -> {
+ Log.i(TAG, "notifyStateChange() oldState:" + oldState + " newState:" + newState);
+ mState = newState;
+ mRcsStateChangedCallback.notifyStateChange(oldState, newState);
+ });
+ mImsService.setListener((session) -> {
+ Log.i(TAG, "onIncomingSession()");
+ mContactSessionMap.put(session.getRemoteUri(), session);
+ });
+ }
+
+ /**
+ * Create ChatManager with a specific subId.
+ */
+ public static ChatManager getInstance(Context context, int subId) {
+ synchronized (sChatManagerInstances) {
+ if (sChatManagerInstances.containsKey(subId)) {
+ return sChatManagerInstances.get(subId);
+ }
+ ChatManager chatManager = new ChatManager(context, subId);
+ sChatManagerInstances.put(subId, chatManager);
+ return chatManager;
+ }
+ }
+
+ /**
+ * Try to parse the given uri.
+ *
+ * @throws IllegalArgumentException in case of parsing error.
+ */
+ public static URI createUri(String uri) {
+ try {
+ return sAddressFactory.createURI(uri);
+ } catch (ParseException exception) {
+ throw new IllegalArgumentException("URI cannot be created", exception);
+ }
+ }
+
+ private static String getNumberFromUri(String number) {
+ String[] numberParts = number.split("[@;:]");
+ if (numberParts.length < 2) {
+ return null;
+ }
+ return numberParts[1];
+ }
+
+ /**
+ * set callback for RCS state change.
+ */
+ public void setRcsStateChangedCallback(RcsStateChangedCallback callback) {
+ mRcsStateChangedCallback = callback;
+ }
+
+ /**
+ * Start to register by doing provisioning and creating SipDelegate
+ */
+ public void register() {
+ Log.i(TAG, "do start(), State State = " + mState);
+ if (mState == State.NEW) {
+ mSimpleRcsClient.start();
+ }
+ }
+
+ /**
+ * Deregister chat feature.
+ */
+ public void deregister() {
+ Log.i(TAG, "deregister");
+ sChatManagerInstances.remove(mSubId);
+ mSimpleRcsClient.stop();
+ }
+
+ /**
+ * Initiate 1 to 1 chat session.
+ * @param telUriContact destination tel Uri.
+ * @param callback callback for session state.
+ */
+ public void initChatSession(String telUriContact, SessionStateCallback callback) {
+ if (mState != State.REGISTERED) {
+ Log.i(TAG, "Could not init session due to State = " + mState);
+ return;
+ }
+ URI uri = createUri(telUriContact);
+ if (mContactSessionMap.containsKey(uri)) {
+ callback.onSuccess();
+ }
+ Futures.addCallback(
+ mImsService.startOriginatingChatSession(telUriContact),
+ new FutureCallback<SimpleChatSession>() {
+ @Override
+ public void onSuccess(SimpleChatSession chatSession) {
+ mContactSessionMap.put(chatSession.getRemoteUri(), chatSession);
+ chatSession.setListener(
+ // implement onMessageReceived()
+ (message) -> {
+ mFixedThreadPool.execute(() -> {
+ String msg = message.content();
+ String phoneNumber = getNumberFromUri(
+ chatSession.getRemoteUri().toString());
+ if (TextUtils.isEmpty(phoneNumber)) {
+ Log.i(TAG, "dest number is empty, uri:"
+ + chatSession.getRemoteUri());
+ } else {
+ addNewMessage(msg, phoneNumber, SELF);
+ }
+ });
+
+ });
+ callback.onSuccess();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.onFailure();
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ /**
+ * Send a chat message.
+ * @param telUriContact destination tel Uri.
+ * @param message chat message.
+ */
+ public void sendMessage(String telUriContact, String message) {
+ if (mState != State.REGISTERED) {
+ Log.i(TAG, "Could not send msg due to State = " + mState);
+ return;
+ }
+ SimpleChatSession chatSession = mContactSessionMap.get(createUri(telUriContact));
+ if (chatSession == null) {
+ Log.i(TAG, "session is unavailable for telUriContact = " + telUriContact);
+ return;
+ }
+ chatSession.sendMessage(message);
+ }
+
+ /**
+ * Insert chat information into database.
+ * @param message chat message.
+ * @param src source phone number.
+ * @param dest destination phone number.
+ */
+ public void addNewMessage(String message, String src, String dest) {
+ long currentTime = Instant.now().getEpochSecond();
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ChatProvider.RcsColumns.SRC_PHONE_NUMBER, src);
+ contentValues.put(ChatProvider.RcsColumns.DEST_PHONE_NUMBER, dest);
+ contentValues.put(ChatProvider.RcsColumns.CHAT_MESSAGE, message);
+ contentValues.put(ChatProvider.RcsColumns.MSG_TIMESTAMP, currentTime);
+ contentValues.put(ChatProvider.RcsColumns.IS_READ, Boolean.TRUE);
+ // insert chat table
+ mContext.getContentResolver().insert(ChatProvider.CHAT_URI, contentValues);
+
+ ContentValues summary = new ContentValues();
+ summary.put(ChatProvider.SummaryColumns.LATEST_MESSAGE, message);
+ summary.put(ChatProvider.SummaryColumns.MSG_TIMESTAMP, currentTime);
+ summary.put(ChatProvider.SummaryColumns.IS_READ, Boolean.TRUE);
+
+ String remoteNumber = src.equals(SELF) ? dest : src;
+ if (remoteNumberExists(remoteNumber)) {
+ mContext.getContentResolver().update(ChatProvider.SUMMARY_URI, summary,
+ ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER + "=?",
+ new String[]{remoteNumber});
+ } else {
+ summary.put(ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER, remoteNumber);
+ mContext.getContentResolver().insert(ChatProvider.SUMMARY_URI, summary);
+ }
+ }
+
+ /**
+ * Check if the number exists in the database.
+ */
+ public boolean remoteNumberExists(String number) {
+ Cursor cursor = mContext.getContentResolver().query(ChatProvider.SUMMARY_URI, null,
+ ChatProvider.SummaryColumns.REMOTE_PHONE_NUMBER + "=?", new String[]{number},
+ null);
+ if (cursor != null) {
+ int count = cursor.getCount();
+ return count > 0;
+ }
+ return false;
+ }
+
+}
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java
new file mode 100644
index 0000000..050da1f
--- /dev/null
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatProvider.java
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+package com.google.android.sample.rcsclient.util;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Log;
+
+/** A database to store chat message. */
+public class ChatProvider extends ContentProvider {
+ public static final Uri CHAT_URI = Uri.parse("content://rcsprovider/chat");
+ public static final Uri SUMMARY_URI = Uri.parse("content://rcsprovider/summary");
+ public static final String AUTHORITY = "rcsprovider";
+ private static final String TAG = "TestRcsApp.ChatProvider";
+ private static final int DATABASE_VERSION = 1;
+ private static final String CHAT_TABLE_NAME = "chat";
+ private static final String SUMMARY_TABLE_NAME = "summary";
+
+ private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ private static final int URI_CHAT = 1;
+ private static final int URI_SUMMARY = 2;
+ private static final String QUERY_CHAT_TABLE = " SELECT * FROM " + CHAT_TABLE_NAME;
+
+ static {
+ URI_MATCHER.addURI(AUTHORITY, "chat", URI_CHAT);
+ URI_MATCHER.addURI(AUTHORITY, "summary", URI_SUMMARY);
+ }
+
+ private RcsDatabaseHelper mRcsHelper;
+
+ @Override
+ public boolean onCreate() {
+ mRcsHelper = new RcsDatabaseHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ SQLiteDatabase db = mRcsHelper.getReadableDatabase();
+ int match = URI_MATCHER.match(uri);
+
+ Log.d(TAG, "Query URI: " + match);
+ switch (match) {
+ case URI_CHAT:
+ qb.setTables(CHAT_TABLE_NAME);
+ return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ case URI_SUMMARY:
+ qb.setTables(SUMMARY_TABLE_NAME);
+ return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ default:
+ Log.e(TAG, "no such uri");
+ return null;
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues contentValues) {
+ SQLiteDatabase db = mRcsHelper.getWritableDatabase();
+ int match = URI_MATCHER.match(uri);
+ long id;
+ switch (match) {
+ case URI_CHAT:
+ id = db.insert(CHAT_TABLE_NAME, "", contentValues);
+ break;
+ case URI_SUMMARY:
+ id = db.insert(SUMMARY_TABLE_NAME, "", contentValues);
+ break;
+ default:
+ Log.e(TAG, "no such uri");
+ throw new SQLException("no such uri");
+ }
+ if (id > 0) {
+ Uri msgUri = Uri.withAppendedPath(uri, String.valueOf(id));
+ getContext().getContentResolver().notifyChange(uri, null);
+ Log.i(TAG, "msgUri:" + msgUri);
+ return msgUri;
+ } else {
+ throw new SQLException("fail to add chat message");
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String s, String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues contentValues, String selection,
+ String[] selectionArgs) {
+ SQLiteDatabase db = mRcsHelper.getWritableDatabase();
+ int match = URI_MATCHER.match(uri);
+ int result = 0;
+ String tableName = "";
+ switch (match) {
+ case URI_CHAT:
+ tableName = CHAT_TABLE_NAME;
+ break;
+ case URI_SUMMARY:
+ tableName = SUMMARY_TABLE_NAME;
+ break;
+ }
+ if (!TextUtils.isEmpty(tableName)) {
+ result = db.updateWithOnConflict(tableName, contentValues,
+ selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
+ getContext().getContentResolver().notifyChange(uri, null);
+ Log.d(TAG, "Update uri: " + match + " result: " + result);
+ } else {
+ Log.d(TAG, "Update. Not support URI.");
+ }
+ return result;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ /** Define columns for the chat table. */
+ public static class RcsColumns implements BaseColumns {
+ public static final String SRC_PHONE_NUMBER = "source_phone_number";
+ public static final String DEST_PHONE_NUMBER = "destination_phone_number";
+ public static final String CHAT_MESSAGE = "chat_message";
+ public static final String SUBSCRIPTION_ID = "subscription_id";
+ public static final String MSG_TIMESTAMP = "msg_timestamp";
+ public static final String IS_READ = "is_read";
+ }
+
+ /** Define columns for the summary table. */
+ public static class SummaryColumns implements BaseColumns {
+ public static final String REMOTE_PHONE_NUMBER = "remote_phone_number";
+ public static final String LATEST_MESSAGE = "latest_message";
+ public static final String MSG_TIMESTAMP = "msg_timestamp";
+ public static final String IS_READ = "is_read";
+ }
+
+ /** Database helper */
+ public static final class RcsDatabaseHelper extends SQLiteOpenHelper {
+ public static final String SQL_CREATE_RCS_TABLE = "CREATE TABLE "
+ + CHAT_TABLE_NAME
+ + " ("
+ + RcsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + RcsColumns.SRC_PHONE_NUMBER + " Text DEFAULT NULL, "
+ + RcsColumns.DEST_PHONE_NUMBER + " Text DEFAULT NULL, "
+ + RcsColumns.CHAT_MESSAGE + " Text DEFAULT NULL, "
+ + RcsColumns.MSG_TIMESTAMP + " LONG DEFAULT NULL, "
+ + RcsColumns.IS_READ + " BOOLEAN DEFAULT false);";
+ public static final String SQL_CREATE_SUMMARY_TABLE = "CREATE TABLE "
+ + SUMMARY_TABLE_NAME
+ + " ("
+ + SummaryColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + SummaryColumns.REMOTE_PHONE_NUMBER + " Text DEFAULT NULL, "
+ + SummaryColumns.LATEST_MESSAGE + " Text DEFAULT NULL, "
+ + SummaryColumns.MSG_TIMESTAMP + " LONG DEFAULT NULL, "
+ + SummaryColumns.IS_READ + " BOOLEAN DEFAULT false);";
+ private static final String DB_NAME = "RcsDatabase";
+
+ public RcsDatabaseHelper(Context context) {
+ super(context, DB_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_RCS_TABLE);
+ db.execSQL(SQL_CREATE_SUMMARY_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+ Log.d(TAG, "DB upgrade from " + oldVersion + " to " + newVersion);
+ }
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp b/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp
new file mode 100644
index 0000000..eef34c8
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/Android.bp
@@ -0,0 +1,27 @@
+
+
+android_library {
+ name: "aosp_test_rcs_client_base",
+
+ srcs: ["src/com/android/libraries/rcs/**/*.java"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.concurrent_concurrent-futures",
+ "guava",
+ "nist-sip",
+ ],
+
+ libs: [
+ "auto_value_annotations",
+ ],
+
+ plugins: [
+ "auto_value_plugin",
+ ],
+
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+}
+
+
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/AndroidManifest.xml b/testapps/TestRcsApp/aosp_test_rcsclient/AndroidManifest.xml
new file mode 100644
index 0000000..8fdc894
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //packages/services/Telephony/testapps/TestRcsApp/aosp_test_rcsclient/AndroidManifest.xml
+**
+** Copyright 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.
+*/
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.libraries.rcs.simpleclient"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="23" />
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
+</manifest>
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/LICENSE b/testapps/TestRcsApp/aosp_test_rcsclient/LICENSE
new file mode 100644
index 0000000..b9b9d2a
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/LICENSE
@@ -0,0 +1,176 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunkTest.java b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunkTest.java
new file mode 100644
index 0000000..90f1714
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunkTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MsrpChunkTest {
+
+ private static MsrpChunk writeAndReadChunk(MsrpChunk chunk) throws IOException {
+ ByteArrayOutputStream bo = new ByteArrayOutputStream();
+ MsrpSerializer.serialize(bo, chunk);
+
+ ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
+ return MsrpParser.parse(bi);
+ }
+
+ @Test
+ public void whenSerializeParseRequest_success() throws IOException {
+ MsrpChunk chunk = MsrpChunk.newBuilder()
+ .method(MsrpChunk.Method.SEND)
+ .addHeader("To-Path", "msrp://123.1.11:9/testreceiver;tcp")
+ .addHeader("From-Path", "msrp://123.1.11:9/testsender;tcp")
+ .addHeader("Byte-Range", "1-*/*")
+ .transactionId("123123")
+ .addHeader("Content-Type", "text/plain")
+ .content("Hallo Welt\r\n".getBytes(UTF_8))
+ .continuation(Continuation.COMPLETE)
+ .build();
+
+ MsrpChunk chunk2 = writeAndReadChunk(chunk);
+
+ assertThat(chunk2).isEqualTo(chunk);
+ }
+
+ @Test
+ public void whenSerializeParseEmptyRequest_success() throws IOException {
+ MsrpChunk chunk = MsrpChunk.newBuilder()
+ .method(MsrpChunk.Method.SEND)
+ .transactionId("testtransaction")
+ .addHeader("To-Path", "msrp://123.1.11:9/testreceiver;tcp")
+ .addHeader("From-Path", "msrp://123.1.11:9/testsender;tcp")
+ .continuation(Continuation.COMPLETE)
+ .build();
+
+ MsrpChunk chunk2 = writeAndReadChunk(chunk);
+
+ assertThat(chunk2).isEqualTo(chunk);
+ }
+
+ @Test
+ public void whenSerializeParseResponse_success() throws IOException {
+ MsrpChunk chunk = MsrpChunk.newBuilder()
+ .responseCode(200)
+ .responseReason("OK")
+ .transactionId("testtransaction")
+ .addHeader("To-Path", "msrp://123.1.11:9/testreceiver;tcp")
+ .addHeader("From-Path", "msrp://123.1.11:9/testsender;tcp")
+ .continuation(Continuation.COMPLETE)
+ .build();
+
+ MsrpChunk chunk2 = writeAndReadChunk(chunk);
+
+ assertThat(chunk2).isEqualTo(chunk);
+ }
+
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSessionTest.java b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSessionTest.java
new file mode 100644
index 0000000..dc60d37
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSessionTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation;
+
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ *
+ */
+@RunWith(AndroidJUnit4.class)
+public class MsrpSessionTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private Socket socket;
+
+ @Before
+ public void setUp() throws IOException {
+ PipedInputStream input = new PipedInputStream();
+ when(socket.getInputStream()).thenReturn(input);
+ when(socket.getOutputStream()).thenReturn(new PipedOutputStream(input));
+ when(socket.isConnected()).thenReturn(true);
+ }
+
+ @Test
+ public void foo() throws IOException, ExecutionException, InterruptedException {
+
+ AtomicReference<MsrpChunk> receivedRequest = new AtomicReference<>();
+
+ final MsrpSession session =
+ new MsrpSession(
+ socket,
+ (m) -> {
+ receivedRequest.set(m);
+ });
+
+ MsrpChunk request = generateRequest();
+ Future<MsrpChunk> future = session.send(request);
+
+ Executors.newSingleThreadExecutor().execute(session::run);
+
+ MsrpChunk response = future.get();
+
+ assertThat(request).isEqualTo(receivedRequest.get());
+ assertThat(response).isEqualTo(generateSuccessResponse());
+ }
+
+ private MsrpChunk generateRequest() {
+ return MsrpChunk.newBuilder()
+ .transactionId("txid")
+ .method(MsrpChunk.Method.SEND)
+ .addHeader(MsrpConstants.HEADER_TO_PATH, "msrp://test:1234/sessionA;tcp")
+ .addHeader(MsrpConstants.HEADER_FROM_PATH, "msrp://test:1234/sessionB;tcp")
+ .addHeader(MsrpConstants.HEADER_BYTE_RANGE, "1-*/*")
+ .addHeader(MsrpConstants.HEADER_MESSAGE_ID, "abcde")
+ .addHeader(MsrpConstants.HEADER_CONTENT_TYPE, "text/plain")
+ .content("Hallo Welt\r\n".getBytes(StandardCharsets.UTF_8))
+ .continuation(Continuation.COMPLETE)
+ .build();
+ }
+
+ private MsrpChunk generateSuccessResponse() {
+ return MsrpChunk.newBuilder()
+ .transactionId("txid")
+ .responseCode(200)
+ .responseReason("OK")
+ .addHeader(MsrpConstants.HEADER_TO_PATH, "msrp://test:1234/sessionB;tcp")
+ .addHeader(MsrpConstants.HEADER_FROM_PATH, "msrp://test:1234/sessionA;tcp")
+ .continuation(Continuation.COMPLETE)
+ .build();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/sdp/SimpleSdpMessageTest.java b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/sdp/SimpleSdpMessageTest.java
new file mode 100644
index 0000000..4b5f31a
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/sdp/SimpleSdpMessageTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sdp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import java.io.ByteArrayInputStream;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SimpleSdpMessageTest {
+ private static final String SAMPLE_SDP_REGEX =
+ "v=0\r\n"
+ + "o=TestRcsClient .+ .+ IN IP4 192.168.1.1\r\n"
+ + "s=-\r\n"
+ + "c=IN IP4 192.168.1.1\r\n"
+ + "t=0 0\r\n"
+ + "m=message 9 TCP/MSRP \\*\r\n"
+ + "a=path:msrp://192.168.1.1:9/.+;tcp\r\n"
+ + "a=setup:active\r\n"
+ + "a=accept-types:message/cpim application/im-iscomposing\\+xml\r\n"
+ + "a=accept-wrapped-types:text/plain message/imdn\\+xml"
+ + " application/vnd.gsma.rcs-ft-http\\+xml application/vnd.gsma"
+ + ".rcspushlocation\\+xml\r\n"
+ + "a=sendrecv\r\n";
+
+ @Test
+ public void createForMsrp_returnExpectedSdpString() {
+ SimpleSdpMessage sdp =
+ SdpUtils.createSdpForMsrp(/* address= */ "192.168.1.1", /* isTls= */ false);
+
+ assertThat(sdp.encode()).matches(SAMPLE_SDP_REGEX);
+ }
+
+ @Test
+ public void encodeAndParse_shouldBeEqualToOriginal() throws Exception {
+ SimpleSdpMessage original =
+ SdpUtils.createSdpForMsrp(/* address= */ "192.168.1.1", /* isTls= */ false);
+
+ SimpleSdpMessage parsedSdp =
+ SimpleSdpMessage.parse(new ByteArrayInputStream(original.encode().getBytes(UTF_8)));
+
+ assertThat(parsedSdp).isEqualTo(original);
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtilsTest.java b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtilsTest.java
new file mode 100644
index 0000000..be043f5
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtilsTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.common.collect.Lists;
+
+import gov.nist.javax.sip.message.SIPRequest;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SipUtilsTest {
+ private static final String LOCAL_URI = "tel:+1234567890";
+ private static final String REMOTE_URI = "tel:+1234567891";
+ private static final String CONVERSATION_ID = "abcd-1234";
+
+ SipSessionConfiguration configuration =
+ new SipSessionConfiguration() {
+ @Override
+ public long getVersion() {
+ return 0;
+ }
+
+ @Override
+ public String getOutboundProxyAddr() {
+ return "3001:4870:e00b:5e94:21b8:8d20:c425:5e6c";
+ }
+
+ @Override
+ public int getOutboundProxyPort() {
+ return 5060;
+ }
+
+ @Override
+ public String getLocalIpAddress() {
+ return "2001:4870:e00b:5e94:21b8:8d20:c425:5e6c";
+ }
+
+ @Override
+ public int getLocalPort() {
+ return 5060;
+ }
+
+ @Override
+ public String getSipTransport() {
+ return "TCP";
+ }
+
+ @Override
+ public String getPublicUserIdentity() {
+ return "sip:+1234567890@foo.bar";
+ }
+
+ @Override
+ public String getDomain() {
+ return "foo.bar";
+ }
+
+ @Override
+ public List<String> getAssociatedUris() {
+ return Lists.newArrayList(LOCAL_URI, "sip:+1234567890@foo.bar");
+ }
+
+ @Override
+ public String getSecurityVerifyHeader() {
+ return "ipsec-3gpp;q=0.5;alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=null;"
+ + "spi-c=983227540;spi-s=2427966379;port-c=65528;port-s=65529";
+ }
+
+ @Override
+ public List<String> getServiceRouteHeaders() {
+ return Lists.newArrayList();
+ }
+
+ @Override
+ public String getContactUser() {
+ return "abcd-efgh";
+ }
+
+ @Override
+ public String getImei() {
+ return "35293211-111080-0";
+ }
+
+ @Override
+ public String getPaniHeader() {
+ return null;
+ }
+
+ @Override
+ public String getPlaniHeader() {
+ return null;
+ }
+
+ @Override
+ public int getMaxPayloadSizeOnUdp() {
+ return 0;
+ }
+ };
+
+ @Test
+ public void buildInvite_returnExpectedInviteMessage() throws Exception {
+ SIPRequest request = SipUtils.buildInvite(configuration, REMOTE_URI, CONVERSATION_ID);
+
+ assertThat(request.getRequestURI().toString()).isEqualTo(REMOTE_URI);
+ assertThat(request.getFrom().getAddress().getURI().toString()).isEqualTo(LOCAL_URI);
+ assertThat(request.getTo().getAddress().getURI().toString()).isEqualTo(REMOTE_URI);
+ assertThat(request.hasHeader("Conversation-ID")).isTrue();
+ assertThat(request.hasHeader("Contribution-ID")).isTrue();
+ assertThat(request.hasHeader("Accept-Contact")).isTrue();
+ assertThat(request.hasHeader("Security-Verify")).isTrue();
+ }
+
+ @Test
+ public void buildInvite_sizeIsGreaterThanMaxPayloadSize_transportShouldBeTcp()
+ throws Exception {
+ SIPRequest request = SipUtils.buildInvite(configuration, REMOTE_URI, CONVERSATION_ID);
+
+ // The size is always greater than maxPayloadSizeOnUdp = 0
+ assertThat(request.getTopmostVia().getTransport()).isEqualTo("TCP");
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/provisioning/StaticConfigProvisioningControllerTest.java b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/provisioning/StaticConfigProvisioningControllerTest.java
new file mode 100644
index 0000000..b9065de
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/provisioning/StaticConfigProvisioningControllerTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.provisioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.support.annotation.RequiresPermission;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import java.util.Optional;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StaticConfigProvisioningControllerTest {
+
+ private static final byte[] CONFIG_DATA = "<xml></xml>".getBytes();
+
+ private StaticConfigProvisioningController client;
+ private Optional<byte[]> configXmlData = Optional.empty();
+ private ProvisioningStateChangeCallback cb =
+ configXml -> configXmlData = Optional.ofNullable(configXml);
+
+ @Before
+ public void setUp() {
+ client = StaticConfigProvisioningController.createForSubscriptionId(/*subscriptionId=*/ 2);
+ client.onConfigurationChange(cb);
+ }
+
+ @Test
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void whenGetConfigCalled_returnsCorrectXmlData() throws Exception {
+ client.register();
+ client.getProvisioningManager().getCallbackForTests().onConfigurationChanged(CONFIG_DATA);
+
+ assertThat(client.isRcsVolteSingleRegistrationCapable()).isTrue();
+ assertThat(client.getLatestConfiguration()).isEqualTo(CONFIG_DATA);
+ assertThat(configXmlData.get()).isEqualTo(CONFIG_DATA);
+ client.unRegister();
+ }
+
+ @Test
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void whenGetConfigCalled_throwsErrorWhenNoConfigPresent() throws Exception {
+ client.register();
+ client.triggerReconfiguration();
+
+ assertThat(client.isRcsVolteSingleRegistrationCapable()).isTrue();
+ assertThrows(IllegalStateException.class, () -> client.getLatestConfiguration());
+ assertThat(configXmlData.isPresent()).isFalse();
+
+ client.unRegister();
+ }
+
+ @Test
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void unRegister_failsWhenCalledWithoutRegister() {
+ assertThrows(IllegalStateException.class, () -> client.unRegister());
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSessionTest.java b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSessionTest.java
new file mode 100644
index 0000000..a898189
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/javatests/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSessionTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service.chat;
+
+import static com.google.common.labs.truth.FutureSubject.assertThat;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext;
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager;
+import com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionConfiguration;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionListener;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+
+import java.util.List;
+
+import javax.sip.message.Message;
+import javax.sip.message.Request;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SimpleChatSessionTest {
+ private static final String LOCAL_URI = "tel:+1234567890";
+ private static final String REMOTE_URI = "tel:+1234567891";
+ private final MsrpManager msrpManager =
+ new MsrpManager(ApplicationProvider.getApplicationContext());
+ SipSessionConfiguration configuration =
+ new SipSessionConfiguration() {
+ @Override
+ public int getVersion() {
+ return 0;
+ }
+
+ @Override
+ public String getOutboundProxyAddr() {
+ return "3001:4870:e00b:5e94:21b8:8d20:c425:5e6c";
+ }
+
+ @Override
+ public int getOutboundProxyPort() {
+ return 5060;
+ }
+
+ @Override
+ public String getLocalIpAddress() {
+ return "2001:4870:e00b:5e94:21b8:8d20:c425:5e6c";
+ }
+
+ @Override
+ public int getLocalPort() {
+ return 5060;
+ }
+
+ @Override
+ public String getSipTransport() {
+ return "TCP";
+ }
+
+ @Override
+ public String getPublicUserIdentity() {
+ return "sip:+1234567890@foo.bar";
+ }
+
+ @Override
+ public String getDomain() {
+ return "foo.bar";
+ }
+
+ @Override
+ public List<String> getAssociatedUris() {
+ return Lists.newArrayList(LOCAL_URI, "sip:+1234567890@foo.bar");
+ }
+
+ @Override
+ public String getSecurityVerifyHeader() {
+ return "ipsec-3gpp;q=0.5;alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=null;"
+ + "spi-c=983227540;spi-s=2427966379;port-c=65528;port-s=65529";
+ }
+
+ @Override
+ public List<String> getServiceRouteHeaders() {
+ return Lists.newArrayList();
+ }
+
+ @Override
+ public String getContactUser() {
+ return "abcd-efgh";
+ }
+
+ @Override
+ public String getImei() {
+ return "35293211-111080-0";
+ }
+
+ @Override
+ public String getPaniHeader() {
+ return "IEEE-802.11;i-wlan-node-id=PANIC01EB5B0";
+ }
+
+ @Override
+ public String getPlaniHeader() {
+ return "IEEE-802.11;i-wlan-node-id=PLANI01EB5B0";
+ }
+ };
+ private final SimpleRcsClientContext context =
+ new SimpleRcsClientContext(
+ /* provisioningController= */ null,
+ /* registrationController= */ null,
+ /* imsService= */ null,
+ new SipSession() {
+ @Override
+ public SipSessionConfiguration getSessionConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public ListenableFuture<Boolean> send(Message message) {
+ return Futures.immediateFuture(true);
+ }
+
+ @Override
+ public void setSessionListener(SipSessionListener listener) {
+ }
+ });
+
+ @Test
+ public void start_reply200_returnSuccessfulFuture() throws Exception {
+ SimpleChatSession session =
+ new SimpleChatSession(
+ context,
+ new MinimalCpmChatService(ApplicationProvider.getApplicationContext()) {
+ @Override
+ ListenableFuture<Boolean> sendSipRequest(SIPRequest msg,
+ SimpleChatSession session) {
+ if (msg.getMethod().equals(Request.INVITE)) {
+ SIPResponse response = msg.createResponse(/* statusCode= */
+ 200);
+ response.setMessageContent(
+ /* type= */ "application",
+ /* subType= */ "sdp",
+ SdpUtils.createSdpForMsrp(/* address= */
+ "127.0.0.1", /* isTls= */ false)
+ .encode());
+ session.receiveMessage(response);
+ }
+ return Futures.immediateFuture(true);
+ }
+ },
+ msrpManager);
+
+ // session.start should return the successful void future.
+ assertThat(session.start(REMOTE_URI)).whenDone().isSuccessful();
+ }
+
+ @Test
+ public void start_reply404_returnFailedFuture() throws Exception {
+ SimpleChatSession session =
+ new SimpleChatSession(
+ context,
+ new MinimalCpmChatService(ApplicationProvider.getApplicationContext()) {
+ @Override
+ ListenableFuture<Boolean> sendSipRequest(SIPRequest msg,
+ SimpleChatSession session) {
+ if (msg.getMethod().equals(Request.INVITE)) {
+ SIPResponse response = msg.createResponse(/* statusCode= */
+ 404);
+ session.receiveMessage(response);
+ }
+ return Futures.immediateFuture(true);
+ }
+ },
+ msrpManager);
+
+ // session.start should return the failed future with the exception.
+ assertThat(session.start(REMOTE_URI)).whenDone().isFailedWith(ChatServiceException.class);
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/SimpleRcsClient.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/SimpleRcsClient.java
new file mode 100644
index 0000000..c299cc9
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/SimpleRcsClient.java
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient;
+
+import android.os.Build.VERSION_CODES;
+import android.telephony.ims.ImsException;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession;
+import com.android.libraries.rcs.simpleclient.provisioning.ProvisioningController;
+import com.android.libraries.rcs.simpleclient.provisioning.StaticConfigProvisioningController;
+import com.android.libraries.rcs.simpleclient.registration.RegistrationController;
+import com.android.libraries.rcs.simpleclient.service.ImsService;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Simple RCS client implementation.
+ *
+ * State is covered by a context instance.
+ */
+@RequiresApi(api = VERSION_CODES.R)
+public class SimpleRcsClient {
+ private static final String TAG = SimpleRcsClient.class.getSimpleName();
+ private final AtomicReference<State> state = new AtomicReference<>(State.NEW);
+ private ProvisioningController provisioningController;
+ private RegistrationController registrationController;
+ private ImsService imsService;
+ private Executor executor;
+ private SimpleRcsClientContext context;
+ private StateChangedCallback stateChangedCallback;
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public SimpleRcsClientContext getContext() {
+ return context;
+ }
+
+ public void start() {
+ provision();
+ }
+
+ public void stop() {
+ Log.i(TAG, "stop..");
+ registrationController.deregister();
+ provisioningController.unRegister();
+ provisioningController = null;
+ registrationController = null;
+ imsService = null;
+ }
+
+ public void onStateChanged(StateChangedCallback cb) {
+ this.stateChangedCallback = cb;
+ }
+
+ private boolean enterState(State expected, State newState) {
+ boolean result = state.compareAndSet(expected, newState);
+
+ if (result && stateChangedCallback != null) {
+ try {
+ stateChangedCallback.notifyStateChange(expected, newState);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception on calling state change callback", e);
+ }
+ }
+ Log.i(TAG, "expected:" + expected + " new:" + newState + " res:" + result);
+ return result;
+ }
+
+ private void provision() {
+ if (!enterState(State.NEW, State.PROVISIONING)) {
+ return;
+ }
+ provisioningController.onConfigurationChange(configXml -> {
+ register();
+ });
+ try {
+ provisioningController.triggerProvisioning();
+ } catch (ImsException e) {
+ // TODO: ...
+ }
+ }
+
+ private void register() {
+ if (!enterState(State.PROVISIONING, State.REGISTERING)) {
+ return;
+ }
+
+ Futures.addCallback(registrationController.register(imsService),
+ new FutureCallback<SipSession>() {
+ @Override
+ public void onSuccess(SipSession result) {
+ Log.i(TAG, "onSuccess:" + result);
+ registered(result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.i(TAG, "onFailure:" + t);
+ }
+ }, executor);
+ }
+
+ private void registered(SipSession session) {
+ enterState(State.REGISTERING, State.REGISTERED);
+
+ context = new SimpleRcsClientContext(provisioningController, registrationController,
+ imsService,
+ session);
+
+ imsService.start(context);
+ }
+
+ /**
+ * Possible client states.
+ */
+ public enum State {
+ NEW,
+ PROVISIONING,
+ REGISTERING,
+ REGISTERED,
+ }
+
+ /**
+ * Builder for creating new SimpleRcsClient instances.
+ */
+ public static class Builder {
+
+ private ProvisioningController provisioningController;
+ private RegistrationController registrationController;
+ private ImsService imsService;
+ private Executor executor;
+
+ public Builder provisioningController(ProvisioningController controller) {
+ this.provisioningController = controller;
+ return this;
+ }
+
+ public Builder registrationController(RegistrationController controller) {
+ this.registrationController = controller;
+ return this;
+ }
+
+ public Builder imsService(ImsService imsService) {
+ this.imsService = imsService;
+ return this;
+ }
+
+ public Builder executor(Executor executor) {
+ this.executor = executor;
+ return this;
+ }
+
+ public SimpleRcsClient build() {
+ SimpleRcsClient client = new SimpleRcsClient();
+ client.registrationController = registrationController;
+ client.provisioningController = provisioningController;
+ client.imsService = imsService;
+ client.executor = executor;
+
+ return client;
+ }
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/SimpleRcsClientContext.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/SimpleRcsClientContext.java
new file mode 100644
index 0000000..1be6403
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/SimpleRcsClientContext.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient;
+
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession;
+import com.android.libraries.rcs.simpleclient.provisioning.ProvisioningController;
+import com.android.libraries.rcs.simpleclient.registration.RegistrationController;
+import com.android.libraries.rcs.simpleclient.service.ImsService;
+
+/**
+ * State container for a {@link SimpleRcsClient} instance.
+ */
+public class SimpleRcsClientContext {
+
+ private final ProvisioningController provisioningController;
+
+ private final RegistrationController registrationController;
+
+ private final ImsService imsService;
+
+ private final SipSession sipSession;
+
+ public SimpleRcsClientContext(
+ ProvisioningController provisioningController,
+ RegistrationController registrationController,
+ ImsService imsService,
+ SipSession sipSession) {
+ this.provisioningController = provisioningController;
+ this.registrationController = registrationController;
+ this.imsService = imsService;
+ this.sipSession = sipSession;
+ }
+
+ public ProvisioningController getProvisioningController() {
+ return provisioningController;
+ }
+
+ public RegistrationController getRegistrationController() {
+ return registrationController;
+ }
+
+ public ImsService getImsService() {
+ return imsService;
+ }
+
+ public SipSession getSipSession() {
+ return sipSession;
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/StateChangedCallback.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/StateChangedCallback.java
new file mode 100644
index 0000000..87f6566
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/StateChangedCallback.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClient.State;
+
+/**
+ * Callback for processing state changes.
+ */
+public interface StateChangedCallback {
+ void notifyStateChange(State oldState, State newState);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/cpim/CpimUtils.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/cpim/CpimUtils.java
new file mode 100644
index 0000000..74bcce8
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/cpim/CpimUtils.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.cpim;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Random;
+
+/** Collections of utility functions for CPIM */
+public class CpimUtils {
+ private static final String ANONYMOUS_URI = "<sip:anonymous@anonymous.invalid>";
+
+ private CpimUtils() {
+ }
+
+ public static SimpleCpimMessage createForText(String text) {
+ return SimpleCpimMessage.newBuilder()
+ .addNamespace("imdn", "urn:ietf:params:imdn")
+ .addHeader("imdn.Message-ID", generateImdnMessageId())
+ .addHeader("imdn.Disposition-Notification", "positive-delivery, display")
+ .addHeader("To", ANONYMOUS_URI)
+ .addHeader("From", ANONYMOUS_URI)
+ .addHeader("DateTime", LocalDate.now(ZoneId.systemDefault()).toString())
+ .setContentType("text/plain")
+ .setContent(text)
+ .build();
+ }
+
+ private static String generateImdnMessageId() {
+ Random random = new Random();
+ return "Test_" + random.nextLong();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/cpim/SimpleCpimMessage.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/cpim/SimpleCpimMessage.java
new file mode 100644
index 0000000..aeb6b11
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/cpim/SimpleCpimMessage.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.cpim;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Utf8;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * The CPIM implementation as per RFC 3862. This class supports minimal fields that is required to
+ * represent a simple message for test purpose.
+ */
+@AutoValue
+public abstract class SimpleCpimMessage {
+ private static final String CRLF = "\r\n";
+ private static final String COLSP = ": ";
+
+ public static SimpleCpimMessage.Builder newBuilder() {
+ return new AutoValue_SimpleCpimMessage.Builder();
+ }
+
+ public abstract ImmutableMap<String, String> namespaces();
+
+ public abstract ImmutableMap<String, String> headers();
+
+ public abstract String contentType();
+
+ public abstract String content();
+
+ public String encode() {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry<String, String> entry : namespaces().entrySet()) {
+ builder
+ .append("NS: ")
+ .append(entry.getKey())
+ .append(" <")
+ .append(entry.getValue())
+ .append(">")
+ .append(CRLF);
+ }
+
+ for (Map.Entry<String, String> entry : headers().entrySet()) {
+ builder.append(entry.getKey()).append(COLSP).append(entry.getValue()).append(CRLF);
+ }
+
+ builder.append(CRLF);
+ builder.append("Content-Type").append(COLSP).append(contentType());
+ builder.append("Content-Length").append(COLSP).append(Utf8.encodedLength(content()));
+ builder.append(CRLF);
+ builder.append(content());
+
+ return builder.toString();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract ImmutableMap.Builder<String, String> namespacesBuilder();
+
+ public abstract ImmutableMap.Builder<String, String> headersBuilder();
+
+ public abstract Builder setContentType(String value);
+
+ public abstract Builder setContent(String value);
+
+ public abstract SimpleCpimMessage build();
+
+ public Builder addNamespace(String name, String value) {
+ namespacesBuilder().put(name, value);
+ return this;
+ }
+
+ public Builder addHeader(String name, String value) {
+ headersBuilder().put(name, value);
+ return this;
+ }
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java
new file mode 100644
index 0000000..0011011
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+
+import androidx.annotation.RequiresPermission;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** A really incomplete implementation for fetching networks from ConnectivityManager. */
+public final class ImsPdnNetworkFetcher {
+
+ private final Context context;
+
+ public ImsPdnNetworkFetcher(Context context) {
+ this.context = context;
+ }
+
+ private static NetworkRequest createNetworkRequest() {
+ NetworkRequest.Builder builder =
+ new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+ return builder.build();
+ }
+
+ ListenableFuture<Network> getImsPdnNetwork() {
+ return requestNetwork();
+ }
+
+ ListenableFuture<List<String>> getImsPdnIpAddresses() {
+ return FluentFuture.from(getImsPdnNetwork())
+ .transform(this::getNetworkIpAddresses, MoreExecutors.directExecutor());
+ }
+
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ List<String> getNetworkIpAddresses(Network network) {
+ return getConnectivityManager().getLinkProperties(network).getLinkAddresses().stream()
+ .map(link -> link.getAddress().getHostAddress())
+ .collect(Collectors.toList());
+ }
+
+ private ListenableFuture<Network> requestNetwork() {
+ SettableFuture<Network> result = SettableFuture.create();
+ ConnectivityManager cm = getConnectivityManager();
+
+ ConnectivityManager.NetworkCallback cb =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ if (!result.isDone() && !result.isCancelled()) {
+ result.set(network);
+ }
+ cm.unregisterNetworkCallback(this);
+ }
+ };
+
+ cm.requestNetwork(createNetworkRequest(), cb);
+ return result;
+ }
+
+ private ConnectivityManager getConnectivityManager() {
+ return context.getSystemService(ConnectivityManager.class);
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunk.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunk.java
new file mode 100644
index 0000000..0d9e62f
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunk.java
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.FLAG_ABORT_CHUNK;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.FLAG_LAST_CHUNK;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.FLAG_MORE_CHUNK;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Single MSRP chunk containing a request or a response.
+ */
+@AutoValue
+public abstract class MsrpChunk {
+
+ public static Builder newBuilder() {
+ return new AutoValue_MsrpChunk.Builder()
+ .method(Method.UNKNOWN)
+ .responseCode(0)
+ .responseReason("")
+ .content(new byte[0])
+ .continuation(Continuation.UNKNOWN);
+ }
+
+ public abstract Method method();
+
+ public abstract String transactionId();
+
+ public abstract Continuation continuation();
+
+ public abstract int responseCode();
+
+ public abstract String responseReason();
+
+ public abstract ImmutableList<MsrpChunkHeader> headers();
+
+ public abstract byte[] content();
+
+ public MsrpChunkHeader header(String headerName) {
+ for (MsrpChunkHeader header : headers()) {
+ if (header.name().equals(headerName)) {
+ return header;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Methods for requests
+ */
+ public enum Method {
+ UNKNOWN,
+ SEND,
+ REPORT,
+ }
+
+
+ /**
+ * Continuation flag for the chunk
+ */
+ public enum Continuation {
+ UNKNOWN(0),
+ COMPLETE(FLAG_LAST_CHUNK),
+ MORE(FLAG_MORE_CHUNK),
+ ABORTED(FLAG_ABORT_CHUNK);
+
+ private final int value;
+
+ Continuation(int value) {
+ this.value = value;
+ }
+
+ public static Continuation valueOf(int read) {
+ if (read == COMPLETE.value) {
+ return COMPLETE;
+ }
+ if (read == MORE.value) {
+ return MORE;
+ }
+ if (read == ABORTED.value) {
+ return ABORTED;
+ }
+ return UNKNOWN;
+ }
+
+ public byte toByte() {
+ return (byte) value;
+ }
+ }
+
+ /**
+ * Builder for new MSRP chunk.
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder method(Method method);
+
+ public abstract Builder transactionId(String id);
+
+ public abstract String transactionId();
+
+ public abstract Continuation continuation();
+
+ public abstract Builder continuation(Continuation continuation);
+
+ public abstract Builder responseCode(int continuation);
+
+ public abstract Builder responseReason(String reason);
+
+ public abstract Builder content(byte[] content);
+
+ public Builder addHeader(MsrpChunkHeader header) {
+ headersBuilder().add(header);
+ return this;
+ }
+
+ public Builder addHeader(String name, String value) {
+ headersBuilder().add(MsrpChunkHeader.newBuilder().name(name).value(value).build());
+ return this;
+ }
+
+ abstract ImmutableList.Builder<MsrpChunkHeader> headersBuilder();
+
+ MsrpChunkHeader header(String name) {
+ for (MsrpChunkHeader header : headersBuilder().build()) {
+ if (header.name().equals(name)) {
+ return header;
+ }
+ }
+ return null;
+ }
+
+ public abstract MsrpChunk build();
+
+
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunkHeader.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunkHeader.java
new file mode 100644
index 0000000..fad17e0
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpChunkHeader.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Single header in MSRP chunk (From-Path, To-Path, ...)
+ */
+@AutoValue
+public abstract class MsrpChunkHeader {
+
+ public static Builder newBuilder() {
+ return new AutoValue_MsrpChunkHeader.Builder();
+ }
+
+ public abstract String name();
+
+ public abstract String value();
+
+ /**
+ * Builder for new MSRP header.
+ */
+ @AutoValue.Builder
+ public static abstract class Builder {
+
+ public abstract Builder name(String name);
+
+ public abstract Builder value(String value);
+
+ public abstract MsrpChunkHeader build();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpConstants.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpConstants.java
new file mode 100644
index 0000000..ad1b98e
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpConstants.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Several constants used for MSRP parsing and serializing.
+ */
+public class MsrpConstants {
+ public static final byte[] HEADER_DELIMITER_BYTES = ": ".getBytes();
+ public static final String MSRP_PROTOCOL = "MSRP";
+ public static final byte[] MSRP_PROTOCOL_BYTES = MSRP_PROTOCOL.getBytes(UTF_8);
+ public static final String NEW_LINE = "\r\n";
+ public static final byte[] NEW_LINE_BYTES = NEW_LINE.getBytes(UTF_8);
+ public static final String END_MSRP_MSG = "-------";
+ public static final byte[] END_MSRP_MSG_BYTES = END_MSRP_MSG.getBytes(UTF_8);
+ public static final String NEW_LINE_END_MSRP_MSG = NEW_LINE + END_MSRP_MSG;
+ public static final int END_MSRP_MSG_LENGTH = END_MSRP_MSG.length();
+ public static final int FLAG_LAST_CHUNK = '$';
+ public static final int FLAG_MORE_CHUNK = '+';
+ public static final int FLAG_ABORT_CHUNK = '#';
+ public static final byte CHAR_SP = ' ';
+ public static final byte CHAR_LF = '\r';
+ public static final byte CHAR_MIN = '-';
+ public static final byte CHAR_DOUBLE_POINT = ':';
+ public static final String HEADER_BYTE_RANGE = "Byte-Range";
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+ public static final String HEADER_MESSAGE_ID = "Message-ID";
+ public static final String HEADER_TO_PATH = "To-Path";
+ public static final String HEADER_FROM_PATH = "From-Path";
+ public static final String HEADER_FAILURE_REPORT = "Failure-Report";
+ public static final int RESPONSE_CODE_OK = 200;
+
+ private MsrpConstants() {
+ }
+}
\ No newline at end of file
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java
new file mode 100644
index 0000000..47326bd
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import android.content.Context;
+import android.net.Network;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/** Provides creating and managing {@link MsrpSession} */
+public class MsrpManager {
+ private final ImsPdnNetworkFetcher imsPdnNetworkFetcher;
+
+ public MsrpManager(Context context) {
+ imsPdnNetworkFetcher = new ImsPdnNetworkFetcher(context);
+ }
+
+ private static MsrpSession createMsrpSession(
+ Network network, String host, int port, MsrpSessionListener listener)
+ throws IOException {
+ Socket socket = network.getSocketFactory().createSocket(host, port);
+ MsrpSession msrpSession = new MsrpSession(socket, listener);
+ Thread thread = new Thread(msrpSession::run);
+ thread.start();
+ return msrpSession;
+ }
+
+ public ListenableFuture<MsrpSession> createMsrpSession(
+ String host, int port, MsrpSessionListener listener) {
+ return Futures.transformAsync(
+ imsPdnNetworkFetcher.getImsPdnNetwork(),
+ network -> {
+ if (network != null) {
+ return Futures.immediateFuture(
+ createMsrpSession(network, host, port, listener));
+ } else {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("Network is null"));
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpParser.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpParser.java
new file mode 100644
index 0000000..3376544
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpParser.java
@@ -0,0 +1,355 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.CHAR_DOUBLE_POINT;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.CHAR_LF;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.CHAR_MIN;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.CHAR_SP;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.END_MSRP_MSG_LENGTH;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.HEADER_BYTE_RANGE;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpConstants.NEW_LINE_END_MSRP_MSG;
+
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Simple parser for reading MSRP messages from a stream.
+ */
+public final class MsrpParser {
+
+ private MsrpParser() {
+ }
+
+ public static MsrpChunk parse(final InputStream stream) throws IOException {
+ MsrpChunk.Builder transaction = MsrpChunk.newBuilder();
+
+ // Read a chunk (blocking method)
+ int i = stream.read();
+
+ final StringBuilder value = new StringBuilder();
+ // Read MSRP tag
+ skipWithDelimiter(stream, CHAR_SP);
+
+ if (i == -1) {
+ // End of stream
+ return null;
+ }
+
+ // Read the transaction ID
+ do {
+ i = stream.read();
+ if (i != CHAR_SP) {
+ value.append((char) i);
+ }
+ } while ((i != CHAR_SP) && (i != -1));
+
+ if (i == -1) {
+ return null;
+ }
+
+ final String txId = value.toString();
+ value.setLength(0);
+
+ // Read response code or method name
+ MsrpChunk.Method method = MsrpChunk.Method.UNKNOWN;
+ int responseCode = -1;
+ for (i = stream.read(); (i != CHAR_LF) && (i != -1); i = stream.read()) {
+ if (i == CHAR_SP && responseCode == -1) {
+ // There is a space -> it's a response
+ try {
+ responseCode = Integer.parseInt(value.toString());
+ } catch (NumberFormatException nfe) {
+ // This is an invalid response.
+ return null;
+ }
+ value.setLength(0);
+ continue;
+ }
+ value.append((char) i);
+ }
+
+ if (responseCode == -1) {
+ try {
+ responseCode = Integer.parseInt(value.toString());
+ value.setLength(0);
+ } catch (final NumberFormatException e) {
+ method = MsrpChunk.Method.valueOf(value.toString());
+ }
+ }
+
+ i = stream.read();
+
+ if (i == -1) {
+ // End of stream
+ return null;
+ }
+
+ final boolean isResponse = responseCode > -1;
+ if (isResponse) {
+ transaction.transactionId(txId).responseCode(responseCode).responseReason(
+ value.toString());
+ } else {
+ transaction.transactionId(txId).method(method);
+ }
+
+ value.setLength(0);
+
+ // Read MSRP headers
+ readHeaders(stream, transaction, value);
+
+ // We already received end of message
+ if (transaction.continuation() != Continuation.UNKNOWN) {
+ return transaction.build();
+ }
+
+ i = stream.read();
+ if (i == -1) {
+ // End of stream
+ return null;
+ }
+
+ // Process MSRP request
+ if (method == MsrpChunk.Method.SEND) {
+ readChunk(stream, transaction);
+ }
+
+ return transaction.build();
+ }
+
+ private static void readHeaders(
+ final InputStream stream, final MsrpChunk.Builder transaction,
+ final StringBuilder value)
+ throws IOException {
+ for (int i = stream.read(); (i != CHAR_LF) && (i != -1); ) {
+
+ for (; (i != CHAR_DOUBLE_POINT) && (i != -1); i = stream.read()) {
+ value.append((char) i);
+ }
+
+ final String headerName = value.toString();
+ value.setLength(0);
+
+ stream.read(); // skip space
+
+ for (i = stream.read(); (i != CHAR_LF) && (i != -1); i = stream.read()) {
+ value.append((char) i);
+ }
+
+ final String headerValue = value.toString();
+ value.setLength(0);
+
+ transaction.addHeader(headerName, headerValue);
+
+ stream.read();
+
+ // It's the end of the header part
+ i = stream.read();
+ if (i == CHAR_MIN) {
+ final int length = END_MSRP_MSG_LENGTH - 1 + transaction.transactionId().length();
+ stream.skip(length);
+ transaction.continuation(Continuation.valueOf(stream.read()));
+
+ // For response
+ for (; (i != CHAR_LF) && (i != -1); i = stream.read()) {
+ }
+ break;
+ }
+ }
+ }
+
+ private static void readChunk(final InputStream stream, final MsrpChunk.Builder chunk)
+ throws IOException {
+ final String byteRange = chunk.header(HEADER_BYTE_RANGE).value();
+
+ if (byteRange == null) {
+ throw new IllegalStateException("expected non-null byteRange");
+ }
+ final int chunkSize = getChunkSize(byteRange);
+ final long totalSize = getTotalSize(byteRange);
+
+ if (totalSize == Integer.MIN_VALUE || chunkSize < -1) {
+ throw new IOException("Invalid byte range: " + byteRange);
+ }
+
+ if (chunkSize == -1) {
+ readUnknownChunk(stream, chunk);
+ } else {
+ readKnownChunk(stream, chunk, chunkSize);
+ skipEndLine(stream, chunk);
+ }
+
+ readContinuationFlag(stream, chunk);
+ }
+
+ private static void readKnownChunk(
+ final InputStream stream, final MsrpChunk.Builder chunk, final int chunkSize)
+ throws IOException {
+ // Read the data
+ final byte[] data = new byte[chunkSize];
+ int nbRead = 0;
+ int nbData = -1;
+ while ((nbRead < chunkSize)
+ && ((nbData = stream.read(data, nbRead, chunkSize - nbRead)) != -1)) {
+ nbRead += nbData;
+ }
+
+ chunk.content(data);
+
+ stream.read();
+ stream.read();
+ }
+
+ private static void readUnknownChunk(final InputStream stream, final MsrpChunk.Builder chunk)
+ throws IOException {
+
+ final byte[] bufferArray = new byte[4096];
+ final byte[] endOfChunkPattern =
+ (NEW_LINE_END_MSRP_MSG + chunk.transactionId()).getBytes();
+ int pp = 0;
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ final ByteBuffer buffer = ByteBuffer.wrap(bufferArray);
+ while (true) {
+ final int i = stream.read();
+
+ if (i < 0) {
+ throw new IOException("EOS reached");
+ }
+
+ if (i == endOfChunkPattern[pp]) {
+ pp++;
+ } else if (i == endOfChunkPattern[0]) {
+ pp = 1;
+ } else {
+ pp = 0;
+ }
+
+ buffer.put((byte) i);
+
+ if (pp == endOfChunkPattern.length) {
+ outputStream.write(bufferArray, 0, buffer.position() - endOfChunkPattern.length);
+ break;
+ }
+
+ if (buffer.remaining() == 0) {
+ if (pp > 0) {
+ outputStream.write(bufferArray, 0, bufferArray.length - pp);
+ System.arraycopy(endOfChunkPattern, 0, bufferArray, 0, pp);
+ buffer.position(pp);
+ } else {
+ outputStream.write(bufferArray, 0, bufferArray.length);
+ buffer.rewind();
+ }
+ }
+ }
+
+ chunk.content(outputStream.toByteArray());
+ }
+
+ private static void skipEndLine(final InputStream stream, final MsrpChunk.Builder chunk)
+ throws IOException {
+ // skip the "-------" + txid
+ final int length = END_MSRP_MSG_LENGTH + chunk.transactionId().length();
+ final byte[] endline = new byte[256];
+ readFromStream(stream, endline, 0, length);
+ }
+
+ private static void readContinuationFlag(
+ final InputStream stream, final MsrpChunk.Builder transaction) throws IOException {
+ transaction.continuation(Continuation.valueOf(stream.read()));
+ stream.read();
+ stream.read();
+ }
+
+ /**
+ * Get the chunk size
+ *
+ * @param header MSRP header
+ * @return Size in bytes
+ */
+ private static int getChunkSize(final String header) {
+ final int index1 = header.indexOf("-");
+ final int index2 = header.indexOf("/");
+ if ((index1 != -1) && (index2 != -1)) {
+ final String lowByteValue = header.substring(0, index1);
+ final String highByteValue = header.substring(index1 + 1, index2);
+
+ if ("*".equals(highByteValue)) {
+ return -1;
+ } else {
+ try {
+ final int lowByte = Integer.parseInt(lowByteValue);
+ final int highByte = Integer.parseInt(highByteValue);
+ if (lowByte > highByte) {
+ return Integer.MIN_VALUE;
+ }
+ return (highByte - lowByte) + 1;
+ } catch (NumberFormatException e) {
+ throw new IllegalStateException("Could not read chunksize!");
+ }
+ }
+ }
+ return Integer.MIN_VALUE;
+ }
+
+ /**
+ * Get the total size
+ *
+ * @param header MSRP header
+ * @return Size in bytes
+ */
+ private static long getTotalSize(final String header) {
+ final int index = header.indexOf("/");
+ if (index != -1) {
+ if ("*".equals(header.substring(index + 1))) {
+ return -1;
+ }
+ try {
+ return Long.parseLong(header.substring(index + 1));
+ } catch (NumberFormatException e) {
+ throw new IllegalStateException("Could not read total size!");
+ }
+ }
+ return Integer.MIN_VALUE;
+ }
+
+ private static void readFromStream(
+ InputStream stream, final byte[] buffer, final int offset, final int length)
+ throws IOException {
+ int read = 0;
+ while (read < length) {
+ try {
+ read += stream.read(buffer, offset + read, length - read);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("Invalid ID length", e);
+ }
+ }
+ }
+
+ private static int skipWithDelimiter(InputStream stream, byte delimiter) throws IOException {
+ int i = stream.read();
+ for (; (i != delimiter) && (i != -1); i = stream.read()) {
+ }
+ return i;
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSerializer.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSerializer.java
new file mode 100644
index 0000000..bd4daa5
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSerializer.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Serializer for writing messages
+ */
+public final class MsrpSerializer {
+
+ private MsrpSerializer() {
+ }
+
+ public static void serialize(OutputStream outputStream, MsrpChunk message) throws IOException {
+
+ writeRequestLine(outputStream, message);
+ for (MsrpChunkHeader header : message.headers()) {
+ writeHeader(outputStream, header);
+ }
+
+ if (message.content().length > 0) {
+ outputStream.write(MsrpConstants.NEW_LINE_BYTES);
+ outputStream.write(message.content());
+ outputStream.write(MsrpConstants.NEW_LINE_BYTES);
+ }
+
+ writeEndLine(outputStream, message);
+ }
+
+ private static void writeRequestLine(OutputStream outputStream, MsrpChunk chunk)
+ throws IOException {
+
+ outputStream.write(MsrpConstants.MSRP_PROTOCOL_BYTES);
+ outputStream.write(MsrpConstants.CHAR_SP);
+ outputStream.write(chunk.transactionId().getBytes());
+ outputStream.write(MsrpConstants.CHAR_SP);
+
+ if (chunk.method() != MsrpChunk.Method.UNKNOWN) {
+ outputStream.write(chunk.method().name().getBytes(UTF_8));
+ } else {
+ outputStream.write(
+ (chunk.responseCode() + " " + chunk.responseReason()).getBytes(UTF_8));
+ }
+
+ outputStream.write(MsrpConstants.NEW_LINE_BYTES);
+ }
+
+ private static void writeHeader(OutputStream outputStream, MsrpChunkHeader header)
+ throws IOException {
+ outputStream.write(header.name().getBytes(UTF_8));
+ outputStream.write(MsrpConstants.HEADER_DELIMITER_BYTES);
+ outputStream.write(header.value().getBytes(UTF_8));
+ outputStream.write(MsrpConstants.NEW_LINE_BYTES);
+ }
+
+ private static void writeEndLine(OutputStream outputStream, MsrpChunk chunk)
+ throws IOException {
+ outputStream.write(MsrpConstants.END_MSRP_MSG_BYTES);
+ outputStream.write(chunk.transactionId().getBytes(UTF_8));
+ outputStream.write(chunk.continuation().toByte());
+ outputStream.write(MsrpConstants.NEW_LINE_BYTES);
+ }
+}
\ No newline at end of file
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java
new file mode 100644
index 0000000..96ca19c
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Method.SEND;
+import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Method.UNKNOWN;
+
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Continuation;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provides MSRP sending and receiving messages ability.
+ */
+public class MsrpSession {
+ private final Socket socket;
+ private final InputStream input;
+ private final OutputStream output;
+ private final AtomicBoolean isOpen = new AtomicBoolean(true);
+ private final ConcurrentHashMap<String, MsrpTransaction> transactions =
+ new ConcurrentHashMap<>();
+ private final MsrpSessionListener listener;
+
+ /** Creates a new MSRP session on the given listener and the provided streams. */
+ MsrpSession(Socket socket, MsrpSessionListener listener) throws IOException {
+ this.socket = socket;
+ this.input = socket.getInputStream();
+ this.output = socket.getOutputStream();
+ this.listener = listener;
+ }
+
+ /**
+ * Sends the given MSRP chunk.
+ */
+ public ListenableFuture<MsrpChunk> send(MsrpChunk request) {
+ if (request.method() == UNKNOWN) {
+ throw new IllegalArgumentException("Given chunk must be a request");
+ }
+
+ if (!isOpen.get()) {
+ throw new IllegalStateException("Session terminated");
+ }
+
+ if (!socket.isConnected()) {
+ throw new IllegalStateException("Socket is not connected");
+ }
+
+ if (request.method() == SEND) {
+ return CallbackToFutureAdapter.getFuture(
+ completer -> {
+ final MsrpTransaction transaction = new MsrpTransaction(completer);
+ transactions.put(request.transactionId(), transaction);
+ try {
+ synchronized (output) {
+ MsrpSerializer.serialize(output, request);
+ }
+ output.flush();
+ } catch (IOException e) {
+ completer.setException(e);
+ }
+ return "MsrpSession.send(" + request.transactionId() + ")";
+ }
+ );
+ } else {
+ try {
+ synchronized (output) {
+ MsrpSerializer.serialize(output, request);
+ }
+ return Futures.immediateFuture(request);
+ } catch (IOException e) {
+ return Futures.immediateFailedFuture(e);
+ }
+ }
+ }
+
+ /**
+ * Blocking method which reads from the provided InputStream until the session
+ * is terminated or the stream read throws an exception.
+ */
+ public void run() {
+ new StreamReader(this).run();
+ }
+
+ public void terminate() throws IOException {
+ if (isOpen.getAndSet(false)) {
+ output.flush();
+ }
+ socket.close();
+ }
+
+ /**
+ * Reads and parses MSRP messages from the session input stream.
+ */
+ private static class StreamReader {
+
+ private final MsrpSession session;
+ private final InputStream stream;
+ private final AtomicBoolean active;
+
+ StreamReader(MsrpSession session) {
+ this.session = session;
+ this.stream = session.input;
+ this.active = session.isOpen;
+ }
+
+ void run() {
+ while (active.get()) {
+ MsrpChunk chunk = null;
+ try {
+ chunk = MsrpParser.parse(stream);
+
+ if (chunk.method() == UNKNOWN) {
+ completeTransaction(chunk);
+ } else {
+ receiveRequest(chunk);
+ }
+ } catch (IOException e) {
+ active.compareAndSet(true, false);
+ }
+ }
+ }
+
+ private void receiveRequest(MsrpChunk chunk) throws IOException {
+ sendResponse(chunk);
+ session.listener.onChunkReceived(chunk);
+ }
+
+ private void completeTransaction(MsrpChunk chunk) {
+ MsrpTransaction transaction = session.transactions.remove(chunk.transactionId());
+ if (transaction != null) {
+ transaction.complete(chunk);
+ }
+ }
+
+ private void sendResponse(MsrpChunk chunk) throws IOException {
+ // check if response is required
+ MsrpChunkHeader failureReport = chunk.header(MsrpConstants.HEADER_FAILURE_REPORT);
+ if (failureReport == null || failureReport.value().equals("yes")) {
+ MsrpChunkHeader toPath = chunk.header(MsrpConstants.HEADER_TO_PATH);
+ MsrpChunkHeader fromPath = chunk.header(MsrpConstants.HEADER_FROM_PATH);
+
+ MsrpChunk response = MsrpChunk.newBuilder()
+ .transactionId(chunk.transactionId())
+ .responseCode(200)
+ .responseReason("OK")
+ .addHeader(MsrpConstants.HEADER_TO_PATH, fromPath.value())
+ .addHeader(MsrpConstants.HEADER_FROM_PATH, toPath.value())
+ .continuation(Continuation.COMPLETE)
+ .build();
+
+ synchronized (session.output) {
+ MsrpSerializer.serialize(session.output, response);
+ session.output.flush();
+ }
+ }
+ }
+ }
+
+ /**
+ * Transaction holder.
+ */
+ private static class MsrpTransaction {
+ private final Completer<MsrpChunk> completed;
+
+ public MsrpTransaction(Completer<MsrpChunk> chunkCompleter) {
+ this.completed = chunkCompleter;
+ }
+
+ public void complete(MsrpChunk response) {
+ completed.set(response);
+ }
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSessionListener.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSessionListener.java
new file mode 100644
index 0000000..4235c25
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSessionListener.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+/**
+ * Listener for MSRP session events.
+ */
+public interface MsrpSessionListener {
+ void onChunkReceived(MsrpChunk chunk);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpUtils.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpUtils.java
new file mode 100644
index 0000000..238ce37
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpUtils.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.msrp;
+
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils;
+
+/** Collections of utility functions for MSRP */
+public final class MsrpUtils {
+
+ private MsrpUtils() {
+ }
+
+ /** Generate a path attribute defined in RFC 4975 for the given address, port. */
+ public static String generatePath(String address, int port, boolean isSecure) {
+ StringBuilder builder = new StringBuilder();
+
+ if (SipUtils.isIPv6Address(address)) {
+ address = "[" + address + "]";
+ }
+
+ builder
+ .append(isSecure ? "msrps" : "msrp")
+ .append("://")
+ .append(address)
+ .append(":")
+ .append(port)
+ .append("/")
+ .append(System.currentTimeMillis())
+ .append(";tcp");
+
+ return builder.toString();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SdpMedia.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SdpMedia.java
new file mode 100644
index 0000000..bdb34ba
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SdpMedia.java
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sdp;
+
+import android.text.TextUtils;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The media part of SDP implementation as per RFC 4566. This class supports minimal fields that is
+ * required to represent MSRP session.
+ */
+@AutoValue
+public abstract class SdpMedia {
+ private static final String CRLF = "\r\n";
+
+ public static Builder parseMediaLine(String line) throws ParseException {
+ List<String> elements = Splitter.on(" ").limit(4).splitToList(line);
+
+ // The valid media line should have 4 elements:
+ // m=<name> <port> <protocol> <format>
+ if (elements.size() != 4) {
+ throw new ParseException("Invalid media line", 0);
+ }
+
+ // Parse each field from the media line.
+ Builder builder = SdpMedia.newBuilder();
+ builder
+ .setName(elements.get(0))
+ .setPort(Integer.parseInt(elements.get(1)))
+ .setProtocol(elements.get(2))
+ .setFormat(elements.get(3));
+
+ return builder;
+ }
+
+ public static Builder newBuilder() {
+ return new AutoValue_SdpMedia.Builder();
+ }
+
+ public abstract String name();
+
+ public abstract int port();
+
+ public abstract String protocol();
+
+ public abstract String format();
+
+ public abstract ImmutableMap<String, String> attributes();
+
+ /** Encode the media section as a string. */
+ public String encode() {
+ StringBuilder builder = new StringBuilder();
+ builder
+ .append("m=")
+ .append(name())
+ .append(" ")
+ .append(port())
+ .append(" ")
+ .append(protocol())
+ .append(" ")
+ .append(format())
+ .append(CRLF);
+
+ for (Map.Entry<String, String> attribute : attributes().entrySet()) {
+ builder.append("a=").append(attribute.getKey());
+ if (!TextUtils.isEmpty(attribute.getValue())) {
+ builder.append(":").append(attribute.getValue());
+ }
+ builder.append(CRLF);
+ }
+
+ return builder.toString();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setName(String name);
+
+ public abstract Builder setPort(int port);
+
+ public abstract Builder setProtocol(String protocol);
+
+ public abstract Builder setFormat(String payload);
+
+ public abstract ImmutableMap.Builder<String, String> attributesBuilder();
+
+ public Builder addAttribute(String name, String value) {
+ attributesBuilder().put(name, value);
+ return this;
+ }
+
+ public Builder addAttribute(String name) {
+ return addAttribute(name, "");
+ }
+
+ public abstract SdpMedia build();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SdpUtils.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SdpUtils.java
new file mode 100644
index 0000000..e290b29
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SdpUtils.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sdp;
+
+import static com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils.isIPv6Address;
+
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpUtils;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+/** Collections of utility functions for SDP */
+public final class SdpUtils {
+ public static final String SDP_CONTENT_TYPE = "application";
+ public static final String SDP_CONTENT_SUB_TYPE = "sdp";
+
+ private static final ImmutableSet<String> DEFAULT_ACCEPT_TYPES =
+ ImmutableSet.of("message/cpim", "application/im-iscomposing+xml");
+ private static final ImmutableSet<String> DEFAULT_ACCEPT_WRAPPED_TYPES =
+ ImmutableSet.of(
+ "text/plain",
+ "message/imdn+xml",
+ "application/vnd.gsma.rcs-ft-http+xml",
+ "application/vnd.gsma.rcspushlocation+xml");
+
+ private static final String DEFAULT_NAME = "message";
+ private static final String DEFAULT_SETUP = "active";
+ private static final String DEFAULT_DIRECTION = "sendrecv";
+ private static final int DEFAULT_MSRP_PORT = 9;
+ private static final String PROTOCOL_TCP_MSRP = "TCP/MSRP";
+ private static final String PROTOCOL_TLS_MSRP = "TCP/TLS/MSRP";
+ private static final String DEFAULT_FORMAT = "*";
+
+ private static final String ATTRIBUTE_PATH = "path";
+ private static final String ATTRIBUTE_SETUP = "setup";
+ private static final String ATTRIBUTE_ACCEPT_TYPES = "accept-types";
+ private static final String ATTRIBUTE_ACCEPT_WRAPPED_TYPES = "accept-wrapped-types";
+
+ private SdpUtils() {
+ }
+
+ /**
+ * Create a simple SDP message for MSRP. Most attributes except address and transport type
+ * will be
+ * generated automatically.
+ *
+ * @param address The local IP address of the MSRP connection.
+ * @param isTls True if the MSRP connection uses TLS.
+ */
+ public static SimpleSdpMessage createSdpForMsrp(String address, boolean isTls) {
+ return SimpleSdpMessage.newBuilder()
+ .setVersion("0")
+ .setOrigin(generateOrigin(address))
+ .setSession("-")
+ .setConnection(generateConnection(address))
+ .setTime("0 0")
+ .addMedia(createSdpMediaForMsrp(address, isTls))
+ .build();
+ }
+
+ private static String generateOrigin(String address) {
+ StringBuilder builder = new StringBuilder();
+ builder
+ .append("TestRcsClient ")
+ .append(System.currentTimeMillis())
+ .append(" ")
+ .append(System.currentTimeMillis())
+ .append(" IN ")
+ .append(isIPv6Address(address) ? "IP6 " : "IP4 ")
+ .append(address);
+
+ return builder.toString();
+ }
+
+ private static String generateConnection(String address) {
+ return "IN " + (isIPv6Address(address) ? "IP6 " : "IP4 ") + address;
+ }
+
+ /**
+ * Create a media part of the SDP message for MSRP. Most attributes except address and transport
+ * type will be generated automatically.
+ *
+ * @param address The local IP address of the MSRP connection.
+ * @param isTls True if the MSRP connection uses TLS.
+ */
+ public static SdpMedia createSdpMediaForMsrp(String address, boolean isTls) {
+ return SdpMedia.newBuilder()
+ .setName(DEFAULT_NAME)
+ .setPort(DEFAULT_MSRP_PORT)
+ .setProtocol(isTls ? PROTOCOL_TLS_MSRP : PROTOCOL_TCP_MSRP)
+ .setFormat(DEFAULT_FORMAT)
+ .addAttribute(ATTRIBUTE_PATH,
+ MsrpUtils.generatePath(address, DEFAULT_MSRP_PORT, isTls))
+ .addAttribute(ATTRIBUTE_SETUP, DEFAULT_SETUP)
+ .addAttribute(ATTRIBUTE_ACCEPT_TYPES, Joiner.on(" ").join(DEFAULT_ACCEPT_TYPES))
+ .addAttribute(
+ ATTRIBUTE_ACCEPT_WRAPPED_TYPES,
+ Joiner.on(" ").join(DEFAULT_ACCEPT_WRAPPED_TYPES))
+ .addAttribute(DEFAULT_DIRECTION)
+ .build();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SimpleSdpMessage.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SimpleSdpMessage.java
new file mode 100644
index 0000000..4308ecc
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sdp/SimpleSdpMessage.java
@@ -0,0 +1,188 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sdp;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.ParseException;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalInt;
+
+/**
+ * The SDP implementation as per RFC 4566. This class supports minimal fields that is required to
+ * represent MSRP session.
+ */
+@AutoValue
+public abstract class SimpleSdpMessage {
+ private static final String CRLF = "\r\n";
+
+ private static final String PREFIX_VERSION = "v";
+ private static final String PREFIX_ORIGIN = "o";
+ private static final String PREFIX_SESSION = "s";
+ private static final String PREFIX_CONNECTION = "c";
+ private static final String PREFIX_TIME = "t";
+ private static final String PREFIX_MEDIA = "m";
+ private static final String PREFIX_ATTRIBUTE = "a";
+ private static final String EQUAL = "=";
+
+ public static SimpleSdpMessage parse(InputStream stream) throws ParseException, IOException {
+ Builder builder = new AutoValue_SimpleSdpMessage.Builder();
+ SdpMedia.Builder currentMediaBuilder = null;
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+ String line = reader.readLine();
+ while (line != null) {
+ List<String> parts = Splitter.on("=").trimResults().limit(2).splitToList(line);
+ if (parts.size() != 2) {
+ throw new ParseException("Invalid SDP format", 0);
+ }
+ String prefix = parts.get(0);
+ String value = parts.get(1);
+
+ switch (prefix) {
+ case PREFIX_VERSION:
+ builder.setVersion(value);
+ break;
+ case PREFIX_ORIGIN:
+ builder.setOrigin(value);
+ break;
+ case PREFIX_SESSION:
+ builder.setSession(value);
+ break;
+ case PREFIX_CONNECTION:
+ builder.setConnection(value);
+ break;
+ case PREFIX_TIME:
+ builder.setTime(value);
+ break;
+ case PREFIX_MEDIA:
+ if (currentMediaBuilder != null) {
+ builder.addMedia(currentMediaBuilder.build());
+ }
+ currentMediaBuilder = SdpMedia.parseMediaLine(value);
+ break;
+ case PREFIX_ATTRIBUTE:
+ if (currentMediaBuilder != null) {
+ List<String> kv = Splitter.on(":").trimResults().limit(2).splitToList(
+ value);
+ currentMediaBuilder.addAttribute(kv.get(0), kv.size() < 2 ? "" : kv.get(1));
+ }
+ break;
+ default:
+ // Rest of the fields are ignored as they're not used for describing MSRP
+ // session.
+ break;
+ }
+ line = reader.readLine();
+ }
+
+ if (currentMediaBuilder != null) {
+ builder.addMedia(currentMediaBuilder.build());
+ }
+
+ return builder.build();
+ }
+
+ private static String encodeLine(String prefix, String value) {
+ return prefix + EQUAL + value + CRLF;
+ }
+
+ public static Builder newBuilder() {
+ return new AutoValue_SimpleSdpMessage.Builder();
+ }
+
+ public abstract String version();
+
+ public abstract String origin();
+
+ public abstract String session();
+
+ public abstract String connection();
+
+ public abstract String time();
+
+ public abstract ImmutableList<SdpMedia> media();
+
+ /** Return the IP address in the connection line. */
+ public Optional<String> getAddress() {
+ if (connection() == null) {
+ return Optional.empty();
+ }
+
+ List<String> parts = Splitter.on(" ").limit(3).trimResults().splitToList(connection());
+ if (parts.size() != 3) {
+ return Optional.empty();
+ }
+
+ return Optional.of(parts.get(2));
+ }
+
+ /** Return the port in the first media line. */
+ public OptionalInt getPort() {
+ if (media().isEmpty()) {
+ return OptionalInt.empty();
+ }
+
+ return OptionalInt.of(media().get(0).port());
+ }
+
+ /** Encode the entire SDP fields as a string. */
+ public String encode() {
+ StringBuilder builder = new StringBuilder();
+ builder
+ .append(encodeLine(PREFIX_VERSION, version()))
+ .append(encodeLine(PREFIX_ORIGIN, origin()))
+ .append(encodeLine(PREFIX_SESSION, session()))
+ .append(encodeLine(PREFIX_CONNECTION, connection()))
+ .append(encodeLine(PREFIX_TIME, time()));
+
+ for (SdpMedia media : media()) {
+ builder.append(media.encode());
+ }
+
+ return builder.toString();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setVersion(String version);
+
+ public abstract Builder setOrigin(String origin);
+
+ public abstract Builder setSession(String session);
+
+ public abstract Builder setConnection(String connection);
+
+ public abstract Builder setTime(String connection);
+
+ public abstract ImmutableList.Builder<SdpMedia> mediaBuilder();
+
+ public Builder addMedia(SdpMedia media) {
+ mediaBuilder().add(media);
+ return this;
+ }
+
+ public abstract SimpleSdpMessage build();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSession.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSession.java
new file mode 100644
index 0000000..9629961
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSession.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sip;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import javax.sip.message.Message;
+
+/**
+ * Abstraction of the underlying SIP channel for sending and receiving SIP messages.
+ */
+public interface SipSession {
+
+ SipSessionConfiguration getSessionConfiguration();
+
+ ListenableFuture<Boolean> send(Message message);
+
+ void setSessionListener(SipSessionListener listener);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSessionConfiguration.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSessionConfiguration.java
new file mode 100644
index 0000000..a1356e5
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSessionConfiguration.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sip;
+
+import java.util.List;
+
+public interface SipSessionConfiguration {
+ public long getVersion();
+
+ String getOutboundProxyAddr();
+
+ int getOutboundProxyPort();
+
+ String getLocalIpAddress();
+
+ int getLocalPort();
+
+ String getSipTransport();
+
+ String getPublicUserIdentity();
+
+ String getDomain();
+
+ List<String> getAssociatedUris();
+
+ String getSecurityVerifyHeader();
+
+ List<String> getServiceRouteHeaders();
+
+ String getContactUser();
+
+ String getImei();
+
+ String getPaniHeader();
+
+ String getPlaniHeader();
+
+ default int getMaxPayloadSizeOnUdp() {
+ return 0;
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSessionListener.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSessionListener.java
new file mode 100644
index 0000000..5fe61e6
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipSessionListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sip;
+
+import javax.sip.message.Message;
+
+/**
+ * Listener for incoming messages on a {@link SipSession}.
+ */
+public interface SipSessionListener {
+
+ void onMessageReceived(Message sipMessage);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java
new file mode 100644
index 0000000..5ce8639
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java
@@ -0,0 +1,323 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.protocol.sip;
+
+import static com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils.SDP_CONTENT_SUB_TYPE;
+import static com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils.SDP_CONTENT_TYPE;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.libraries.rcs.simpleclient.protocol.sdp.SdpUtils;
+import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage;
+
+import com.google.common.base.Ascii;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.net.InetAddresses;
+
+import gov.nist.javax.sip.Utils;
+import gov.nist.javax.sip.address.AddressFactoryImpl;
+import gov.nist.javax.sip.header.ContentType;
+import gov.nist.javax.sip.header.HeaderFactoryImpl;
+import gov.nist.javax.sip.header.Via;
+import gov.nist.javax.sip.header.extensions.SessionExpires;
+import gov.nist.javax.sip.header.ims.PPreferredIdentityHeader;
+import gov.nist.javax.sip.header.ims.PPreferredServiceHeader;
+import gov.nist.javax.sip.header.ims.SecurityVerifyHeader;
+import gov.nist.javax.sip.message.SIPMessage;
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+
+import java.net.Inet6Address;
+import java.text.ParseException;
+import java.util.List;
+import java.util.UUID;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.address.URI;
+import javax.sip.header.ContactHeader;
+import javax.sip.header.HeaderFactory;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/** Collections of utility functions for SIP */
+public final class SipUtils {
+ private static final String TAG = "SipUtils";
+ private static final String SUPPORTED_TIMER_TAG = "timer";
+ private static final String ICSI_REF_PARAM_NAME = "+g.3gpp.icsi-ref";
+ private static final String SIP_INSTANCE_PARAM_NAME = "+sip.instance";
+ private static final String CPM_SESSION_FEATURE_TAG_PARAM_VALUE =
+ "\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+ private static final String CPM_SESSION_FEATURE_TAG_FULL_STRING =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+ private static final String CPM_SESSION_SERVICE_NAME =
+ "urn:urn-7:3gpp-service.ims.icsi.oma.cpm.session";
+ private static final String CONTRIBUTION_ID_HEADER_NAME = "Contribution-ID";
+ private static final String CONVERSATION_ID_HEADER_NAME = "Conversation-ID";
+ private static final String ACCEPT_CONTACT_HEADER_NAME = "Accept-Contact";
+ private static final String PANI_HEADER_NAME = "P-Access-Network-Info";
+ private static final String PLANI_HEADER_NAME = "P-Last-Access-Network-Info";
+ private static final String USER_AGENT_HEADER = "RcsTestClient";
+
+ private static AddressFactory sAddressFactory = new AddressFactoryImpl();
+ private static HeaderFactory sHeaderFactory = new HeaderFactoryImpl();
+
+ private SipUtils() {
+ }
+
+ /**
+ * Try to parse the given uri.
+ *
+ * @throws IllegalArgumentException in case of parsing error.
+ */
+ public static URI createUri(String uri) {
+ try {
+ return sAddressFactory.createURI(uri);
+ } catch (ParseException exception) {
+ throw new IllegalArgumentException("URI cannot be created", exception);
+ }
+ }
+
+ /**
+ * Create SIP INVITE request for a CPM 1:1 chat.
+ *
+ * @param configuration The SipSessionConfiguration instance used for populating SIP headers.
+ * @param targetUri The uri to be targeted.
+ * @param conversationId The id to be contained in Conversation-ID header.
+ */
+ public static SIPRequest buildInvite(
+ SipSessionConfiguration configuration, String targetUri, String conversationId)
+ throws ParseException {
+ String address = configuration.getLocalIpAddress();
+ int port = configuration.getLocalPort();
+ String transport = configuration.getSipTransport();
+ List<String> associatedUris = configuration.getAssociatedUris();
+ String preferredUri = Iterables.getFirst(associatedUris,
+ configuration.getPublicUserIdentity());
+
+ SIPRequest request = new SIPRequest();
+ request.setMethod(Request.INVITE);
+
+ URI remoteUri = createUri(targetUri);
+ request.setRequestURI(remoteUri);
+ request.setFrom(
+ sHeaderFactory.createFromHeader(
+ sAddressFactory.createAddress(preferredUri),
+ Utils.getInstance().generateTag()));
+ request.setTo(
+ sHeaderFactory.createToHeader(sAddressFactory.createAddress(remoteUri), null));
+
+ ViaHeader viaHeader = null;
+
+ try {
+ // Set a default Max-Forwards header.
+ request.setMaxForwards(sHeaderFactory.createMaxForwardsHeader(70));
+ request.setCSeq(sHeaderFactory.createCSeqHeader(1L, Request.INVITE));
+ viaHeader =
+ sHeaderFactory.createViaHeader(
+ address, port, transport, Utils.getInstance().generateBranchId());
+ request.setVia(ImmutableList.of(viaHeader));
+
+ // Set a default Session-Expires header.
+ SessionExpires sessionExpires = new SessionExpires();
+ sessionExpires.setRefresher("uac");
+ sessionExpires.setExpires(1800);
+ request.setHeader(sessionExpires);
+
+ // Set a Contact header.
+ request.setHeader(generateContactHeader(configuration));
+
+ // Set PANI and PLANI if exists
+ if (configuration.getPaniHeader() != null) {
+ request.setHeader(
+ sHeaderFactory.createHeader(PANI_HEADER_NAME,
+ configuration.getPaniHeader()));
+ }
+ if (configuration.getPlaniHeader() != null) {
+ request.setHeader(
+ sHeaderFactory.createHeader(PLANI_HEADER_NAME,
+ configuration.getPaniHeader()));
+ }
+ } catch (InvalidArgumentException e) {
+ // Nothing to do here
+ Log.e(TAG, e.getMessage());
+ }
+
+ request.setCallId(UUID.randomUUID().toString());
+ request.setHeader(sHeaderFactory.createHeader(CONVERSATION_ID_HEADER_NAME, conversationId));
+ request.setHeader(
+ sHeaderFactory.createHeader(CONTRIBUTION_ID_HEADER_NAME,
+ UUID.randomUUID().toString()));
+
+ String acceptContact = "*;" + CPM_SESSION_FEATURE_TAG_FULL_STRING;
+ request.setHeader(sHeaderFactory.createHeader(ACCEPT_CONTACT_HEADER_NAME, acceptContact));
+ request.setHeader(sHeaderFactory.createSupportedHeader(SUPPORTED_TIMER_TAG));
+ request.setHeader(sHeaderFactory.createHeader(PPreferredIdentityHeader.NAME, preferredUri));
+ request.setHeader(
+ sHeaderFactory.createHeader(PPreferredServiceHeader.NAME,
+ CPM_SESSION_SERVICE_NAME));
+
+ // Set a Security-Verify header if exist.
+ String securityVerify = configuration.getSecurityVerifyHeader();
+ if (!TextUtils.isEmpty(securityVerify)) {
+ request.setHeader(
+ sHeaderFactory.createHeader(SecurityVerifyHeader.NAME, securityVerify));
+ }
+
+ // Add Route headers.
+ List<String> serviceRoutes = configuration.getServiceRouteHeaders();
+ if (!serviceRoutes.isEmpty()) {
+ for (String sr : serviceRoutes) {
+ request.addHeader(
+ sHeaderFactory.createRouteHeader(sAddressFactory.createAddress(sr)));
+ }
+ }
+
+ request.addHeader(
+ sHeaderFactory.createUserAgentHeader(ImmutableList.of(USER_AGENT_HEADER)));
+
+ SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(address, false);
+ request.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE,
+ sdp.encode().getBytes(UTF_8));
+
+ if (viaHeader != null && Ascii.equalsIgnoreCase("udp", transport)) {
+ String newTransport = determineTransportBySize(configuration,
+ request.encodeAsBytes("udp").length);
+ if (!Ascii.equalsIgnoreCase(transport, newTransport)) {
+ viaHeader.setTransport(newTransport);
+ }
+ }
+
+ return request;
+ }
+
+ private static ContactHeader generateContactHeader(SipSessionConfiguration configuration)
+ throws ParseException {
+ String host = configuration.getLocalIpAddress();
+ if (isIPv6Address(host)) {
+ host = "[" + host + "]";
+ }
+
+ String userPart = configuration.getContactUser();
+ SipURI uri = sAddressFactory.createSipURI(userPart, host);
+ try {
+ uri.setPort(configuration.getLocalPort());
+ uri.setTransportParam(configuration.getSipTransport());
+ } catch (Exception e) {
+ // Shouldn't be here.
+ }
+
+ ContactHeader contactHeader =
+ sHeaderFactory.createContactHeader(sAddressFactory.createAddress(uri));
+
+ // Add +sip.instance param.
+ String sipInstance = "\"<urn:gsma:imei:" + configuration.getImei() + ">\"";
+ contactHeader.setParameter(SIP_INSTANCE_PARAM_NAME, sipInstance);
+
+ // Add CPM feature tag.
+ uri.setTransportParam(configuration.getSipTransport());
+ contactHeader.setParameter(ICSI_REF_PARAM_NAME, CPM_SESSION_FEATURE_TAG_PARAM_VALUE);
+
+ return contactHeader;
+ }
+
+ /**
+ * Create a SIP BYE request for terminating the chat session.
+ *
+ * @param invite the initial INVITE request of the chat session.
+ */
+ public static SIPRequest buildBye(SIPRequest invite) throws ParseException {
+ SIPRequest request = new SIPRequest();
+ request.setRequestURI(invite.getRequestURI());
+ request.setMethod(Request.BYE);
+ try {
+ long cSeqNumber = invite.getCSeq().getSeqNumber();
+ request.setHeader(sHeaderFactory.createCSeqHeader(cSeqNumber, Request.BYE));
+ } catch (InvalidArgumentException e) {
+ // Nothing to do here
+ }
+
+ request.setCallId(invite.getCallId());
+
+ Via via = (Via) request.getTopmostVia().clone();
+ via.removeParameter("branch");
+ request.addHeader(via);
+ request.addHeader(
+ sHeaderFactory.createFromHeader(invite.getFrom().getAddress(),
+ invite.getFrom().getTag()));
+ request.addHeader(
+ sHeaderFactory.createToHeader(invite.getTo().getAddress(),
+ invite.getTo().getTag()));
+
+ return request;
+ }
+
+ /**
+ * Create SIP INVITE response for a CPM 1:1 chat.
+ *
+ * @param configuration The SipSessionConfiguration instance used for populating SIP headers.
+ * @param invite the initial INVITE request of the chat session.
+ * @param code The status code of the response.
+ */
+ public static SIPResponse buildInviteResponse(
+ SipSessionConfiguration configuration, SIPRequest invite, int code)
+ throws ParseException {
+ SIPResponse response = invite.createResponse(code);
+ if (code == Response.OK) {
+ SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(),
+ false);
+ response.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE, sdp.encode());
+ }
+
+ // Set PANI and PLANI if exists
+ if (configuration.getPaniHeader() != null) {
+ response.setHeader(
+ sHeaderFactory.createHeader(PANI_HEADER_NAME, configuration.getPaniHeader()));
+ }
+ if (configuration.getPlaniHeader() != null) {
+ response.setHeader(
+ sHeaderFactory.createHeader(PLANI_HEADER_NAME, configuration.getPaniHeader()));
+ }
+ return response;
+ }
+
+ public static boolean isIPv6Address(String address) {
+ return InetAddresses.forString(address) instanceof Inet6Address;
+ }
+
+ /** Return whether the SIP message has a SDP content or not */
+ public static boolean hasSdpContent(SIPMessage message) {
+ ContentType contentType = message.getContentTypeHeader();
+ return contentType != null
+ && TextUtils.equals(contentType.getContentType(), SDP_CONTENT_TYPE)
+ && TextUtils.equals(contentType.getContentSubType(), SDP_CONTENT_SUB_TYPE);
+ }
+
+ private static String determineTransportBySize(SipSessionConfiguration configuration,
+ int size) {
+ if (size > configuration.getMaxPayloadSizeOnUdp()) {
+ return "tcp";
+ }
+ return "udp";
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningController.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningController.java
new file mode 100644
index 0000000..f987c67
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningController.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.provisioning;
+
+import android.telephony.ims.ImsException;
+
+/**
+ * Access to provisioning functionality and data.
+ */
+public interface ProvisioningController {
+
+ /**
+ * Triggers a new provisioning request. If the device is not already provisioned, it requests
+ * the
+ * provisioning flow and sets up callbacks. If the provisioning is already present, it
+ * requests a
+ * new provisioning config from the server.
+ *
+ * @throws ImsException if there is an error.
+ */
+ void triggerProvisioning() throws ImsException;
+
+ /** Is Single-Reg enabled for the default call SIM ? */
+ boolean isRcsVolteSingleRegistrationCapable() throws ImsException;
+
+ void onConfigurationChange(ProvisioningStateChangeCallback cb);
+
+ // Unregister the callback to the framework's provisioning change.
+ void unRegister();
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningControllerImpl.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningControllerImpl.java
new file mode 100644
index 0000000..06d3835
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningControllerImpl.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.provisioning;
+
+
+/**
+ * Actual implementation build upon ProvisioningManager.
+ */
+public class ProvisioningControllerImpl implements ProvisioningController {
+
+ @Override
+ public void triggerProvisioning() {
+ throw new IllegalStateException("Not implemented!");
+ }
+
+ @Override
+ public void onConfigurationChange(ProvisioningStateChangeCallback cb) {
+ throw new IllegalStateException("Not implemented!");
+ }
+
+ @Override
+ public boolean isRcsVolteSingleRegistrationCapable() {
+ throw new IllegalStateException("Not implemented.");
+ }
+
+ @Override
+ public void unRegister() {
+ throw new IllegalStateException("Not implemented.");
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningStateChangeCallback.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningStateChangeCallback.java
new file mode 100644
index 0000000..17a0291
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/ProvisioningStateChangeCallback.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.provisioning;
+
+/**
+ * A callback for provisioning state change notifications.
+ */
+public interface ProvisioningStateChangeCallback {
+ void notifyConfigChanged(byte[] configXml);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/StaticConfigProvisioningController.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/StaticConfigProvisioningController.java
new file mode 100644
index 0000000..350f43c
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/provisioning/StaticConfigProvisioningController.java
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.provisioning;
+
+import android.os.Build.VERSION_CODES;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.ProvisioningManager.RcsProvisioningCallback;
+import android.telephony.ims.RcsClientConfiguration;
+import android.util.Log;
+
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * "Fake" provisioning implementation for supplying a static config when testing ProvisioningManager
+ * is unnecessary. State changes are invoked manually.
+ */
+public class StaticConfigProvisioningController implements ProvisioningController {
+
+ private static final String TAG = StaticConfigProvisioningController.class.getSimpleName();
+ private final ProvisioningManager provisioningManager;
+ private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ private Optional<RcsProvisioningCallback> storedCallback = Optional.empty();
+ private Optional<ProvisioningStateChangeCallback> stateChangeCallback = Optional.empty();
+ private Optional<byte[]> configXmlData = Optional.empty();
+
+ private StaticConfigProvisioningController(int subId) {
+ this.provisioningManager = ProvisioningManager.createForSubscriptionId(subId);
+ }
+
+ @RequiresApi(api = VERSION_CODES.R)
+ public static StaticConfigProvisioningController createWithDefaultSubscriptionId() {
+ return new StaticConfigProvisioningController(
+ SubscriptionManager.getActiveDataSubscriptionId());
+ }
+
+ public static StaticConfigProvisioningController createForSubscriptionId(int subscriptionId) {
+ return new StaticConfigProvisioningController(subscriptionId);
+ }
+
+ // Static configuration.
+ private static RcsClientConfiguration getDefaultClientConfiguration() {
+
+ return new RcsClientConfiguration(
+ /*rcsVersion=*/ "6.0",
+ /*rcsProfile=*/ "UP_2.3",
+ /*clientVendor=*/ "Goog",
+ /*clientVersion=*/ "RCSAndrd-1.0");//"RCS fake library 1.0");
+ }
+
+ @Override
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void triggerProvisioning() throws ImsException {
+ boolean isRegistered = false;
+ synchronized (this) {
+ isRegistered = storedCallback.isPresent();
+ }
+
+ if (isRegistered) {
+ triggerReconfiguration();
+ } else {
+ register();
+ }
+ }
+
+ @Override
+ public void onConfigurationChange(ProvisioningStateChangeCallback cb) {
+ stateChangeCallback = Optional.of(cb);
+ }
+
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void register() throws ImsException {
+ register(getDefaultClientConfiguration());
+ }
+
+ @SuppressWarnings("LogConditional")
+ // TODO(b/171976006) Use 'tools:ignore=' in manifest instead.
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void register(@NonNull RcsClientConfiguration clientConfiguration) throws ImsException {
+ Log.i(TAG, "Using configuration: " + clientConfiguration.toString());
+ provisioningManager.setRcsClientConfiguration(clientConfiguration);
+
+ RcsProvisioningCallback callback =
+ new RcsProvisioningCallback() {
+ @Override
+ public void onConfigurationChanged(@NonNull byte[] configXml) {
+ Log.i(TAG, "RcsProvisioningCallback.onConfigurationChanged called.");
+ synchronized (this) {
+ configXmlData = Optional.of(configXml);
+ }
+ stateChangeCallback.ifPresent(cb -> cb.notifyConfigChanged(configXml));
+ }
+
+ @RequiresApi(api = VERSION_CODES.R)
+ @Override
+ public void onConfigurationReset() {
+ Log.i(TAG, "RcsProvisioningCallback.onConfigurationReset called.");
+ synchronized (this) {
+ configXmlData = Optional.empty();
+ }
+ stateChangeCallback.ifPresent(cb -> cb.notifyConfigChanged(null));
+ }
+
+ @RequiresApi(api = VERSION_CODES.R)
+ @Override
+ public void onRemoved() {
+ Log.i(TAG, "RcsProvisioningCallback.onRemoved called.");
+ synchronized (this) {
+ configXmlData = Optional.empty();
+ }
+ stateChangeCallback.ifPresent(cb -> cb.notifyConfigChanged(null));
+ }
+ };
+
+ Log.i(TAG, "Registering the callback.");
+ synchronized (this) {
+ provisioningManager.registerRcsProvisioningChangedCallback(executorService, callback);
+ storedCallback = Optional.of(callback);
+ }
+ }
+
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public void unRegister() {
+ synchronized (this) {
+ RcsProvisioningCallback callback =
+ storedCallback.orElseThrow(
+ () -> new IllegalStateException("No callback present."));
+ provisioningManager.unregisterRcsProvisioningChangedCallback(callback);
+ storedCallback = Optional.empty();
+ }
+ }
+
+ @Override
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ public boolean isRcsVolteSingleRegistrationCapable() throws ImsException {
+ return provisioningManager.isRcsVolteSingleRegistrationCapable();
+ }
+
+ public synchronized byte[] getLatestConfiguration() {
+ return configXmlData.orElseThrow(() -> new IllegalStateException("No config present"));
+ }
+
+ @VisibleForTesting
+ @RequiresPermission(value = "Manifest.permission.READ_PRIVILEGED_PHONE_STATE")
+ void triggerReconfiguration() {
+ provisioningManager.triggerRcsReconfiguration();
+ }
+
+ @VisibleForTesting
+ ProvisioningManager getProvisioningManager() {
+ return provisioningManager;
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java
new file mode 100644
index 0000000..bffb938
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/MessageConverter.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.registration;
+
+import android.telephony.ims.SipMessage;
+
+import gov.nist.javax.sip.header.SIPHeader;
+import gov.nist.javax.sip.message.SIPMessage;
+import gov.nist.javax.sip.parser.ParseExceptionListener;
+import gov.nist.javax.sip.parser.StringMsgParser;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.text.ParseException;
+import java.util.Iterator;
+
+import javax.sip.header.ContentLengthHeader;
+import javax.sip.message.Message;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/***
+ * Class responsible of converting an RCS SIP Message
+ * {@link Message} to a Platform SIP message
+ * {@link SipMessage} and vice versa.
+ */
+public final class MessageConverter {
+
+ private MessageConverter() {
+ }
+
+ public static SipMessage toPlatformMessage(Message message) {
+ String startLine;
+ if (message instanceof Request) {
+ startLine = getRequestStartLine((Request) message);
+ } else {
+ startLine = getResponseStartLine((Response) message);
+ }
+
+ StringBuilder headers = new StringBuilder();
+ for (Iterator<SIPHeader> it = ((SIPMessage) message).getHeaders(); it.hasNext(); ) {
+ SIPHeader header = it.next();
+ if (header instanceof ContentLengthHeader) {
+ continue;
+ }
+ headers.append(header);
+ }
+
+ int length = message.getRawContent() != null ? message.getRawContent().length : 0;
+ headers
+ .append(SIPHeader.CONTENT_LENGTH)
+ .append(": ")
+ .append(length)
+ .append("\r\n");
+
+ return new SipMessage(startLine, headers.toString(), message.getRawContent());
+ }
+
+ public static Message toStackMessage(SipMessage message) throws ParseException {
+ // The AOSP version of nist-sip has a parseSIPMessage() method that has a different
+ // contract.
+ // Fallback to parseSIPMessage(byte[] msgBuffer) in case the first attempt fails.
+ Method method;
+ try {
+ method =
+ StringMsgParser.class.getDeclaredMethod(
+ "parseSIPMessage",
+ byte[].class,
+ boolean.class,
+ boolean.class,
+ ParseExceptionListener.class);
+ return (Message)
+ method.invoke(
+ new StringMsgParser(),
+ message.getEncodedMessage(),
+ true,
+ false,
+ (ParseExceptionListener)
+ (ex, sipMessage, headerClass, headerText, messageText) -> {
+ throw ex;
+ });
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+ try {
+ method = StringMsgParser.class.getDeclaredMethod("parseSIPMessage", byte[].class);
+ return (Message) method.invoke(new StringMsgParser(), message.getEncodedMessage());
+ } catch (IllegalAccessException | InvocationTargetException
+ | NoSuchMethodException ex) {
+ ex.printStackTrace();
+ throw new ParseException("Failed to invoke parseSIPMessage", 0);
+ }
+ }
+ }
+
+ private static String getRequestStartLine(Request request) {
+ StringBuilder startLine = new StringBuilder();
+
+ startLine.append(request.getMethod());
+ startLine.append(" ");
+ startLine.append(request.getRequestURI());
+ startLine.append(" SIP/2.0\r\n");
+
+ return startLine.toString();
+ }
+
+ private static String getResponseStartLine(Response response) {
+ StringBuilder startLine = new StringBuilder();
+
+ startLine.append("SIP/2.0 ");
+ startLine.append(response.getStatusCode());
+ startLine.append(" ");
+ startLine.append(response.getReasonPhrase());
+ startLine.append("\r\n");
+
+ return startLine.toString();
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationController.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationController.java
new file mode 100644
index 0000000..64d93b2
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationController.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.registration;
+
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession;
+import com.android.libraries.rcs.simpleclient.service.ImsService;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Access to registration functionality.
+ */
+public interface RegistrationController {
+
+ /**
+ * Registers the given ImsService with the backend and returns a SipSession for sending and
+ * receiving SIP messages.
+ */
+ ListenableFuture<SipSession> register(ImsService imsService);
+
+ void deregister();
+
+ void onRegistrationStateChange(RegistrationStateChangeCallback callback);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java
new file mode 100644
index 0000000..66f2566
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java
@@ -0,0 +1,421 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.registration;
+
+import android.os.Build.VERSION_CODES;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.SipDelegateConnection;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.telephony.ims.stub.DelegateConnectionMessageCallback;
+import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionConfiguration;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionListener;
+import com.android.libraries.rcs.simpleclient.service.ImsService;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.text.ParseException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+import javax.sip.message.Message;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Actual implementation built upon SipDelegateConnection as a SIP transport.
+ * Feature tag registration state changes will trigger callbacks SimpleRcsClient to
+ * enable/disable related ImsServices.
+ */
+@RequiresApi(api = VERSION_CODES.R)
+public class RegistrationControllerImpl implements RegistrationController {
+ private static final String TAG = RegistrationControllerImpl.class.getCanonicalName();
+
+ private final Executor executor;
+ private final int subscriptionId;
+ private SipDelegateManager sipDelegateManager;
+ private RegistrationContext context;
+
+ public RegistrationControllerImpl(int subscriptionId, Executor executor,
+ ImsManager imsManager) {
+ this.subscriptionId = subscriptionId;
+ this.executor = executor;
+ this.sipDelegateManager = imsManager.getSipDelegateManager(subscriptionId);
+ }
+
+ @Override
+ public ListenableFuture<SipSession> register(ImsService imsService) {
+ Log.i(TAG, "register");
+ context = new RegistrationContext(this, imsService);
+ context.register();
+ return context.getFuture();
+ }
+
+ @Override
+ public void deregister() {
+ Log.i(TAG, "deregister");
+ if (context != null && context.sipDelegateConnection != null) {
+ sipDelegateManager.destroySipDelegate(context.sipDelegateConnection,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+ }
+
+ @Override
+ public void onRegistrationStateChange(RegistrationStateChangeCallback callback) {
+ throw new IllegalStateException("Not implemented!");
+ }
+
+ /**
+ * Envelopes the registration data for a single ImsService instance.
+ */
+ private static class RegistrationContext implements SipSession, SipSessionConfiguration {
+
+ private final RegistrationControllerImpl controller;
+ private final ImsService imsService;
+ private final SettableFuture<SipSession> sessionFuture = SettableFuture.create();
+
+ protected SipDelegateConnection sipDelegateConnection;
+ private SipDelegateImsConfiguration configuration;
+ private final DelegateConnectionStateCallback connectionCallback =
+ new DelegateConnectionStateCallback() {
+
+ @Override
+ public void onCreated(SipDelegateConnection c) {
+ sipDelegateConnection = c;
+ }
+
+ @Override
+ public void onImsConfigurationChanged(
+ SipDelegateImsConfiguration registeredSipConfig) {
+ Log.d(
+ TAG,
+ "onSipConfigurationChanged: version="
+ + registeredSipConfig.getVersion()
+ + " bundle="
+ + registeredSipConfig.copyBundle());
+ dumpConfig(registeredSipConfig);
+ RegistrationContext.this.configuration = registeredSipConfig;
+ }
+
+ @Override
+ public void onFeatureTagStatusChanged(
+ @NonNull DelegateRegistrationState registrationState,
+ @NonNull Set<FeatureTagState> deniedFeatureTags) {
+ dumpFeatureTagState(registrationState, deniedFeatureTags);
+ if (registrationState
+ .getRegisteredFeatureTags()
+ .containsAll(imsService.getFeatureTags())) {
+ // registered;
+ sessionFuture.set(RegistrationContext.this);
+ }
+ }
+
+ @Override
+ public void onDestroyed(int reason) {
+ }
+ };
+ private SipSessionListener sipSessionListener;
+ // Callback for incoming messages on the modem connection
+ private final DelegateConnectionMessageCallback messageCallback =
+ new DelegateConnectionMessageCallback() {
+ @Override
+ public void onMessageReceived(@NonNull SipMessage message) {
+ SipSessionListener listener = sipSessionListener;
+ if (listener != null) {
+ try {
+ listener.onMessageReceived(
+ MessageConverter.toStackMessage(message));
+ } catch (ParseException e) {
+ // TODO: logging here
+ }
+ }
+ }
+
+ @Override
+ public void onMessageSendFailure(@NonNull String viaTransactionId, int reason) {
+ }
+
+ @Override
+ public void onMessageSent(@NonNull String viaTransactionId) {
+ }
+
+ };
+
+ public RegistrationContext(RegistrationControllerImpl controller,
+ ImsService imsService) {
+ this.controller = controller;
+ this.imsService = imsService;
+ }
+
+ public ListenableFuture<SipSession> getFuture() {
+ return sessionFuture;
+ }
+
+ @Override
+ public SipSessionConfiguration getSessionConfiguration() {
+ return this;
+ }
+
+ public void register() {
+ Log.i(TAG, "createSipDelegate");
+ DelegateRequest request = new DelegateRequest(imsService.getFeatureTags());
+ try {
+ controller.sipDelegateManager.createSipDelegate(
+ request, controller.executor, connectionCallback, messageCallback);
+ } catch (ImsException e) {
+ // TODO: ...
+ }
+ }
+
+ private void dumpFeatureTagState(DelegateRegistrationState registrationState,
+ Set<FeatureTagState> deniedFeatureTags) {
+ StringBuilder stringBuilder = new StringBuilder(
+ "onFeatureTagStatusChanged ").append(
+ " deniedFeatureTags:[");
+ Iterator<FeatureTagState> iterator = deniedFeatureTags.iterator();
+ while (iterator.hasNext()) {
+ FeatureTagState featureTagState = iterator.next();
+ stringBuilder.append(featureTagState.getFeatureTag()).append(" ").append(
+ featureTagState.getState());
+ }
+ Set<String> registeredFt = registrationState.getRegisteredFeatureTags();
+ Iterator<String> iteratorStr = registeredFt.iterator();
+ stringBuilder.append("] registeredFT:[");
+ while (iteratorStr.hasNext()) {
+ String ft = iteratorStr.next();
+ stringBuilder.append(ft).append(" ");
+ }
+ stringBuilder.append("]");
+ String result = stringBuilder.toString();
+ Log.i(TAG, result);
+ }
+
+ private void dumpConfig(SipDelegateImsConfiguration config) {
+ Log.i(TAG, "KEY_SIP_CONFIG_TRANSPORT_TYPE_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_TRANSPORT_TYPE_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_USER_ID_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_USER_ID_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PRIVATE_USER_ID_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PRIVATE_USER_ID_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_HOME_DOMAIN_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_HOME_DOMAIN_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_IMEI_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IMEI_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_IPTYPE_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IPTYPE_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_DEFAULT_IPADDRESS_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_DEFAULT_IPADDRESS_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_DEFAULT_IPADDRESS_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_DEFAULT_IPADDRESS_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_IPADDRESS_WITH_NAT_STRING:" +
+ config.getString(SipDelegateImsConfiguration.
+ KEY_SIP_CONFIG_UE_PUBLIC_IPADDRESS_WITH_NAT_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_GRUU_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_GRUU_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_AUTHENTICATION_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_AUTHENTICATION_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_AUTHENTICATION_NONCE_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_AUTHENTICATION_NONCE_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVICE_ROUTE_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVICE_ROUTE_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_SECURITY_VERIFY_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SECURITY_VERIFY_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_PATH_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_PATH_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_URI_USER_PART_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_URI_USER_PART_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_P_ACCESS_NETWORK_INFO_HEADER_STRING:" +
+ config.getString(SipDelegateImsConfiguration.
+ KEY_SIP_CONFIG_P_ACCESS_NETWORK_INFO_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_P_LAST_ACCESS_NETWORK_INFO_HEADER_STRING:" +
+ config.getString(SipDelegateImsConfiguration.
+ KEY_SIP_CONFIG_P_LAST_ACCESS_NETWORK_INFO_HEADER_STRING));
+ Log.i(TAG, "KEY_SIP_CONFIG_P_ASSOCIATED_URI_HEADER_STRING:" + config.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_P_ASSOCIATED_URI_HEADER_STRING));
+
+ Log.i(TAG, "KEY_SIP_CONFIG_MAX_PAYLOAD_SIZE_ON_UDP_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_MAX_PAYLOAD_SIZE_ON_UDP_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_DEFAULT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_DEFAULT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_DEFAULT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_DEFAULT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_PUBLIC_PORT_WITH_NAT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_PUBLIC_PORT_WITH_NAT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_IPSEC_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_IPSEC_CLIENT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_IPSEC_SERVER_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_IPSEC_SERVER_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_UE_IPSEC_OLD_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_IPSEC_OLD_CLIENT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_IPSEC_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_IPSEC_CLIENT_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_IPSEC_SERVER_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_IPSEC_SERVER_PORT_INT, -99));
+ Log.i(TAG, "KEY_SIP_CONFIG_SERVER_IPSEC_OLD_CLIENT_PORT_INT:" + config.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_IPSEC_OLD_CLIENT_PORT_INT,
+ -99));
+
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_COMPACT_FORM_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_COMPACT_FORM_ENABLED_BOOL,
+ false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_KEEPALIVE_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_KEEPALIVE_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_NAT_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_NAT_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_GRUU_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_GRUU_ENABLED_BOOL, false));
+ Log.i(TAG, "KEY_SIP_CONFIG_IS_IPSEC_ENABLED_BOOL:" + config.getBoolean(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_IS_IPSEC_ENABLED_BOOL, false));
+ }
+
+ @Override
+ public void setSessionListener(SipSessionListener listener) {
+ sipSessionListener = listener;
+ }
+
+ @Override
+ public ListenableFuture<Boolean> send(Message message) {
+ sipDelegateConnection.sendMessage(MessageConverter.toPlatformMessage(message),
+ getVersion());
+ // TODO: check on transaction
+ return Futures.immediateFuture(true);
+ }
+
+ // Config values here.
+
+ @Override
+ public long getVersion() {
+ return configuration.getVersion();
+ }
+
+ @Override
+ public String getOutboundProxyAddr() {
+ return configuration.getString(SipDelegateImsConfiguration.
+ KEY_SIP_CONFIG_SERVER_DEFAULT_IPADDRESS_STRING);
+ }
+
+ @Override
+ public int getOutboundProxyPort() {
+ return configuration.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVER_DEFAULT_PORT_INT, -1);
+ }
+
+ @Override
+ public String getLocalIpAddress() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_DEFAULT_IPADDRESS_STRING);
+ }
+
+ @Override
+ public int getLocalPort() {
+ return configuration.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_UE_DEFAULT_PORT_INT, -1);
+ }
+
+ @Override
+ public String getSipTransport() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_TRANSPORT_TYPE_STRING);
+ }
+
+ @Override
+ public String getPublicUserIdentity() {
+ return null;
+ }
+
+ @Override
+ public String getDomain() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_HOME_DOMAIN_STRING);
+ }
+
+ @Override
+ public List<String> getAssociatedUris() {
+ String associatedUris = configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_P_ASSOCIATED_URI_HEADER_STRING);
+ if (!TextUtils.isEmpty(associatedUris)) {
+ return Splitter.on(',').trimResults(CharMatcher.anyOf("<>")).splitToList(
+ associatedUris);
+ }
+
+ return ImmutableList.of();
+ }
+
+ @Override
+ public String getSecurityVerifyHeader() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SECURITY_VERIFY_HEADER_STRING);
+ }
+
+ @Override
+ public List<String> getServiceRouteHeaders() {
+ String serviceRoutes =
+ configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVICE_ROUTE_HEADER_STRING);
+ return Splitter.on(',').trimResults().splitToList(serviceRoutes);
+ }
+
+ @Override
+ public String getContactUser() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_URI_USER_PART_STRING);
+ }
+
+ @Override
+ public String getImei() {
+ return configuration.getString(SipDelegateImsConfiguration.KEY_SIP_CONFIG_IMEI_STRING);
+ }
+
+ @Override
+ public String getPaniHeader() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_P_ACCESS_NETWORK_INFO_HEADER_STRING);
+ }
+
+ @Override
+ public String getPlaniHeader() {
+ return configuration.getString(
+ SipDelegateImsConfiguration.
+ KEY_SIP_CONFIG_P_LAST_ACCESS_NETWORK_INFO_HEADER_STRING);
+ }
+
+ @Override
+ public int getMaxPayloadSizeOnUdp() {
+ return configuration.getInt(
+ SipDelegateImsConfiguration.KEY_SIP_CONFIG_MAX_PAYLOAD_SIZE_ON_UDP_INT, 1500);
+ }
+ }
+}
+
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationStateChangeCallback.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationStateChangeCallback.java
new file mode 100644
index 0000000..4f36ce5
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationStateChangeCallback.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.registration;
+
+import com.android.libraries.rcs.simpleclient.service.ImsService;
+
+/**
+ * Callback for Registration state changes.
+ */
+public interface RegistrationStateChangeCallback {
+
+ /**
+ * The given feature tags are registered with the backend and the service would be able to
+ * send and receive messages.
+ *
+ * @param imsService the newly registered service.
+ */
+ void notifyRegStateChanged(ImsService imsService);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/ImsService.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/ImsService.java
new file mode 100644
index 0000000..e4dca1a
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/ImsService.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext;
+
+import java.util.Set;
+
+/**
+ * Covers service state and feature tag association.
+ */
+public interface ImsService {
+
+ /**
+ * Associated feature tags.
+ * Services will started and stopped according to the registration state of the feature tags.
+ */
+ Set<String> getFeatureTags();
+
+ /**
+ * Services started when their feature tags are enabled from the
+ * {@link com.android.libraries.rcs.simpleclient.registration.RegistrationController}.
+ * Context is made available to the ImsService here.
+ */
+ void start(SimpleRcsClientContext context);
+
+ /**
+ * Services stopped when their feature tags are disabled from
+ * {@link com.android.libraries.rcs.simpleclient.registration.RegistrationController}
+ */
+ void stop();
+
+ /**
+ * Simple callback mechanism for monitoring feature tag/ims service state.
+ */
+ void onStateChange(StateChangeCallback cb);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/StateChangeCallback.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/StateChangeCallback.java
new file mode 100644
index 0000000..038023a
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/StateChangeCallback.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service;
+
+/**
+ * Callback for ImsService state changes.
+ */
+public interface StateChangeCallback {
+
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatServiceException.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatServiceException.java
new file mode 100644
index 0000000..94850fd
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatServiceException.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service.chat;
+
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class defines an exception that can be thrown during the operation in {@link
+ * MinimalCpmChatService}
+ */
+public final class ChatServiceException extends Exception {
+
+ public static final int CODE_ERROR_UNSPECIFIED = 0;
+ private int mCode = CODE_ERROR_UNSPECIFIED;
+
+ /**
+ * A new {@link ChatServiceException} with an unspecified {@link ErrorCode} code.
+ *
+ * @param message an optional message to detail the error condition more specifically.
+ */
+ public ChatServiceException(@Nullable String message) {
+ super(getMessage(message, CODE_ERROR_UNSPECIFIED));
+ }
+
+ /**
+ * A new {@link ChatServiceException} that includes an {@link ErrorCode} error code.
+ *
+ * @param message an optional message to detail the error condition more specifically.
+ */
+ public ChatServiceException(@Nullable String message, @ErrorCode int code) {
+ super(getMessage(message, code));
+ mCode = code;
+ }
+
+ /**
+ * A new {@link ChatServiceException} that includes an {@link ErrorCode} error code and a {@link
+ * Throwable} that contains the original error that was thrown to lead to this Exception.
+ *
+ * @param message an optional message to detail the error condition more specifically.
+ * @param cause the {@link Throwable} that caused this {@link ChatServiceException} to be
+ * created.
+ */
+ public ChatServiceException(
+ @Nullable String message, @ErrorCode int code, @Nullable Throwable cause) {
+ super(getMessage(message, code), cause);
+ mCode = code;
+ }
+
+ private static String getMessage(String message, int code) {
+ StringBuilder builder;
+ if (!TextUtils.isEmpty(message)) {
+ builder = new StringBuilder(message);
+ builder.append(" (code: ");
+ builder.append(code);
+ builder.append(")");
+ return builder.toString();
+ } else {
+ return "code: " + code;
+ }
+ }
+
+ @ErrorCode
+ public int getCode() {
+ return mCode;
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ CODE_ERROR_UNSPECIFIED,
+ })
+ public @interface ErrorCode {
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatServiceListener.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatServiceListener.java
new file mode 100644
index 0000000..5fb9dee
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatServiceListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service.chat;
+
+/** Listener for chat service events */
+public interface ChatServiceListener {
+
+ /**
+ * Received a new incoming chat session from the RCS server. The session is ready to exchange
+ * messages since it is already established once this callback is called.
+ */
+ void onIncomingSession(SimpleChatSession session);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatSessionListener.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatSessionListener.java
new file mode 100644
index 0000000..eab571e
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/ChatSessionListener.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service.chat;
+
+import com.android.libraries.rcs.simpleclient.protocol.cpim.SimpleCpimMessage;
+
+/** Listener for chat session events */
+public interface ChatSessionListener {
+
+ /**
+ * Received a new CPIM message via the {@link SimpleChatSession} associated with this listener.
+ *
+ * @param message Received message in the form of {@link SimpleCpimMessage}
+ */
+ void onMessageReceived(SimpleCpimMessage message);
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java
new file mode 100644
index 0000000..01a1061
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java
@@ -0,0 +1,204 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service.chat;
+
+import android.content.Context;
+import android.telephony.ims.SipDelegateConnection;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext;
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSession;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipSessionListener;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils;
+import com.android.libraries.rcs.simpleclient.service.ImsService;
+import com.android.libraries.rcs.simpleclient.service.StateChangeCallback;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Minimal CPM chat session service that provides the interface creating a {@link SimpleChatSession}
+ * instance using {@link SipDelegateConnection}.
+ */
+public class MinimalCpmChatService implements ImsService {
+ public static final String CPM_SESSION_TAG =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+ private static final String TAG = SimpleChatSession.class.getSimpleName();
+ private final Map<String, SimpleChatSession> mTransactions = new HashMap<>();
+ private final Map<String, SimpleChatSession> mDialogs = new HashMap<>();
+
+ private final MsrpManager mMsrpManager;
+ private SimpleRcsClientContext mContext;
+
+ @Nullable
+ private ChatServiceListener mListener;
+
+ private final SipSessionListener mSipSessionListener =
+ sipMessage -> {
+ if (sipMessage instanceof SIPRequest) {
+ handleRequest((SIPRequest) sipMessage);
+ } else if (sipMessage instanceof SIPResponse) {
+ handleResponse((SIPResponse) sipMessage);
+ }
+ };
+
+ public MinimalCpmChatService(Context context) {
+ mMsrpManager = new MsrpManager(context);
+ }
+
+ @Override
+ public Set<String> getFeatureTags() {
+ return ImmutableSet.of(CPM_SESSION_TAG);
+ }
+
+ @Override
+ public void start(SimpleRcsClientContext context) {
+ mContext = context;
+ context.getSipSession().setSessionListener(mSipSessionListener);
+ }
+
+ @Override
+ public void stop() {
+ }
+
+ @Override
+ public void onStateChange(StateChangeCallback cb) {
+ }
+
+ /**
+ * Start an originating 1:1 chat session interacting with the RCS server.
+ *
+ * @param telUriContact The remote contact in the from of TEL URI
+ * @return The future will be completed with SimpleChatSession once the session is established
+ * successfully. If the session fails for any reason, return the failed future with {@link
+ * ChatServiceException}
+ */
+ public ListenableFuture<SimpleChatSession> startOriginatingChatSession(String telUriContact) {
+ Log.i(TAG, "startOriginatingChatSession");
+ SimpleChatSession session = new SimpleChatSession(mContext, this, mMsrpManager);
+ return Futures.transform(
+ session.start(telUriContact), v -> session, MoreExecutors.directExecutor());
+ }
+
+ ListenableFuture<Boolean> sendSipRequest(SIPRequest msg, SimpleChatSession session) {
+ Log.i(TAG, "sendSipRequest");
+ if (!TextUtils.equals(msg.getMethod(), Request.ACK)) {
+ mTransactions.put(msg.getTransactionId(), session);
+ }
+
+ if (TextUtils.equals(msg.getMethod(), Request.BYE)) {
+ mDialogs.remove(msg.getDialogId(/* isServer= */ false));
+ }
+
+ SipSession sipSession = mContext.getSipSession();
+ return sipSession.send(msg);
+ }
+
+ ListenableFuture<Boolean> sendSipResponse(SIPResponse msg, SimpleChatSession session) {
+ Log.i(TAG, "sendSipRequest");
+ if (TextUtils.equals(msg.getCSeq().getMethod(), Request.BYE)) {
+ mDialogs.remove(msg.getDialogId(/* isServer= */ true));
+ } else if (TextUtils.equals(msg.getCSeq().getMethod(), Request.INVITE)
+ && msg.getStatusCode() == Response.OK) {
+ // Cache the dialog in order to route in-dialog request to the corresponding session.
+ mDialogs.put(msg.getDialogId(/* isServer= */ true), session);
+ }
+ SipSession sipSession = mContext.getSipSession();
+ return sipSession.send(msg);
+ }
+
+ private void handleRequest(SIPRequest request) {
+ String dialogId = request.getDialogId(/* isServer= */ true);
+ if (mDialogs.containsKey(dialogId)) {
+ SimpleChatSession session = mDialogs.get(dialogId);
+ session.receiveMessage(request);
+ } else if (TextUtils.equals(request.getMethod(), Request.INVITE)) {
+ SimpleChatSession session = new SimpleChatSession(mContext, this, mMsrpManager);
+ session
+ .start(request)
+ .addListener(
+ () -> {
+ ChatServiceListener listener = mListener;
+ if (listener != null) {
+ listener.onIncomingSession(session);
+ }
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ // Reject non-INVITE request.
+ try {
+ SIPResponse response =
+ SipUtils.buildInviteResponse(
+ mContext.getSipSession().getSessionConfiguration(),
+ request,
+ Response.METHOD_NOT_ALLOWED);
+ sendSipResponse(response, /* session= */ null)
+ .addListener(() -> {
+ }, MoreExecutors.directExecutor());
+ } catch (ParseException e) {
+ Log.e(TAG, "Exception while sending response", e);
+ }
+ }
+ }
+
+ private void handleResponse(SIPResponse response) {
+ Log.i(TAG, "handleResponse:\r\n" + response);
+ // catch the exception because abnormal response always causes App to crash.
+ try {
+ SimpleChatSession session = mTransactions.get(response.getTransactionId());
+ if (session != null) {
+ if (response.isFinalResponse()) {
+ mTransactions.remove(response.getTransactionId());
+
+ // Cache the dialog in order to route in-dialog request to the corresponding
+ // session.
+ if (TextUtils.equals(response.getCSeq().getMethod(), Request.INVITE)
+ && response.getStatusCode() == Response.OK) {
+ mDialogs.put(response.getDialogId(/* isServer= */ false), session);
+ }
+ }
+
+ session.receiveMessage(response);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /** Set new listener for the chat service. */
+ public void setListener(@Nullable ChatServiceListener listener) {
+ mListener = listener;
+ }
+}
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java
new file mode 100644
index 0000000..b72f861
--- /dev/null
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java
@@ -0,0 +1,405 @@
+/*
+ * 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.
+ */
+
+package com.android.libraries.rcs.simpleclient.service.chat;
+
+import static com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.CODE_ERROR_UNSPECIFIED;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.libraries.rcs.simpleclient.SimpleRcsClientContext;
+import com.android.libraries.rcs.simpleclient.protocol.cpim.CpimUtils;
+import com.android.libraries.rcs.simpleclient.protocol.cpim.SimpleCpimMessage;
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk;
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpManager;
+import com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpSession;
+import com.android.libraries.rcs.simpleclient.protocol.sdp.SimpleSdpMessage;
+import com.android.libraries.rcs.simpleclient.protocol.sip.SipUtils;
+import com.android.libraries.rcs.simpleclient.service.chat.ChatServiceException.ErrorCode;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+
+import gov.nist.javax.sip.header.To;
+import gov.nist.javax.sip.header.ims.PAssertedIdentityHeader;
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.UUID;
+
+import javax.sip.address.URI;
+import javax.sip.message.Message;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/**
+ * Simple chat session implementation in order to send/receive a text message via SIP/MSRP
+ * connection. Currently, this supports only a outgoing CPM session.
+ */
+public class SimpleChatSession {
+ private static final String TAG = SimpleChatSession.class.getSimpleName();
+ private final SimpleRcsClientContext mContext;
+ private final MinimalCpmChatService mService;
+ private final MsrpManager mMsrpManager;
+ private final String mConversationId = UUID.randomUUID().toString();
+ private SettableFuture<Void> mStartFuture;
+ @Nullable
+ private SIPRequest mInviteRequest;
+ @Nullable
+ private URI mRemoteUri;
+ @Nullable
+ private SimpleSdpMessage mRemoteSdp;
+ @Nullable
+ private MsrpSession mMsrpSession;
+ @Nullable
+ private ChatSessionListener mListener;
+
+
+ public SimpleChatSession(
+ SimpleRcsClientContext context, MinimalCpmChatService service,
+ MsrpManager msrpManager) {
+ mService = service;
+ mContext = context;
+ mMsrpManager = msrpManager;
+ }
+
+ public URI getRemoteUri() {
+ return mRemoteUri;
+ }
+
+ /** Send a text message via MSRP session associated with this session. */
+ public void sendMessage(String msg) {
+ MsrpSession session = mMsrpSession;
+ if (session == null) {
+ return;
+ }
+
+ // Build a new CPIM message and send it out through the MSRP session.
+ SimpleCpimMessage cpim = CpimUtils.createForText(msg);
+ MsrpChunk msrpChunk =
+ MsrpChunk.newBuilder()
+ .method(MsrpChunk.Method.SEND)
+ .content(cpim.encode().getBytes(UTF_8))
+ .build();
+ Futures.addCallback(
+ session.send(msrpChunk),
+ new FutureCallback<MsrpChunk>() {
+ @Override
+ public void onSuccess(MsrpChunk result) {
+ if (result.responseCode() != 200) {
+ Log.d(
+ TAG,
+ "Received error response id="
+ + result.transactionId()
+ + " code="
+ + result.responseCode());
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.d(TAG, "Failed to send msrp chunk", t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ /** Start outgoing chat session. */
+ ListenableFuture<Void> start(String telUriContact) {
+ if (mStartFuture != null) {
+ return Futures.immediateFailedFuture(
+ new ChatServiceException("Session already started"));
+ }
+
+ SettableFuture<Void> future = SettableFuture.create();
+ mStartFuture = future;
+ mRemoteUri = SipUtils.createUri(telUriContact);
+ try {
+ SIPRequest invite =
+ SipUtils.buildInvite(
+ mContext.getSipSession().getSessionConfiguration(), telUriContact,
+ mConversationId);
+ Log.i(TAG, "buildInvite done");
+ mInviteRequest = invite;
+ Futures.addCallback(
+ mService.sendSipRequest(invite, this),
+ new FutureCallback<Boolean>() {
+ @Override
+ public void onSuccess(Boolean result) {
+ Log.i(TAG, "onSuccess:" + result);
+ if (!result) {
+ notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.i(TAG, "onFailure:" + t.getMessage());
+ notifyFailure("Failed to send INVITE", CODE_ERROR_UNSPECIFIED);
+ }
+ },
+ MoreExecutors.directExecutor());
+ } catch (ParseException e) {
+ Log.e(TAG, e.getMessage());
+ e.printStackTrace();
+ return Futures.immediateFailedFuture(
+ new ChatServiceException("Failed to build INVITE"));
+ }
+
+ return future;
+ }
+
+ /** Start incoming chat session. */
+ ListenableFuture<Void> start(SIPRequest invite) {
+ mInviteRequest = invite;
+ int statusCode = Response.OK;
+ if (!SipUtils.hasSdpContent(invite)) {
+ statusCode = Response.NOT_ACCEPTABLE_HERE;
+ } else {
+ try {
+ mRemoteSdp = SimpleSdpMessage.parse(
+ new ByteArrayInputStream(invite.getRawContent()));
+ } catch (ParseException | IOException e) {
+ statusCode = Response.BAD_REQUEST;
+ }
+ }
+
+ updateRemoteUri(mInviteRequest);
+
+ // Automatically reply back to the invite by building a pre-canned response.
+ try {
+ SIPResponse response =
+ SipUtils.buildInviteResponse(
+ mContext.getSipSession().getSessionConfiguration(), invite, statusCode);
+ return Futures.transform(
+ mService.sendSipResponse(response, this), result -> null,
+ MoreExecutors.directExecutor());
+ } catch (ParseException e) {
+ Log.e(TAG, "Exception while building response", e);
+ return Futures.immediateFailedFuture(e);
+ }
+ }
+
+ /** Terminate the current SIP session. */
+ public ListenableFuture<Void> terminate() {
+ if (mInviteRequest == null) {
+ return Futures.immediateFuture(null);
+ }
+ try {
+ mMsrpSession.terminate();
+ } catch (IOException e) {
+ return Futures.immediateFailedFuture(
+ new ChatServiceException(
+ "Exception while terminating MSRP session", CODE_ERROR_UNSPECIFIED));
+ }
+ try {
+
+ SettableFuture<Void> future = SettableFuture.create();
+ Futures.addCallback(
+ mService.sendSipRequest(SipUtils.buildBye(mInviteRequest), this),
+ new FutureCallback<Boolean>() {
+ @Override
+ public void onSuccess(Boolean result) {
+ future.set(null);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ future.setException(
+ new ChatServiceException("Failed to send BYE",
+ CODE_ERROR_UNSPECIFIED, t));
+ }
+ },
+ MoreExecutors.directExecutor());
+ return future;
+ } catch (ParseException e) {
+ return Futures.immediateFailedFuture(
+ new ChatServiceException("Failed to build BYE", CODE_ERROR_UNSPECIFIED));
+ }
+ }
+
+ void receiveMessage(Message msg) {
+ if (msg instanceof SIPRequest) {
+ handleSipRequest((SIPRequest) msg);
+ } else {
+ handleSipResponse((SIPResponse) msg);
+ }
+ }
+
+ private void handleSipRequest(SIPRequest request) {
+ SIPResponse response;
+ if (TextUtils.equals(request.getMethod(), Request.ACK)) {
+ // Terminating session established, start a msrp session.
+ if (mRemoteSdp != null) {
+ startMsrpSession(mRemoteSdp);
+ }
+ return;
+ }
+
+ if (TextUtils.equals(request.getMethod(), Request.BYE)) {
+ response = request.createResponse(Response.OK);
+ } else {
+ // Currently we support only INVITE and BYE.
+ response = request.createResponse(Response.METHOD_NOT_ALLOWED);
+ }
+ Futures.addCallback(
+ mService.sendSipResponse(response, this),
+ new FutureCallback<Boolean>() {
+ @Override
+ public void onSuccess(Boolean result) {
+ if (result) {
+ Log.d(
+ TAG,
+ "Response to Call-Id: "
+ + response.getCallId().getCallId()
+ + " sent successfully");
+ } else {
+ Log.d(TAG, "Failed to send response");
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.d(TAG, "Exception while sending response: ", t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void handleSipResponse(SIPResponse response) {
+ int code = response.getStatusCode();
+
+ // Nothing to do for a provisional response.
+ if (response.isFinalResponse()) {
+ if (code == Response.OK) {
+ handle200OK(response);
+ } else {
+ handleNon200(response);
+ }
+ }
+ }
+
+ private void handleNon200(SIPResponse response) {
+ Log.d(TAG, "Received error response code=" + response.getStatusCode());
+ notifyFailure("Received non-200 INVITE response", CODE_ERROR_UNSPECIFIED);
+ }
+
+ private void handle200OK(SIPResponse response) {
+ if (!SipUtils.hasSdpContent(response)) {
+ notifyFailure("Content is not a SDP", CODE_ERROR_UNSPECIFIED);
+ return;
+ }
+
+ try {
+ SimpleSdpMessage sdp =
+ SimpleSdpMessage.parse(new ByteArrayInputStream(response.getRawContent()));
+ startMsrpSession(sdp);
+ } catch (ParseException | IOException e) {
+ notifyFailure("Invalid SDP in INVITE", CODE_ERROR_UNSPECIFIED);
+ }
+
+ if (mInviteRequest != null) {
+ SIPRequest ack = mInviteRequest.createAckRequest((To) response.getToHeader());
+ Futures.addCallback(
+ mService.sendSipRequest(ack, this),
+ new FutureCallback<Boolean>() {
+ @Override
+ public void onSuccess(Boolean result) {
+ if (result) {
+ mStartFuture.set(null);
+ mStartFuture = null;
+ } else {
+ notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ notifyFailure("Failed to send ACK", CODE_ERROR_UNSPECIFIED);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+ }
+
+ private void notifyFailure(String message, @ErrorCode int code) {
+ mStartFuture.setException(new ChatServiceException(message, code));
+ mStartFuture = null;
+ }
+
+ private void startMsrpSession(SimpleSdpMessage remoteSdp) {
+ Log.d(TAG, "Start MSRP session: " + remoteSdp);
+ if (remoteSdp.getAddress().isPresent() && remoteSdp.getPort().isPresent()) {
+ Futures.addCallback(
+ mMsrpManager.createMsrpSession(
+ remoteSdp.getAddress().get(), remoteSdp.getPort().getAsInt(),
+ this::receiveMsrpChunk),
+ new FutureCallback<MsrpSession>() {
+ @Override
+ public void onSuccess(MsrpSession result) {
+ mMsrpSession = result;
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.e(TAG, "Failed to create msrp session", t);
+ terminate()
+ .addListener(
+ () -> {
+ Log.d(TAG, "Session terminated");
+ },
+ MoreExecutors.directExecutor());
+ }
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ Log.e(TAG, "Address or port is not present");
+ }
+ }
+
+ private void receiveMsrpChunk(MsrpChunk chunk) {
+ Log.d(TAG, "Received msrp= " + chunk + " conversation=" + mConversationId);
+ if (mListener != null) {
+ // TODO(b/173186571): Parse CPIM and invoke onMessageReceived()
+ }
+ }
+
+ /** Set new listener for this session. */
+ public void setListener(@Nullable ChatSessionListener listener) {
+ mListener = listener;
+ }
+
+ private void updateRemoteUri(SIPRequest request) {
+ PAssertedIdentityHeader pAssertedIdentityHeader =
+ (PAssertedIdentityHeader) request.getHeader("P-Asserted-Identity");
+ if (pAssertedIdentityHeader == null) {
+ mRemoteUri = request.getFrom().getAddress().getURI();
+ } else {
+ mRemoteUri = pAssertedIdentityHeader.getAddress().getURI();
+ }
+ }
+}