Handle prioritized car mode in Telecom.

Add basic support for receiving the broadcast that the prioritized car
mode has changed in Telecom; rest of implementation TBD.
Also added a fancy new car mode dialer app which can be used in car mode.

Bug: 136109592
Test: Use new test app to verify enter/exit car mode with priority.
Test: Run new CTS tests.
Change-Id: I93daadf341e7c158260a1cc76efc6085a29f7a38
Merged-In: I93daadf341e7c158260a1cc76efc6085a29f7a38
diff --git a/testapps/carmodedialer/Android.bp b/testapps/carmodedialer/Android.bp
new file mode 100644
index 0000000..7179b1f
--- /dev/null
+++ b/testapps/carmodedialer/Android.bp
@@ -0,0 +1,26 @@
+//
+// Copyright (C) 2019 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.
+//
+
+android_test {
+    name: "TelecomCarModeApp",
+    static_libs: [
+        "androidx.legacy_legacy-support-v4",
+        "guava",
+    ],
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+}
diff --git a/testapps/carmodedialer/AndroidManifest.xml b/testapps/carmodedialer/AndroidManifest.xml
new file mode 100644
index 0000000..7f55f7e
--- /dev/null
+++ b/testapps/carmodedialer/AndroidManifest.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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"
+          coreApp="true"
+          package="com.android.server.telecom.carmodedialer">
+
+    <uses-sdk
+        android:minSdkVersion="28"
+        android:targetSdkVersion="29" />
+
+    <uses-permission android:name="android.permission.ACCEPT_HANDOVER" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+    <uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+
+    <application android:label="Telecom CarMode">
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name="com.android.server.telecom.carmodedialer.CarModeInCallServiceImpl"
+                 android:permission="android.permission.BIND_INCALL_SERVICE" >
+          <meta-data android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI"
+                     android:value="true"/>
+          <intent-filter>
+              <action android:name="android.telecom.InCallService"/>
+          </intent-filter>
+        </service>
+
+        <activity android:name="com.android.server.telecom.carmodedialer.CarModeInCallUI"
+                android:label="CarMode Dialer"
+                android:launchMode="singleInstance">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name="com.android.server.telecom.carmodedialer.CarModeDialerActivity"
+                  android:label="CarMode Dialer">
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:mimeType="vnd.android.cursor.item/phone" />
+                <data android:mimeType="vnd.android.cursor.item/person" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="voicemail" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="tel" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/testapps/carmodedialer/res/drawable-hdpi/ic_android_black_24dp.png b/testapps/carmodedialer/res/drawable-hdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..ed3ee45
--- /dev/null
+++ b/testapps/carmodedialer/res/drawable-hdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/carmodedialer/res/drawable-mdpi/ic_android_black_24dp.png b/testapps/carmodedialer/res/drawable-mdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..a4add51
--- /dev/null
+++ b/testapps/carmodedialer/res/drawable-mdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/carmodedialer/res/drawable-xhdpi/ic_android_black_24dp.png b/testapps/carmodedialer/res/drawable-xhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..41558f2
--- /dev/null
+++ b/testapps/carmodedialer/res/drawable-xhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/carmodedialer/res/drawable-xhdpi/stat_sys_phone_call.png b/testapps/carmodedialer/res/drawable-xhdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..1bb4340
--- /dev/null
+++ b/testapps/carmodedialer/res/drawable-xhdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/testapps/carmodedialer/res/drawable-xxhdpi/ic_android_black_24dp.png b/testapps/carmodedialer/res/drawable-xxhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..6006b12
--- /dev/null
+++ b/testapps/carmodedialer/res/drawable-xxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/carmodedialer/res/drawable-xxxhdpi/ic_android_black_24dp.png b/testapps/carmodedialer/res/drawable-xxxhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..4f935bf
--- /dev/null
+++ b/testapps/carmodedialer/res/drawable-xxxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/carmodedialer/res/layout/call_list_item.xml b/testapps/carmodedialer/res/layout/call_list_item.xml
new file mode 100644
index 0000000..9a77ceb
--- /dev/null
+++ b/testapps/carmodedialer/res/layout/call_list_item.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical" >
+    <TextView
+            android:id="@+id/phoneNumber"
+            android:layout_gravity="left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="25dp"
+            android:text="TextView" />
+    <TextView
+            android:id="@+id/callState"
+            android:layout_gravity="left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="25dp"
+            android:text="TextView" />
+    <TextView
+            android:id="@+id/duration"
+            android:layout_gravity="right"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="25dp"
+            android:text="TextView" />
+</LinearLayout>
diff --git a/testapps/carmodedialer/res/layout/incall_screen.xml b/testapps/carmodedialer/res/layout/incall_screen.xml
new file mode 100644
index 0000000..bef6915
--- /dev/null
+++ b/testapps/carmodedialer/res/layout/incall_screen.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+    <ListView
+            android:id="@+id/callListView"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:divider="#FFCC00"
+            android:dividerHeight="4px">
+    </ListView>
+    <GridLayout
+        android:columnCount="3"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/end_call_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/endCallButton" />
+        <Button
+            android:id="@+id/mute_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/muteButton" />
+        <Button
+            android:id="@+id/hold_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/holdButton"/>
+        <Button
+            android:id="@+id/rtt_iface_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/rttIfaceButton"/>
+        <Button
+            android:id="@+id/answer_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/answerCallButton"/>
+        <Button
+            android:id="@+id/start_rtt_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/startRttButton"/>
+        <Button
+            android:id="@+id/accept_rtt_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/acceptRttButton"/>
+        <Button
+            android:id="@+id/request_handover_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/handoverButton"/>
+    </GridLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Spinner
+            android:id="@+id/available_bt_devices"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+        <Button
+            android:id="@+id/set_bt_device_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/setBtDeviceButton"/>
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/earpiece_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/earpieceButton"/>
+        <Button
+            android:id="@+id/speaker_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/speakerButton"/>
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <TextView
+            android:id="@+id/current_route_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/currentRouteLabel"/>
+        <TextView
+            android:id="@+id/current_audio_route"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10dp"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/testapps/carmodedialer/res/layout/testdialer_main.xml b/testapps/carmodedialer/res/layout/testdialer_main.xml
new file mode 100644
index 0000000..c332d19
--- /dev/null
+++ b/testapps/carmodedialer/res/layout/testdialer_main.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+    <TextView
+        android:id="@+id/appLabel"
+        android:layout_gravity="left"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="25dp"
+        android:text="Telecom CarMode Dialer" />
+    <EditText
+        android:id="@+id/number"
+        android:inputType="number"
+        android:layout_width="200dp"
+        android:layout_height="wrap_content" />
+    <Button
+        android:id="@+id/place_call_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/placeCallButton" />
+    <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:textSize="15dp"
+            android:text="Priority:" />
+        <EditText
+            android:id="@+id/priority"
+            android:inputType="number"
+            android:text="100"
+            android:layout_width="50dp"
+            android:layout_height="wrap_content" />
+        <Button
+            android:id="@+id/enable_car_mode"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Enable Car mode" />
+        <Button
+            android:id="@+id/disable_car_mode"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Disable Car mode" />
+    </LinearLayout>
+    <Button
+        android:id="@+id/toggle_incallservice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Toggle InCallService" />
+</LinearLayout>
diff --git a/testapps/carmodedialer/res/values/donottranslate_strings.xml b/testapps/carmodedialer/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..3b5fa65
--- /dev/null
+++ b/testapps/carmodedialer/res/values/donottranslate_strings.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <!-- Application label -->
+    <string name="app_name">TelecommTests</string>
+
+    <!-- String for the TestCallActivity -->
+    <string name="testCallActivityLabel">Test Connection Service App</string>
+
+    <!-- String for the CarModeDialerActivity -->
+    <string name="testDialerActivityLabel">Test Dialer</string>
+
+    <!-- String for button in CarModeDialerActivity that reassigns the default Dialer -->
+    <string name="defaultDialerButton">Default dialer request</string>
+
+    <!-- String for button in CarModeDialerActivity that places a test call -->
+    <string name="placeCallButton">Place call</string>
+
+    <!-- String for button in CarModeDialerActivity that performs voicemail requests to verify
+            voicemail permissions -->
+    <string name="testVoicemailButton">Exercise voicemail permissions</string>
+
+    <!-- String for button in CarModeDialerActivity that tries to exercise the
+            TelecomManager.cancelMissedCallNotifications() functionality -->
+    <string name="cancelMissedButton">Cancel missed calls</string>
+
+    <string name="endCallButton">End Call</string>
+
+    <string name="answerCallButton">Answer</string>
+
+    <string name="startCallWithRtt">Start call with RTT</string>
+
+    <string name="rttIfaceButton">RTT</string>
+
+    <string name="endRttButton">End RTT</string>
+
+    <string name="startRttButton">Start RTT</string>
+
+    <string name="acceptRttButton">Accept RTT request</string>
+
+    <string name="muteButton">Mute</string>
+
+    <string name="holdButton">Hold</string>
+
+    <string name="handoverButton">Handover</string>
+
+    <string name="inCallUiAppLabel">Test InCall UI</string>
+
+    <string name="UssdUiAppLabel">Test Ussd UI</string>
+
+    <string name="placeUssdButton">Send USSD</string>
+
+    <string name="KeyUiAppLabel">Get Key UI</string>
+
+    <string name="getKeyButton">Get Key Json</string>
+
+    <string name="earpieceButton">Earpiece/Wired</string>
+
+    <string name="speakerButton">Speakerphone</string>
+
+    <string name="setBtDeviceButton">Set BT device</string>
+
+    <string name="currentRouteLabel">Current audio route</string>
+    <!-- String for button in SelfManagedCallingActivity. -->
+    <string name="checkIfPermittedBeforeCallingButton">Check if calls permitted before calling</string>
+
+    <string name="selfManagedCallingActivityLabel">Self-Managed Sample</string>
+
+    <string name="outgoingCallNotPermitted">Outgoing call not permitted.</string>
+
+    <string name="outgoingCallNotPermittedCS">Outgoing call not permitted (CS Reported).</string>
+
+    <string name="incomingCallNotPermitted">Incoming call not permitted.</string>
+
+    <string name="incomingCallNotPermittedCS">Incoming call not permitted (CS Reported).</string>
+
+    <string name="rttUiLabel">Test RTT UI</string>
+
+    <string-array name="rtt_mode_array">
+        <item>Full</item>
+        <item>HCO</item>
+        <item>VCO</item>
+    </string-array>
+
+    <string-array name="rtt_reply_one_liners">
+        <item>To RTT or not to RTT, that is the question...</item>
+        <item>Making TTY great again!</item>
+        <item>I would be more comfortable with real "Thyme" chatting. I don\'t know how to end
+        this pun</item>
+        <item>お疲れ様でした</item>
+        <item>The FCC has mandated that I respond... I will do so begrudgingly</item>
+        <item>😂😂😂💯</item>
+    </string-array>
+</resources>
diff --git a/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CallListAdapter.java b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CallListAdapter.java
new file mode 100644
index 0000000..8ba59a7
--- /dev/null
+++ b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CallListAdapter.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.carmodedialer;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.telecom.Call;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class CallListAdapter extends BaseAdapter {
+    private static final String TAG = "CallListAdapter";
+
+    private final CarModeCallList.Listener mListener = new CarModeCallList.Listener() {
+        @Override
+        public void onCallAdded(Call call) {
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onCallRemoved(Call call) {
+            notifyDataSetChanged();
+            if (mCallList.size() == 0) {
+                mCallList.removeListener(this);
+            }
+        }
+    };
+
+    private final LayoutInflater mLayoutInflater;
+    private final CarModeCallList mCallList;
+    private final Handler mHandler = new Handler();
+    private final Runnable mSecondsRunnable = new Runnable() {
+        @Override
+        public void run() {
+            notifyDataSetChanged();
+            if (mCallList.size() > 0) {
+                mHandler.postDelayed(this, 1000);
+            }
+        }
+    };
+
+    public CallListAdapter(Context context) {
+        mLayoutInflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mCallList = CarModeCallList.getInstance();
+        mCallList.addListener(mListener);
+        mHandler.postDelayed(mSecondsRunnable, 1000);
+    }
+
+
+    @Override
+    public int getCount() {
+        Log.i(TAG, "size reporting: " + mCallList.size());
+        return mCallList.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return position;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+        Log.i(TAG, "getView: " + position);
+        if (convertView == null) {
+            convertView = mLayoutInflater.inflate(R.layout.call_list_item, parent, false);
+        }
+
+        TextView phoneNumber = convertView.findViewById(R.id.phoneNumber);
+        TextView duration = convertView.findViewById(R.id.duration);
+        TextView state = convertView.findViewById(R.id.callState);
+
+        Call call = mCallList.getCall(position);
+        Uri handle = call.getDetails().getHandle();
+        phoneNumber.setText(handle == null ? "No number" : handle.getSchemeSpecificPart());
+
+        long durationMs = System.currentTimeMillis() - call.getDetails().getConnectTimeMillis();
+        duration.setText((durationMs / 1000) + " secs");
+
+        state.setText(getStateString(call));
+
+        Log.i(TAG, "Call found: " + ((handle == null) ? "null" : handle.getSchemeSpecificPart())
+                + ", " + durationMs);
+        Log.i(TAG, "Call extras: " + extrasToString(call.getDetails().getExtras()));
+        Log.i(TAG, "Call intent extras: " + extrasToString(call.getDetails().getIntentExtras()));
+
+        return convertView;
+    }
+
+    private String extrasToString(Bundle bundle) {
+        StringBuilder sb = new StringBuilder("[");
+        for (String key : bundle.keySet()) {
+            sb.append(key);
+            sb.append(": ");
+            sb.append(bundle.get(key));
+            sb.append("\n");
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private static String getStateString(Call call) {
+        switch (call.getState()) {
+            case Call.STATE_ACTIVE:
+                return "active";
+            case Call.STATE_CONNECTING:
+                return "connecting";
+            case Call.STATE_DIALING:
+                return "dialing";
+            case Call.STATE_DISCONNECTED:
+                return "disconnected";
+            case Call.STATE_DISCONNECTING:
+                return "disconnecting";
+            case Call.STATE_HOLDING:
+                return "on hold";
+            case Call.STATE_NEW:
+                return "new";
+            case Call.STATE_RINGING:
+                return "ringing";
+            case Call.STATE_SELECT_PHONE_ACCOUNT:
+                return "select phone account";
+            default:
+                return "unknown";
+        }
+    }
+}
diff --git a/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeCallList.java b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeCallList.java
new file mode 100644
index 0000000..5fba5bd
--- /dev/null
+++ b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeCallList.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.carmodedialer;
+
+import android.content.Context;
+import android.telecom.Call;
+import android.telecom.InCallService;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Maintains a list of calls received via the {@link TestInCallServiceImpl}.
+ */
+public class CarModeCallList extends Call.Callback {
+
+    public static abstract class Listener {
+        public void onCallAdded(Call call) {}
+        public void onCallRemoved(Call call) {}
+        public void onRttStarted(Call call) {}
+        public void onRttStopped(Call call) {}
+        public void onRttInitiationFailed(Call call, int reason) {}
+        public void onRttRequest(Call call, int id) {}
+    }
+
+    private static final com.android.server.telecom.carmodedialer.CarModeCallList
+            INSTANCE = new com.android.server.telecom.carmodedialer.CarModeCallList();
+    private static final String TAG = "TestCallList";
+
+    private class TestVideoCallListener extends InCallService.VideoCall.Callback {
+        private Call mCall;
+
+        public TestVideoCallListener(Call call) {
+            mCall = call;
+        }
+
+        @Override
+        public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+            Log.v(TAG,
+                    "onSessionModifyRequestReceived: videoState = " + videoProfile.getVideoState()
+                            + " call = " + mCall);
+        }
+
+        @Override
+        public void onSessionModifyResponseReceived(int status, VideoProfile requestedProfile,
+                VideoProfile responseProfile) {
+            Log.v(TAG,
+                    "onSessionModifyResponseReceived: status = " + status + " videoState = "
+                            + responseProfile.getVideoState()
+                            + " call = " + mCall);
+        }
+
+        @Override
+        public void onCallSessionEvent(int event) {
+
+        }
+
+        @Override
+        public void onPeerDimensionsChanged(int width, int height) {
+
+        }
+
+        @Override
+        public void onVideoQualityChanged(int videoQuality) {
+            Log.v(TAG,
+                    "onVideoQualityChanged: videoQuality = " + videoQuality + " call = " + mCall);
+        }
+
+        @Override
+        public void onCallDataUsageChanged(long dataUsage) {
+
+        }
+
+        @Override
+        public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+
+        }
+    }
+
+    // The calls the call list knows about.
+    private List<Call> mCalls = new LinkedList<Call>();
+    private Map<Call, TestVideoCallListener> mVideoCallListeners =
+            new ArrayMap<Call, TestVideoCallListener>();
+    private Set<Listener> mListeners = new ArraySet<Listener>();
+    private Context mContext;
+    private int mLastRttRequestId = -1;
+
+    /**
+     * Singleton accessor.
+     */
+    public static com.android.server.telecom.carmodedialer.CarModeCallList getInstance() {
+        return INSTANCE;
+    }
+
+    public void addListener(Listener listener) {
+        if (listener != null) {
+            mListeners.add(listener);
+        }
+    }
+
+    public boolean removeListener(Listener listener) {
+        return mListeners.remove(listener);
+    }
+
+    public Call getCall(int position) {
+        return mCalls.get(position);
+    }
+
+    public void addCall(Call call) {
+        if (mCalls.contains(call)) {
+            Log.e(TAG, "addCall: Call already added.");
+            return;
+        }
+        Log.i(TAG, "addCall: " + call + " " + System.identityHashCode(this));
+        mCalls.add(call);
+        call.registerCallback(this);
+
+        for (Listener l : mListeners) {
+            l.onCallAdded(call);
+        }
+    }
+
+    public void removeCall(Call call) {
+        if (!mCalls.contains(call)) {
+            Log.e(TAG, "removeCall: Call cannot be removed -- doesn't exist.");
+            return;
+        }
+        Log.i(TAG, "removeCall: " + call);
+        mCalls.remove(call);
+        call.unregisterCallback(this);
+
+        for (Listener l : mListeners) {
+            if (l != null) {
+                l.onCallRemoved(call);
+            }
+        }
+    }
+
+    public void clearCalls() {
+        for (Call call : new LinkedList<Call>(mCalls)) {
+            removeCall(call);
+        }
+
+        for (Call call : mVideoCallListeners.keySet()) {
+            if (call.getVideoCall() != null) {
+                call.getVideoCall().destroy();
+            }
+        }
+        mVideoCallListeners.clear();
+    }
+
+    public int size() {
+        return mCalls.size();
+    }
+
+    public int getLastRttRequestId() {
+        return mLastRttRequestId;
+    }
+
+    /**
+     * For any video calls tracked, sends an upgrade to video request.
+     */
+    public void sendUpgradeToVideoRequest(int videoState) {
+        Log.v(TAG, "sendUpgradeToVideoRequest : videoState = " + videoState);
+
+        for (Call call : mCalls) {
+            InCallService.VideoCall videoCall = call.getVideoCall();
+            Log.v(TAG, "sendUpgradeToVideoRequest: checkCall "+call);
+            if (videoCall == null) {
+                continue;
+            }
+
+            Log.v(TAG, "send upgrade to video request for call: " + call);
+            videoCall.sendSessionModifyRequest(new VideoProfile(videoState));
+        }
+    }
+
+    /**
+     * For any video calls which are active, sends an upgrade to video response with the specified
+     * video state.
+     *
+     * @param videoState The video state to respond with.
+     */
+    public void sendUpgradeToVideoResponse(int videoState) {
+        Log.v(TAG, "sendUpgradeToVideoResponse : videoState = " + videoState);
+
+        for (Call call : mCalls) {
+            InCallService.VideoCall videoCall = call.getVideoCall();
+            if (videoCall == null) {
+                continue;
+            }
+
+            Log.v(TAG, "send upgrade to video response for call: " + call);
+            videoCall.sendSessionModifyResponse(new VideoProfile(videoState));
+        }
+    }
+
+    @Override
+    public void onVideoCallChanged(Call call, InCallService.VideoCall videoCall) {
+        Log.v(TAG, "onVideoCallChanged: call = " + call + " " + System.identityHashCode(this));
+        if (videoCall != null) {
+            if (!mVideoCallListeners.containsKey(call)) {
+                TestVideoCallListener listener = new TestVideoCallListener(call);
+                videoCall.registerCallback(listener);
+                mVideoCallListeners.put(call, listener);
+                Log.v(TAG, "onVideoCallChanged: added new listener");
+            }
+        }
+    }
+
+    @Override
+    public void onRttStatusChanged(Call call, boolean enabled, Call.RttCall rttCall) {
+        Log.v(TAG, "onRttStatusChanged: call = " + call + " " + System.identityHashCode(this));
+        if (enabled) {
+            for (Listener l : mListeners) {
+                l.onRttStarted(call);
+            }
+        } else {
+            for (Listener l : mListeners) {
+                l.onRttStopped(call);
+            }
+        }
+    }
+
+    @Override
+    public void onRttInitiationFailure(Call call, int reason) {
+        for (Listener l : mListeners) {
+            l.onRttInitiationFailed(call, reason);
+        }
+    }
+
+    @Override
+    public void onRttRequest(Call call, int id) {
+        mLastRttRequestId = id;
+        for (Listener l : mListeners) {
+            l.onRttRequest(call, id);
+        }
+    }
+}
diff --git a/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeDialerActivity.java b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeDialerActivity.java
new file mode 100644
index 0000000..e46d64f
--- /dev/null
+++ b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeDialerActivity.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.carmodedialer;
+
+import android.app.Activity;
+import android.app.UiModeManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class CarModeDialerActivity extends Activity {
+    private static final int REQUEST_CODE_SET_DEFAULT_DIALER = 1;
+
+    private EditText mNumberView;
+    private EditText mPriorityView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.testdialer_main);
+
+        findViewById(R.id.place_call_button).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                placeCall();
+            }
+        });
+
+        mNumberView = (EditText) findViewById(R.id.number);
+        findViewById(R.id.enable_car_mode).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                enableCarMode();
+            }
+        });
+        findViewById(R.id.disable_car_mode).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                disableCarMode();
+            }
+        });
+        findViewById(R.id.toggle_incallservice).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                toggleInCallService();
+            }
+        });
+
+        mPriorityView = findViewById(R.id.priority);
+
+        updateMutableUi();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER) {
+            if (resultCode == RESULT_OK) {
+                showToast("User accepted request to become default dialer");
+            } else if (resultCode == RESULT_CANCELED) {
+                showToast("User declined request to become default dialer");
+            }
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        updateMutableUi();
+    }
+
+    private void updateMutableUi() {
+        Intent intent = getIntent();
+        if (intent != null) {
+            mNumberView.setText(intent.getDataString());
+        }
+    }
+
+    private void placeCall() {
+        final TelecomManager telecomManager =
+                (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
+        telecomManager.placeCall(Uri.fromParts(PhoneAccount.SCHEME_TEL,
+                mNumberView.getText().toString(), null), createCallIntentExtras());
+    }
+
+    private void showToast(String message) {
+        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+    }
+
+    private Bundle createCallIntentExtras() {
+        Bundle extras = new Bundle();
+        extras.putString("com.android.server.telecom.carmodedialer.CALL_EXTRAS", "Tyler was here");
+
+        Bundle intentExtras = new Bundle();
+        intentExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
+        return intentExtras;
+    }
+
+    private void enableCarMode() {
+        int priority;
+        try {
+            priority = Integer.parseInt(mPriorityView.getText().toString());
+        } catch (NumberFormatException nfe) {
+            Toast.makeText(this, "Invalid priority; not enabling car mode.",
+                    Toast.LENGTH_LONG).show();
+            return;
+        }
+        UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
+        uiModeManager.enableCarMode(priority, 0);
+        Toast.makeText(this, "Enabling car mode with priority " + priority,
+                Toast.LENGTH_LONG).show();
+    }
+
+    private void disableCarMode() {
+        UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
+        uiModeManager.disableCarMode(0);
+        Toast.makeText(this, "Disabling car mode", Toast.LENGTH_LONG).show();
+    }
+
+    private void toggleInCallService() {
+        ComponentName uiComponent = new ComponentName(
+                com.android.server.telecom.carmodedialer.CarModeInCallServiceImpl.class.getPackage().getName(),
+                com.android.server.telecom.carmodedialer.CarModeInCallServiceImpl.class.getName());
+        boolean isEnabled = getPackageManager().getComponentEnabledSetting(uiComponent)
+                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+        getPackageManager().setComponentEnabledSetting(uiComponent,
+                isEnabled ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+                        : PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                PackageManager.DONT_KILL_APP);
+        isEnabled = getPackageManager().getComponentEnabledSetting(uiComponent)
+                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+        Toast.makeText(this, "Is UI enabled? " + isEnabled, Toast.LENGTH_LONG).show();
+    }
+}
diff --git a/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeInCallServiceImpl.java b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeInCallServiceImpl.java
new file mode 100644
index 0000000..2879bde
--- /dev/null
+++ b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeInCallServiceImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.carmodedialer;
+
+import android.content.Intent;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.InCallService;
+import android.telecom.Phone;
+import android.util.Log;
+
+import java.lang.Override;
+import java.lang.String;
+
+/**
+ * Test In-Call service implementation.  Logs incoming events.  Mainly used to test binding to
+ * multiple {@link InCallService} implementations.
+ */
+public class CarModeInCallServiceImpl extends InCallService {
+    private static final String TAG = "TestInCallServiceImpl";
+    public static com.android.server.telecom.carmodedialer.CarModeInCallServiceImpl sInstance;
+
+    private Phone mPhone;
+
+    private Phone.Listener mPhoneListener = new Phone.Listener() {
+        @Override
+        public void onCallAdded(Phone phone, Call call) {
+            Log.i(TAG, "onCallAdded: " + call.toString());
+            CarModeCallList callList = CarModeCallList.getInstance();
+            callList.addCall(call);
+
+            if (callList.size() == 1) {
+                startInCallUI();
+            }
+        }
+
+        @Override
+        public void onCallRemoved(Phone phone, Call call) {
+            Log.i(TAG, "onCallRemoved: "+call.toString());
+            CarModeCallList.getInstance().removeCall(call);
+        }
+    };
+
+    @Override
+    public void onPhoneCreated(Phone phone) {
+        Log.i(TAG, "onPhoneCreated");
+        mPhone = phone;
+        mPhone.addListener(mPhoneListener);
+        CarModeCallList.getInstance().clearCalls();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        Log.i(TAG, "onPhoneDestroyed");
+        mPhone.removeListener(mPhoneListener);
+        mPhone = null;
+        CarModeCallList.getInstance().clearCalls();
+        sInstance = null;
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onCallAudioStateChanged(CallAudioState cas) {
+        if (CarModeInCallUI.sInstance != null) {
+            CarModeInCallUI.sInstance.updateCallAudioState(cas);
+        }
+    }
+
+    private void startInCallUI() {
+        sInstance = this;
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setClass(this, CarModeInCallUI.class);
+        startActivity(intent);
+    }
+}
diff --git a/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeInCallUI.java b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeInCallUI.java
new file mode 100644
index 0000000..6d06862
--- /dev/null
+++ b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/CarModeInCallUI.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2019 Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.carmodedialer;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothDevice;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Collection;
+
+public class CarModeInCallUI extends Activity {
+    private class BluetoothDeviceAdapter extends ArrayAdapter<BluetoothDevice> {
+        public BluetoothDeviceAdapter() {
+            super(com.android.server.telecom.carmodedialer.CarModeInCallUI.this, android.R.layout.simple_spinner_item);
+            setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            BluetoothDevice info = getItem(position);
+            TextView result = new TextView(com.android.server.telecom.carmodedialer.CarModeInCallUI.this);
+            result.setText(info.getName());
+            return result;
+        }
+
+        public void update(Collection<BluetoothDevice> devices) {
+            clear();
+            addAll(devices);
+        }
+    }
+
+    public static com.android.server.telecom.carmodedialer.CarModeInCallUI sInstance;
+    private ListView mListView;
+    private CarModeCallList mCallList;
+    private Spinner mBtDeviceList;
+    private BluetoothDeviceAdapter mBluetoothDeviceAdapter;
+    private TextView mCurrentRouteDisplay;
+
+    /** ${inheritDoc} */
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        sInstance = this;
+
+        setContentView(R.layout.incall_screen);
+
+        mListView = (ListView) findViewById(R.id.callListView);
+        mListView.setAdapter(new CallListAdapter(this));
+        mListView.setVisibility(View.VISIBLE);
+
+        mCallList = CarModeCallList.getInstance();
+        mCallList.addListener(new CarModeCallList.Listener() {
+            @Override
+            public void onCallRemoved(Call call) {
+                if (mCallList.size() == 0) {
+                    Log.i(CarModeInCallUI.class.getSimpleName(), "Ending the incall UI");
+                    finish();
+                }
+            }
+
+            @Override
+            public void onRttStarted(Call call) {
+                Toast.makeText(CarModeInCallUI.this, "RTT now enabled", Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onRttStopped(Call call) {
+                Toast.makeText(CarModeInCallUI.this, "RTT now disabled", Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onRttInitiationFailed(Call call, int reason) {
+                Toast.makeText(CarModeInCallUI.this, String.format("RTT failed to init: %d", reason),
+                        Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onRttRequest(Call call, int id) {
+                Toast.makeText(CarModeInCallUI.this, String.format("RTT request: %d", id),
+                        Toast.LENGTH_SHORT).show();
+            }
+        });
+
+        View endCallButton = findViewById(R.id.end_call_button);
+        View holdButton = findViewById(R.id.hold_button);
+        View muteButton = findViewById(R.id.mute_button);
+        View answerButton = findViewById(R.id.answer_button);
+        View setBtDeviceButton = findViewById(R.id.set_bt_device_button);
+        View earpieceButton = findViewById(R.id.earpiece_button);
+        View speakerButton = findViewById(R.id.speaker_button);
+        mBtDeviceList = findViewById(R.id.available_bt_devices);
+        mBluetoothDeviceAdapter = new BluetoothDeviceAdapter();
+        mBtDeviceList.setAdapter(mBluetoothDeviceAdapter);
+        mCurrentRouteDisplay = findViewById(R.id.current_audio_route);
+
+        endCallButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                Call call = mCallList.getCall(0);
+                if (call != null) {
+                    call.disconnect();
+                }
+            }
+        });
+        holdButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                Call call = mCallList.getCall(0);
+                if (call != null) {
+                    if (call.getState() == Call.STATE_HOLDING) {
+                        call.unhold();
+                    } else {
+                        call.hold();
+                    }
+                }
+            }
+        });
+        muteButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                Call call = mCallList.getCall(0);
+                if (call != null) {
+
+                }
+            }
+        });
+
+        answerButton.setOnClickListener(view -> {
+            Call call = mCallList.getCall(0);
+            if (call.getState() == Call.STATE_RINGING) {
+                call.answer(VideoProfile.STATE_AUDIO_ONLY);
+            }
+        });
+
+        earpieceButton.setOnClickListener(view -> {
+            CarModeInCallServiceImpl.sInstance.setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
+        });
+
+        speakerButton.setOnClickListener(view -> {
+            CarModeInCallServiceImpl.sInstance.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
+        });
+
+        setBtDeviceButton.setOnClickListener(view -> {
+            if (mBtDeviceList.getSelectedItem() != null
+                    && CarModeInCallServiceImpl.sInstance != null) {
+                CarModeInCallServiceImpl.sInstance.requestBluetoothAudio(
+                        (BluetoothDevice) mBtDeviceList.getSelectedItem());
+            }
+        });
+
+    }
+
+    public void updateCallAudioState(CallAudioState cas) {
+        mBluetoothDeviceAdapter.update(cas.getSupportedBluetoothDevices());
+        String routeText;
+        switch (cas.getRoute()) {
+            case CallAudioState.ROUTE_EARPIECE:
+                routeText = "Earpiece";
+                break;
+            case CallAudioState.ROUTE_SPEAKER:
+                routeText = "Speaker";
+                break;
+            case CallAudioState.ROUTE_WIRED_HEADSET:
+                routeText = "Wired";
+                break;
+            case CallAudioState.ROUTE_BLUETOOTH:
+                BluetoothDevice activeDevice = cas.getActiveBluetoothDevice();
+                routeText = activeDevice == null ? "null bt" : activeDevice.getName();
+                break;
+            default:
+                routeText = "unknown: " + cas.getRoute();
+        }
+        mCurrentRouteDisplay.setText(routeText);
+    }
+
+    /** ${inheritDoc} */
+    @Override
+    protected void onDestroy() {
+        sInstance = null;
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+    }
+}
diff --git a/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/TestInCallServiceBroadcastReceiver.java b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/TestInCallServiceBroadcastReceiver.java
new file mode 100644
index 0000000..6b2b009
--- /dev/null
+++ b/testapps/carmodedialer/src/com/android/server/telecom/carmodedialer/TestInCallServiceBroadcastReceiver.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.carmodedialer;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+/**
+ * Test in call service broadcast receiver.
+ */
+public class TestInCallServiceBroadcastReceiver extends BroadcastReceiver {
+    private static final String TAG = "TestInCallServiceBR";
+
+    /**
+     * Sends an upgrade to video request for any live calls.
+     */
+    public static final String ACTION_SEND_UPDATE_REQUEST_FROM_TEST_INCALL_SERVICE =
+            "android.server.telecom.testapps.ACTION_SEND_UPDATE_REQUEST_FROM_TEST_INCALL_SERVICE";
+
+    /**
+     * Sends an a response to an upgrade to video request.
+     */
+    public static final String ACTION_SEND_UPGRADE_RESPONSE =
+            "android.server.telecom.testapps.ACTION_SEND_UPGRADE_RESPONSE";
+
+    /**
+     * Handles broadcasts directed at the {@link CarModeInCallServiceImpl}.
+     *
+     * @param context The Context in which the receiver is running.
+     * @param intent  The Intent being received.
+     */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        Log.v(TAG, "onReceive: " + action);
+
+        if (ACTION_SEND_UPDATE_REQUEST_FROM_TEST_INCALL_SERVICE.equals(action)) {
+            final int videoState = Integer.parseInt(intent.getData().getSchemeSpecificPart());
+            CarModeCallList.getInstance().sendUpgradeToVideoRequest(videoState);
+        } else if (ACTION_SEND_UPGRADE_RESPONSE.equals(action)) {
+            final int videoState = Integer.parseInt(intent.getData().getSchemeSpecificPart());
+            CarModeCallList.getInstance().sendUpgradeToVideoResponse(videoState);
+        } else if (TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED.equals(action)) {
+            Log.i(TAG, "onReceive: registered " + intent.getExtras().get(
+                    TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
+        } else if (TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED.equals(action)) {
+            Log.i(TAG, "onReceive: unregistered " + intent.getExtras().get(
+                    TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
+        }
+    }
+}
diff --git a/testapps/res/layout/testdialer_main.xml b/testapps/res/layout/testdialer_main.xml
index a9507c3..e2ea30c 100644
--- a/testapps/res/layout/testdialer_main.xml
+++ b/testapps/res/layout/testdialer_main.xml
@@ -59,11 +59,32 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/startCallWithRtt"/>
-    <Button
-        android:id="@+id/toggle_car_mode"
+    <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="Toggle Car mode" />
+        android:orientation="horizontal" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="15dp"
+            android:text="Priority:" />
+        <EditText
+            android:id="@+id/priority"
+            android:inputType="number"
+            android:text="100"
+            android:layout_width="50dp"
+            android:layout_height="wrap_content" />
+        <Button
+            android:id="@+id/enable_car_mode"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Enable Car mode" />
+        <Button
+            android:id="@+id/disable_car_mode"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Disable Car mode" />
+    </LinearLayout>
     <Button
         android:id="@+id/toggle_incallservice"
         android:layout_width="wrap_content"
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 6a085ee..aa34070 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -21,20 +21,20 @@
     <!-- String for the TestCallActivity -->
     <string name="testCallActivityLabel">Test Connection Service App</string>
 
-    <!-- String for the TestDialerActivity -->
+    <!-- String for the CarModeDialerActivity -->
     <string name="testDialerActivityLabel">Test Dialer</string>
 
-    <!-- String for button in TestDialerActivity that reassigns the default Dialer -->
+    <!-- String for button in CarModeDialerActivity that reassigns the default Dialer -->
     <string name="defaultDialerButton">Default dialer request</string>
 
-    <!-- String for button in TestDialerActivity that places a test call -->
+    <!-- String for button in CarModeDialerActivity that places a test call -->
     <string name="placeCallButton">Place call</string>
 
-    <!-- String for button in TestDialerActivity that performs voicemail requests to verify
+    <!-- String for button in CarModeDialerActivity that performs voicemail requests to verify
             voicemail permissions -->
     <string name="testVoicemailButton">Exercise voicemail permissions</string>
 
-    <!-- String for button in TestDialerActivity that tries to exercise the
+    <!-- String for button in CarModeDialerActivity that tries to exercise the
             TelecomManager.cancelMissedCallNotifications() functionality -->
     <string name="cancelMissedButton">Cancel missed calls</string>
 
diff --git a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
index 9aa076f..19f09e6 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
@@ -26,6 +26,7 @@
 
     private EditText mNumberView;
     private CheckBox mRttCheckbox;
+    private EditText mPriorityView;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -61,10 +62,16 @@
 
         mNumberView = (EditText) findViewById(R.id.number);
         mRttCheckbox = (CheckBox) findViewById(R.id.call_with_rtt_checkbox);
-        findViewById(R.id.toggle_car_mode).setOnClickListener(new OnClickListener() {
+        findViewById(R.id.enable_car_mode).setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                toggleCarMode();
+                enableCarMode();
+            }
+        });
+        findViewById(R.id.disable_car_mode).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                disableCarMode();
             }
         });
         findViewById(R.id.toggle_incallservice).setOnClickListener(new OnClickListener() {
@@ -73,6 +80,7 @@
                 toggleInCallService();
             }
         });
+        mPriorityView = findViewById(R.id.priority);
         updateMutableUi();
     }
 
@@ -158,14 +166,25 @@
         return intentExtras;
     }
 
-    private void toggleCarMode() {
-        UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
-        boolean isCarMode = uiModeManager.getCurrentModeType() == UI_MODE_TYPE_CAR;
-        if (isCarMode) {
-            uiModeManager.disableCarMode(0);
-        } else {
-            uiModeManager.enableCarMode(0);
+    private void enableCarMode() {
+        int priority;
+        try {
+            priority = Integer.parseInt(mPriorityView.getText().toString());
+        } catch (NumberFormatException nfe) {
+            Toast.makeText(this, "Invalid priority; not enabling car mode.",
+                    Toast.LENGTH_LONG).show();
+            return;
         }
+        UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
+        uiModeManager.enableCarMode(priority, 0);
+        Toast.makeText(this, "Enabling car mode with priority " + priority,
+                Toast.LENGTH_LONG).show();
+    }
+
+    private void disableCarMode() {
+        UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
+        uiModeManager.disableCarMode(0);
+        Toast.makeText(this, "Disabling car mode", Toast.LENGTH_LONG).show();
     }
 
     private void toggleInCallService() {