[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();
+        }
+    }
+}