[RCS] Implement TestRcsApp

Bug: 176472177
Test: manual
Change-Id: I740b1be2e8c2acdaf317bff6cdcf8e267226f855
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);
+        }
+    }
+}