Merge history of car-libs into packages/apps/Car/systemlibs

BUG: 196593308
Test: TH
Change-Id: I0a26b55ec2b796af0bc609948f7b2d99457e6021
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bdfa81c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# Local configuration
+local.properties
+gradle-wrapper.properties
+
+# Gradle
+.gradle/
+build/
+gradle-app.setting
+.gradletasknamecache
+
+# IntelliJ
+.idea/
+*.iml
+
+# Python
+*.pyc
+
+# Android studio's layout inspector captures
+captures/
+
+# A file created when launching android emulators
+read-snapshot.txt
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..15d29ad
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,14 @@
+# People who can approve changes for submission.
+
+# TLs
+ajchen@google.com
+rlagos@google.com
+stenning@google.com
+yizheng@google.com
+robertoalexis@google.com
+farivar@google.com
+
+# TLMs
+johnchoi@google.com
+nicksauer@google.com
+igorr@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..e438129
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,10 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+chassis_current_hook = car-ui-lib/tests/apitest/auto-generate-resources.py --sha ${PREUPLOAD_COMMIT} --compare
+chassis_findviewbyid_check = car-ui-lib/findviewbyid-preupload-hook.sh
+chassis_trailing_blank_line_check = car-ui-lib/trailing-blank-line-hook.sh
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/androidx-car/Android.bp b/androidx-car/Android.bp
new file mode 100644
index 0000000..ec9738f
--- /dev/null
+++ b/androidx-car/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library_import {
+    name: "androidx.car_car-resources-partially-dejetified-nodeps",
+    aars: ["androidx-car-resources.aar"],
+    sdk_version: "current",
+    static_libs: [
+        "com.google.android.material_material",
+        "androidx.appcompat_appcompat",
+        "androidx.cardview_cardview",
+        "androidx.recyclerview_recyclerview",
+        "androidx.gridlayout_gridlayout",
+        "androidx.preference_preference",
+        "androidx-constraintlayout_constraintlayout",
+    ],
+}
+android_library {
+    name: "androidx.car_car-resources-partially-dejetified",
+    sdk_version: "current",
+    min_sdk_version: "21",
+    manifest: "AndroidManifest.xml",
+    static_libs: [
+        "androidx.car_car-resources-partially-dejetified-nodeps",
+        "com.google.android.material_material",
+        "androidx.appcompat_appcompat",
+        "androidx.cardview_cardview",
+        "androidx.recyclerview_recyclerview",
+        "androidx.gridlayout_gridlayout",
+        "androidx.preference_preference",
+        "androidx-constraintlayout_constraintlayout",
+    ],
+    java_version: "1.7",
+}
diff --git a/androidx-car/AndroidManifest.xml b/androidx-car/AndroidManifest.xml
new file mode 100644
index 0000000..87ef2c9
--- /dev/null
+++ b/androidx-car/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.car" >
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="28" />
+
+</manifest>
diff --git a/androidx-car/androidx-car-resources.aar b/androidx-car/androidx-car-resources.aar
new file mode 100755
index 0000000..8f8aeaf
--- /dev/null
+++ b/androidx-car/androidx-car-resources.aar
Binary files differ
diff --git a/car-assist-client-lib/Android.bp b/car-assist-client-lib/Android.bp
new file mode 100644
index 0000000..465254e
--- /dev/null
+++ b/car-assist-client-lib/Android.bp
@@ -0,0 +1,37 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "car-assist-client-lib",
+
+    srcs: ["src/**/*.java"],
+
+    resource_dirs: ["res"],
+
+    optimize: {
+        enabled: false,
+    },
+
+    static_libs: [
+        "androidx.legacy_legacy-support-v4",
+        "car-assist-lib",
+    ],
+
+}
diff --git a/car-assist-client-lib/AndroidManifest.xml b/car-assist-client-lib/AndroidManifest.xml
new file mode 100644
index 0000000..62d2f4d
--- /dev/null
+++ b/car-assist-client-lib/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?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"
+    package="com.android.car.assist.client">
+</manifest>
diff --git a/car-assist-client-lib/OWNERS b/car-assist-client-lib/OWNERS
new file mode 100644
index 0000000..185f5c6
--- /dev/null
+++ b/car-assist-client-lib/OWNERS
@@ -0,0 +1,3 @@
+# People who can approve changes for submission.
+igorr@google.com
+uokoye@google.com
diff --git a/car-assist-client-lib/res/values-af/strings.xml b/car-assist-client-lib/res/values-af/strings.xml
new file mode 100644
index 0000000..d6e54fe
--- /dev/null
+++ b/car-assist-client-lib/res/values-af/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Kon nie handeling deur Assistent versoek nie!"</string>
+    <string name="says" msgid="8575666015622916107">"sê"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-am/strings.xml b/car-assist-client-lib/res/values-am/strings.xml
new file mode 100644
index 0000000..2f0855b
--- /dev/null
+++ b/car-assist-client-lib/res/values-am/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"ከረዳት እርምጃ መጠየቅ አልተቻለም!"</string>
+    <string name="says" msgid="8575666015622916107">"እንዲህ ይላሉ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ar/strings.xml b/car-assist-client-lib/res/values-ar/strings.xml
new file mode 100644
index 0000000..8252183
--- /dev/null
+++ b/car-assist-client-lib/res/values-ar/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"تعذَّر طلَب إجراء من \"مساعد Google\"."</string>
+    <string name="says" msgid="8575666015622916107">"يقول"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-as/strings.xml b/car-assist-client-lib/res/values-as/strings.xml
new file mode 100644
index 0000000..eec6ebd
--- /dev/null
+++ b/car-assist-client-lib/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistantএ কামটো কৰিব নোৱাৰিলে!"</string>
+    <string name="says" msgid="8575666015622916107">"এ কৈছে"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-az/strings.xml b/car-assist-client-lib/res/values-az/strings.xml
new file mode 100644
index 0000000..386d253
--- /dev/null
+++ b/car-assist-client-lib/res/values-az/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistentdən əməliyyat sorğulamaq mümkün olmadı!"</string>
+    <string name="says" msgid="8575666015622916107">"deyir"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-b+sr+Latn/strings.xml b/car-assist-client-lib/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..eaa8a69
--- /dev/null
+++ b/car-assist-client-lib/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Slanje zahteva za radnju Pomoćnika nije uspelo!"</string>
+    <string name="says" msgid="8575666015622916107">"kaže"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-be/strings.xml b/car-assist-client-lib/res/values-be/strings.xml
new file mode 100644
index 0000000..a35d999
--- /dev/null
+++ b/car-assist-client-lib/res/values-be/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Не ўдалося папрасіць Памочніка выканаць дзеянне."</string>
+    <string name="says" msgid="8575666015622916107">"гаворыць"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-bg/strings.xml b/car-assist-client-lib/res/values-bg/strings.xml
new file mode 100644
index 0000000..26f63a4
--- /dev/null
+++ b/car-assist-client-lib/res/values-bg/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Не можа да се заяви действие от Асистент!"</string>
+    <string name="says" msgid="8575666015622916107">"казва"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-bn/strings.xml b/car-assist-client-lib/res/values-bn/strings.xml
new file mode 100644
index 0000000..22ac99f
--- /dev/null
+++ b/car-assist-client-lib/res/values-bn/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"নির্দেশ অনুসারে Assistant কাজ করতে পারেনি!"</string>
+    <string name="says" msgid="8575666015622916107">"বলেছেন"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-bs/strings.xml b/car-assist-client-lib/res/values-bs/strings.xml
new file mode 100644
index 0000000..8f87241
--- /dev/null
+++ b/car-assist-client-lib/res/values-bs/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nije zatražena akcija Asistenta!"</string>
+    <string name="says" msgid="8575666015622916107">"kaže"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ca/strings.xml b/car-assist-client-lib/res/values-ca/strings.xml
new file mode 100644
index 0000000..78437cb
--- /dev/null
+++ b/car-assist-client-lib/res/values-ca/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"No s\'ha pogut sol·licitar l\'acció a l\'Assistent."</string>
+    <string name="says" msgid="8575666015622916107">"diu"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-cs/strings.xml b/car-assist-client-lib/res/values-cs/strings.xml
new file mode 100644
index 0000000..8f54a84
--- /dev/null
+++ b/car-assist-client-lib/res/values-cs/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nebylo možné požádat Asistenta o akci."</string>
+    <string name="says" msgid="8575666015622916107">"říká"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-da/strings.xml b/car-assist-client-lib/res/values-da/strings.xml
new file mode 100644
index 0000000..38dc862
--- /dev/null
+++ b/car-assist-client-lib/res/values-da/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Handlingen kunne ikke håndteres af Assistent."</string>
+    <string name="says" msgid="8575666015622916107">"siger"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-de/strings.xml b/car-assist-client-lib/res/values-de/strings.xml
new file mode 100644
index 0000000..8f553cb
--- /dev/null
+++ b/car-assist-client-lib/res/values-de/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Aktion konnte nicht vom Assistant angefordert werden."</string>
+    <string name="says" msgid="8575666015622916107">"sagt"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-el/strings.xml b/car-assist-client-lib/res/values-el/strings.xml
new file mode 100644
index 0000000..9bdc6ee
--- /dev/null
+++ b/car-assist-client-lib/res/values-el/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Δεν ήταν δυνατό το αίτημα για ενέργεια στον Βοηθό!"</string>
+    <string name="says" msgid="8575666015622916107">"λέει"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-en-rAU/strings.xml b/car-assist-client-lib/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..2f5a3b7
--- /dev/null
+++ b/car-assist-client-lib/res/values-en-rAU/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Could not request action from Assistant!"</string>
+    <string name="says" msgid="8575666015622916107">"says"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-en-rCA/strings.xml b/car-assist-client-lib/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..2f5a3b7
--- /dev/null
+++ b/car-assist-client-lib/res/values-en-rCA/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Could not request action from Assistant!"</string>
+    <string name="says" msgid="8575666015622916107">"says"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-en-rGB/strings.xml b/car-assist-client-lib/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..2f5a3b7
--- /dev/null
+++ b/car-assist-client-lib/res/values-en-rGB/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Could not request action from Assistant!"</string>
+    <string name="says" msgid="8575666015622916107">"says"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-en-rIN/strings.xml b/car-assist-client-lib/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..2f5a3b7
--- /dev/null
+++ b/car-assist-client-lib/res/values-en-rIN/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Could not request action from Assistant!"</string>
+    <string name="says" msgid="8575666015622916107">"says"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-en-rXC/strings.xml b/car-assist-client-lib/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..d6611c0
--- /dev/null
+++ b/car-assist-client-lib/res/values-en-rXC/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‎‏‎‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎‏‏‎‏‏‎‎‎‎‎‏‏‏‎‏‎‎‏‎‎‎‎‎‏‎‎Could not request action from Assistant!‎‏‎‎‏‎"</string>
+    <string name="says" msgid="8575666015622916107">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‎‎‏‎‏‏‏‏‏‏‎‏‎‎‎‎‎‎‎‏‎‏‏‎says‎‏‎‎‏‎"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-es-rUS/strings.xml b/car-assist-client-lib/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..307e7ec
--- /dev/null
+++ b/car-assist-client-lib/res/values-es-rUS/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"No se pudo solicitar la acción del Asistente"</string>
+    <string name="says" msgid="8575666015622916107">"dice"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-es/strings.xml b/car-assist-client-lib/res/values-es/strings.xml
new file mode 100644
index 0000000..e4e7e97
--- /dev/null
+++ b/car-assist-client-lib/res/values-es/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"No se ha podido solicitar la acción al Asistente."</string>
+    <string name="says" msgid="8575666015622916107">"dice"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-et/strings.xml b/car-assist-client-lib/res/values-et/strings.xml
new file mode 100644
index 0000000..91363e6
--- /dev/null
+++ b/car-assist-client-lib/res/values-et/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistendilt ei õnnestunud toimingut taotleda."</string>
+    <string name="says" msgid="8575666015622916107">"ütleb"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-eu/strings.xml b/car-assist-client-lib/res/values-eu/strings.xml
new file mode 100644
index 0000000..10bdbe1
--- /dev/null
+++ b/car-assist-client-lib/res/values-eu/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Ezin izan da eskatu Laguntzailea zerbitzuaren ekintza!"</string>
+    <string name="says" msgid="8575666015622916107">"kontaktuak hau dio:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-fa/strings.xml b/car-assist-client-lib/res/values-fa/strings.xml
new file mode 100644
index 0000000..00c7f18
--- /dev/null
+++ b/car-assist-client-lib/res/values-fa/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"نمی‌توانید از «دستیار» بخواهید کاری انجام دهد!"</string>
+    <string name="says" msgid="8575666015622916107">"می‌گوید"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-fi/strings.xml b/car-assist-client-lib/res/values-fi/strings.xml
new file mode 100644
index 0000000..613428a
--- /dev/null
+++ b/car-assist-client-lib/res/values-fi/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Toiminnon pyytäminen Assistantilta epäonnistui."</string>
+    <string name="says" msgid="8575666015622916107">"sanoo"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-fr-rCA/strings.xml b/car-assist-client-lib/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..5791a4b
--- /dev/null
+++ b/car-assist-client-lib/res/values-fr-rCA/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Impossible de demander à l\'Assistant Google d\'effectuer une action!"</string>
+    <string name="says" msgid="8575666015622916107">"dit"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-fr/strings.xml b/car-assist-client-lib/res/values-fr/strings.xml
new file mode 100644
index 0000000..7004545
--- /dev/null
+++ b/car-assist-client-lib/res/values-fr/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Impossible de demander à l\'Assistant d\'effectuer cette action."</string>
+    <string name="says" msgid="8575666015622916107">"dit"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-gl/strings.xml b/car-assist-client-lib/res/values-gl/strings.xml
new file mode 100644
index 0000000..baec894
--- /dev/null
+++ b/car-assist-client-lib/res/values-gl/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Non se puido solicitar a acción ao Asistente."</string>
+    <string name="says" msgid="8575666015622916107">"di"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-gu/strings.xml b/car-assist-client-lib/res/values-gu/strings.xml
new file mode 100644
index 0000000..63e313c
--- /dev/null
+++ b/car-assist-client-lib/res/values-gu/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"આસિસ્ટંટને ક્રિયાની વિનંતી કરી શક્યાં નથી!"</string>
+    <string name="says" msgid="8575666015622916107">"કહે છે કે"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-hi/strings.xml b/car-assist-client-lib/res/values-hi/strings.xml
new file mode 100644
index 0000000..ba7afeb
--- /dev/null
+++ b/car-assist-client-lib/res/values-hi/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistant से कार्रवाई का अनुरोध नहीं किया जा सका!"</string>
+    <string name="says" msgid="8575666015622916107">"कहा है कि"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-hr/strings.xml b/car-assist-client-lib/res/values-hr/strings.xml
new file mode 100644
index 0000000..2da146b
--- /dev/null
+++ b/car-assist-client-lib/res/values-hr/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nije bilo moguće zatražiti radnju od Asisitenta!"</string>
+    <string name="says" msgid="8575666015622916107">"kaže"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-hu/strings.xml b/car-assist-client-lib/res/values-hu/strings.xml
new file mode 100644
index 0000000..faa43e0
--- /dev/null
+++ b/car-assist-client-lib/res/values-hu/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nem sikerült a Segéd-művelet kérése!"</string>
+    <string name="says" msgid="8575666015622916107">"azt mondja, hogy"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-hy/strings.xml b/car-assist-client-lib/res/values-hy/strings.xml
new file mode 100644
index 0000000..219b14b
--- /dev/null
+++ b/car-assist-client-lib/res/values-hy/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Չհաջողվեց Օգնականին խնդրել գործողություն կատարել"</string>
+    <string name="says" msgid="8575666015622916107">"օգտատերն ասում է."</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-in/strings.xml b/car-assist-client-lib/res/values-in/strings.xml
new file mode 100644
index 0000000..cedc390
--- /dev/null
+++ b/car-assist-client-lib/res/values-in/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Tidak dapat meminta tindakan dari Asisten!"</string>
+    <string name="says" msgid="8575666015622916107">"mengatakan"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-is/strings.xml b/car-assist-client-lib/res/values-is/strings.xml
new file mode 100644
index 0000000..1162c77
--- /dev/null
+++ b/car-assist-client-lib/res/values-is/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Ekki tókst að biðja hjálparann um aðgerð!"</string>
+    <string name="says" msgid="8575666015622916107">"segir"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-it/strings.xml b/car-assist-client-lib/res/values-it/strings.xml
new file mode 100644
index 0000000..4bac32b
--- /dev/null
+++ b/car-assist-client-lib/res/values-it/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Impossibile richiedere l\'azione all\'assistente"</string>
+    <string name="says" msgid="8575666015622916107">"dice"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-iw/strings.xml b/car-assist-client-lib/res/values-iw/strings.xml
new file mode 100644
index 0000000..dc96640
--- /dev/null
+++ b/car-assist-client-lib/res/values-iw/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"לא ניתן לבקש מ-Assistant לבצע פעולה!"</string>
+    <string name="says" msgid="8575666015622916107">"רוצה להודיע כי"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ja/strings.xml b/car-assist-client-lib/res/values-ja/strings.xml
new file mode 100644
index 0000000..2065208
--- /dev/null
+++ b/car-assist-client-lib/res/values-ja/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"アシスタント アクションをリクエストできませんでした"</string>
+    <string name="says" msgid="8575666015622916107">"さんからのメッセージです"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ka/strings.xml b/car-assist-client-lib/res/values-ka/strings.xml
new file mode 100644
index 0000000..8076df9
--- /dev/null
+++ b/car-assist-client-lib/res/values-ka/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"ასისტენტისგან ქმედების მოთხოვნა ვერ მოხერხდა"</string>
+    <string name="says" msgid="8575666015622916107">"ამბობს"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-kk/strings.xml b/car-assist-client-lib/res/values-kk/strings.xml
new file mode 100644
index 0000000..b96f97f
--- /dev/null
+++ b/car-assist-client-lib/res/values-kk/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistant әрекетін сұрау мүмкін болмады."</string>
+    <string name="says" msgid="8575666015622916107">"былай дейді:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-km/strings.xml b/car-assist-client-lib/res/values-km/strings.xml
new file mode 100644
index 0000000..64d1004
--- /dev/null
+++ b/car-assist-client-lib/res/values-km/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"មិនអាចស្នើសុំ​សកម្មភាពពី​ជំនួយការបានទេ!"</string>
+    <string name="says" msgid="8575666015622916107">"និយាយថា"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-kn/strings.xml b/car-assist-client-lib/res/values-kn/strings.xml
new file mode 100644
index 0000000..561840e
--- /dev/null
+++ b/car-assist-client-lib/res/values-kn/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistant ನಿಂದ ಕ್ರಿಯೆಯನ್ನು ವಿನಂತಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ!"</string>
+    <string name="says" msgid="8575666015622916107">"ಹೇಳುತ್ತಾರೆ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ko/strings.xml b/car-assist-client-lib/res/values-ko/strings.xml
new file mode 100644
index 0000000..e0bc7e8
--- /dev/null
+++ b/car-assist-client-lib/res/values-ko/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"어시스턴트에서 요청 작업을 찾을 수 없습니다."</string>
+    <string name="says" msgid="8575666015622916107">"님의 메시지:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ky/strings.xml b/car-assist-client-lib/res/values-ky/strings.xml
new file mode 100644
index 0000000..f278353
--- /dev/null
+++ b/car-assist-client-lib/res/values-ky/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Жардамчы бул аракетти аткара албайт!"</string>
+    <string name="says" msgid="8575666015622916107">"төмөнкүнү айтты:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-lo/strings.xml b/car-assist-client-lib/res/values-lo/strings.xml
new file mode 100644
index 0000000..80c7d53
--- /dev/null
+++ b/car-assist-client-lib/res/values-lo/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"ບໍ່ສາມາດຂໍການດຳເນີນການຈາກຜູ້ຊ່ວຍໄດ້!"</string>
+    <string name="says" msgid="8575666015622916107">"ເວົ້າ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-lt/strings.xml b/car-assist-client-lib/res/values-lt/strings.xml
new file mode 100644
index 0000000..87374c1
--- /dev/null
+++ b/car-assist-client-lib/res/values-lt/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nepavyko pateikti Padėjėjui veiksmo užklausos!"</string>
+    <string name="says" msgid="8575666015622916107">"sako"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-lv/strings.xml b/car-assist-client-lib/res/values-lv/strings.xml
new file mode 100644
index 0000000..3223c67
--- /dev/null
+++ b/car-assist-client-lib/res/values-lv/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nevarēja pieprasīt darbību no Asistenta."</string>
+    <string name="says" msgid="8575666015622916107">"saka"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-mk/strings.xml b/car-assist-client-lib/res/values-mk/strings.xml
new file mode 100644
index 0000000..08f56ac
--- /dev/null
+++ b/car-assist-client-lib/res/values-mk/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Не може да се побара дејство од „Помошникот“!"</string>
+    <string name="says" msgid="8575666015622916107">"вели"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ml/strings.xml b/car-assist-client-lib/res/values-ml/strings.xml
new file mode 100644
index 0000000..641ae11
--- /dev/null
+++ b/car-assist-client-lib/res/values-ml/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistant-ൽ നിന്ന് പ്രവർത്തനം അഭ്യർത്ഥിക്കാനായില്ല!"</string>
+    <string name="says" msgid="8575666015622916107">"പറയുന്നു"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-mn/strings.xml b/car-assist-client-lib/res/values-mn/strings.xml
new file mode 100644
index 0000000..3050b61
--- /dev/null
+++ b/car-assist-client-lib/res/values-mn/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Туслахаас үйлдэл хийхийг хүсэж чадсангүй!"</string>
+    <string name="says" msgid="8575666015622916107">"хэлэхдээ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-mr/strings.xml b/car-assist-client-lib/res/values-mr/strings.xml
new file mode 100644
index 0000000..a896ce6
--- /dev/null
+++ b/car-assist-client-lib/res/values-mr/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"असिस्टंटकडे क्रियेची विनंती करता आली नाही!"</string>
+    <string name="says" msgid="8575666015622916107">"म्हणाले"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ms/strings.xml b/car-assist-client-lib/res/values-ms/strings.xml
new file mode 100644
index 0000000..aa8192b
--- /dev/null
+++ b/car-assist-client-lib/res/values-ms/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Tidak dapat meminta tindakan daripada Assistant!"</string>
+    <string name="says" msgid="8575666015622916107">"berkata"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-my/strings.xml b/car-assist-client-lib/res/values-my/strings.xml
new file mode 100644
index 0000000..54ac60f
--- /dev/null
+++ b/car-assist-client-lib/res/values-my/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistant မှ လုပ်ဆောင်ချက်ကို တောင်းဆို၍မရပါ။"</string>
+    <string name="says" msgid="8575666015622916107">"ဆိုထားသည်မှာ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-nb/strings.xml b/car-assist-client-lib/res/values-nb/strings.xml
new file mode 100644
index 0000000..279b16b
--- /dev/null
+++ b/car-assist-client-lib/res/values-nb/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Kunne ikke forespørre handlinger fra assistenten!"</string>
+    <string name="says" msgid="8575666015622916107">"sier"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ne/strings.xml b/car-assist-client-lib/res/values-ne/strings.xml
new file mode 100644
index 0000000..d20a73a
--- /dev/null
+++ b/car-assist-client-lib/res/values-ne/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"सहायकलाई कारबाही गर्ने अनुरोध गर्न सकिएन!"</string>
+    <string name="says" msgid="8575666015622916107">"भन्नुहुन्छ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-nl/strings.xml b/car-assist-client-lib/res/values-nl/strings.xml
new file mode 100644
index 0000000..aca2bb7
--- /dev/null
+++ b/car-assist-client-lib/res/values-nl/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Kan actie niet aanvragen bij de Assistent."</string>
+    <string name="says" msgid="8575666015622916107">"zegt"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-or/strings.xml b/car-assist-client-lib/res/values-or/strings.xml
new file mode 100644
index 0000000..81480e1
--- /dev/null
+++ b/car-assist-client-lib/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"ଆସିଷ୍ଟାଣ୍ଟ ଠାରୁ କାର୍ଯ୍ୟ ଅନୁରୋଧ କରାଯାଇପାରିଲା ନାହିଁ!"</string>
+    <string name="says" msgid="8575666015622916107">"କୁହେ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-pa/strings.xml b/car-assist-client-lib/res/values-pa/strings.xml
new file mode 100644
index 0000000..9b01451
--- /dev/null
+++ b/car-assist-client-lib/res/values-pa/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistant ਤੋਂ ਕਾਰਵਾਈ ਦੀ ਬੇਨਤੀ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ!"</string>
+    <string name="says" msgid="8575666015622916107">"ਕਿਹਾ"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-pl/strings.xml b/car-assist-client-lib/res/values-pl/strings.xml
new file mode 100644
index 0000000..4ccfa00
--- /dev/null
+++ b/car-assist-client-lib/res/values-pl/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nie udało się wysłać prośby do Asystenta."</string>
+    <string name="says" msgid="8575666015622916107">"mówi"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-pt-rPT/strings.xml b/car-assist-client-lib/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..bb902ea
--- /dev/null
+++ b/car-assist-client-lib/res/values-pt-rPT/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Não foi possível solicitar a ação do Assistente."</string>
+    <string name="says" msgid="8575666015622916107">"diz"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-pt/strings.xml b/car-assist-client-lib/res/values-pt/strings.xml
new file mode 100644
index 0000000..bb902ea
--- /dev/null
+++ b/car-assist-client-lib/res/values-pt/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Não foi possível solicitar a ação do Assistente."</string>
+    <string name="says" msgid="8575666015622916107">"diz"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ro/strings.xml b/car-assist-client-lib/res/values-ro/strings.xml
new file mode 100644
index 0000000..45cfcb6
--- /dev/null
+++ b/car-assist-client-lib/res/values-ro/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nu s-a putut solicita acțiunea de la Asistent!"</string>
+    <string name="says" msgid="8575666015622916107">"spune"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ru/strings.xml b/car-assist-client-lib/res/values-ru/strings.xml
new file mode 100644
index 0000000..7c668c1
--- /dev/null
+++ b/car-assist-client-lib/res/values-ru/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Не удалось выполнить действие с помощью Ассистента"</string>
+    <string name="says" msgid="8575666015622916107">"говорит:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-si/strings.xml b/car-assist-client-lib/res/values-si/strings.xml
new file mode 100644
index 0000000..0a233a0
--- /dev/null
+++ b/car-assist-client-lib/res/values-si/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"සහායකගෙන් ක්‍රියාව ඉල්ලීමට නොහැකි විය!"</string>
+    <string name="says" msgid="8575666015622916107">"කියයි"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-sk/strings.xml b/car-assist-client-lib/res/values-sk/strings.xml
new file mode 100644
index 0000000..8043b1e
--- /dev/null
+++ b/car-assist-client-lib/res/values-sk/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Vyžiadanie akcie od Asistenta zlyhalo."</string>
+    <string name="says" msgid="8575666015622916107">"hovorí"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-sl/strings.xml b/car-assist-client-lib/res/values-sl/strings.xml
new file mode 100644
index 0000000..ab58a0a
--- /dev/null
+++ b/car-assist-client-lib/res/values-sl/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Ni bilo mogoče zahtevati dejanja Pomočnika."</string>
+    <string name="says" msgid="8575666015622916107">"pravi"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-sq/strings.xml b/car-assist-client-lib/res/values-sq/strings.xml
new file mode 100644
index 0000000..acc0431
--- /dev/null
+++ b/car-assist-client-lib/res/values-sq/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Nuk mund të kërkohet veprim nga \"Asistenti\"!"</string>
+    <string name="says" msgid="8575666015622916107">"thotë"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-sr/strings.xml b/car-assist-client-lib/res/values-sr/strings.xml
new file mode 100644
index 0000000..7d73ea8
--- /dev/null
+++ b/car-assist-client-lib/res/values-sr/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Слање захтева за радњу Помоћника није успело!"</string>
+    <string name="says" msgid="8575666015622916107">"каже"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-sv/strings.xml b/car-assist-client-lib/res/values-sv/strings.xml
new file mode 100644
index 0000000..d22371e
--- /dev/null
+++ b/car-assist-client-lib/res/values-sv/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Det gick inte att begära åtgärden från assistenten."</string>
+    <string name="says" msgid="8575666015622916107">"säger"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-sw/strings.xml b/car-assist-client-lib/res/values-sw/strings.xml
new file mode 100644
index 0000000..dcbddff
--- /dev/null
+++ b/car-assist-client-lib/res/values-sw/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Imeshndwa kuomba kitendo kutoka kwenye programu ya Mratibu wa Google!"</string>
+    <string name="says" msgid="8575666015622916107">"anasema"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ta/strings.xml b/car-assist-client-lib/res/values-ta/strings.xml
new file mode 100644
index 0000000..9317977
--- /dev/null
+++ b/car-assist-client-lib/res/values-ta/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistantடிடம் இருந்து உதவியைக் கோர இயலவில்லை!"</string>
+    <string name="says" msgid="8575666015622916107">"கூறுகிறார்"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-te/strings.xml b/car-assist-client-lib/res/values-te/strings.xml
new file mode 100644
index 0000000..4d0ad26
--- /dev/null
+++ b/car-assist-client-lib/res/values-te/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"ఈ చర్యను Assistant నుండి రిక్వెస్ట్ చేయడం సాధ్యపడలేదు!"</string>
+    <string name="says" msgid="8575666015622916107">"ఇలా చెప్పారు"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-th/strings.xml b/car-assist-client-lib/res/values-th/strings.xml
new file mode 100644
index 0000000..d88bdb1
--- /dev/null
+++ b/car-assist-client-lib/res/values-th/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"การขอให้ Assistant ดำเนินการไม่สำเร็จ"</string>
+    <string name="says" msgid="8575666015622916107">"พูดว่า"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-tl/strings.xml b/car-assist-client-lib/res/values-tl/strings.xml
new file mode 100644
index 0000000..2bcb098
--- /dev/null
+++ b/car-assist-client-lib/res/values-tl/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Hindi makahiling ng pagkilos mula sa Assistant!"</string>
+    <string name="says" msgid="8575666015622916107">"ay nagsabing"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-tr/strings.xml b/car-assist-client-lib/res/values-tr/strings.xml
new file mode 100644
index 0000000..6ef140b
--- /dev/null
+++ b/car-assist-client-lib/res/values-tr/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"İşlem Asistan\'dan istenemedi!"</string>
+    <string name="says" msgid="8575666015622916107">"der ki:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-uk/strings.xml b/car-assist-client-lib/res/values-uk/strings.xml
new file mode 100644
index 0000000..8ba7886
--- /dev/null
+++ b/car-assist-client-lib/res/values-uk/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Не вдалося надіслати запит на дію Асистента."</string>
+    <string name="says" msgid="8575666015622916107">"говорить"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-ur/strings.xml b/car-assist-client-lib/res/values-ur/strings.xml
new file mode 100644
index 0000000..5e248a8
--- /dev/null
+++ b/car-assist-client-lib/res/values-ur/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"\'اسسٹنٹ\' کی طرف سے کارروائی کی درخواست نہیں کی جا سکی!"</string>
+    <string name="says" msgid="8575666015622916107">"کہ رہا ہے"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-uz/strings.xml b/car-assist-client-lib/res/values-uz/strings.xml
new file mode 100644
index 0000000..3b89e4e
--- /dev/null
+++ b/car-assist-client-lib/res/values-uz/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Assistent orqali amal soʻrovi yuborilmadi!"</string>
+    <string name="says" msgid="8575666015622916107">"dedi:"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-vi/strings.xml b/car-assist-client-lib/res/values-vi/strings.xml
new file mode 100644
index 0000000..4832f26
--- /dev/null
+++ b/car-assist-client-lib/res/values-vi/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Không thể yêu cầu hành động từ Trợ lý!"</string>
+    <string name="says" msgid="8575666015622916107">"nói"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-zh-rCN/strings.xml b/car-assist-client-lib/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..637124f
--- /dev/null
+++ b/car-assist-client-lib/res/values-zh-rCN/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"无法从 Google 助理请求操作!"</string>
+    <string name="says" msgid="8575666015622916107">"说"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-zh-rHK/strings.xml b/car-assist-client-lib/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..1c22eee
--- /dev/null
+++ b/car-assist-client-lib/res/values-zh-rHK/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"「Google 助理」無法執行要求的操作!"</string>
+    <string name="says" msgid="8575666015622916107">"話"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-zh-rTW/strings.xml b/car-assist-client-lib/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..2b73ccf
--- /dev/null
+++ b/car-assist-client-lib/res/values-zh-rTW/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"無法要求 Google 助理執行動作!"</string>
+    <string name="says" msgid="8575666015622916107">"說"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values-zu/strings.xml b/car-assist-client-lib/res/values-zu/strings.xml
new file mode 100644
index 0000000..1fcd4cc
--- /dev/null
+++ b/car-assist-client-lib/res/values-zu/strings.xml
@@ -0,0 +1,21 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="assist_action_failed_toast" msgid="3250146468076483714">"Ayikwazanga ukucela isenzo kusuka kumsizi!"</string>
+    <string name="says" msgid="8575666015622916107">"ithi"</string>
+</resources>
diff --git a/car-assist-client-lib/res/values/config.xml b/car-assist-client-lib/res/values/config.xml
new file mode 100644
index 0000000..16ceea5
--- /dev/null
+++ b/car-assist-client-lib/res/values/config.xml
@@ -0,0 +1,20 @@
+<?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>
+    <!-- Whether FallbackAssistant is enabled. -->
+    <bool name="config_enableFallbackAssistant">false</bool>
+</resources>
diff --git a/car-assist-client-lib/res/values/strings.xml b/car-assist-client-lib/res/values/strings.xml
new file mode 100644
index 0000000..261c8bc
--- /dev/null
+++ b/car-assist-client-lib/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?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>
+    <!-- The toast message shown on Assistant action failures [CHAR_LIMIT=NONE]-->
+    <string name="assist_action_failed_toast">Could not request action from Assistant!</string>
+    <!-- The verb meaning "to speak". Used when FallbackAssistant is reading out messages from
+     a sender. (i.e. <Sender_name> says <Message>) [CHAR_LIMIT=NONE]-->
+    <string name="says">says</string>
+</resources>
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java b/car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java
new file mode 100644
index 0000000..9428247
--- /dev/null
+++ b/car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java
@@ -0,0 +1,78 @@
+/*
+ * 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.car.assist.client;
+
+import static com.android.car.assist.CarVoiceInteractionSession.KEY_ACTION;
+import static com.android.car.assist.CarVoiceInteractionSession.KEY_EXCEPTION;
+import static com.android.car.assist.CarVoiceInteractionSession.KEY_FALLBACK_ASSISTANT_ENABLED;
+import static com.android.car.assist.CarVoiceInteractionSession.KEY_NOTIFICATION;
+import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION;
+import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
+import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
+
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+
+import com.android.car.assist.CarVoiceInteractionSession.ExceptionValue;
+
+/**
+ * Helper class for building Bundle arguments. Used by {@link CarAssistUtils}.
+ */
+class BundleBuilder {
+    /**
+     * Returns a {@link Bundle} to be delivered to Assistant to indicate that the notification
+     * should be read out.
+     *
+     * @param notification The notification that will be added to the bundle.
+     * @return The bundle that can be sent to Assistant.
+     */
+    static Bundle buildAssistantReadBundle(StatusBarNotification notification) {
+        Bundle args = new Bundle();
+        args.putString(KEY_ACTION, VOICE_ACTION_READ_NOTIFICATION);
+        args.putParcelable(KEY_NOTIFICATION, notification);
+        return args;
+    }
+
+    /**
+     * Returns a {@link Bundle} to be delivered to Assistant to indicate that the notification
+     * should be replied to.
+     *
+     * @param notification The notification that will be added to the bundle.
+     * @return The bundle that can be sent to Assistant.
+     */
+    static Bundle buildAssistantReplyBundle(StatusBarNotification notification) {
+        Bundle args = new Bundle();
+        args.putString(KEY_ACTION, VOICE_ACTION_REPLY_NOTIFICATION);
+        args.putParcelable(KEY_NOTIFICATION, notification);
+        return args;
+    }
+
+    /**
+     * Returns a {@link Bundle} to be delivered to Assistant to indicate that it should handle
+     * the specified {@input exception}.
+     *
+     * @return The bundle that can be sent to Assistant.
+     */
+    static Bundle buildAssistantHandleExceptionBundle(
+            @ExceptionValue String exception,
+            boolean fallbackAssistantEnabled) {
+        Bundle args = new Bundle();
+        args.putString(KEY_ACTION, VOICE_ACTION_HANDLE_EXCEPTION);
+        args.putString(KEY_EXCEPTION, exception);
+        args.putBoolean(KEY_FALLBACK_ASSISTANT_ENABLED, fallbackAssistantEnabled);
+        return args;
+    }
+}
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
new file mode 100644
index 0000000..de2871a
--- /dev/null
+++ b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
@@ -0,0 +1,466 @@
+/*
+ * 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.car.assist.client;
+
+import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_AS_READ;
+import static android.app.Notification.Action.SEMANTIC_ACTION_REPLY;
+import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_NOTIFICATION;
+
+import static com.android.car.assist.CarVoiceInteractionSession.EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import androidx.annotation.StringDef;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationCompat.Action;
+
+import com.android.car.assist.CarVoiceInteractionSession;
+import com.android.internal.app.AssistUtils;
+import com.android.internal.app.IVoiceActionCheckCallback;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Util class providing helper methods to interact with the current active voice service,
+ * while ensuring that the active voice service has the required permissions.
+ */
+public class CarAssistUtils {
+    public static final String TAG = "CarAssistUtils";
+    private static final List<Integer> REQUIRED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
+            Arrays.asList(
+                    SEMANTIC_ACTION_MARK_AS_READ
+            )
+    );
+
+    private static final List<Integer> SUPPORTED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
+            Arrays.asList(
+                    SEMANTIC_ACTION_MARK_AS_READ,
+                    SEMANTIC_ACTION_REPLY
+            )
+    );
+
+    private final Context mContext;
+    private final AssistUtils mAssistUtils;
+    @Nullable
+    private final FallbackAssistant mFallbackAssistant;
+    private final String mErrorMessage;
+    private final boolean mIsFallbackAssistantEnabled;
+
+    /** Interface used to receive callbacks from voice action requests. */
+    public interface ActionRequestCallback {
+        /**
+         * The action was successfully completed either by the active or fallback assistant.
+         **/
+        String RESULT_SUCCESS = "SUCCESS";
+
+        /**
+         * The action was not successfully completed, but the active assistant has been prompted to
+         * alert the user of this error and handle it. The caller of this callback is recommended
+         * to NOT alert the user of this error again.
+         */
+        String RESULT_FAILED_WITH_ERROR_HANDLED = "FAILED_WITH_ERROR_HANDLED";
+
+        /**
+         * The action has not been successfully completed, and the error has not been handled.
+         **/
+        String RESULT_FAILED = "FAILED";
+
+        /**
+         * The list of result states.
+         */
+        @StringDef({RESULT_FAILED, RESULT_FAILED_WITH_ERROR_HANDLED, RESULT_SUCCESS})
+        @interface ResultState {
+        }
+
+        /** Callback containing the result of completing the voice action request. */
+        void onResult(@ResultState String state);
+    }
+
+    public CarAssistUtils(Context context) {
+        mContext = context;
+        mAssistUtils = new AssistUtils(context);
+        mErrorMessage = context.getString(R.string.assist_action_failed_toast);
+
+        mIsFallbackAssistantEnabled =
+                context.getResources().getBoolean(R.bool.config_enableFallbackAssistant);
+        mFallbackAssistant = mIsFallbackAssistantEnabled ? new FallbackAssistant(context) : null;
+    }
+
+    /**
+     * @return {@code true} if there is an active assistant.
+     */
+    public boolean hasActiveAssistant() {
+        return mAssistUtils.getActiveServiceComponentName() != null;
+    }
+
+    /**
+     * Returns {@code true} if the fallback assistant is enabled.
+     */
+    public boolean isFallbackAssistantEnabled() {
+        return mIsFallbackAssistantEnabled;
+    }
+
+    /**
+     * Returns true if the current active assistant has notification listener permissions.
+     */
+    public boolean assistantIsNotificationListener() {
+        if (!hasActiveAssistant()) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "No active assistant was found.");
+            }
+            return false;
+        }
+        final String activeComponent = mAssistUtils.getActiveServiceComponentName()
+                .flattenToString();
+        int slashIndex = activeComponent.indexOf("/");
+        final String activePackage = activeComponent.substring(0, slashIndex);
+
+        final String listeners = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, ActivityManager.getCurrentUser());
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Current user: " + ActivityManager.getCurrentUser()
+                    + " has active voice service: " + activePackage + " and enabled notification "
+                    + " listeners: " + listeners);
+        }
+
+        if (listeners != null) {
+            for (String listener : Arrays.asList(listeners.split(":"))) {
+                if (listener.contains(activePackage)) {
+                    return true;
+                }
+            }
+        }
+        Log.w(TAG, "No notification listeners found for assistant: " + activeComponent);
+        return false;
+    }
+
+    /**
+     * Checks whether the notification is a car-compatible messaging notification.
+     *
+     * @param sbn The notification being checked.
+     * @return true if the notification is a car-compatible messaging notification.
+     */
+    public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
+        Notification notification = sbn.getNotification();
+        return hasMessagingStyle(notification)
+                && hasRequiredAssistantCallbacks(notification)
+                && ((getReplyAction(notification) == null)
+                    || replyCallbackHasRemoteInput(notification))
+                && assistantCallbacksShowNoUi(notification);
+    }
+
+    /** Returns true if the semantic action provided can be supported. */
+    public static boolean isSupportedSemanticAction(int semanticAction) {
+        return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
+    }
+
+    /**
+     * Returns true if the notification has a messaging style.
+     * <p/>
+     * This is the case if the notification in question was provided an instance of
+     * {@link Notification.MessagingStyle} (or an instance of
+     * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
+     */
+    private static boolean hasMessagingStyle(Notification notification) {
+        return NotificationCompat.MessagingStyle
+                .extractMessagingStyleFromNotification(notification) != null;
+    }
+
+    /**
+     * Returns true if the notification has the required Assistant callbacks to be considered
+     * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
+     * is returned if multiple callbacks exist for any semantic action that is supported.
+     */
+    private static boolean hasRequiredAssistantCallbacks(Notification notification) {
+        List<Integer> semanticActionList = getAllActions(notification)
+                .stream()
+                .map(NotificationCompat.Action::getSemanticAction)
+                .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
+                .collect(Collectors.toList());
+        Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
+        return semanticActionList.size() == semanticActionSet.size()
+                && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
+    }
+
+    /** Retrieves visible and invisible {@link Action}s from the {@link Notification}. */
+    public static List<Action> getAllActions(Notification notification) {
+        List<Action> actions = new ArrayList<>(
+                NotificationCompat.getInvisibleActions(notification)
+        );
+        int visibleActionCount = NotificationCompat.getActionCount(notification);
+        for (int i = 0; i < visibleActionCount; i++) {
+            actions.add(NotificationCompat.getAction(notification, i));
+        }
+        return actions;
+    }
+
+    /**
+     * Retrieves the {@link NotificationCompat.Action} containing the
+     * {@link NotificationCompat.Action#SEMANTIC_ACTION_MARK_AS_READ} semantic action.
+     */
+    @Nullable
+    public static NotificationCompat.Action getMarkAsReadAction(Notification notification) {
+        for (NotificationCompat.Action action : getAllActions(notification)) {
+            if (action.getSemanticAction()
+                    == NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) {
+                return action;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves the {@link NotificationCompat.Action} containing the
+     * {@link NotificationCompat.Action#SEMANTIC_ACTION_MUTE semantic action.
+     */
+    @Nullable
+    public static NotificationCompat.Action getMuteAction(Notification notification) {
+        for (NotificationCompat.Action action : getAllActions(notification)) {
+            if (action.getSemanticAction() == Action.SEMANTIC_ACTION_MUTE) {
+                return action;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves the {@link NotificationCompat.Action} containing the
+     * {@link NotificationCompat.Action#SEMANTIC_ACTION_REPLY} semantic action.
+     */
+    @Nullable
+    private static NotificationCompat.Action getReplyAction(Notification notification) {
+        for (NotificationCompat.Action action : getAllActions(notification)) {
+            if (action.getSemanticAction()
+                    == NotificationCompat.Action.SEMANTIC_ACTION_REPLY) {
+                return action;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the reply callback has at least one {@link RemoteInput}.
+     * <p/>
+     * Precondition: There exists only one reply callback.
+     */
+    private static boolean replyCallbackHasRemoteInput(Notification notification) {
+        return getAllActions(notification)
+                .stream()
+                .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
+                .map(NotificationCompat.Action::getRemoteInputs)
+                .filter(Objects::nonNull)
+                .anyMatch(remoteInputs -> remoteInputs.length > 0);
+    }
+
+    /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
+    private static boolean assistantCallbacksShowNoUi(final Notification notification) {
+        return getAllActions(notification)
+                .stream()
+                .filter(Objects::nonNull)
+                .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
+                .noneMatch(NotificationCompat.Action::getShowsUserInterface);
+    }
+
+    /**
+     * Requests a given action from the current active Assistant.
+     *
+     * @param sbn         the notification payload to deliver to assistant
+     * @param voiceAction must be a valid {@link CarVoiceInteractionSession} VOICE_ACTION
+     * @param callback    the callback to issue on success/error
+     */
+    public void requestAssistantVoiceAction(StatusBarNotification sbn, String voiceAction,
+            ActionRequestCallback callback) {
+        if (!isCarCompatibleMessagingNotification(sbn)) {
+            Log.w(TAG, "Assistant action requested for non-compatible notification.");
+            callback.onResult(ActionRequestCallback.RESULT_FAILED);
+            return;
+        }
+
+        switch (voiceAction) {
+            case CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION:
+                readMessageNotification(sbn, callback);
+                return;
+            case CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION:
+                replyMessageNotification(sbn, callback);
+                return;
+            default:
+                Log.w(TAG, "Requested Assistant action for unsupported semantic action.");
+                callback.onResult(ActionRequestCallback.RESULT_FAILED);
+                return;
+        }
+    }
+
+    /**
+     * Requests a read action for the notification from the current active Assistant.
+     * If the Assistant cannot handle the request, a fallback implementation will attempt to
+     * handle it.
+     *
+     * @param sbn      the notification to deliver as the payload
+     * @param callback the callback to issue on success/error
+     */
+    private void readMessageNotification(StatusBarNotification sbn,
+            ActionRequestCallback callback) {
+        Bundle args = BundleBuilder.buildAssistantReadBundle(sbn);
+        String action = CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
+
+        requestAction(action, sbn, args, callback);
+    }
+
+    /**
+     * Requests a reply action for the notification from the current active Assistant.
+     * If the Assistant cannot handle the request, a fallback implementation will attempt to
+     * handle it.
+     *
+     * @param sbn      the notification to deliver as the payload
+     * @param callback the callback to issue on success/error
+     */
+    private void replyMessageNotification(StatusBarNotification sbn,
+            ActionRequestCallback callback) {
+        Bundle args = BundleBuilder.buildAssistantReplyBundle(sbn);
+        String action = CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
+
+        requestAction(action, sbn, args, callback);
+    }
+
+    private void requestAction(String action, StatusBarNotification sbn, Bundle payloadArguments,
+            ActionRequestCallback callback) {
+
+        if (!hasActiveAssistant()) {
+            if (mIsFallbackAssistantEnabled) {
+                handleFallback(sbn, action, callback);
+            } else {
+                // If there is no active assistant, and fallback assistant is not enabled, then
+                // there is nothing for us to do.
+                callback.onResult(ActionRequestCallback.RESULT_FAILED);
+            }
+            return;
+        }
+
+        if (!assistantIsNotificationListener()) {
+            if (mIsFallbackAssistantEnabled) {
+                handleFallback(sbn, action, callback);
+            } else {
+                // If there is an active assistant, alert them to request permissions.
+                Bundle handleExceptionBundle = BundleBuilder
+                        .buildAssistantHandleExceptionBundle(
+                                EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING,
+                                /* fallbackAssistantEnabled */ false);
+                fireAssistantAction(CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION,
+                        handleExceptionBundle, callback);
+            }
+            return;
+        }
+
+        fireAssistantAction(action, payloadArguments, callback);
+    }
+
+    private void fireAssistantAction(String action, Bundle payloadArguments,
+            ActionRequestCallback callback) {
+        IVoiceActionCheckCallback actionCheckCallback = new IVoiceActionCheckCallback.Stub() {
+            @Override
+            public void onComplete(List<String> supportedActions) {
+                String resultState = ActionRequestCallback.RESULT_FAILED;
+                if (supportedActions != null && supportedActions.contains(action)) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Launching active Assistant for action: " + action);
+                    }
+                    if (mAssistUtils.showSessionForActiveService(payloadArguments,
+                            SHOW_SOURCE_NOTIFICATION, null, null)) {
+                        resultState = ActionRequestCallback.RESULT_SUCCESS;
+                    }
+                } else {
+                    Log.w(TAG, "Active Assistant does not support voice action: " + action);
+                }
+                callback.onResult(resultState);
+            }
+        };
+
+        Set<String> actionSet = new HashSet<>(Collections.singletonList(action));
+        mAssistUtils.getActiveServiceSupportedActions(actionSet, actionCheckCallback);
+    }
+
+    private void handleFallback(StatusBarNotification sbn, String action,
+            ActionRequestCallback callback) {
+        if (mFallbackAssistant == null) {
+            return;
+        }
+
+        FallbackAssistant.Listener listener = new FallbackAssistant.Listener() {
+            @Override
+            public void onMessageRead(boolean hasError) {
+                // Tracks if the FallbackAssistant successfully handled the action.
+                final String fallbackActionResult = hasError ? ActionRequestCallback.RESULT_FAILED
+                        : ActionRequestCallback.RESULT_SUCCESS;
+                if (hasActiveAssistant()) {
+                    // If there is an active assistant, alert them to request permissions.
+                    Bundle handleExceptionBundle = BundleBuilder
+                            .buildAssistantHandleExceptionBundle(
+                                    EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING,
+                                    /* fallbackAssistantEnabled */ true);
+                    fireAssistantAction(CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION,
+                            handleExceptionBundle, new ActionRequestCallback() {
+                                @Override
+                                public void onResult(String requestActionFromAssistantResult) {
+                                    if (fallbackActionResult.equals(
+                                            ActionRequestCallback.RESULT_FAILED)
+                                            && requestActionFromAssistantResult
+                                            == ActionRequestCallback.RESULT_SUCCESS) {
+                                        // Only change the callback.ResultState if fallback failed,
+                                        // and assistant session is shown.
+                                        callback.onResult(
+                                                ActionRequestCallback
+                                                        .RESULT_FAILED_WITH_ERROR_HANDLED);
+                                    } else {
+                                        callback.onResult(fallbackActionResult);
+                                    }
+                                }
+                            });
+                } else {
+                    callback.onResult(fallbackActionResult);
+                }
+            }
+        };
+
+        switch (action) {
+            case CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION:
+                mFallbackAssistant.handleReadAction(sbn, listener);
+                break;
+            case CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION:
+                mFallbackAssistant.handleErrorMessage(mErrorMessage, listener);
+                break;
+            default:
+                Log.w(TAG, "Requested unsupported FallbackAssistant action.");
+                callback.onResult(ActionRequestCallback.RESULT_FAILED);
+                return;
+        }
+    }
+}
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java b/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java
new file mode 100644
index 0000000..fe8c5ae
--- /dev/null
+++ b/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java
@@ -0,0 +1,256 @@
+/*
+ * 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.car.assist.client;
+
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.Notification.MessagingStyle.Message;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcelable;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.core.app.NotificationCompat;
+
+import com.android.car.assist.client.tts.TextToSpeechHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Handles Assistant request fallbacks in the case that Assistant cannot fulfill the request for
+ * any given reason.
+ * <p/>
+ * Simply reads out the notification messages for read requests, and speaks out
+ * an error message for other requests.
+ */
+public class FallbackAssistant {
+
+    private static final String TAG = FallbackAssistant.class.getSimpleName();
+
+    private final Context mContext;
+    private final TextToSpeechHelper mTextToSpeechHelper;
+    private final RequestIdGenerator mRequestIdGenerator;
+    private Map<Long, ActionRequestInfo> mRequestIdToActionRequestInfo = new HashMap<>();
+    // String that means "says", to be used when reading out a message (i.e. <Sender> says
+    // <Message).
+    private final String mVerbForSays;
+
+    private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() {
+        @Override
+        public void onTextToSpeechStarted(long requestId) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onTextToSpeechStarted");
+            }
+        }
+
+        @Override
+        public void onTextToSpeechStopped(long requestId, boolean error) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onTextToSpeechStopped");
+            }
+
+            if (error) {
+                Toast.makeText(mContext, mContext.getString(R.string.assist_action_failed_toast),
+                        Toast.LENGTH_LONG).show();
+            }
+            finishAction(requestId, error);
+        }
+    };
+
+    /** Listener to allow clients to be alerted when their requested message has been read. **/
+    public interface Listener {
+        /**
+         * Called after the TTS engine has finished reading aloud the message.
+         */
+        void onMessageRead(boolean hasError);
+    }
+
+    public FallbackAssistant(Context context) {
+        mContext = context;
+        mTextToSpeechHelper = new TextToSpeechHelper(context, mListener);
+        mRequestIdGenerator = new RequestIdGenerator();
+        mVerbForSays = mContext.getString(R.string.says);
+    }
+
+    /**
+     * Handles a fallback read action by reading all messages in the notification.
+     *
+     * @param sbn the payload notification from which to extract messages from
+     */
+    public void handleReadAction(StatusBarNotification sbn, Listener listener) {
+        if (mTextToSpeechHelper.isSpeaking()) {
+            mTextToSpeechHelper.requestStop();
+        }
+
+        Parcelable[] messagesBundle = sbn.getNotification().extras
+                .getParcelableArray(Notification.EXTRA_MESSAGES);
+
+        if (messagesBundle == null || messagesBundle.length == 0) {
+            listener.onMessageRead(/* hasError= */ true);
+            return;
+        }
+
+        List<CharSequence> messages = new ArrayList<>();
+        List<Message> messageList = Message.getMessagesFromBundleArray(messagesBundle);
+        if (messageList == null || messageList.isEmpty()) {
+            Log.w(TAG, "No messages could be extracted from the bundle");
+            listener.onMessageRead(/* hasError= */ true);
+            return;
+        }
+
+        Person previousSender = messageList.get(0).getSenderPerson();
+        if (previousSender != null) {
+            messages.add(previousSender.getName());
+            messages.add(mVerbForSays);
+        }
+        for (Message message : messageList) {
+            if (!message.getSenderPerson().equals(previousSender)) {
+                messages.add(message.getSenderPerson().getName());
+                messages.add(mVerbForSays);
+                previousSender = message.getSenderPerson();
+            }
+            messages.add(message.getText());
+        }
+
+        long requestId = mRequestIdGenerator.generateRequestId();
+
+        if (mTextToSpeechHelper.requestPlay(messages, requestId)) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Requesting TTS to read message with requestId: " + requestId);
+            }
+            mRequestIdToActionRequestInfo.put(requestId, new ActionRequestInfo(sbn, listener));
+        } else {
+            listener.onMessageRead(/* hasError= */ true);
+        }
+    }
+
+    /**
+     * Handles generic (non-read) actions by reading out an error message.
+     *
+     * @param errorMessage the error message to read out
+     */
+    public void handleErrorMessage(CharSequence errorMessage, Listener listener) {
+        if (mTextToSpeechHelper.isSpeaking()) {
+            mTextToSpeechHelper.requestStop();
+        }
+
+        long requestId = mRequestIdGenerator.generateRequestId();
+        if (mTextToSpeechHelper.requestPlay(Collections.singletonList(errorMessage),
+                requestId)) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Requesting TTS to read error with requestId: " + requestId);
+            }
+            mRequestIdToActionRequestInfo.put(requestId, new ActionRequestInfo(
+                    /* statusBarNotification= */ null,
+                    listener));
+        } else {
+            listener.onMessageRead(/* hasError= */ true);
+        }
+    }
+
+    private void finishAction(long requestId, boolean hasError) {
+        if (!mRequestIdToActionRequestInfo.containsKey(requestId)) {
+            Log.w(TAG, "No actionRequestInfo found for requestId: " + requestId);
+            return;
+        }
+
+        ActionRequestInfo info = mRequestIdToActionRequestInfo.remove(requestId);
+
+        if (info.getStatusBarNotification() != null && !hasError) {
+            sendMarkAsReadIntent(info.getStatusBarNotification());
+        }
+
+        info.getListener().onMessageRead(hasError);
+    }
+
+    private void sendMarkAsReadIntent(StatusBarNotification sbn) {
+        NotificationCompat.Action markAsReadAction = CarAssistUtils.getMarkAsReadAction(
+                sbn.getNotification());
+        boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG);
+
+        if (markAsReadAction != null) {
+            if (sendPendingIntent(markAsReadAction.getActionIntent(),
+                    null /* resultIntent */) != ActivityManager.START_SUCCESS
+                    && isDebugLoggable) {
+                Log.d(TAG, "Could not relay mark as read event to the messaging app.");
+            }
+        } else if (isDebugLoggable) {
+            Log.d(TAG, "Car compat message notification has no mark as read action: "
+                    + sbn.getKey());
+        }
+    }
+
+    private int sendPendingIntent(PendingIntent pendingIntent, Intent resultIntent) {
+        try {
+            return pendingIntent.sendAndReturnResult(/* context= */ mContext, /* code= */ 0,
+                    /* intent= */ resultIntent, /* onFinished= */null,
+                    /* handler= */ null, /* requiredPermissions= */ null,
+                    /* options= */ null);
+        } catch (PendingIntent.CanceledException e) {
+            // Do not take down the app over this
+            Log.w(TAG, "Sending contentIntent failed: " + e);
+            return ActivityManager.START_ABORTED;
+        }
+    }
+
+    /** Helper class that generates unique IDs per TTS request. **/
+    private class RequestIdGenerator {
+        private long mCounter;
+
+        RequestIdGenerator() {
+            mCounter = 0;
+        }
+
+        public long generateRequestId() {
+            return ++mCounter;
+        }
+    }
+
+    /**
+     * Contains all of the information needed to start and finish actions supported by the
+     * FallbackAssistant.
+     **/
+    private class ActionRequestInfo {
+        private final StatusBarNotification mStatusBarNotification;
+        private final Listener mListener;
+
+        ActionRequestInfo(@Nullable StatusBarNotification statusBarNotification,
+                Listener listener) {
+            mStatusBarNotification = statusBarNotification;
+            mListener = listener;
+        }
+
+        @Nullable
+        StatusBarNotification getStatusBarNotification() {
+            return mStatusBarNotification;
+        }
+
+        Listener getListener() {
+            return mListener;
+        }
+    }
+}
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java b/car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java
new file mode 100644
index 0000000..86f880f
--- /dev/null
+++ b/car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java
@@ -0,0 +1,100 @@
+/*
+ * 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.car.assist.client.tts;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+
+/**
+ * Implementation of {@link TextToSpeechEngine} by delegating to Android's {@link TextToSpeech} API.
+ * <p>
+ * NOTE: {@link #initialize(Context, TextToSpeech.OnInitListener)} must be called to use this
+ * engine. After {@link #shutdown()}, {@link #initialize(Context, TextToSpeech.OnInitListener)} may
+ * be called again to re-use it.
+ */
+class AndroidTextToSpeechEngine implements TextToSpeechEngine {
+    private TextToSpeech mTextToSpeech;
+
+    @Override
+    public void initialize(Context context, TextToSpeech.OnInitListener initListener) {
+        if (mTextToSpeech == null) {
+            mTextToSpeech = new TextToSpeech(context, initListener);
+        }
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return mTextToSpeech != null;
+    }
+
+    @Override
+    public void setOnUtteranceProgressListener(UtteranceProgressListener progressListener)
+            throws IllegalStateException {
+        assertInit();
+        mTextToSpeech.setOnUtteranceProgressListener(progressListener);
+    }
+
+    @Override
+    public void setAudioAttributes(AudioAttributes audioAttributes) {
+        assertInit();
+        mTextToSpeech.setAudioAttributes(audioAttributes);
+    }
+
+    @Override
+    public int speak(CharSequence text, int queueMode, Bundle params, String utteranceId)
+            throws IllegalStateException {
+        assertInit();
+        return mTextToSpeech.speak(text, queueMode, params, utteranceId);
+    }
+
+    @Override
+    public void stop() throws IllegalStateException {
+        assertInit();
+        mTextToSpeech.stop();
+    }
+
+    @Override
+    public boolean isSpeaking() {
+        return mTextToSpeech != null && mTextToSpeech.isSpeaking();
+    }
+
+    @Override
+    public void shutdown() throws IllegalStateException {
+        assertInit();
+        mTextToSpeech.shutdown();
+        mTextToSpeech = null;
+    }
+
+    @Override
+    public int getStream() {
+        return TextToSpeech.Engine.DEFAULT_STREAM;
+    }
+
+    /**
+     * Asserts that the TTS engine has been initialized.
+     *
+     * @throws IllegalStateException if the TTS has not been initialized.
+     */
+    private void assertInit() throws IllegalStateException {
+        if (!isInitialized()) {
+            throw new IllegalStateException("TTS Engine must be initialized before use.");
+        }
+    }
+}
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java
new file mode 100644
index 0000000..a680cd1
--- /dev/null
+++ b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java
@@ -0,0 +1,89 @@
+/*
+ * 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.car.assist.client.tts;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+
+/**
+ * Interface for TTS Engine that closely matches {@link TextToSpeech}; facilitates mocking/faking.
+ */
+public interface TextToSpeechEngine {
+    /**
+     * Initializes the TTS engine.
+     *
+     * @param context the context to use
+     * @param initListener listener to monitor initialization result
+     */
+    void initialize(Context context, TextToSpeech.OnInitListener initListener);
+
+    /**
+     * Returns true if the engine is initialized.
+     */
+    boolean isInitialized();
+
+    /**
+     * Sets the UtteranceProgressListener.
+     *
+     * @see TextToSpeech#setOnUtteranceProgressListener(UtteranceProgressListener)
+     */
+    void setOnUtteranceProgressListener(UtteranceProgressListener progressListener);
+
+    /**
+     * Sets the audio attributes to be used when speaking text or playing
+     * back a file.
+     *
+     * @see TextToSpeech#setAudioAttributes(AudioAttributes)
+     */
+    void setAudioAttributes(AudioAttributes audioAttributes);
+
+    /**
+     * Speaks out the provided text.
+     *
+     * @see TextToSpeech#speak(CharSequence, int, Bundle, String)
+     */
+    int speak(CharSequence text, int queueMode, Bundle params, String utteranceId);
+
+    /**
+     * Stops play-out.
+     *
+     * @see TextToSpeech#stop()
+     */
+    void stop();
+
+    /**
+     * Returns true if the TTS engine is currently playing out.
+     */
+    boolean isSpeaking();
+
+    /**
+     * Shuts down the engine and releases resources.
+     * {@link #initialize(Context, TextToSpeech.OnInitListener)} will need to be called again before
+     * using this engine.
+     */
+    void shutdown();
+
+    /**
+     * Returns the stream used by this TTS engine.
+     * <p/>
+     * The streams are defined in {@link android.media.AudioManager}.
+     */
+    int getStream();
+}
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java
new file mode 100644
index 0000000..c22e832
--- /dev/null
+++ b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java
@@ -0,0 +1,399 @@
+/*
+ * 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.car.assist.client.tts;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+/**
+ * Component that wraps platform TTS engine and supports play-out of batches of text.
+ * <p>
+ * It takes care of setting up TTS Engine when text is played out and shutting it down after an idle
+ * period with no play-out. This is desirable since the owning app is long-lived and the TTS Engine
+ * brings up another service-process.
+ * <p>
+ * As batches of text are played-out, they issue callbacks on the {@link Listener} provided with the
+ * batch.
+ */
+public class TextToSpeechHelper {
+    /**
+     * Listener interface used by clients to be notified as batch of text is played out.
+     */
+    public interface Listener {
+        /**
+         * Called when play-out starts for batch. May never get called if batch has errors or
+         * interruptions.
+         */
+        void onTextToSpeechStarted(long requestId);
+
+        /**
+         * Called when play-out ends for batch.
+         *
+         * @param error Whether play-out ended due to an error or not. Note: if it was aborted, it's
+         *              not considered an error.
+         */
+        void onTextToSpeechStopped(long requestId, boolean error);
+    }
+
+    private static final String TAG = "CM#TextToSpeechHelper";
+
+    private static final String UTTERANCE_ID_SEPARATOR = ";";
+    private static final long DEFAULT_SHUTDOWN_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
+    private final Map<String, BatchListener> mListeners = new HashMap<>();
+    private final Handler mHandler = new Handler();
+    private final Context mContext;
+    private final TextToSpeechHelper.Listener mListener;
+    private final AudioManager.OnAudioFocusChangeListener mNoOpListener = (f) -> { /* NO-OP */ };
+    private final AudioManager mAudioManager;
+    private final AudioAttributes mAudioAttributes;
+    private final AudioFocusRequest mAudioFocusRequest;
+    private final long mShutdownDelayMillis;
+    private TextToSpeechEngine mTextToSpeechEngine;
+    private int mInitStatus;
+    private SpeechRequest mPendingRequest;
+    private String mCurrentBatchId;
+
+    private final Runnable mMaybeShutdownRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mListeners.isEmpty() || mPendingRequest == null) {
+                shutdownEngine();
+            } else {
+                mHandler.postDelayed(this, mShutdownDelayMillis);
+            }
+        }
+    };
+
+    public TextToSpeechHelper(Context context, TextToSpeechHelper.Listener listener) {
+        this(context, new AndroidTextToSpeechEngine(), DEFAULT_SHUTDOWN_DELAY_MILLIS, listener);
+    }
+
+    @VisibleForTesting
+    TextToSpeechHelper(Context context, TextToSpeechEngine ttsEngine, long shutdownDelayMillis,
+            TextToSpeechHelper.Listener listener) {
+        mContext = context;
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mTextToSpeechEngine = ttsEngine;
+        mShutdownDelayMillis = shutdownDelayMillis;
+        // OnInitListener will only set to SUCCESS/ERROR. So we initialize to STOPPED.
+        mInitStatus = TextToSpeech.STOPPED;
+        mListener = listener;
+        mAudioAttributes =  new AudioAttributes.Builder()
+                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+                .setUsage(AudioAttributes.USAGE_ASSISTANT)
+                .build();
+        mAudioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+                .setAudioAttributes(mAudioAttributes)
+                .setOnAudioFocusChangeListener(mNoOpListener)
+                .build();
+    }
+
+    private void maybeInitAndKeepAlive() {
+        if (!mTextToSpeechEngine.isInitialized()) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Initializing TTS Engine");
+            }
+            mTextToSpeechEngine.initialize(mContext, this::handleInitCompleted);
+            mTextToSpeechEngine.setOnUtteranceProgressListener(mProgressListener);
+            mTextToSpeechEngine.setAudioAttributes(mAudioAttributes);
+        }
+        // Since we're handling a request, delay engine shutdown.
+        mHandler.removeCallbacks(mMaybeShutdownRunnable);
+        mHandler.postDelayed(mMaybeShutdownRunnable, mShutdownDelayMillis);
+    }
+
+    private void handleInitCompleted(int initStatus) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("Init completed. Status: %d", initStatus));
+        }
+        mInitStatus = initStatus;
+        if (mPendingRequest != null) {
+            playInternal(mPendingRequest.mTextToSpeak, mPendingRequest.mRequestId);
+            mPendingRequest = null;
+        }
+    }
+
+    /**
+     * Plays out given batch of text. If engine is not active, it is setup and the request is stored
+     * until then. Only one batch is supported at a time; If a previous batch is waiting engine
+     * setup, that batch is dropped. If a previous batch is playing, the play-out is stopped and
+     * next one is passed to the TTS Engine. Callbacks are issued on the provided {@code listener}.
+     * Will request audio focus first, failure will trigger onAudioFocusFailed in listener.
+     * <p/>
+     * NOTE: Underlying engine may have limit on length of text in each element of the batch; it
+     * will reject anything longer. See {@link TextToSpeech#getMaxSpeechInputLength()}.
+     *
+     * @param textToSpeak Batch of text to play-out.
+     * @param requestId The tracking request id
+     * @return true if the request to play was successful
+     */
+    public boolean requestPlay(List<CharSequence> textToSpeak, long requestId) {
+        if (textToSpeak.isEmpty()) {
+            /* no-op */
+            return true;
+        }
+        int result = mAudioManager.requestAudioFocus(mAudioFocusRequest);
+        if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            return false;
+        }
+        maybeInitAndKeepAlive();
+
+        // Check if its still initializing.
+        if (mInitStatus == TextToSpeech.STOPPED) {
+            // Squash any already queued request.
+            if (mPendingRequest != null) {
+                onTtsStopped(requestId, /* error= */ false);
+            }
+            mPendingRequest = new SpeechRequest(textToSpeak, requestId);
+        } else {
+            playInternal(textToSpeak, requestId);
+        }
+        return true;
+    }
+
+    /** Requests that all play-out be stopped. */
+    public void requestStop() {
+        mTextToSpeechEngine.stop();
+        mCurrentBatchId = null;
+    }
+
+    public boolean isSpeaking() {
+        return mTextToSpeechEngine.isSpeaking();
+    }
+
+    // wrap call back to listener.onTextToSpeechStopped with adandonAudioFocus.
+    private void onTtsStopped(long requestId, boolean error) {
+        mAudioManager.abandonAudioFocusRequest(mAudioFocusRequest);
+        mHandler.post(() -> mListener.onTextToSpeechStopped(requestId, error));
+    }
+
+    private void playInternal(List<CharSequence> textToSpeak, long requestId) {
+        if (mInitStatus == TextToSpeech.ERROR) {
+            Log.e(TAG, "TTS setup failed!");
+            onTtsStopped(requestId, /* error= */ true);
+            return;
+        }
+
+        // Abort anything currently playing and flushes queue.
+        mTextToSpeechEngine.stop();
+
+        // Queue up new batch. We assign id's = "batchId;index" where index increments from 0
+        // to batchSize - 1. If queueing fails, we abort the whole batch.
+        mCurrentBatchId = Long.toString(requestId);
+        for (int i = 0; i < textToSpeak.size(); i++) {
+            CharSequence text = textToSpeak.get(i);
+            String utteranceId =
+                    String.format("%s%s%d", mCurrentBatchId, UTTERANCE_ID_SEPARATOR, i);
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("Queueing tts: '%s' [%s]", text, utteranceId));
+            }
+            if (mTextToSpeechEngine.speak(text, TextToSpeech.QUEUE_ADD, /* params= */ null,
+                    utteranceId) != TextToSpeech.SUCCESS) {
+                mTextToSpeechEngine.stop();
+                mCurrentBatchId = null;
+                Log.e(TAG, "Queuing text failed!");
+                onTtsStopped(requestId, /* error= */ true);
+                return;
+            }
+        }
+        // Register BatchListener for entire batch. Will invoke callbacks on Listener as batch
+        // progresses.
+        mListeners.put(mCurrentBatchId, new BatchListener(requestId, textToSpeak.size()));
+    }
+
+    /**
+     * Releases resources and shuts down TTS Engine.
+     */
+    public void cleanup() {
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        shutdownEngine();
+    }
+
+    /** Returns the stream used by the TTS engine. */
+    public int getStream() {
+        return mTextToSpeechEngine.getStream();
+    }
+
+    private void shutdownEngine() {
+        if (mTextToSpeechEngine.isInitialized()) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Shutting down TTS Engine");
+            }
+            mTextToSpeechEngine.stop();
+            mTextToSpeechEngine.shutdown();
+            mInitStatus = TextToSpeech.STOPPED;
+        }
+    }
+
+    private static Pair<String, Integer> parse(String utteranceId) {
+        try {
+            String[] pair = utteranceId.split(UTTERANCE_ID_SEPARATOR);
+            String batchId = pair[0];
+            int index = Integer.valueOf(pair[1]);
+            return Pair.create(batchId, index);
+        } catch (IndexOutOfBoundsException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Utterance ID is invalid: %s.", utteranceId)
+            );
+        }
+    }
+
+    // Handles all callbacks from TextToSpeechEngine. Possible order of callbacks:
+    // - onStart, onDone: successful play-out.
+    // - onStart, onStop: play-out starts, but interrupted.
+    // - onStart, onError: play-out starts and fails.
+    // - onStop: play-out never starts, but aborted.
+    // - onError: play-out never starts, but fails.
+    // Since the callbacks arrive on other threads, they are dispatched onto mHandler where the
+    // appropriate BatchListener is invoked.
+    private final UtteranceProgressListener mProgressListener = new UtteranceProgressListener() {
+        private void safeInvokeAsync(String utteranceId,
+                BiConsumer<BatchListener, Pair<String, Integer>> callback) {
+            mHandler.post(() -> {
+                Pair<String, Integer> parsedId = parse(utteranceId);
+                BatchListener listener = mListeners.get(parsedId.first);
+                if (listener != null) {
+                    callback.accept(listener, parsedId);
+                } else {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Missing batch listener: " + utteranceId);
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onStart(String utteranceId) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "TTS onStart: " + utteranceId);
+            }
+            mHandler.post(() -> {
+                Pair<String, Integer> parsedId = parse(utteranceId);
+                BatchListener listener = mListeners.get(parsedId.first);
+                if (listener != null) {
+                    listener.onStart();
+                } else {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Missing batch listener: " + utteranceId);
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onDone(String utteranceId) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "TTS onDone: " + utteranceId);
+            }
+            safeInvokeAsync(utteranceId, BatchListener::onDone);
+        }
+
+        @Override
+        public void onStop(String utteranceId, boolean interrupted) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "TTS onStop: " + utteranceId);
+            }
+            safeInvokeAsync(utteranceId, BatchListener::onStop);
+        }
+
+        @Override
+        public void onError(String utteranceId) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "TTS onError: " + utteranceId);
+            }
+            safeInvokeAsync(utteranceId, BatchListener::onError);
+        }
+    };
+
+    /**
+     * Handles callbacks for a single batch of TTS text and issues callbacks on wrapped
+     * {@link Listener} that client is listening on.
+     */
+    private class BatchListener {
+        private boolean mBatchStarted;
+        private final long mRequestId;
+        private final int mUtteranceCount;
+
+        BatchListener(long requestId, int utteranceCount) {
+            mRequestId = requestId;
+            mUtteranceCount = utteranceCount;
+        }
+
+        // Issues Listener.onTextToSpeechStarted when first item of batch starts.
+        void onStart() {
+            if (!mBatchStarted) {
+                mBatchStarted = true;
+                mListener.onTextToSpeechStarted(mRequestId);
+            }
+        }
+
+        // Issues Listener.onTextToSpeechStopped when last item of batch finishes.
+        void onDone(Pair<String, Integer> parsedId) {
+            // parseId is zero-indexed, mUtteranceCount is not.
+            if (parsedId.second == (mUtteranceCount - 1)) {
+                handleBatchFinished(parsedId, /* error= */ false);
+            }
+        }
+
+        // If any item of batch fails, abort the batch and issue Listener.onTextToSpeechStopped.
+        void onError(Pair<String, Integer> parsedId) {
+            if (parsedId.first.equals(mCurrentBatchId)) {
+                mTextToSpeechEngine.stop();
+            }
+            handleBatchFinished(parsedId, /* error= */ true);
+        }
+
+        // If any item of batch is preempted (rest should also be),
+        // issue Listener.onTextToSpeechStopped.
+        void onStop(Pair<String, Integer> parsedId) {
+            handleBatchFinished(parsedId, /* error= */ false);
+        }
+
+        // Handles terminal callbacks for the batch. We invoke stopped and remove ourselves.
+        // No further callbacks will be handled for the batch.
+        private void handleBatchFinished(Pair<String, Integer> parsedId, boolean error) {
+            onTtsStopped(mRequestId, error);
+            mListeners.remove(parsedId.first);
+        }
+    }
+
+    private static class SpeechRequest {
+        final List<CharSequence> mTextToSpeak;
+        final long mRequestId;
+
+        SpeechRequest(List<CharSequence> textToSpeak, long requestId) {
+            mTextToSpeak = textToSpeak;
+            mRequestId = requestId;
+        }
+    }
+}
diff --git a/car-broadcastradio-support/Android.bp b/car-broadcastradio-support/Android.bp
new file mode 100644
index 0000000..d0e24e1
--- /dev/null
+++ b/car-broadcastradio-support/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2018 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "car-broadcastradio-support",
+
+    srcs: ["src/**/*.java"],
+    aidl: {
+        export_include_dirs: ["src"],
+    },
+    resource_dirs: ["res"],
+
+    optimize: {
+        enabled: false,
+    },
+
+    dist: {
+        targets: ["dist_files"],
+    },
+}
diff --git a/car-broadcastradio-support/AndroidManifest.xml b/car-broadcastradio-support/AndroidManifest.xml
new file mode 100644
index 0000000..e592ee1
--- /dev/null
+++ b/car-broadcastradio-support/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.broadcastradio.support">
+    <uses-sdk
+            android:minSdkVersion="28"
+            android:targetSdkVersion='28'/>
+</manifest>
diff --git a/car-broadcastradio-support/OWNERS b/car-broadcastradio-support/OWNERS
new file mode 100644
index 0000000..01124ba
--- /dev/null
+++ b/car-broadcastradio-support/OWNERS
@@ -0,0 +1,3 @@
+# People who can approve changes for submission.
+# Automotive team
+twasilczyk@google.com
diff --git a/car-broadcastradio-support/res/values-af/strings.xml b/car-broadcastradio-support/res/values-af/strings.xml
new file mode 100644
index 0000000..0c08586
--- /dev/null
+++ b/car-broadcastradio-support/res/values-af/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stasies"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Gunstelinge"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-am/strings.xml b/car-broadcastradio-support/res/values-am/strings.xml
new file mode 100644
index 0000000..81b128a
--- /dev/null
+++ b/car-broadcastradio-support/res/values-am/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"ኤኤም"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"ኤፍኤም"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ጣቢያዎች"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ተወዳጆች"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ar/strings.xml b/car-broadcastradio-support/res/values-ar/strings.xml
new file mode 100644
index 0000000..129c8c5
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ar/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"المحطات"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"المفضّلة"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-as/strings.xml b/car-broadcastradio-support/res/values-as/strings.xml
new file mode 100644
index 0000000..862e9bb
--- /dev/null
+++ b/car-broadcastradio-support/res/values-as/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"কেন্দ্ৰসমূহ"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"প্ৰিয় বস্তুসমূহ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-az/strings.xml b/car-broadcastradio-support/res/values-az/strings.xml
new file mode 100644
index 0000000..3b40ff9
--- /dev/null
+++ b/car-broadcastradio-support/res/values-az/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stansiyalar"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Sevimlilər"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-b+sr+Latn/strings.xml b/car-broadcastradio-support/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..e29e90e
--- /dev/null
+++ b/car-broadcastradio-support/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stanice"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Omiljeno"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-be/strings.xml b/car-broadcastradio-support/res/values-be/strings.xml
new file mode 100644
index 0000000..cde54f4
--- /dev/null
+++ b/car-broadcastradio-support/res/values-be/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Радыёстанцыі"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Абранае"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-bg/strings.xml b/car-broadcastradio-support/res/values-bg/strings.xml
new file mode 100644
index 0000000..6d7b2a0
--- /dev/null
+++ b/car-broadcastradio-support/res/values-bg/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"Цифрово радиоразпръскване"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станции"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Любими"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-bn/strings.xml b/car-broadcastradio-support/res/values-bn/strings.xml
new file mode 100644
index 0000000..245ce98
--- /dev/null
+++ b/car-broadcastradio-support/res/values-bn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"স্টেশন"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"পছন্দসই"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-bs/strings.xml b/car-broadcastradio-support/res/values-bs/strings.xml
new file mode 100644
index 0000000..e29e90e
--- /dev/null
+++ b/car-broadcastradio-support/res/values-bs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stanice"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Omiljeno"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ca/strings.xml b/car-broadcastradio-support/res/values-ca/strings.xml
new file mode 100644
index 0000000..3737632
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ca/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Emissores"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Preferides"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-cs/strings.xml b/car-broadcastradio-support/res/values-cs/strings.xml
new file mode 100644
index 0000000..db20752
--- /dev/null
+++ b/car-broadcastradio-support/res/values-cs/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stanice"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Oblíbené"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-da/strings.xml b/car-broadcastradio-support/res/values-da/strings.xml
new file mode 100644
index 0000000..46d1603
--- /dev/null
+++ b/car-broadcastradio-support/res/values-da/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Kanaler"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoritter"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-de/strings.xml b/car-broadcastradio-support/res/values-de/strings.xml
new file mode 100644
index 0000000..c6e2e9f
--- /dev/null
+++ b/car-broadcastradio-support/res/values-de/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"MW"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"UKW"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Sender"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoriten"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-el/strings.xml b/car-broadcastradio-support/res/values-el/strings.xml
new file mode 100644
index 0000000..f5452ec
--- /dev/null
+++ b/car-broadcastradio-support/res/values-el/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Σταθμοί"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Αγαπημένα"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-en-rAU/strings.xml b/car-broadcastradio-support/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..806c9c8
--- /dev/null
+++ b/car-broadcastradio-support/res/values-en-rAU/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"a.m."</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stations"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favourites"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-en-rCA/strings.xml b/car-broadcastradio-support/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..806c9c8
--- /dev/null
+++ b/car-broadcastradio-support/res/values-en-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"a.m."</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stations"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favourites"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-en-rGB/strings.xml b/car-broadcastradio-support/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..806c9c8
--- /dev/null
+++ b/car-broadcastradio-support/res/values-en-rGB/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"a.m."</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stations"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favourites"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-en-rIN/strings.xml b/car-broadcastradio-support/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..806c9c8
--- /dev/null
+++ b/car-broadcastradio-support/res/values-en-rIN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"a.m."</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stations"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favourites"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-en-rXC/strings.xml b/car-broadcastradio-support/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..b06db6c
--- /dev/null
+++ b/car-broadcastradio-support/res/values-en-rXC/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‎‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‏‏‎‏‏‎‏‏‏‏‎‎‎AM‎‏‎‎‏‎"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‎‎‏‏‎‏‎‏‎‏‎‎‎‎‎‎‎‏‏‎‎‏‎‎‏‏‏‎‎‎‎‏‏‏‏‎‏‏‎‏‎‏‎‏‎‏‏‎‏‏‎‎FM‎‏‎‎‏‎"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‎‏‏‎‏‎‏‎‏‏‏‏‏‏‏‏‎‏‏‎‏‎‏‎‎‏‏‎‏‏‏‎‎‏‎‎‏‎‏‏‎‎‏‏‎‎‏‎‎‏‏‎DAB‎‏‎‎‏‎"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‏‎‎‏‎‏‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‏‏‎‏‏‏‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎Stations‎‏‎‎‏‎"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‏‏‎‏‎‎‏‎‎‏‎‏‏‎‎‎‎‎‎‏‏‏‎‏‎‏‎‎‏‎‏‎‏‎‏‎‎‎‏‏‎Favorites‎‏‎‎‏‎"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-es-rUS/strings.xml b/car-broadcastradio-support/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..4862ab2
--- /dev/null
+++ b/car-broadcastradio-support/res/values-es-rUS/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Estaciones"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoritos"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-es/strings.xml b/car-broadcastradio-support/res/values-es/strings.xml
new file mode 100644
index 0000000..f0f383e
--- /dev/null
+++ b/car-broadcastradio-support/res/values-es/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Emisoras"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoritos"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-et/strings.xml b/car-broadcastradio-support/res/values-et/strings.xml
new file mode 100644
index 0000000..810c07e
--- /dev/null
+++ b/car-broadcastradio-support/res/values-et/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Jaamad"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Lemmikud"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-eu/strings.xml b/car-broadcastradio-support/res/values-eu/strings.xml
new file mode 100644
index 0000000..999d986
--- /dev/null
+++ b/car-broadcastradio-support/res/values-eu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Kateak"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Gogokoak"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-fa/strings.xml b/car-broadcastradio-support/res/values-fa/strings.xml
new file mode 100644
index 0000000..1828fc3
--- /dev/null
+++ b/car-broadcastradio-support/res/values-fa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ایستگاه‌ها"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"موارد دلخواه"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-fi/strings.xml b/car-broadcastradio-support/res/values-fi/strings.xml
new file mode 100644
index 0000000..a1b60db
--- /dev/null
+++ b/car-broadcastradio-support/res/values-fi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Kanavat"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Suosikit"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-fr-rCA/strings.xml b/car-broadcastradio-support/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..89bd122
--- /dev/null
+++ b/car-broadcastradio-support/res/values-fr-rCA/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stations"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favorites"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-fr/strings.xml b/car-broadcastradio-support/res/values-fr/strings.xml
new file mode 100644
index 0000000..f12123f
--- /dev/null
+++ b/car-broadcastradio-support/res/values-fr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Radios"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoris"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-gl/strings.xml b/car-broadcastradio-support/res/values-gl/strings.xml
new file mode 100644
index 0000000..beccab0
--- /dev/null
+++ b/car-broadcastradio-support/res/values-gl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Emisoras"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Programas favoritos"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-gu/strings.xml b/car-broadcastradio-support/res/values-gu/strings.xml
new file mode 100644
index 0000000..8da5f9d
--- /dev/null
+++ b/car-broadcastradio-support/res/values-gu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"સ્ટેશન"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"મનપસંદ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-hi/strings.xml b/car-broadcastradio-support/res/values-hi/strings.xml
new file mode 100644
index 0000000..230e18b
--- /dev/null
+++ b/car-broadcastradio-support/res/values-hi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"एएम रेडियो"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"एफ़एम रेडियो"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"डीएबी"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"स्टेशन"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"पसंदीदा"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-hr/strings.xml b/car-broadcastradio-support/res/values-hr/strings.xml
new file mode 100644
index 0000000..63cc855
--- /dev/null
+++ b/car-broadcastradio-support/res/values-hr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Postaje"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoriti"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-hu/strings.xml b/car-broadcastradio-support/res/values-hu/strings.xml
new file mode 100644
index 0000000..5a16d62
--- /dev/null
+++ b/car-broadcastradio-support/res/values-hu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Állomások"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Kedvencek"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-hy/strings.xml b/car-broadcastradio-support/res/values-hy/strings.xml
new file mode 100644
index 0000000..77efc29
--- /dev/null
+++ b/car-broadcastradio-support/res/values-hy/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Ռադիոկայաններ"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Ընտրանի"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-in/strings.xml b/car-broadcastradio-support/res/values-in/strings.xml
new file mode 100644
index 0000000..e40e53f
--- /dev/null
+++ b/car-broadcastradio-support/res/values-in/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stasiun"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favorit"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-is/strings.xml b/car-broadcastradio-support/res/values-is/strings.xml
new file mode 100644
index 0000000..b7e9135
--- /dev/null
+++ b/car-broadcastradio-support/res/values-is/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stöðvar"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Uppáhald"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-it/strings.xml b/car-broadcastradio-support/res/values-it/strings.xml
new file mode 100644
index 0000000..eef0a6e
--- /dev/null
+++ b/car-broadcastradio-support/res/values-it/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stazioni"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Preferiti"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-iw/strings.xml b/car-broadcastradio-support/res/values-iw/strings.xml
new file mode 100644
index 0000000..e990b26
--- /dev/null
+++ b/car-broadcastradio-support/res/values-iw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB ‏(Digital Audio Broadcasting)"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"תחנות"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"מועדפים"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ja/strings.xml b/car-broadcastradio-support/res/values-ja/strings.xml
new file mode 100644
index 0000000..21582d0
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ja/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ステーション"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"お気に入り"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ka/strings.xml b/car-broadcastradio-support/res/values-ka/strings.xml
new file mode 100644
index 0000000..2f26b0a
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ka/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"ციფრული აუდიომაუწყებლობა"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"სადგურები"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"რჩეულები"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-kk/strings.xml b/car-broadcastradio-support/res/values-kk/strings.xml
new file mode 100644
index 0000000..740afce
--- /dev/null
+++ b/car-broadcastradio-support/res/values-kk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станциялар"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Таңдаулылар"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-km/strings.xml b/car-broadcastradio-support/res/values-km/strings.xml
new file mode 100644
index 0000000..be70353
--- /dev/null
+++ b/car-broadcastradio-support/res/values-km/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB​"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ស្ថានីយ"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"សំណព្វ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-kn/strings.xml b/car-broadcastradio-support/res/values-kn/strings.xml
new file mode 100644
index 0000000..9dadc54
--- /dev/null
+++ b/car-broadcastradio-support/res/values-kn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"ಬೆಳಿಗ್ಗೆ"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ಸ್ಟೇಶನ್‌ಗಳು"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ಮೆಚ್ಚಿನವುಗಳು"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ko/strings.xml b/car-broadcastradio-support/res/values-ko/strings.xml
new file mode 100644
index 0000000..abebfa4
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ko/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"채널"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"즐겨찾기"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ky/strings.xml b/car-broadcastradio-support/res/values-ky/strings.xml
new file mode 100644
index 0000000..ee67f46
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ky/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станциялар"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Тандалмалар"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-lo/strings.xml b/car-broadcastradio-support/res/values-lo/strings.xml
new file mode 100644
index 0000000..fbfe882
--- /dev/null
+++ b/car-broadcastradio-support/res/values-lo/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ສະຖານີ"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ລາຍການທີ່ມັກ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-lt/strings.xml b/car-broadcastradio-support/res/values-lt/strings.xml
new file mode 100644
index 0000000..6a68328
--- /dev/null
+++ b/car-broadcastradio-support/res/values-lt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stotys"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Mėgstamiausi"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-lv/strings.xml b/car-broadcastradio-support/res/values-lv/strings.xml
new file mode 100644
index 0000000..75db93a
--- /dev/null
+++ b/car-broadcastradio-support/res/values-lv/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Programmas"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Izlase"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-mk/strings.xml b/car-broadcastradio-support/res/values-mk/strings.xml
new file mode 100644
index 0000000..c7410af
--- /dev/null
+++ b/car-broadcastradio-support/res/values-mk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станици"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Омилени"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ml/strings.xml b/car-broadcastradio-support/res/values-ml/strings.xml
new file mode 100644
index 0000000..498bfd7
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ml/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"രാവിലെ"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"സ്റ്റേഷനുകള്‍"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"പ്രിയപ്പെട്ടവ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-mn/strings.xml b/car-broadcastradio-support/res/values-mn/strings.xml
new file mode 100644
index 0000000..3deca28
--- /dev/null
+++ b/car-broadcastradio-support/res/values-mn/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"ӨГЛӨӨ"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станцууд"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Дуртай"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-mr/strings.xml b/car-broadcastradio-support/res/values-mr/strings.xml
new file mode 100644
index 0000000..0fc95e4
--- /dev/null
+++ b/car-broadcastradio-support/res/values-mr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"स्टेशन"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"आवडीचे"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ms/strings.xml b/car-broadcastradio-support/res/values-ms/strings.xml
new file mode 100644
index 0000000..257cb5e
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ms/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stesen"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Kegemaran"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-my/strings.xml b/car-broadcastradio-support/res/values-my/strings.xml
new file mode 100644
index 0000000..692d19d
--- /dev/null
+++ b/car-broadcastradio-support/res/values-my/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"အသံလွှင့်ရုံများ"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"အသုံးအများဆုံးများ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-nb/strings.xml b/car-broadcastradio-support/res/values-nb/strings.xml
new file mode 100644
index 0000000..79544cb
--- /dev/null
+++ b/car-broadcastradio-support/res/values-nb/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stasjoner"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoritter"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ne/strings.xml b/car-broadcastradio-support/res/values-ne/strings.xml
new file mode 100644
index 0000000..ee1887b
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ne/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"पूर्वाह्न"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"एफएम"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"स्टेसनहरू"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"मन पर्ने कार्यक्रमहरू"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-nl/strings.xml b/car-broadcastradio-support/res/values-nl/strings.xml
new file mode 100644
index 0000000..2f1a5e1
--- /dev/null
+++ b/car-broadcastradio-support/res/values-nl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Zenders"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favorieten"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-or/strings.xml b/car-broadcastradio-support/res/values-or/strings.xml
new file mode 100644
index 0000000..ab448c8
--- /dev/null
+++ b/car-broadcastradio-support/res/values-or/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ଷ୍ଟେଶନ୍"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ପସନ୍ଦର"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-pa/strings.xml b/car-broadcastradio-support/res/values-pa/strings.xml
new file mode 100644
index 0000000..5f7ce46
--- /dev/null
+++ b/car-broadcastradio-support/res/values-pa/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"ਸਟੇਸ਼ਨ"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ਮਨਪਸੰਦ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-pl/strings.xml b/car-broadcastradio-support/res/values-pl/strings.xml
new file mode 100644
index 0000000..1fc4d67
--- /dev/null
+++ b/car-broadcastradio-support/res/values-pl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stacje"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Ulubione"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-pt-rPT/strings.xml b/car-broadcastradio-support/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..a4b120a
--- /dev/null
+++ b/car-broadcastradio-support/res/values-pt-rPT/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Estações"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoritas"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-pt/strings.xml b/car-broadcastradio-support/res/values-pt/strings.xml
new file mode 100644
index 0000000..9d72f03
--- /dev/null
+++ b/car-broadcastradio-support/res/values-pt/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Estações"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoritos"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ro/strings.xml b/car-broadcastradio-support/res/values-ro/strings.xml
new file mode 100644
index 0000000..7ab0b50
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ro/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Posturi"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Preferate"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ru/strings.xml b/car-broadcastradio-support/res/values-ru/strings.xml
new file mode 100644
index 0000000..2fed0f4
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ru/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"Цифровое радио"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Радиостанции"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Избранное"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-si/strings.xml b/car-broadcastradio-support/res/values-si/strings.xml
new file mode 100644
index 0000000..53f72c6
--- /dev/null
+++ b/car-broadcastradio-support/res/values-si/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"නාලිකා"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ප්‍රියතම"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-sk/strings.xml b/car-broadcastradio-support/res/values-sk/strings.xml
new file mode 100644
index 0000000..fcdab92
--- /dev/null
+++ b/car-broadcastradio-support/res/values-sk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stanice"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Obľúbené"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-sl/strings.xml b/car-broadcastradio-support/res/values-sl/strings.xml
new file mode 100644
index 0000000..f0d3770
--- /dev/null
+++ b/car-broadcastradio-support/res/values-sl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Postaje"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Priljubljene"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-sq/strings.xml b/car-broadcastradio-support/res/values-sq/strings.xml
new file mode 100644
index 0000000..a4c1c95
--- /dev/null
+++ b/car-broadcastradio-support/res/values-sq/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stacionet"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Të preferuarat"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-sr/strings.xml b/car-broadcastradio-support/res/values-sr/strings.xml
new file mode 100644
index 0000000..8321b8c
--- /dev/null
+++ b/car-broadcastradio-support/res/values-sr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станице"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Омиљено"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-sv/strings.xml b/car-broadcastradio-support/res/values-sv/strings.xml
new file mode 100644
index 0000000..5475b03
--- /dev/null
+++ b/car-broadcastradio-support/res/values-sv/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Kanaler"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoriter"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-sw/strings.xml b/car-broadcastradio-support/res/values-sw/strings.xml
new file mode 100644
index 0000000..879df73
--- /dev/null
+++ b/car-broadcastradio-support/res/values-sw/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Stesheni"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Unavyopenda"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ta/strings.xml b/car-broadcastradio-support/res/values-ta/strings.xml
new file mode 100644
index 0000000..6b91b37
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ta/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"நிலையங்கள்"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"பிடித்தவை"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-te/strings.xml b/car-broadcastradio-support/res/values-te/strings.xml
new file mode 100644
index 0000000..864e222
--- /dev/null
+++ b/car-broadcastradio-support/res/values-te/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"స్టేషన్‌లు"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ఇష్టమైనవి"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-th/strings.xml b/car-broadcastradio-support/res/values-th/strings.xml
new file mode 100644
index 0000000..0370e76
--- /dev/null
+++ b/car-broadcastradio-support/res/values-th/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"สถานี"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"รายการโปรด"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-tl/strings.xml b/car-broadcastradio-support/res/values-tl/strings.xml
new file mode 100644
index 0000000..932e133
--- /dev/null
+++ b/car-broadcastradio-support/res/values-tl/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Mga Istasyon"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Mga Paborito"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-tr/strings.xml b/car-broadcastradio-support/res/values-tr/strings.xml
new file mode 100644
index 0000000..136387f
--- /dev/null
+++ b/car-broadcastradio-support/res/values-tr/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"İstasyonlar"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Favoriler"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-uk/strings.xml b/car-broadcastradio-support/res/values-uk/strings.xml
new file mode 100644
index 0000000..cdd259a
--- /dev/null
+++ b/car-broadcastradio-support/res/values-uk/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Станції"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Вибране"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-ur/strings.xml b/car-broadcastradio-support/res/values-ur/strings.xml
new file mode 100644
index 0000000..a511d89
--- /dev/null
+++ b/car-broadcastradio-support/res/values-ur/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"اسٹیشنز"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"پسندیدہ"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-uz/strings.xml b/car-broadcastradio-support/res/values-uz/strings.xml
new file mode 100644
index 0000000..bfb89d7
--- /dev/null
+++ b/car-broadcastradio-support/res/values-uz/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"Raqamli radio"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Radiostansiyalar"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Saralangan"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-vi/strings.xml b/car-broadcastradio-support/res/values-vi/strings.xml
new file mode 100644
index 0000000..067c179
--- /dev/null
+++ b/car-broadcastradio-support/res/values-vi/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"Phát thanh kỹ thuật số"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Đài"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Đài yêu thích"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-zh-rCN/strings.xml b/car-broadcastradio-support/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..1789a58
--- /dev/null
+++ b/car-broadcastradio-support/res/values-zh-rCN/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"电台"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"收藏"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-zh-rHK/strings.xml b/car-broadcastradio-support/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..bb596c2
--- /dev/null
+++ b/car-broadcastradio-support/res/values-zh-rHK/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"數碼聲音廣播"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"電台"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"收藏"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-zh-rTW/strings.xml b/car-broadcastradio-support/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..0ba44bc
--- /dev/null
+++ b/car-broadcastradio-support/res/values-zh-rTW/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"電台"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"收藏"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values-zu/strings.xml b/car-broadcastradio-support/res/values-zu/strings.xml
new file mode 100644
index 0000000..dd5578b
--- /dev/null
+++ b/car-broadcastradio-support/res/values-zu/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2018 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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="radio_am_text" msgid="571324921988967868">"AM"</string>
+    <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
+    <string name="radio_dab_text" msgid="8456449462266648979">"I-DAB"</string>
+    <string name="program_list_text" msgid="4414150317304422313">"Iziteshi"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"Izintandokazi"</string>
+</resources>
diff --git a/car-broadcastradio-support/res/values/strings.xml b/car-broadcastradio-support/res/values/strings.xml
new file mode 100644
index 0000000..9151c21
--- /dev/null
+++ b/car-broadcastradio-support/res/values/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Text to denote the AM radio band. -->
+    <string name="radio_am_text">AM</string>
+
+    <!-- Text to denote the FM radio band. -->
+    <string name="radio_fm_text">FM</string>
+
+    <!-- Text to denote the DAB radio band. -->
+    <string name="radio_dab_text">DAB</string>
+
+    <!-- Text to denote the list of programs (stations). -->
+    <string name="program_list_text">Stations</string>
+
+    <!-- Text to denote the list of favorite programs (stations). -->
+    <string name="favorites_list_text">Favorites</string>
+</resources>
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl
new file mode 100644
index 0000000..99c9a93
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl
@@ -0,0 +1,18 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support;
+
+parcelable Program;
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java
new file mode 100644
index 0000000..af57c90
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager.ProgramInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
+
+import java.util.Objects;
+
+/**
+ * Holds storable information about a Program.
+ *
+ * Contrary to {@link android.hardware.radio.RadioManager.ProgramInfo}, it doesn't hold runtime
+ * information, like artist or signal quality.
+ */
+public final class Program implements Parcelable {
+    private final @NonNull ProgramSelector mSelector;
+    private final @NonNull String mName;
+
+    public Program(@NonNull ProgramSelector selector, @NonNull String name) {
+        mSelector = Objects.requireNonNull(selector);
+        mName = Objects.requireNonNull(name);
+    }
+
+    public @NonNull ProgramSelector getSelector() {
+        return mSelector;
+    }
+
+    public @NonNull String getName() {
+        return mName;
+    }
+
+    /** @hide */
+    public @Nullable Bitmap getIcon() {
+        // TODO(b/73950974): implement saving icons
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return "Program(\"" + mName + "\", " + mSelector + ")";
+    }
+
+    @Override
+    public int hashCode() {
+        return mSelector.hashCode();
+    }
+
+    /**
+     * Two programs are considered equal if their selectors are equal.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof Program)) return false;
+        Program other = (Program) obj;
+        return mSelector.equals(other.mSelector);
+    }
+
+    /**
+     * Builds a new {@link Program} object from {@link ProgramInfo}.
+     */
+    public static @NonNull Program fromProgramInfo(@NonNull ProgramInfo info) {
+        return new Program(info.getSelector(), ProgramInfoExt.getProgramName(info, 0));
+    }
+
+    private Program(Parcel in) {
+        mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR));
+        mName = Objects.requireNonNull(in.readString());
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeTypedObject(mSelector, 0);
+        dest.writeString(mName);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<Program> CREATOR = new Parcelable.Creator<Program>() {
+        public Program createFromParcel(Parcel in) {
+            return new Program(in);
+        }
+
+        public Program[] newArray(int size) {
+            return new Program[size];
+        }
+    };
+}
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java
new file mode 100644
index 0000000..39d23dc
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java
@@ -0,0 +1,505 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.graphics.Bitmap;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioManager.BandDescriptor;
+import android.hardware.radio.RadioMetadata;
+import android.media.MediaDescription;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+import android.service.media.MediaBrowserService.BrowserRoot;
+import android.service.media.MediaBrowserService.Result;
+import android.util.Log;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.broadcastradio.support.R;
+import com.android.car.broadcastradio.support.platform.ImageResolver;
+import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
+import com.android.car.broadcastradio.support.platform.RadioMetadataExt;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Implementation of MediaBrowserService logic regarding browser tree.
+ */
+public class BrowseTree {
+    private static final String TAG = "BcRadioApp.BrowseTree";
+
+    /**
+     * Used as a long extra field to indicate the Broadcast Radio folder type of the media item.
+     * The value should be one of the following:
+     * <ul>
+     * <li>{@link #BCRADIO_FOLDER_TYPE_PROGRAMS}</li>
+     * <li>{@link #BCRADIO_FOLDER_TYPE_FAVORITES}</li>
+     * <li>{@link #BCRADIO_FOLDER_TYPE_BAND}</li>
+     * </ul>
+     *
+     * @see android.media.MediaDescription#getExtras()
+     */
+    public static final String EXTRA_BCRADIO_FOLDER_TYPE =
+            "android.media.extra.EXTRA_BCRADIO_FOLDER_TYPE";
+
+    /**
+     * The type of folder that contains a list of Broadcast Radio programs available
+     * to tune at the moment.
+     */
+    public static final long BCRADIO_FOLDER_TYPE_PROGRAMS = 1;
+
+    /**
+     * The type of folder that contains a list of Broadcast Radio programs added
+     * to favorites (not necessarily available to tune at the moment).
+     *
+     * If this folder has {@link android.media.browse.MediaBrowser.MediaItem#FLAG_PLAYABLE} flag
+     * set, it can be used to play some program from the favorite list (selection depends on the
+     * radio app implementation).
+     */
+    public static final long BCRADIO_FOLDER_TYPE_FAVORITES = 2;
+
+    /**
+     * The type of folder that contains the list of all Broadcast Radio channels
+     * (frequency values valid in the current region) for a given band.
+     * Each band (like AM, FM) has its own, separate folder.
+     * These lists include all channels, whether or not some program is tunable through it.
+     *
+     * If this folder has {@link android.media.browse.MediaBrowser.MediaItem#FLAG_PLAYABLE} flag
+     * set, it can be used to tune to some channel within a given band (selection depends on the
+     * radio app implementation).
+     */
+    public static final long BCRADIO_FOLDER_TYPE_BAND = 3;
+
+    /**
+     * Non-localized name of the band.
+     *
+     * For now, it can only take one of the following values:
+     *  - AM;
+     *  - FM;
+     *  - DAB;
+     *  - SXM.
+     *
+     * However, in future releases the list might get extended.
+     */
+    public static final String EXTRA_BCRADIO_BAND_NAME_EN =
+            "android.media.extra.EXTRA_BCRADIO_BAND_NAME_EN";
+
+    /**
+     * General play intent action.
+     *
+     * MediaBrowserService of the radio app must handle this command to perform general
+     * "play" command. It usually means starting playback of recently tuned station.
+     */
+    public static final String ACTION_PLAY_BROADCASTRADIO =
+            "android.car.intent.action.PLAY_BROADCASTRADIO";
+
+    private static final String NODE_ROOT = "root_id";
+    public static final String NODE_PROGRAMS = "programs_id";
+    public static final String NODE_FAVORITES = "favorites_id";
+
+    private static final String NODEPREFIX_BAND = "band:";
+    public static final String NODE_BAND_AM = NODEPREFIX_BAND + "am";
+    public static final String NODE_BAND_FM = NODEPREFIX_BAND + "fm";
+    public static final String NODE_BAND_DAB = NODEPREFIX_BAND + "dab";
+
+    private static final String NODEPREFIX_AMFMCHANNEL = "amfm:";
+    private static final String NODEPREFIX_PROGRAM = "program:";
+
+    private final BrowserRoot mRoot = new BrowserRoot(NODE_ROOT, null);
+
+    private final Object mLock = new Object();
+    private final @NonNull MediaBrowserService mBrowserService;
+    private final @Nullable ImageResolver mImageResolver;
+
+    private List<MediaItem> mRootChildren;
+
+    private final AmFmChannelList mAmChannels = new AmFmChannelList(
+            NODE_BAND_AM, R.string.radio_am_text, "AM");
+    private final AmFmChannelList mFmChannels = new AmFmChannelList(
+            NODE_BAND_FM, R.string.radio_fm_text, "FM");
+    private boolean mDABEnabled;
+
+    private final ProgramList.OnCompleteListener mProgramListCompleteListener =
+            this::onProgramListUpdated;
+    @Nullable private ProgramList mProgramList;
+    @Nullable private List<RadioManager.ProgramInfo> mProgramListSnapshot;
+    @Nullable private List<MediaItem> mProgramListCache;
+    private final List<Runnable> mProgramListTasks = new ArrayList<>();
+    private final Map<String, ProgramSelector> mProgramSelectors = new HashMap<>();
+
+    @Nullable Set<Program> mFavorites;
+    @Nullable private List<MediaItem> mFavoritesCache;
+
+    public BrowseTree(@NonNull MediaBrowserService browserService,
+            @Nullable ImageResolver imageResolver) {
+        mBrowserService = Objects.requireNonNull(browserService);
+        mImageResolver = imageResolver;
+    }
+
+    public BrowserRoot getRoot() {
+        return mRoot;
+    }
+
+    private static MediaItem createChild(MediaDescription.Builder descBuilder,
+            String mediaId, String title, ProgramSelector sel, Bitmap icon) {
+        MediaDescription desc = descBuilder
+                .setMediaId(mediaId)
+                .setMediaUri(ProgramSelectorExt.toUri(sel))
+                .setTitle(title)
+                .setIconBitmap(icon)
+                .build();
+        return new MediaItem(desc, MediaItem.FLAG_PLAYABLE);
+    }
+
+    private static MediaItem createFolder(MediaDescription.Builder descBuilder, String mediaId,
+            String title, boolean isBrowseable, boolean isPlayable, long folderType,
+            Bundle extras) {
+        if (extras == null) extras = new Bundle();
+        extras.putLong(EXTRA_BCRADIO_FOLDER_TYPE, folderType);
+
+        MediaDescription desc = descBuilder
+                .setMediaId(mediaId).setTitle(title).setExtras(extras).build();
+
+        int flags = 0;
+        if (isBrowseable) flags |= MediaItem.FLAG_BROWSABLE;
+        if (isPlayable) flags |= MediaItem.FLAG_PLAYABLE;
+        return new MediaItem(desc, flags);
+    }
+
+    /**
+     * Sets AM/FM region configuration.
+     *
+     * This method is meant to be called shortly after initialization, if AM/FM is supported.
+     */
+    public void setAmFmRegionConfig(@Nullable List<BandDescriptor> amFmBands) {
+        List<BandDescriptor> amBands = new ArrayList<>();
+        List<BandDescriptor> fmBands = new ArrayList<>();
+
+        if (amFmBands != null) {
+            for (BandDescriptor band : amFmBands) {
+                final int freq = band.getLowerLimit();
+                if (ProgramSelectorExt.isAmFrequency(freq)) {
+                    amBands.add(band);
+                } else if (ProgramSelectorExt.isFmFrequency(freq)) {
+                    fmBands.add(band);
+                }
+            }
+        }
+
+        synchronized (mLock) {
+            mAmChannels.setBands(amBands);
+            mFmChannels.setBands(fmBands);
+            mRootChildren = null;
+            mBrowserService.notifyChildrenChanged(NODE_ROOT);
+        }
+    }
+
+    /**
+     * Configures the BrowseTree to include a DAB node or not
+     */
+    public void setDABEnabled(boolean enabled) {
+        synchronized (mLock) {
+            if (mDABEnabled != enabled) {
+                mDABEnabled = enabled;
+                mRootChildren = null;
+                mBrowserService.notifyChildrenChanged(NODE_ROOT);
+            }
+        }
+    }
+
+    private void onProgramListUpdated() {
+        synchronized (mLock) {
+            mProgramListSnapshot = mProgramList.toList();
+            mProgramListCache = null;
+            mBrowserService.notifyChildrenChanged(NODE_PROGRAMS);
+
+            for (Runnable task : mProgramListTasks) {
+                task.run();
+            }
+            mProgramListTasks.clear();
+        }
+    }
+
+    /**
+     * Binds program list.
+     *
+     * This method is meant to be called shortly after opening a new tuner session.
+     */
+    public void setProgramList(@Nullable ProgramList programList) {
+        synchronized (mLock) {
+            if (mProgramList != null) {
+                mProgramList.removeOnCompleteListener(mProgramListCompleteListener);
+            }
+            mProgramList = programList;
+            if (programList != null) {
+                mProgramList.addOnCompleteListener(mProgramListCompleteListener);
+            }
+            mBrowserService.notifyChildrenChanged(NODE_ROOT);
+        }
+    }
+
+    private List<MediaItem> getPrograms() {
+        synchronized (mLock) {
+            if (mProgramListSnapshot == null) {
+                Log.w(TAG, "There is no snapshot of the program list");
+                return null;
+            }
+
+            if (mProgramListCache != null) return mProgramListCache;
+            mProgramListCache = new ArrayList<>();
+
+            MediaDescription.Builder dbld = new MediaDescription.Builder();
+
+            for (RadioManager.ProgramInfo program : mProgramListSnapshot) {
+                ProgramSelector sel = program.getSelector();
+                String mediaId = selectorToMediaId(sel);
+                mProgramSelectors.put(mediaId, sel);
+
+                Bitmap icon = null;
+                RadioMetadata meta = program.getMetadata();
+                if (meta != null && mImageResolver != null) {
+                    long id = RadioMetadataExt.getGlobalBitmapId(meta,
+                            RadioMetadata.METADATA_KEY_ICON);
+                    if (id != 0) icon = mImageResolver.resolve(id);
+                }
+
+                mProgramListCache.add(createChild(dbld, mediaId,
+                        ProgramInfoExt.getProgramName(program, 0), program.getSelector(), icon));
+            }
+
+            if (mProgramListCache.size() == 0) {
+                Log.v(TAG, "Program list is empty");
+            }
+            return mProgramListCache;
+        }
+    }
+
+    private void sendPrograms(final Result<List<MediaItem>> result) {
+        synchronized (mLock) {
+            if (mProgramListSnapshot != null) {
+                result.sendResult(getPrograms());
+            } else {
+                Log.d(TAG, "Program list is not ready yet");
+                result.detach();
+                mProgramListTasks.add(() -> result.sendResult(getPrograms()));
+            }
+        }
+    }
+
+    /**
+     * Updates favorites list.
+     */
+    public void setFavorites(@Nullable Set<Program> favorites) {
+        synchronized (mLock) {
+            boolean rootChanged = (mFavorites == null) != (favorites == null);
+            mFavorites = favorites;
+            mFavoritesCache = null;
+            mBrowserService.notifyChildrenChanged(NODE_FAVORITES);
+            if (rootChanged) mBrowserService.notifyChildrenChanged(NODE_ROOT);
+        }
+    }
+
+    private List<MediaItem> getFavorites() {
+        synchronized (mLock) {
+            if (mFavorites == null) return null;
+            if (mFavoritesCache != null) return mFavoritesCache;
+            mFavoritesCache = new ArrayList<>();
+
+            MediaDescription.Builder dbld = new MediaDescription.Builder();
+
+            for (Program fav : mFavorites) {
+                ProgramSelector sel = fav.getSelector();
+                String mediaId = selectorToMediaId(sel);
+                mProgramSelectors.putIfAbsent(mediaId, sel);  // prefer program list entries
+                mFavoritesCache.add(createChild(dbld, mediaId, fav.getName(), sel, fav.getIcon()));
+            }
+
+            return mFavoritesCache;
+        }
+    }
+
+    private List<MediaItem> getRootChildren() {
+        synchronized (mLock) {
+            if (mRootChildren != null) return mRootChildren;
+            mRootChildren = new ArrayList<>();
+
+            MediaDescription.Builder dbld = new MediaDescription.Builder();
+            if (mProgramList != null) {
+                mRootChildren.add(createFolder(dbld, NODE_PROGRAMS,
+                        mBrowserService.getString(R.string.program_list_text),
+                        true, false, BCRADIO_FOLDER_TYPE_PROGRAMS, null));
+            }
+            if (mFavorites != null) {
+                mRootChildren.add(createFolder(dbld, NODE_FAVORITES,
+                        mBrowserService.getString(R.string.favorites_list_text),
+                        true, true, BCRADIO_FOLDER_TYPE_FAVORITES, null));
+            }
+
+            MediaItem amRoot = mAmChannels.getBandRoot();
+            if (amRoot != null) mRootChildren.add(amRoot);
+            MediaItem fmRoot = mFmChannels.getBandRoot();
+            if (fmRoot != null) mRootChildren.add(fmRoot);
+
+            if (mDABEnabled) {
+                mRootChildren.add(createFolder(dbld, NODE_BAND_DAB,
+                        mBrowserService.getString(R.string.radio_dab_text),
+                        false, true, BCRADIO_FOLDER_TYPE_BAND, null));
+            }
+
+            return mRootChildren;
+        }
+    }
+
+    private class AmFmChannelList {
+        public final @NonNull String mMediaId;
+        private final @StringRes int mBandName;
+        private final @NonNull String mBandNameEn;
+        private @Nullable List<BandDescriptor> mBands;
+        private @Nullable List<MediaItem> mChannels;
+
+        private AmFmChannelList(@NonNull String mediaId, @StringRes int bandName,
+                @NonNull String bandNameEn) {
+            mMediaId = Objects.requireNonNull(mediaId);
+            mBandName = bandName;
+            mBandNameEn = Objects.requireNonNull(bandNameEn);
+        }
+
+        public void setBands(List<BandDescriptor> bands) {
+            synchronized (mLock) {
+                mBands = bands;
+                mChannels = null;
+                mBrowserService.notifyChildrenChanged(mMediaId);
+            }
+        }
+
+        private boolean isEmpty() {
+            if (mBands == null) {
+                Log.w(TAG, "AM/FM configuration not set");
+                return true;
+            }
+            return mBands.isEmpty();
+        }
+
+        public @Nullable MediaItem getBandRoot() {
+            if (isEmpty()) return null;
+            Bundle extras = new Bundle();
+            extras.putString(EXTRA_BCRADIO_BAND_NAME_EN, mBandNameEn);
+            return createFolder(new MediaDescription.Builder(), mMediaId,
+                    mBrowserService.getString(mBandName), true, true, BCRADIO_FOLDER_TYPE_BAND,
+                    extras);
+        }
+
+        public List<MediaItem> getChannels() {
+            synchronized (mLock) {
+                if (mChannels != null) return mChannels;
+                if (isEmpty()) return null;
+                mChannels = new ArrayList<>();
+
+                MediaDescription.Builder dbld = new MediaDescription.Builder();
+
+                for (BandDescriptor band : mBands) {
+                    final int lowerLimit = band.getLowerLimit();
+                    final int upperLimit = band.getUpperLimit();
+                    final int spacing = band.getSpacing();
+                    for (int ch = lowerLimit; ch <= upperLimit; ch += spacing) {
+                        ProgramSelector sel = ProgramSelectorExt.createAmFmSelector(ch);
+                        mChannels.add(createChild(dbld, NODEPREFIX_AMFMCHANNEL + ch,
+                                ProgramSelectorExt.getDisplayName(sel, 0), sel, null));
+                    }
+                }
+
+                return mChannels;
+            }
+        }
+    }
+
+    /**
+     * Loads subtree children.
+     *
+     * This method is meant to be used in MediaBrowserService's onLoadChildren callback.
+     */
+    public void loadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+        if (parentMediaId == null || result == null) return;
+
+        if (NODE_ROOT.equals(parentMediaId)) {
+            result.sendResult(getRootChildren());
+        } else if (NODE_PROGRAMS.equals(parentMediaId)) {
+            sendPrograms(result);
+        } else if (NODE_FAVORITES.equals(parentMediaId)) {
+            result.sendResult(getFavorites());
+        } else if (parentMediaId.equals(mAmChannels.mMediaId)) {
+            result.sendResult(mAmChannels.getChannels());
+        } else if (parentMediaId.equals(mFmChannels.mMediaId)) {
+            result.sendResult(mFmChannels.getChannels());
+        } else {
+            Log.w(TAG, "Invalid parent media ID: " + parentMediaId);
+            result.sendResult(null);
+        }
+    }
+
+    private static @NonNull String selectorToMediaId(@NonNull ProgramSelector sel) {
+        ProgramSelector.Identifier id = sel.getPrimaryId();
+        return NODEPREFIX_PROGRAM + id.getType() + '/' + id.getValue();
+    }
+
+    /**
+     * Resolves mediaId to a tunable {@link ProgramSelector}.
+     *
+     * This method is meant to be used in MediaSession's onPlayFromMediaId callback.
+     */
+    public @Nullable ProgramSelector parseMediaId(@Nullable String mediaId) {
+        if (mediaId == null) return null;
+
+        if (mediaId.startsWith(NODEPREFIX_AMFMCHANNEL)) {
+            String freqStr = mediaId.substring(NODEPREFIX_AMFMCHANNEL.length());
+            int freqInt;
+            try {
+                freqInt = Integer.parseInt(freqStr);
+            } catch (NumberFormatException ex) {
+                Log.e(TAG, "Invalid frequency", ex);
+                return null;
+            }
+            return ProgramSelectorExt.createAmFmSelector(freqInt);
+        } else if (mediaId.startsWith(NODEPREFIX_PROGRAM)) {
+            return mProgramSelectors.get(mediaId);
+        } else if (mediaId.equals(NODE_FAVORITES)) {
+            if (mFavorites == null || mFavorites.isEmpty()) return null;
+            return mFavorites.iterator().next().getSelector();
+        } else if (mediaId.equals(NODE_PROGRAMS)) {
+            if (mProgramListSnapshot == null || mProgramListSnapshot.isEmpty()) return null;
+            return mProgramListSnapshot.get(0).getSelector();
+        } else if (mediaId.equals(NODE_BAND_AM)) {
+            if (mAmChannels.mBands == null || mAmChannels.mBands.isEmpty()) return null;
+            return ProgramSelectorExt.createAmFmSelector(mAmChannels.mBands.get(0).getLowerLimit());
+        } else if (mediaId.equals(NODE_BAND_FM)) {
+            if (mFmChannels.mBands == null || mFmChannels.mBands.isEmpty()) return null;
+            return ProgramSelectorExt.createAmFmSelector(mFmChannels.mBands.get(0).getLowerLimit());
+        }
+        return null;
+    }
+}
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java
new file mode 100644
index 0000000..5538a58
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support.platform;
+
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+
+/**
+ * Resolves metadata images.
+ */
+public interface ImageResolver {
+    /**
+     * Resolve a given metadata image global id to a bitmap.
+     *
+     * @param globalId metadata image id
+     * @return A bitmap, or null if it was not available or invalid
+     */
+    @Nullable Bitmap resolve(long globalId);
+}
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java
new file mode 100644
index 0000000..ce3d014
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java
@@ -0,0 +1,173 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support.platform;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager.ProgramInfo;
+import android.hardware.radio.RadioMetadata;
+import android.media.MediaMetadata;
+import android.media.Rating;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Proposed extensions to android.hardware.radio.RadioManager.ProgramInfo.
+ *
+ * They might eventually get pushed to the framework.
+ */
+public class ProgramInfoExt {
+    private static final String TAG = "BcRadioApp.pinfoext";
+
+    /**
+     * If there is no suitable program name, return null instead of doing
+     * a fallback to channel display name.
+     */
+    public static final int NAME_NO_CHANNEL_FALLBACK = 1 << 16;
+
+    /**
+     * Flags to control how to fetch program name with {@link #getProgramName}.
+     *
+     * Lower 16 bits are reserved for {@link ProgramSelectorExt#NameFlag}.
+     */
+    @IntDef(prefix = { "NAME_" }, flag = true, value = {
+        ProgramSelectorExt.NAME_NO_MODULATION,
+        ProgramSelectorExt.NAME_MODULATION_ONLY,
+        NAME_NO_CHANNEL_FALLBACK,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NameFlag {}
+
+    private static final char EN_DASH = '\u2013';
+    private static final String TITLE_SEPARATOR = " " + EN_DASH + " ";
+
+    private static final String[] PROGRAM_NAME_ORDER = new String[] {
+        RadioMetadata.METADATA_KEY_PROGRAM_NAME,
+        RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME,
+        RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME,
+        RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME,
+        RadioMetadata.METADATA_KEY_RDS_PS,
+    };
+
+    /**
+     * Returns program name suitable to display.
+     *
+     * If there is no program name, it falls back to channel name. Flags related to
+     * the channel name display will be forwarded to the channel name generation method.
+     */
+    public static @NonNull String getProgramName(@NonNull ProgramInfo info, @NameFlag int flags) {
+        RadioMetadata meta = info.getMetadata();
+        if (meta != null) {
+            for (String key : PROGRAM_NAME_ORDER) {
+                String value = meta.getString(key);
+                if (value != null) return value;
+            }
+        }
+
+        if ((flags & NAME_NO_CHANNEL_FALLBACK) != 0) return "";
+
+        ProgramSelector sel = info.getSelector();
+
+        // if it's AM/FM program, prefer to display currently used AF frequency
+        if (ProgramSelectorExt.isAmFmProgram(sel)) {
+            ProgramSelector.Identifier phy = info.getPhysicallyTunedTo();
+            if (phy != null && phy.getType() == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) {
+                String chName = ProgramSelectorExt.formatAmFmFrequency(phy.getValue(), flags);
+                if (chName != null) return chName;
+            }
+        }
+
+        String selName = ProgramSelectorExt.getDisplayName(sel, flags);
+        if (selName != null) return selName;
+
+        Log.w(TAG, "ProgramInfo without a name nor channel name");
+        return "";
+    }
+
+    /**
+     * Proposed reimplementation of {@link RadioManager#ProgramInfo#getMetadata}.
+     *
+     * As opposed to the original implementation, it never returns null.
+     */
+    public static @NonNull RadioMetadata getMetadata(@NonNull ProgramInfo info) {
+        RadioMetadata meta = info.getMetadata();
+        if (meta != null) return meta;
+
+        /* Creating new Metadata object on each get won't be necessary after we
+         * push this code to the framework. */
+        return (new RadioMetadata.Builder()).build();
+    }
+
+    /**
+     * Converts {@ProgramInfo} to {@MediaMetadata}.
+     *
+     * This method is meant to be used for currently playing station in {@link MediaSession}.
+     *
+     * @param info {@link ProgramInfo} to convert
+     * @param isFavorite true, if a given program is a favorite
+     * @param imageResolver metadata images resolver/cache
+     * @return {@link MediaMetadata} object
+     */
+    public static @NonNull MediaMetadata toMediaMetadata(@NonNull ProgramInfo info,
+            boolean isFavorite, @Nullable ImageResolver imageResolver) {
+        MediaMetadata.Builder bld = new MediaMetadata.Builder();
+
+        bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, getProgramName(info, 0));
+
+        RadioMetadata meta = info.getMetadata();
+        if (meta != null) {
+            String title = meta.getString(RadioMetadata.METADATA_KEY_TITLE);
+            if (title != null) {
+                bld.putString(MediaMetadata.METADATA_KEY_TITLE, title);
+            }
+            String artist = meta.getString(RadioMetadata.METADATA_KEY_ARTIST);
+            if (artist != null) {
+                bld.putString(MediaMetadata.METADATA_KEY_ARTIST, artist);
+            }
+            String album = meta.getString(RadioMetadata.METADATA_KEY_ALBUM);
+            if (album != null) {
+                bld.putString(MediaMetadata.METADATA_KEY_ALBUM, album);
+            }
+            if (title != null || artist != null) {
+                String subtitle;
+                if (title == null) {
+                    subtitle = artist;
+                } else if (artist == null) {
+                    subtitle = title;
+                } else {
+                    subtitle = title + TITLE_SEPARATOR + artist;
+                }
+                bld.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
+            }
+            long albumArtId = RadioMetadataExt.getGlobalBitmapId(meta,
+                    RadioMetadata.METADATA_KEY_ART);
+            if (albumArtId != 0 && imageResolver != null) {
+                Bitmap bm = imageResolver.resolve(albumArtId);
+                if (bm != null) bld.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bm);
+            }
+        }
+
+        bld.putRating(MediaMetadata.METADATA_KEY_USER_RATING, Rating.newHeartRating(isFavorite));
+
+        return bld.build();
+    }
+}
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java
new file mode 100644
index 0000000..4b3583b
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java
@@ -0,0 +1,486 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support.platform;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.ProgramSelector.Identifier;
+import android.hardware.radio.RadioManager;
+import android.net.Uri;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+/**
+ * Proposed extensions to android.hardware.radio.ProgramSelector.
+ *
+ * They might eventually get pushed to the framework.
+ */
+public class ProgramSelectorExt {
+    private static final String TAG = "BcRadioApp.pselext";
+
+    /**
+     * If this is AM/FM channel (or any other technology using different modulations),
+     * don't return modulation part.
+     */
+    public static final int NAME_NO_MODULATION = 1 << 0;
+
+    /**
+     * Return only modulation part of channel name.
+     *
+     * If this is not a radio technology using modulation, return nothing
+     * (unless combined with other _ONLY flags in the future).
+     *
+     * If this returns non-null string, it's guaranteed that {@link #NAME_NO_MODULATION}
+     * will return the complement of channel name.
+     */
+    public static final int NAME_MODULATION_ONLY = 1 << 1;
+
+    /**
+     * If the channel name is not human-readable (i.e. DAB SId), radio technology is displayed
+     * instead. This flag prevents that.
+     *
+     * With radio technology fallback, null pointer may still be returned in case of unsupported
+     * radio technologies.
+     */
+    public static final int NAME_NO_PROGRAM_TYPE_FALLBACK = 1 << 2;
+
+    /**
+     * Flags to control how channel values are converted to string with {@link #getDisplayName}.
+     *
+     * Upper 16 bits are reserved for {@link ProgramInfoExt#NameFlag}.
+     */
+    @IntDef(prefix = { "NAME_" }, flag = true, value = {
+        NAME_NO_MODULATION,
+        NAME_MODULATION_ONLY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NameFlag {}
+
+    private static final String URI_SCHEME_BROADCASTRADIO = "broadcastradio";
+    private static final String URI_AUTHORITY_PROGRAM = "program";
+    private static final String URI_VENDOR_PREFIX = "VENDOR_";
+    private static final String URI_HEX_PREFIX = "0x";
+
+    private static final DecimalFormat FORMAT_FM = new DecimalFormat("###.#");
+
+    private static final Map<Integer, String> ID_TO_URI = new HashMap<>();
+    private static final Map<String, Integer> URI_TO_ID = new HashMap<>();
+
+    /**
+     * New proposed constructor for {@link ProgramSelector}.
+     *
+     * As opposed to the current platform API, this one matches more closely simplified HAL 2.0.
+     *
+     * @param primaryId primary program identifier.
+     * @param secondaryIds list of secondary program identifiers.
+     */
+    public static @NonNull ProgramSelector newProgramSelector(@NonNull Identifier primaryId,
+            @Nullable Identifier[] secondaryIds) {
+        return new ProgramSelector(
+                identifierToProgramType(primaryId),
+                primaryId, secondaryIds, null);
+    }
+
+    // when pushed to the framework, remove similar code from HAL 2.0 service
+    private static @ProgramSelector.ProgramType int identifierToProgramType(
+            @NonNull Identifier primaryId) {
+        int idType = primaryId.getType();
+        switch (idType) {
+            case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
+                if (isAmFrequency(primaryId.getValue())) {
+                    return ProgramSelector.PROGRAM_TYPE_AM;
+                } else {
+                    return ProgramSelector.PROGRAM_TYPE_FM;
+                }
+            case ProgramSelector.IDENTIFIER_TYPE_RDS_PI:
+                return ProgramSelector.PROGRAM_TYPE_FM;
+            case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
+                if (isAmFrequency(IdentifierExt.asHdPrimary(primaryId).getFrequency())) {
+                    return ProgramSelector.PROGRAM_TYPE_AM_HD;
+                } else {
+                    return ProgramSelector.PROGRAM_TYPE_FM_HD;
+                }
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+                return ProgramSelector.PROGRAM_TYPE_DAB;
+            case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
+            case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
+                return ProgramSelector.PROGRAM_TYPE_DRMO;
+            case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
+            case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
+                return ProgramSelector.PROGRAM_TYPE_SXM;
+        }
+        if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
+                && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
+            return idType;
+        }
+        return ProgramSelector.PROGRAM_TYPE_INVALID;
+    }
+
+    /**
+     * Checks, if a given AM frequency is roughly valid and in correct unit.
+     *
+     * It does not check the range precisely: it may provide false positives, but not false
+     * negatives. In particular, it may be way off for certain regions.
+     * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
+     * It also can be used to check if a given frequency is likely to be used
+     * with AM or FM modulation.
+     *
+     * @param frequencyKhz the frequency in kHz.
+     * @return true, if the frequency is rougly valid.
+     */
+    public static boolean isAmFrequency(long frequencyKhz) {
+        return frequencyKhz > 150 && frequencyKhz <= 30000;
+    }
+
+    /**
+     * Checks, if a given FM frequency is roughly valid and in correct unit.
+     *
+     * It does not check the range precisely: it may provide false positives, but not false
+     * negatives. In particular, it may be way off for certain regions.
+     * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
+     * It also can be used to check if a given frequency is likely to be used
+     * with AM or FM modulation.
+     *
+     * @param frequencyKhz the frequency in kHz.
+     * @return true, if the frequency is rougly valid.
+     */
+    public static boolean isFmFrequency(long frequencyKhz) {
+        return frequencyKhz > 60000 && frequencyKhz < 110000;
+    }
+
+    /**
+     * Provides human-readable representation of AM/FM frequency.
+     *
+     * @param frequencyKhz the frequency in kHz.
+     * @param flags flags that affect display format
+     * @return human-readable formatted frequency
+     */
+    public static @Nullable String formatAmFmFrequency(long frequencyKhz, @NameFlag int flags) {
+        String channel;
+        String modulation;
+
+        if (isAmFrequency(frequencyKhz)) {
+            channel = Long.toString(frequencyKhz);
+            modulation = "AM";
+        } else if (isFmFrequency(frequencyKhz)) {
+            channel = FORMAT_FM.format(frequencyKhz / 1000f);
+            modulation = "FM";
+        } else {
+            Log.w(TAG, "AM/FM frequency out of range: " + frequencyKhz);
+            return null;
+        }
+
+        if ((flags & NAME_MODULATION_ONLY) != 0) return modulation;
+        if ((flags & NAME_NO_MODULATION) != 0) return channel;
+        return channel + ' ' + modulation;
+    }
+
+    /**
+     * Builds new ProgramSelector for AM/FM frequency.
+     *
+     * @param frequencyKhz the frequency in kHz.
+     * @return new ProgramSelector object representing given frequency.
+     * @throws IllegalArgumentException if provided frequency is out of bounds.
+     */
+    public static @NonNull ProgramSelector createAmFmSelector(long frequencyKhz) {
+        if (frequencyKhz < 0 || frequencyKhz > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("illegal frequency value: " + frequencyKhz);
+        }
+        return ProgramSelector.createAmFmSelector(RadioManager.BAND_INVALID, (int) frequencyKhz);
+    }
+
+    /**
+     * Checks, if {@link ProgramSelector} contains an id of a given type.
+     *
+     * @param sel selector being checked
+     * @param type identifier type to check for
+     * @return true, if sel contains any identifier of a given type
+     */
+    public static boolean hasId(@NonNull ProgramSelector sel,
+            @ProgramSelector.IdentifierType int type) {
+        try {
+            sel.getFirstId(type);
+            return true;
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Checks, if {@link ProgramSelector} is a AM/FM program.
+     *
+     * @return true, if the primary identifier of a selector belongs to one of the following
+     *         technologies:
+     *          - Analogue AM/FM
+     *          - FM RDS
+     *          - HD Radio AM/FM
+     */
+    public static boolean isAmFmProgram(@NonNull ProgramSelector sel) {
+        int priType = sel.getPrimaryId().getType();
+        return priType == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY
+                || priType == ProgramSelector.IDENTIFIER_TYPE_RDS_PI
+                || priType == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT;
+    }
+
+    /**
+     * Returns a channel name that can be displayed to the user.
+     *
+     * It's implemented only for radio technologies where the channel is meant
+     * to be presented to the user.
+     *
+     * @param sel the program selector
+     * @return Channel name or null, if radio technology doesn't present channel names to the user.
+     */
+    public static @Nullable String getDisplayName(@NonNull ProgramSelector sel,
+            @NameFlag int flags) {
+        boolean noProgramTypeFallback = (flags & NAME_NO_PROGRAM_TYPE_FALLBACK) != 0;
+
+        if (isAmFmProgram(sel)) {
+            if (!hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) {
+                if (noProgramTypeFallback) return null;
+                // if there is no frequency assigned, let's assume it's a malformed RDS selector
+                return "FM";
+            }
+            long freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+            return formatAmFmFrequency(freq, flags);
+        }
+
+        if ((flags & NAME_MODULATION_ONLY) != 0) return null;
+
+        if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID
+                && hasId(sel, ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL)) {
+            return Long.toString(sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL));
+        }
+
+        if (noProgramTypeFallback) return null;
+
+        switch (sel.getPrimaryId().getType()) {
+            case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
+                return "SXM";
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
+                return "DAB";
+            case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
+                return "DRMO";
+            default:
+                return null;
+        }
+    }
+
+    static {
+        BiConsumer<Integer, String> add = (idType, name) -> {
+            ID_TO_URI.put(idType, name);
+            URI_TO_ID.put(name, idType);
+        };
+
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, "AMFM_FREQUENCY");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, "RDS_PI");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, "HD_STATION_ID_EXT");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, "HD_STATION_NAME");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, "DAB_SID_EXT");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, "DAB_ENSEMBLE");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, "DAB_SCID");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, "DAB_FREQUENCY");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, "DRMO_SERVICE_ID");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, "DRMO_FREQUENCY");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, "SXM_SERVICE_ID");
+        add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, "SXM_CHANNEL");
+    }
+
+    private static @Nullable String typeToUri(int identifierType) {
+        if (identifierType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_START
+                && identifierType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) {
+            int idx = identifierType - ProgramSelector.IDENTIFIER_TYPE_VENDOR_START;
+            return URI_VENDOR_PREFIX + idx;
+        }
+        return ID_TO_URI.get(identifierType);
+    }
+
+    private static int uriToType(@Nullable String typeUri) {
+        if (typeUri == null) return ProgramSelector.IDENTIFIER_TYPE_INVALID;
+        if (typeUri.startsWith(URI_VENDOR_PREFIX)) {
+            int idx;
+            try {
+                idx = Integer.parseInt(typeUri.substring(URI_VENDOR_PREFIX.length()));
+            } catch (NumberFormatException ex) {
+                return ProgramSelector.IDENTIFIER_TYPE_INVALID;
+            }
+            if (idx > ProgramSelector.IDENTIFIER_TYPE_VENDOR_END
+                    - ProgramSelector.IDENTIFIER_TYPE_VENDOR_START) {
+                return ProgramSelector.IDENTIFIER_TYPE_INVALID;
+            }
+            return ProgramSelector.IDENTIFIER_TYPE_VENDOR_START + idx;
+        }
+        return URI_TO_ID.get(typeUri);
+    }
+
+    private static @NonNull String valueToUri(@NonNull Identifier id) {
+        long val = id.getValue();
+        switch (id.getType()) {
+            case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+            case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
+            case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
+                return Long.toString(val);
+            default:
+                return URI_HEX_PREFIX + Long.toHexString(val);
+        }
+    }
+
+    private static @Nullable Long uriToValue(@Nullable String valUri) {
+        if (valUri == null) return null;
+        try {
+            if (valUri.startsWith(URI_HEX_PREFIX)) {
+                return Long.parseLong(valUri.substring(URI_HEX_PREFIX.length()), 16);
+            } else {
+                return Long.parseLong(valUri, 10);
+            }
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Serialize {@link ProgramSelector} to URI.
+     *
+     * @param sel selector to serialize
+     * @return serialized form of selector
+     */
+    public static @Nullable Uri toUri(@NonNull ProgramSelector sel) {
+        Identifier pri = sel.getPrimaryId();
+        String priType = typeToUri(pri.getType());
+        // unsupported primary identifier, might be from future HAL revision
+        if (priType == null) return null;
+
+        Uri.Builder uri = new Uri.Builder()
+                .scheme(URI_SCHEME_BROADCASTRADIO)
+                .authority(URI_AUTHORITY_PROGRAM)
+                .appendPath(priType)
+                .appendPath(valueToUri(pri));
+
+        for (Identifier sec : sel.getSecondaryIds()) {
+            String secType = typeToUri(sec.getType());
+            if (secType == null) continue;  // skip unsupported secondary identifier
+            uri.appendQueryParameter(secType, valueToUri(sec));
+        }
+        return uri.build();
+    }
+
+    /**
+     * Parse serialized {@link ProgramSelector}.
+     *
+     * @param uri URI-zed form of ProgramSelector
+     * @return de-serialized object or null, if couldn't parse
+     */
+    public static @Nullable ProgramSelector fromUri(@Nullable Uri uri) {
+        if (uri == null) return null;
+
+        if (!URI_SCHEME_BROADCASTRADIO.equals(uri.getScheme())) return null;
+        if (!URI_AUTHORITY_PROGRAM.equals(uri.getAuthority())) {
+            Log.w(TAG, "Unknown URI authority part (might be a future, unsupported version): "
+                    + uri.getAuthority());
+            return null;
+        }
+
+        BiFunction<String, String, Identifier> parseComponents = (typeStr, valueStr) -> {
+            int type = uriToType(typeStr);
+            Long value = uriToValue(valueStr);
+            if (type == ProgramSelector.IDENTIFIER_TYPE_INVALID || value == null) return null;
+            return new Identifier(type, value);
+        };
+
+        List<String> priUri = uri.getPathSegments();
+        if (priUri.size() != 2) return null;
+        Identifier pri = parseComponents.apply(priUri.get(0), priUri.get(1));
+        if (pri == null) return null;
+
+        String query = uri.getQuery();
+        List<Identifier> secIds = new ArrayList<>();
+        if (query != null) {
+            for (String secPair : query.split("&")) {
+                String[] secStr = secPair.split("=");
+                if (secStr.length != 2) continue;
+                Identifier sec = parseComponents.apply(secStr[0], secStr[1]);
+                if (sec != null) secIds.add(sec);
+            }
+        }
+
+        return newProgramSelector(pri, secIds.toArray(new Identifier[secIds.size()]));
+    }
+
+    /**
+     * Proposed extensions to android.hardware.radio.ProgramSelector.Identifier.
+     *
+     * They might eventually get pushed to the framework.
+     */
+    public static class IdentifierExt {
+        /**
+         * Decode {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value.
+         *
+         * @param id identifier to decode
+         * @return value decoder
+         */
+        public static @Nullable HdPrimary asHdPrimary(@NonNull Identifier id) {
+            if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) {
+                return new HdPrimary(id.getValue());
+            }
+            return null;
+        }
+
+        /**
+         * Decoder of {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value.
+         *
+         * When pushed to the framework, it will be non-static class referring
+         * to the original value.
+         */
+        public static class HdPrimary {
+            /* For mValue format (bit shifts and bit masks), please refer to
+             * HD_STATION_ID_EXT from broadcastradio HAL 2.0.
+             */
+            private final long mValue;
+
+            private HdPrimary(long value) {
+                mValue = value;
+            }
+
+            public long getStationId() {
+                return mValue & 0xFFFFFFFF;
+            }
+
+            public int getSubchannel() {
+                return (int) ((mValue >>> 32) & 0xF);
+            }
+
+            public int getFrequency() {
+                return (int) ((mValue >>> (32 + 4)) & 0x3FFFF);
+            }
+        }
+    }
+}
diff --git a/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java
new file mode 100644
index 0000000..e7b6f3b
--- /dev/null
+++ b/car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (C) 2018 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.car.broadcastradio.support.platform;
+
+import android.annotation.NonNull;
+import android.hardware.radio.RadioMetadata;
+
+/**
+ * Proposed extensions to android.hardware.radio.RadioMetadata.
+ *
+ * They might eventually get pushed to the framework.
+ */
+public class RadioMetadataExt {
+    private static int sModuleId;
+
+    /**
+     * A hack to inject module ID for getGlobalBitmapId. When pushed to the framework,
+     * it will be set with RadioMetadata object creation or just separate int field.
+     * @hide
+     */
+    public static void setModuleId(int id) {
+        sModuleId = id;
+    }
+
+    /**
+     * Proposed redefinition of {@link RadioMetadata#getBitmapId}.
+     *
+     * {@link RadioMetadata#getBitmapId} isn't part of the system API yet, so we can skip
+     * deprecation here and jump straight to the correct solution.
+     */
+    public static long getGlobalBitmapId(@NonNull RadioMetadata meta, @NonNull String key) {
+        int localId = meta.getBitmapId(key);
+        if (localId == 0) return 0;
+
+        /* When generating global bitmap ID, we want them to remain stable between sessions
+         * (radio app might cache images to disk between sessions).
+         *
+         * Local IDs are already stable, but module ID is not guaranteed to be stable (i.e. some
+         * module might be not available at each boot, due to HW failure).
+         *
+         * When we push this to the framework, we will need persistence mechanism at the radio
+         * service to permanently match modules to their IDs.
+         */
+        return ((long) sModuleId << 32) | localId;
+    }
+}
diff --git a/car-qc-lib/Android.bp b/car-qc-lib/Android.bp
new file mode 100644
index 0000000..c12fd28
--- /dev/null
+++ b/car-qc-lib/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "car-qc-lib",
+    platform_apis: true,
+    srcs: ["src/**/*.java"],
+    optimize: {
+        enabled: false,
+    },
+    static_libs: [
+        "androidx.annotation_annotation",
+        "car-ui-lib"
+    ],
+}
diff --git a/car-qc-lib/AndroidManifest.xml b/car-qc-lib/AndroidManifest.xml
new file mode 100644
index 0000000..166d9c0
--- /dev/null
+++ b/car-qc-lib/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.qc">
+</manifest>
diff --git a/car-qc-lib/OWNERS b/car-qc-lib/OWNERS
new file mode 100644
index 0000000..7f8081c
--- /dev/null
+++ b/car-qc-lib/OWNERS
@@ -0,0 +1,8 @@
+# People who can approve changes for submission.
+
+# Primary
+alexstetson@google.com
+
+# Secondary (only if people in Primary are unreachable)
+hseog@google.com
+nehah@google.com
diff --git a/car-qc-lib/PREUPLOAD.cfg b/car-qc-lib/PREUPLOAD.cfg
new file mode 100644
index 0000000..38f9800
--- /dev/null
+++ b/car-qc-lib/PREUPLOAD.cfg
@@ -0,0 +1,7 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/car-qc-lib/res/color/qc_toggle_background_color.xml b/car-qc-lib/res/color/qc_toggle_background_color.xml
new file mode 100644
index 0000000..15253ad
--- /dev/null
+++ b/car-qc-lib/res/color/qc_toggle_background_color.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="false" android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="@color/qc_toggle_off_background_color"/>
+    <item android:state_checked="false"
+          android:color="@color/qc_toggle_off_background_color"/>
+    <item android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorAccent"/>
+    <item android:color="?android:attr/colorAccent"/>
+</selector>
diff --git a/car-qc-lib/res/color/qc_toggle_icon_fill_color.xml b/car-qc-lib/res/color/qc_toggle_icon_fill_color.xml
new file mode 100644
index 0000000..bdb5433
--- /dev/null
+++ b/car-qc-lib/res/color/qc_toggle_icon_fill_color.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="false" android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="@android:color/white"/>
+    <item android:state_checked="false"
+          android:color="@android:color/white"/>
+    <item android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="@android:color/black"/>
+    <item android:color="@android:color/black"/>
+</selector>
diff --git a/car-qc-lib/res/drawable/qc_row_action_divider.xml b/car-qc-lib/res/drawable/qc_row_action_divider.xml
new file mode 100644
index 0000000..75ffd46
--- /dev/null
+++ b/car-qc-lib/res/drawable/qc_row_action_divider.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <size
+        android:height="0dp"
+        android:width="@dimen/qc_toggle_margin"/>
+</shape>
diff --git a/car-qc-lib/res/drawable/qc_seekbar_wrapper_background.xml b/car-qc-lib/res/drawable/qc_seekbar_wrapper_background.xml
new file mode 100644
index 0000000..58b9c65
--- /dev/null
+++ b/car-qc-lib/res/drawable/qc_seekbar_wrapper_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Highlight the wrapper when it's focused but not selected. The wrapper is selected in
+    direct manipulation mode. -->
+    <item android:state_focused="true" android:state_selected="false">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/car_ui_rotary_focus_fill_color"/>
+            <stroke android:width="@dimen/car_ui_rotary_focus_stroke_width"
+                    android:color="@color/car_ui_rotary_focus_stroke_color"/>
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/car-qc-lib/res/drawable/qc_toggle_background.xml b/car-qc-lib/res/drawable/qc_toggle_background.xml
new file mode 100644
index 0000000..c139590
--- /dev/null
+++ b/car-qc-lib/res/drawable/qc_toggle_background.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/background"
+          android:width="@dimen/qc_toggle_size"
+          android:height="@dimen/qc_toggle_size">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/qc_toggle_background_color" />
+            <corners android:radius="@dimen/qc_toggle_background_radius" />
+        </shape>
+    </item>
+    <item android:width="@dimen/qc_toggle_size"
+          android:height="@dimen/qc_toggle_size"
+          android:drawable="@drawable/qc_toggle_rotary_background"/>
+</layer-list>
\ No newline at end of file
diff --git a/car-qc-lib/res/drawable/qc_toggle_button_background.xml b/car-qc-lib/res/drawable/qc_toggle_button_background.xml
new file mode 100644
index 0000000..f42ebf8
--- /dev/null
+++ b/car-qc-lib/res/drawable/qc_toggle_button_background.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item app:state_toggle_unavailable="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/qc_toggle_unavailable_background_color" />
+            <stroke android:color="@color/qc_toggle_unavailable_color"
+                android:width="@dimen/qc_toggle_unavailable_outline_width" />
+            <corners android:radius="@dimen/qc_toggle_background_radius" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/qc_toggle_background_color" />
+            <corners android:radius="@dimen/qc_toggle_background_radius" />
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/car-qc-lib/res/drawable/qc_toggle_rotary_background.xml b/car-qc-lib/res/drawable/qc_toggle_rotary_background.xml
new file mode 100644
index 0000000..406c44c
--- /dev/null
+++ b/car-qc-lib/res/drawable/qc_toggle_rotary_background.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_focused="true" android:state_pressed="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/car_ui_rotary_focus_pressed_fill_secondary_color"/>
+            <stroke android:width="@dimen/car_ui_rotary_focus_pressed_stroke_width"
+                    android:color="@color/car_ui_rotary_focus_pressed_stroke_secondary_color"/>
+            <corners android:radius="@dimen/qc_toggle_rotary_background_radius" />
+        </shape>
+    </item>
+    <item android:state_focused="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/car_ui_rotary_focus_fill_secondary_color"/>
+            <stroke android:width="@dimen/car_ui_rotary_focus_stroke_width"
+                    android:color="@color/car_ui_rotary_focus_stroke_secondary_color"/>
+            <corners android:radius="@dimen/qc_toggle_rotary_background_radius" />
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml b/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml
new file mode 100644
index 0000000..98cbded
--- /dev/null
+++ b/car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/background"
+          android:width="@dimen/qc_toggle_size"
+          android:height="@dimen/qc_toggle_size">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/qc_toggle_unavailable_background_color" />
+            <stroke android:color="@color/qc_toggle_unavailable_color"
+                    android:width="@dimen/qc_toggle_unavailable_outline_width" />
+            <corners android:radius="@dimen/qc_toggle_background_radius" />
+        </shape>
+    </item>
+    <item android:width="@dimen/qc_toggle_size"
+          android:height="@dimen/qc_toggle_size"
+          android:drawable="@drawable/qc_toggle_rotary_background"/>
+</layer-list>
diff --git a/car-qc-lib/res/layout/qc_action_switch.xml b/car-qc-lib/res/layout/qc_action_switch.xml
new file mode 100644
index 0000000..9ab57ba
--- /dev/null
+++ b/car-qc-lib/res/layout/qc_action_switch.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.car.ui.uxr.DrawableStateSwitch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/switch_widget"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content" />
diff --git a/car-qc-lib/res/layout/qc_action_toggle.xml b/car-qc-lib/res/layout/qc_action_toggle.xml
new file mode 100644
index 0000000..301e0c4
--- /dev/null
+++ b/car-qc-lib/res/layout/qc_action_toggle.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.car.ui.uxr.DrawableStateToggleButton
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/qc_toggle_button"
+    android:background="@android:color/transparent"
+    android:defaultFocusHighlightEnabled="false"
+    android:minHeight="0dp"
+    android:minWidth="0dp"
+    android:layout_width="@dimen/qc_toggle_size"
+    android:layout_height="@dimen/qc_toggle_size"/>
\ No newline at end of file
diff --git a/car-qc-lib/res/layout/qc_row_view.xml b/car-qc-lib/res/layout/qc_row_view.xml
new file mode 100644
index 0000000..6656b29
--- /dev/null
+++ b/car-qc-lib/res/layout/qc_row_view.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.car.ui.uxr.DrawableStateConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_centerVertical="true"
+    android:layout_marginVertical="@dimen/qc_row_margin_vertical"
+    android:clipToPadding="false"
+    android:minHeight="@dimen/qc_row_min_height"
+    android:paddingEnd="@dimen/qc_row_padding_end"
+    android:paddingStart="@dimen/qc_row_padding_start">
+
+    <LinearLayout
+        android:id="@+id/qc_row_start_items"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qc_action_items_horizontal_margin"
+        android:orientation="horizontal"
+        android:divider="@drawable/qc_row_action_divider"
+        android:showDividers="middle"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/qc_row_content"
+        app:layout_constraintHorizontal_chainStyle="spread_inside"/>
+
+    <com.android.car.ui.uxr.DrawableStateConstraintLayout
+        android:id="@+id/qc_row_content"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="?android:attr/selectableItemBackground"
+        app:layout_constraintStart_toEndOf="@+id/qc_row_start_items"
+        app:layout_constraintEnd_toStartOf="@+id/qc_row_end_items"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHeight_default="wrap"
+        app:layout_constraintHeight_min="@dimen/qc_row_min_height">
+
+        <com.android.car.ui.uxr.DrawableStateImageView
+            android:id="@+id/qc_icon"
+            android:layout_width="@dimen/qc_row_icon_size"
+            android:layout_height="@dimen/qc_row_icon_size"
+            android:layout_marginEnd="@dimen/qc_row_icon_margin_end"
+            android:scaleType="fitCenter"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/barrier1"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@+id/barrier2"/>
+
+        <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/barrier1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:barrierDirection="end"
+            app:constraint_referenced_ids="qc_icon"
+            app:barrierAllowsGoneWidgets="false"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@+id/qc_title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.QC.Title"
+            app:layout_constraintStart_toEndOf="@+id/barrier1"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@+id/qc_summary"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintVertical_chainStyle="packed"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@+id/qc_summary"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:textAppearance="@style/TextAppearance.QC.Subtitle"
+            app:layout_constraintStart_toEndOf="@+id/barrier1"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/qc_title"
+            app:layout_constraintBottom_toTopOf="@+id/barrier2"/>
+
+        <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/barrier2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:barrierDirection="top"
+            app:constraint_referenced_ids="qc_seekbar_wrapper"
+            app:barrierAllowsGoneWidgets="false"/>
+
+        <androidx.preference.UnPressableLinearLayout
+            android:id="@+id/qc_seekbar_wrapper"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:paddingTop="@dimen/qc_seekbar_padding_top"
+            android:focusable="true"
+            android:background="@drawable/qc_seekbar_wrapper_background"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:layout_centerVertical="true"
+            android:orientation="vertical"
+            android:visibility="gone"
+            app:layout_constraintStart_toEndOf="@+id/barrier1"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/barrier2"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <com.android.car.qc.view.QCSeekBarView
+                android:id="@+id/seekbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                style="@style/Widget.QC.SeekBar"/>
+        </androidx.preference.UnPressableLinearLayout>
+
+    </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+    <LinearLayout
+        android:id="@+id/qc_row_end_items"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/qc_action_items_horizontal_margin"
+        android:orientation="horizontal"
+        android:divider="@drawable/qc_row_action_divider"
+        android:showDividers="middle"
+        app:layout_constraintStart_toEndOf="@+id/qc_row_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+</com.android.car.ui.uxr.DrawableStateConstraintLayout>
diff --git a/car-qc-lib/res/layout/qc_tile_view.xml b/car-qc-lib/res/layout/qc_tile_view.xml
new file mode 100644
index 0000000..7fb0884
--- /dev/null
+++ b/car-qc-lib/res/layout/qc_tile_view.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.car.ui.uxr.DrawableStateLinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/qc_tile_wrapper"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:background="?android:attr/selectableItemBackground">
+    <com.android.car.ui.uxr.DrawableStateToggleButton
+        android:id="@+id/qc_tile_toggle_button"
+        android:background="@android:color/transparent"
+        android:layout_width="@dimen/qc_toggle_size"
+        android:layout_height="@dimen/qc_toggle_size"
+        android:layout_marginTop="@dimen/qc_toggle_margin"
+        android:layout_marginBottom="@dimen/qc_toggle_margin"
+        android:layout_marginStart="@dimen/qc_toggle_margin"
+        android:layout_marginEnd="@dimen/qc_toggle_margin"
+        android:clickable="false"
+        android:focusable="false"/>
+    <com.android.car.ui.uxr.DrawableStateTextView
+        android:id="@android:id/summary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.QC.Subtitle"/>
+</com.android.car.ui.uxr.DrawableStateLinearLayout>
\ No newline at end of file
diff --git a/car-qc-lib/res/values/attrs.xml b/car-qc-lib/res/values/attrs.xml
new file mode 100644
index 0000000..94613b9
--- /dev/null
+++ b/car-qc-lib/res/values/attrs.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 2022 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>
+    <attr name="state_toggle_unavailable"/>
+</resources>
\ No newline at end of file
diff --git a/car-qc-lib/res/values/colors.xml b/car-qc-lib/res/values/colors.xml
new file mode 100644
index 0000000..e3fbd6f
--- /dev/null
+++ b/car-qc-lib/res/values/colors.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="qc_start_icon_color">@android:color/white</color>
+    <color name="qc_toggle_off_background_color">#626262</color>
+    <color name="qc_toggle_unavailable_background_color">@android:color/transparent</color>
+    <color name="qc_toggle_unavailable_color">#75FFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/car-qc-lib/res/values/dimens.xml b/car-qc-lib/res/values/dimens.xml
new file mode 100644
index 0000000..6247561
--- /dev/null
+++ b/car-qc-lib/res/values/dimens.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <dimen name="qc_row_padding_start">32dp</dimen>
+    <dimen name="qc_row_padding_end">32dp</dimen>
+    <dimen name="qc_row_min_height">76dp</dimen>
+    <dimen name="qc_row_margin_vertical">10dp</dimen>
+    <dimen name="qc_row_icon_size">44dp</dimen>
+    <dimen name="qc_row_icon_margin_end">32dp</dimen>
+    <dimen name="qc_row_content_margin">16dp</dimen>
+
+    <dimen name="qc_action_items_horizontal_margin">32dp</dimen>
+    <dimen name="qc_toggle_size">72dp</dimen>
+    <dimen name="qc_toggle_margin">12dp</dimen>
+    <dimen name="qc_toggle_background_radius">16dp</dimen>
+    <dimen name="qc_toggle_rotary_background_radius">11dp</dimen>
+    <dimen name="qc_toggle_foreground_icon_inset">14dp</dimen>
+    <dimen name="qc_toggle_unavailable_outline_width">2dp</dimen>
+
+    <dimen name="qc_seekbar_padding_top">16dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car-qc-lib/res/values/styles.xml b/car-qc-lib/res/values/styles.xml
new file mode 100644
index 0000000..587b522
--- /dev/null
+++ b/car-qc-lib/res/values/styles.xml
@@ -0,0 +1,39 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <style name="TextAppearance.QC" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:textColor">@color/car_ui_text_color_primary</item>
+    </style>
+
+    <style name="TextAppearance.QC.Title">
+        <item name="android:textSize">@dimen/car_ui_body1_size</item>
+    </style>
+
+    <style name="TextAppearance.QC.Subtitle">
+        <item name="android:textColor">@color/car_ui_text_color_secondary</item>
+        <item name="android:textSize">@dimen/car_ui_body3_size</item>
+    </style>
+
+    <style name="Widget.QC" parent="android:Widget.DeviceDefault"/>
+
+    <style name="Widget.QC.SeekBar">
+        <item name="android:background">@null</item>
+        <item name="android:clickable">false</item>
+        <item name="android:focusable">false</item>
+        <item name="android:splitTrack">false</item>
+    </style>
+</resources>
diff --git a/car-qc-lib/src/com/android/car/qc/QCActionItem.java b/car-qc-lib/src/com/android/car/qc/QCActionItem.java
new file mode 100644
index 0000000..c476e09
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/QCActionItem.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Quick Control Action that are includes as either start or end actions in {@link QCRow}
+ */
+public class QCActionItem extends QCItem {
+    private final boolean mIsChecked;
+    private final boolean mIsAvailable;
+    private Icon mIcon;
+    private PendingIntent mAction;
+    private PendingIntent mDisabledClickAction;
+
+    public QCActionItem(@NonNull @QCItemType String type, boolean isChecked, boolean isEnabled,
+            boolean isAvailable, boolean isClickableWhileDisabled, @Nullable Icon icon,
+            @Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) {
+        super(type, isEnabled, isClickableWhileDisabled);
+        mIsChecked = isChecked;
+        mIsAvailable = isAvailable;
+        mIcon = icon;
+        mAction = action;
+        mDisabledClickAction = disabledClickAction;
+    }
+
+    public QCActionItem(@NonNull Parcel in) {
+        super(in);
+        mIsChecked = in.readBoolean();
+        mIsAvailable = in.readBoolean();
+        boolean hasIcon = in.readBoolean();
+        if (hasIcon) {
+            mIcon = Icon.CREATOR.createFromParcel(in);
+        }
+        boolean hasAction = in.readBoolean();
+        if (hasAction) {
+            mAction = PendingIntent.CREATOR.createFromParcel(in);
+        }
+        boolean hasDisabledClickAction = in.readBoolean();
+        if (hasDisabledClickAction) {
+            mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeBoolean(mIsChecked);
+        dest.writeBoolean(mIsAvailable);
+        boolean includeIcon = getType().equals(QC_TYPE_ACTION_TOGGLE) && mIcon != null;
+        dest.writeBoolean(includeIcon);
+        if (includeIcon) {
+            mIcon.writeToParcel(dest, flags);
+        }
+        boolean hasAction = mAction != null;
+        dest.writeBoolean(hasAction);
+        if (hasAction) {
+            mAction.writeToParcel(dest, flags);
+        }
+        boolean hasDisabledClickAction = mDisabledClickAction != null;
+        dest.writeBoolean(hasDisabledClickAction);
+        if (hasDisabledClickAction) {
+            mDisabledClickAction.writeToParcel(dest, flags);
+        }
+    }
+
+    @Override
+    public PendingIntent getPrimaryAction() {
+        return mAction;
+    }
+
+    @Override
+    public PendingIntent getDisabledClickAction() {
+        return mDisabledClickAction;
+    }
+
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    public boolean isAvailable() {
+        return mIsAvailable;
+    }
+
+    @Nullable
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    public static Creator<QCActionItem> CREATOR = new Creator<QCActionItem>() {
+        @Override
+        public QCActionItem createFromParcel(Parcel source) {
+            return new QCActionItem(source);
+        }
+
+        @Override
+        public QCActionItem[] newArray(int size) {
+            return new QCActionItem[size];
+        }
+    };
+
+    /**
+     * Builder for {@link QCActionItem}.
+     */
+    public static class Builder {
+        private final String mType;
+        private boolean mIsChecked;
+        private boolean mIsEnabled = true;
+        private boolean mIsAvailable = true;
+        private boolean mIsClickableWhileDisabled = false;
+        private Icon mIcon;
+        private PendingIntent mAction;
+        private PendingIntent mDisabledClickAction;
+
+        public Builder(@NonNull @QCItemType String type) {
+            if (!isValidType(type)) {
+                throw new IllegalArgumentException("Invalid QCActionItem type provided" + type);
+            }
+            mType = type;
+        }
+
+        /**
+         * Sets whether or not the action item should be checked.
+         */
+        public Builder setChecked(boolean checked) {
+            mIsChecked = checked;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the action item should be enabled.
+         */
+        public Builder setEnabled(boolean enabled) {
+            mIsEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the action item is available.
+         */
+        public Builder setAvailable(boolean available) {
+            mIsAvailable = available;
+            return this;
+        }
+
+        /**
+         * Sets whether or not an action item should be clickable while disabled.
+         */
+        public Builder setClickableWhileDisabled(boolean clickable) {
+            mIsClickableWhileDisabled = clickable;
+            return this;
+        }
+
+        /**
+         * Sets the icon for {@link QC_TYPE_ACTION_TOGGLE} actions
+         */
+        public Builder setIcon(@Nullable Icon icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the action item is clicked.
+         */
+        public Builder setAction(@Nullable PendingIntent action) {
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the action item is clicked while disabled.
+         */
+        public Builder setDisabledClickAction(@Nullable PendingIntent action) {
+            mDisabledClickAction = action;
+            return this;
+        }
+
+        /**
+         * Builds the final {@link QCActionItem}.
+         */
+        public QCActionItem build() {
+            return new QCActionItem(mType, mIsChecked, mIsEnabled, mIsAvailable,
+                    mIsClickableWhileDisabled, mIcon, mAction, mDisabledClickAction);
+        }
+
+        private boolean isValidType(String type) {
+            return type.equals(QC_TYPE_ACTION_SWITCH) || type.equals(QC_TYPE_ACTION_TOGGLE);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/QCItem.java b/car-qc-lib/src/com/android/car/qc/QCItem.java
new file mode 100644
index 0000000..c6826ae
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/QCItem.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base class for all quick controls elements.
+ */
+public abstract class QCItem implements Parcelable {
+    public static final String QC_TYPE_LIST = "QC_TYPE_LIST";
+    public static final String QC_TYPE_ROW = "QC_TYPE_ROW";
+    public static final String QC_TYPE_TILE = "QC_TYPE_TILE";
+    public static final String QC_TYPE_SLIDER = "QC_TYPE_SLIDER";
+    public static final String QC_TYPE_ACTION_SWITCH = "QC_TYPE_ACTION_SWITCH";
+    public static final String QC_TYPE_ACTION_TOGGLE = "QC_TYPE_ACTION_TOGGLE";
+
+    public static final String QC_ACTION_TOGGLE_STATE = "QC_ACTION_TOGGLE_STATE";
+    public static final String QC_ACTION_SLIDER_VALUE = "QC_ACTION_SLIDER_VALUE";
+
+    @StringDef(value = {
+            QC_TYPE_LIST,
+            QC_TYPE_ROW,
+            QC_TYPE_TILE,
+            QC_TYPE_SLIDER,
+            QC_TYPE_ACTION_SWITCH,
+            QC_TYPE_ACTION_TOGGLE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface QCItemType {
+    }
+
+    private final String mType;
+    private final boolean mIsEnabled;
+    private final boolean mIsClickableWhileDisabled;
+    private ActionHandler mActionHandler;
+    private ActionHandler mDisabledClickActionHandler;
+
+    public QCItem(@NonNull @QCItemType String type) {
+        this(type, /* isEnabled= */true, /* isClickableWhileDisabled= */ false);
+    }
+
+    public QCItem(@NonNull @QCItemType String type, boolean isEnabled,
+            boolean isClickableWhileDisabled) {
+        mType = type;
+        mIsEnabled = isEnabled;
+        mIsClickableWhileDisabled = isClickableWhileDisabled;
+    }
+
+    public QCItem(@NonNull Parcel in) {
+        mType = in.readString();
+        mIsEnabled = in.readBoolean();
+        mIsClickableWhileDisabled = in.readBoolean();
+    }
+
+    @NonNull
+    @QCItemType
+    public String getType() {
+        return mType;
+    }
+
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    public boolean isClickableWhileDisabled() {
+        return mIsClickableWhileDisabled;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mType);
+        dest.writeBoolean(mIsEnabled);
+        dest.writeBoolean(mIsClickableWhileDisabled);
+    }
+
+    public void setActionHandler(@Nullable ActionHandler handler) {
+        mActionHandler = handler;
+    }
+
+    public void setDisabledClickActionHandler(@Nullable ActionHandler handler) {
+        mDisabledClickActionHandler = handler;
+    }
+
+    @Nullable
+    public ActionHandler getActionHandler() {
+        return mActionHandler;
+    }
+
+    @Nullable
+    public ActionHandler getDisabledClickActionHandler() {
+        return mDisabledClickActionHandler;
+    }
+
+    /**
+     * Returns the PendingIntent that is sent when the item is clicked.
+     */
+    @Nullable
+    public abstract PendingIntent getPrimaryAction();
+
+    /**
+     * Returns the PendingIntent that is sent when the item is clicked while disabled.
+     */
+    @Nullable
+    public abstract PendingIntent getDisabledClickAction();
+
+    /**
+     * Action handler that can listen for an action to occur and notify listeners.
+     */
+    public interface ActionHandler {
+        /**
+         * Callback when an action occurs.
+         * @param item the QCItem that sent the action
+         * @param context the context for the action
+         * @param intent the intent that was sent with the action
+         */
+        void onAction(@NonNull QCItem item, @NonNull Context context, @NonNull Intent intent);
+
+        default boolean isActivity() {
+            return false;
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/QCList.java b/car-qc-lib/src/com/android/car/qc/QCList.java
new file mode 100644
index 0000000..0a19a93
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/QCList.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import android.app.PendingIntent;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Wrapping quick controls element that contains QCRow elements.
+ */
+public class QCList extends QCItem {
+    private final List<QCRow> mRows;
+
+    public QCList(@NonNull List<QCRow> rows) {
+        super(QC_TYPE_LIST);
+        mRows = Collections.unmodifiableList(rows);
+    }
+
+    public QCList(@NonNull Parcel in) {
+        super(in);
+        int rowCount = in.readInt();
+        List<QCRow> rows = new ArrayList<>();
+        for (int i = 0; i < rowCount; i++) {
+            rows.add(QCRow.CREATOR.createFromParcel(in));
+        }
+        mRows = Collections.unmodifiableList(rows);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mRows.size());
+        for (QCRow row : mRows) {
+            row.writeToParcel(dest, flags);
+        }
+    }
+
+    @Override
+    public PendingIntent getPrimaryAction() {
+        return null;
+    }
+
+    @Override
+    public PendingIntent getDisabledClickAction() {
+        return null;
+    }
+
+    @NonNull
+    public List<QCRow> getRows() {
+        return mRows;
+    }
+
+    public static Creator<QCList> CREATOR = new Creator<QCList>() {
+        @Override
+        public QCList createFromParcel(Parcel source) {
+            return new QCList(source);
+        }
+
+        @Override
+        public QCList[] newArray(int size) {
+            return new QCList[size];
+        }
+    };
+
+    /**
+     * Builder for {@link QCList}.
+     */
+    public static class Builder {
+        private final List<QCRow> mRows = new ArrayList<>();
+
+        /**
+         * Adds a {@link QCRow} to the list.
+         */
+        public Builder addRow(@NonNull QCRow row) {
+            mRows.add(row);
+            return this;
+        }
+
+        /**
+         * Builds the final {@link QCList}.
+         */
+        public QCList build() {
+            return new QCList(mRows);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/QCRow.java b/car-qc-lib/src/com/android/car/qc/QCRow.java
new file mode 100644
index 0000000..8d93295
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/QCRow.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Quick Control Row Element
+ * ------------------------------------
+ * |            | Title    |          |
+ * | StartItems | Subtitle | EndItems |
+ * |            | Sliders  |          |
+ * ------------------------------------
+ */
+public class QCRow extends QCItem {
+    private final String mTitle;
+    private final String mSubtitle;
+    private final Icon mStartIcon;
+    private final boolean mIsStartIconTintable;
+    private final QCSlider mSlider;
+    private final List<QCActionItem> mStartItems;
+    private final List<QCActionItem> mEndItems;
+    private final PendingIntent mPrimaryAction;
+    private PendingIntent mDisabledClickAction;
+
+    public QCRow(@Nullable String title, @Nullable String subtitle, boolean isEnabled,
+            boolean isClickableWhileDisabled, @Nullable PendingIntent primaryAction,
+            @Nullable PendingIntent disabledClickAction, @Nullable Icon startIcon,
+            boolean isIconTintable, @Nullable QCSlider slider,
+            @NonNull List<QCActionItem> startItems, @NonNull List<QCActionItem> endItems) {
+        super(QC_TYPE_ROW, isEnabled, isClickableWhileDisabled);
+        mTitle = title;
+        mSubtitle = subtitle;
+        mPrimaryAction = primaryAction;
+        mDisabledClickAction = disabledClickAction;
+        mStartIcon = startIcon;
+        mIsStartIconTintable = isIconTintable;
+        mSlider = slider;
+        mStartItems = Collections.unmodifiableList(startItems);
+        mEndItems = Collections.unmodifiableList(endItems);
+    }
+
+    public QCRow(@NonNull Parcel in) {
+        super(in);
+        mTitle = in.readString();
+        mSubtitle = in.readString();
+        boolean hasIcon = in.readBoolean();
+        if (hasIcon) {
+            mStartIcon = Icon.CREATOR.createFromParcel(in);
+        } else {
+            mStartIcon = null;
+        }
+        mIsStartIconTintable = in.readBoolean();
+        boolean hasSlider = in.readBoolean();
+        if (hasSlider) {
+            mSlider = QCSlider.CREATOR.createFromParcel(in);
+        } else {
+            mSlider = null;
+        }
+        List<QCActionItem> startItems = new ArrayList<>();
+        int startItemCount = in.readInt();
+        for (int i = 0; i < startItemCount; i++) {
+            startItems.add(QCActionItem.CREATOR.createFromParcel(in));
+        }
+        mStartItems = Collections.unmodifiableList(startItems);
+        List<QCActionItem> endItems = new ArrayList<>();
+        int endItemCount = in.readInt();
+        for (int i = 0; i < endItemCount; i++) {
+            endItems.add(QCActionItem.CREATOR.createFromParcel(in));
+        }
+        mEndItems = Collections.unmodifiableList(endItems);
+        boolean hasPrimaryAction = in.readBoolean();
+        if (hasPrimaryAction) {
+            mPrimaryAction = PendingIntent.CREATOR.createFromParcel(in);
+        } else {
+            mPrimaryAction = null;
+        }
+        boolean hasDisabledClickAction = in.readBoolean();
+        if (hasDisabledClickAction) {
+            mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
+        } else {
+            mDisabledClickAction = null;
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(mTitle);
+        dest.writeString(mSubtitle);
+        boolean hasStartIcon = mStartIcon != null;
+        dest.writeBoolean(hasStartIcon);
+        if (hasStartIcon) {
+            mStartIcon.writeToParcel(dest, flags);
+        }
+        dest.writeBoolean(mIsStartIconTintable);
+        boolean hasSlider = mSlider != null;
+        dest.writeBoolean(hasSlider);
+        if (hasSlider) {
+            mSlider.writeToParcel(dest, flags);
+        }
+        dest.writeInt(mStartItems.size());
+        for (QCActionItem startItem : mStartItems) {
+            startItem.writeToParcel(dest, flags);
+        }
+        dest.writeInt(mEndItems.size());
+        for (QCActionItem endItem : mEndItems) {
+            endItem.writeToParcel(dest, flags);
+        }
+        boolean hasPrimaryAction = mPrimaryAction != null;
+        dest.writeBoolean(hasPrimaryAction);
+        if (hasPrimaryAction) {
+            mPrimaryAction.writeToParcel(dest, flags);
+        }
+        boolean hasDisabledClickAction = mDisabledClickAction != null;
+        dest.writeBoolean(hasDisabledClickAction);
+        if (hasDisabledClickAction) {
+            mDisabledClickAction.writeToParcel(dest, flags);
+        }
+    }
+
+    @Override
+    public PendingIntent getPrimaryAction() {
+        return mPrimaryAction;
+    }
+
+    @Override
+    public PendingIntent getDisabledClickAction() {
+        return mDisabledClickAction;
+    }
+
+    @Nullable
+    public String getTitle() {
+        return mTitle;
+    }
+
+    @Nullable
+    public String getSubtitle() {
+        return mSubtitle;
+    }
+
+    @Nullable
+    public Icon getStartIcon() {
+        return mStartIcon;
+    }
+
+    public boolean isStartIconTintable() {
+        return mIsStartIconTintable;
+    }
+
+    @Nullable
+    public QCSlider getSlider() {
+        return mSlider;
+    }
+
+    @NonNull
+    public List<QCActionItem> getStartItems() {
+        return mStartItems;
+    }
+
+    @NonNull
+    public List<QCActionItem> getEndItems() {
+        return mEndItems;
+    }
+
+    public static Creator<QCRow> CREATOR = new Creator<QCRow>() {
+        @Override
+        public QCRow createFromParcel(Parcel source) {
+            return new QCRow(source);
+        }
+
+        @Override
+        public QCRow[] newArray(int size) {
+            return new QCRow[size];
+        }
+    };
+
+    /**
+     * Builder for {@link QCRow}.
+     */
+    public static class Builder {
+        private final List<QCActionItem> mStartItems = new ArrayList<>();
+        private final List<QCActionItem> mEndItems = new ArrayList<>();
+        private Icon mStartIcon;
+        private boolean mIsStartIconTintable = true;
+        private String mTitle;
+        private String mSubtitle;
+        private boolean mIsEnabled = true;
+        private boolean mIsClickableWhileDisabled = false;
+        private QCSlider mSlider;
+        private PendingIntent mPrimaryAction;
+        private PendingIntent mDisabledClickAction;
+
+        /**
+         * Sets the row title.
+         */
+        public Builder setTitle(@Nullable String title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the row subtitle.
+         */
+        public Builder setSubtitle(@Nullable String subtitle) {
+            mSubtitle = subtitle;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the row is enabled. Note that this only affects the main row area,
+         * not the action items contained within the row.
+         */
+        public Builder setEnabled(boolean enabled) {
+            mIsEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the row should be clickable while disabled.
+         */
+        public Builder setClickableWhileDisabled(boolean clickable) {
+            mIsClickableWhileDisabled = clickable;
+            return this;
+        }
+
+        /**
+         * Sets the row icon.
+         */
+        public Builder setIcon(@Nullable Icon icon) {
+            mStartIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the row icon is tintable.
+         */
+        public Builder setIconTintable(boolean tintable) {
+            mIsStartIconTintable = tintable;
+            return this;
+        }
+
+        /**
+         * Adds a {@link QCSlider} to the slider area.
+         */
+        public Builder addSlider(@Nullable QCSlider slider) {
+            mSlider = slider;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the row is clicked.
+         */
+        public Builder setPrimaryAction(@Nullable PendingIntent action) {
+            mPrimaryAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the action item is clicked while disabled.
+         */
+        public Builder setDisabledClickAction(@Nullable PendingIntent action) {
+            mDisabledClickAction = action;
+            return this;
+        }
+
+        /**
+         * Adds a {@link QCActionItem} to the start items area.
+         */
+        public Builder addStartItem(@NonNull QCActionItem item) {
+            mStartItems.add(item);
+            return this;
+        }
+
+        /**
+         * Adds a {@link QCActionItem} to the end items area.
+         */
+        public Builder addEndItem(@NonNull QCActionItem item) {
+            mEndItems.add(item);
+            return this;
+        }
+
+        /**
+         * Builds the final {@link QCRow}.
+         */
+        public QCRow build() {
+            return new QCRow(mTitle, mSubtitle, mIsEnabled, mIsClickableWhileDisabled,
+                    mPrimaryAction, mDisabledClickAction, mStartIcon, mIsStartIconTintable,
+                    mSlider, mStartItems, mEndItems);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/QCSlider.java b/car-qc-lib/src/com/android/car/qc/QCSlider.java
new file mode 100644
index 0000000..612274b
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/QCSlider.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import android.app.PendingIntent;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Quick Control Slider included in {@link QCRow}
+ */
+public class QCSlider extends QCItem {
+    private int mMin = 0;
+    private int mMax = 100;
+    private int mValue = 0;
+    private PendingIntent mInputAction;
+    private PendingIntent mDisabledClickAction;
+
+    public QCSlider(int min, int max, int value, boolean enabled, boolean clickableWhileDisabled,
+            @Nullable PendingIntent inputAction, @Nullable PendingIntent disabledClickAction) {
+        super(QC_TYPE_SLIDER, enabled, clickableWhileDisabled);
+        mMin = min;
+        mMax = max;
+        mValue = value;
+        mInputAction = inputAction;
+        mDisabledClickAction = disabledClickAction;
+    }
+
+    public QCSlider(@NonNull Parcel in) {
+        super(in);
+        mMin = in.readInt();
+        mMax = in.readInt();
+        mValue = in.readInt();
+        boolean hasAction = in.readBoolean();
+        if (hasAction) {
+            mInputAction = PendingIntent.CREATOR.createFromParcel(in);
+        }
+        boolean hasDisabledClickAction = in.readBoolean();
+        if (hasDisabledClickAction) {
+            mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mMin);
+        dest.writeInt(mMax);
+        dest.writeInt(mValue);
+        boolean hasAction = mInputAction != null;
+        dest.writeBoolean(hasAction);
+        if (hasAction) {
+            mInputAction.writeToParcel(dest, flags);
+        }
+        boolean hasDisabledClickAction = mDisabledClickAction != null;
+        dest.writeBoolean(hasDisabledClickAction);
+        if (hasDisabledClickAction) {
+            mDisabledClickAction.writeToParcel(dest, flags);
+        }
+    }
+
+    @Override
+    public PendingIntent getPrimaryAction() {
+        return mInputAction;
+    }
+
+    @Override
+    public PendingIntent getDisabledClickAction() {
+        return mDisabledClickAction;
+    }
+
+    public int getMin() {
+        return mMin;
+    }
+
+    public int getMax() {
+        return mMax;
+    }
+
+    public int getValue() {
+        return mValue;
+    }
+
+    public static Creator<QCSlider> CREATOR = new Creator<QCSlider>() {
+        @Override
+        public QCSlider createFromParcel(Parcel source) {
+            return new QCSlider(source);
+        }
+
+        @Override
+        public QCSlider[] newArray(int size) {
+            return new QCSlider[size];
+        }
+    };
+
+    /**
+     * Builder for {@link QCSlider}.
+     */
+    public static class Builder {
+        private int mMin = 0;
+        private int mMax = 100;
+        private int mValue = 0;
+        private boolean mIsEnabled = true;
+        private boolean mIsClickableWhileDisabled = false;
+        private PendingIntent mInputAction;
+        private PendingIntent mDisabledClickAction;
+
+        /**
+         * Set the minimum allowed value for the slider input.
+         */
+        public Builder setMin(int min) {
+            mMin = min;
+            return this;
+        }
+
+        /**
+         * Set the maximum allowed value for the slider input.
+         */
+        public Builder setMax(int max) {
+            mMax = max;
+            return this;
+        }
+
+        /**
+         * Set the current value for the slider input.
+         */
+        public Builder setValue(int value) {
+            mValue = value;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the slider is enabled.
+         */
+        public Builder setEnabled(boolean enabled) {
+            mIsEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether or not a slider should be clickable while disabled.
+         */
+        public Builder setClickableWhileDisabled(boolean clickable) {
+            mIsClickableWhileDisabled = clickable;
+            return this;
+        }
+
+
+        /**
+         * Set the PendingIntent to be sent when the slider value is changed.
+         */
+        public Builder setInputAction(@Nullable PendingIntent inputAction) {
+            mInputAction = inputAction;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the action item is clicked while disabled.
+         */
+        public Builder setDisabledClickAction(@Nullable PendingIntent action) {
+            mDisabledClickAction = action;
+            return this;
+        }
+
+        /**
+         * Builds the final {@link QCSlider}.
+         */
+        public QCSlider build() {
+            return new QCSlider(mMin, mMax, mValue, mIsEnabled, mIsClickableWhileDisabled,
+                    mInputAction, mDisabledClickAction);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/QCTile.java b/car-qc-lib/src/com/android/car/qc/QCTile.java
new file mode 100644
index 0000000..9ae22e9
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/QCTile.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Quick Control Tile Element
+ * ------------
+ * | -------- |
+ * | | Icon | |
+ * | -------- |
+ * | Subtitle |
+ * ------------
+ */
+public class QCTile extends QCItem {
+    private final boolean mIsChecked;
+    private final boolean mIsAvailable;
+    private final String mSubtitle;
+    private Icon mIcon;
+    private PendingIntent mAction;
+    private PendingIntent mDisabledClickAction;
+
+    public QCTile(boolean isChecked, boolean isEnabled, boolean isAvailable,
+            boolean isClickableWhileDisabled, @Nullable String subtitle, @Nullable Icon icon,
+            @Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) {
+        super(QC_TYPE_TILE, isEnabled, isClickableWhileDisabled);
+        mIsChecked = isChecked;
+        mIsAvailable = isAvailable;
+        mSubtitle = subtitle;
+        mIcon = icon;
+        mAction = action;
+        mDisabledClickAction = disabledClickAction;
+    }
+
+    public QCTile(@NonNull Parcel in) {
+        super(in);
+        mIsChecked = in.readBoolean();
+        mIsAvailable = in.readBoolean();
+        mSubtitle = in.readString();
+        boolean hasIcon = in.readBoolean();
+        if (hasIcon) {
+            mIcon = Icon.CREATOR.createFromParcel(in);
+        }
+        boolean hasAction = in.readBoolean();
+        if (hasAction) {
+            mAction = PendingIntent.CREATOR.createFromParcel(in);
+        }
+        boolean hasDisabledClickAction = in.readBoolean();
+        if (hasDisabledClickAction) {
+            mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeBoolean(mIsChecked);
+        dest.writeBoolean(mIsAvailable);
+        dest.writeString(mSubtitle);
+        boolean hasIcon = mIcon != null;
+        dest.writeBoolean(hasIcon);
+        if (hasIcon) {
+            mIcon.writeToParcel(dest, flags);
+        }
+        boolean hasAction = mAction != null;
+        dest.writeBoolean(hasAction);
+        if (hasAction) {
+            mAction.writeToParcel(dest, flags);
+        }
+        boolean hasDisabledClickAction = mDisabledClickAction != null;
+        dest.writeBoolean(hasDisabledClickAction);
+        if (hasDisabledClickAction) {
+            mDisabledClickAction.writeToParcel(dest, flags);
+        }
+    }
+
+    @Override
+    public PendingIntent getPrimaryAction() {
+        return mAction;
+    }
+
+    @Override
+    public PendingIntent getDisabledClickAction() {
+        return mDisabledClickAction;
+    }
+
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    public boolean isAvailable() {
+        return mIsAvailable;
+    }
+
+    @Nullable
+    public String getSubtitle() {
+        return mSubtitle;
+    }
+
+    @Nullable
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    public static Creator<QCTile> CREATOR = new Creator<QCTile>() {
+        @Override
+        public QCTile createFromParcel(Parcel source) {
+            return new QCTile(source);
+        }
+
+        @Override
+        public QCTile[] newArray(int size) {
+            return new QCTile[size];
+        }
+    };
+
+    /**
+     * Builder for {@link QCTile}.
+     */
+    public static class Builder {
+        private boolean mIsChecked;
+        private boolean mIsEnabled = true;
+        private boolean mIsAvailable = true;
+        private boolean mIsClickableWhileDisabled = false;
+        private String mSubtitle;
+        private Icon mIcon;
+        private PendingIntent mAction;
+        private PendingIntent mDisabledClickAction;
+
+        /**
+         * Sets whether or not the tile should be checked.
+         */
+        public Builder setChecked(boolean checked) {
+            mIsChecked = checked;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the tile should be enabled.
+         */
+        public Builder setEnabled(boolean enabled) {
+            mIsEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the action item is available.
+         */
+        public Builder setAvailable(boolean available) {
+            mIsAvailable = available;
+            return this;
+        }
+
+        /**
+         * Sets whether or not a tile should be clickable while disabled.
+         */
+        public Builder setClickableWhileDisabled(boolean clickable) {
+            mIsClickableWhileDisabled = clickable;
+            return this;
+        }
+
+        /**
+         * Sets the tile's subtitle.
+         */
+        public Builder setSubtitle(@Nullable String subtitle) {
+            mSubtitle = subtitle;
+            return this;
+        }
+
+        /**
+         * Sets the tile's icon.
+         */
+        public Builder setIcon(@Nullable Icon icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the tile is clicked.
+         */
+        public Builder setAction(@Nullable PendingIntent action) {
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the PendingIntent to be sent when the action item is clicked while disabled.
+         */
+        public Builder setDisabledClickAction(@Nullable PendingIntent action) {
+            mDisabledClickAction = action;
+            return this;
+        }
+
+        /**
+         * Builds the final {@link QCTile}.
+         */
+        public QCTile build() {
+            return new QCTile(mIsChecked, mIsEnabled, mIsAvailable, mIsClickableWhileDisabled,
+                    mSubtitle, mIcon, mAction, mDisabledClickAction);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/controller/BaseQCController.java b/car-qc-lib/src/com/android/car/qc/controller/BaseQCController.java
new file mode 100644
index 0000000..ce2bea3
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/BaseQCController.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.lifecycle.Observer;
+
+import com.android.car.qc.QCItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base controller class for Quick Controls.
+ */
+public abstract class BaseQCController implements QCItemCallback {
+    protected final Context mContext;
+    protected final List<Observer<QCItem>> mObservers = new ArrayList<>();
+    protected boolean mShouldListen = false;
+    protected boolean mWasListening = false;
+    protected QCItem mQCItem;
+
+    public BaseQCController(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Update whether or not the controller should be listening to updates from the provider.
+     */
+    public void listen(boolean shouldListen) {
+        mShouldListen = shouldListen;
+        updateListening();
+    }
+
+    /**
+     * Add a QCItem observer to the controller.
+     */
+    @UiThread
+    public void addObserver(Observer<QCItem> observer) {
+        mObservers.add(observer);
+        updateListening();
+    }
+
+    /**
+     * Remove a QCItem observer from the controller.
+     */
+    @UiThread
+    public void removeObserver(Observer<QCItem> observer) {
+        mObservers.remove(observer);
+        updateListening();
+    }
+
+    @UiThread
+    @Override
+    public void onQCItemUpdated(@Nullable QCItem item) {
+        mQCItem = item;
+        mObservers.forEach(o -> o.onChanged(mQCItem));
+    }
+
+    /**
+     * Destroy the controller. This should be called when the controller is no longer needed so
+     * the listeners can be cleaned up.
+     */
+    public void destroy() {
+        mShouldListen = false;
+        mObservers.clear();
+        updateListening();
+    }
+
+    /**
+     * Subclasses must override this method to handle a listening update.
+     */
+    protected abstract void updateListening();
+}
diff --git a/car-qc-lib/src/com/android/car/qc/controller/LocalQCController.java b/car-qc-lib/src/com/android/car/qc/controller/LocalQCController.java
new file mode 100644
index 0000000..01bd6f8
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/LocalQCController.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import android.content.Context;
+
+import com.android.car.qc.provider.BaseLocalQCProvider;
+
+/**
+ * Controller for binding to local quick control providers.
+ */
+public class LocalQCController extends BaseQCController {
+
+    private final BaseLocalQCProvider mProvider;
+
+    private final BaseLocalQCProvider.Notifier mProviderNotifier =
+            new BaseLocalQCProvider.Notifier() {
+                @Override
+                public void notifyUpdate() {
+                    if (mShouldListen && !mObservers.isEmpty()) {
+                        onQCItemUpdated(mProvider.getQCItem());
+                    }
+                }
+            };
+
+    public LocalQCController(Context context, BaseLocalQCProvider provider) {
+        super(context);
+        mProvider = provider;
+        mProvider.setNotifier(mProviderNotifier);
+        mQCItem = mProvider.getQCItem();
+    }
+
+    @Override
+    protected void updateListening() {
+        boolean listen = mShouldListen && !mObservers.isEmpty();
+        if (mWasListening != listen) {
+            mWasListening = listen;
+            mProvider.shouldListen(listen);
+            if (listen) {
+                mQCItem = mProvider.getQCItem();
+                onQCItemUpdated(mQCItem);
+            }
+        }
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        mProvider.onDestroy();
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/controller/QCItemCallback.java b/car-qc-lib/src/com/android/car/qc/controller/QCItemCallback.java
new file mode 100644
index 0000000..b5efdef
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/QCItemCallback.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.qc.QCItem;
+
+/**
+ * Callback to be executed when a QCItem changes.
+ */
+public interface QCItemCallback {
+    /**
+     * Called when QCItem is updated.
+     *
+     * @param item The updated QCItem.
+     */
+    void onQCItemUpdated(@Nullable QCItem item);
+}
diff --git a/car-qc-lib/src/com/android/car/qc/controller/RemoteQCController.java b/car-qc-lib/src/com/android/car/qc/controller/RemoteQCController.java
new file mode 100644
index 0000000..c98ca86
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/RemoteQCController.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import static com.android.car.qc.provider.BaseQCProvider.EXTRA_ITEM;
+import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_BIND;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_DESTROY;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_SUBSCRIBE;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_UNSUBSCRIBE;
+
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.car.qc.QCItem;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controller for binding to remote quick control providers.
+ */
+public class RemoteQCController extends BaseQCController {
+    private static final String TAG = "RemoteQCController";
+    private static final long PROVIDER_ANR_TIMEOUT = 3000L;
+
+    private final Uri mUri;
+    private final Executor mBackgroundExecutor;
+    private final HandlerThread mBackgroundHandlerThread;
+    private final ArrayMap<Pair<Uri, QCItemCallback>, QCObserver> mObserverLookup =
+            new ArrayMap<>();
+
+    public RemoteQCController(Context context, Uri uri) {
+        super(context);
+        mUri = uri;
+        mBackgroundHandlerThread = new HandlerThread(/* name= */ TAG + "HandlerThread");
+        mBackgroundHandlerThread.start();
+        mBackgroundExecutor = new HandlerExecutor(
+                new Handler(mBackgroundHandlerThread.getLooper()));
+    }
+
+    @VisibleForTesting
+    RemoteQCController(Context context, Uri uri, Executor backgroundExecutor) {
+        super(context);
+        mUri = uri;
+        mBackgroundHandlerThread = null;
+        mBackgroundExecutor = backgroundExecutor;
+    }
+
+    @Override
+    protected void updateListening() {
+        boolean listen = mShouldListen && !mObservers.isEmpty();
+        mBackgroundExecutor.execute(() -> updateListeningBg(listen));
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        if (mBackgroundHandlerThread != null) {
+            mBackgroundHandlerThread.quit();
+        }
+        try (ContentProviderClient client = getClient()) {
+            if (client == null) {
+                return;
+            }
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_URI, mUri);
+            try {
+                client.call(METHOD_DESTROY, /* arg= */ null, b);
+            } catch (Exception e) {
+                Log.d(TAG, "Error destroying QCItem", e);
+            }
+        }
+    }
+
+    @WorkerThread
+    private void updateListeningBg(boolean isListening) {
+        if (mWasListening != isListening) {
+            mWasListening = isListening;
+            if (isListening) {
+                registerQCCallback(mContext.getMainExecutor(), /* callback= */ this);
+                // Update one-time on a different thread so that it can display in parallel
+                mBackgroundExecutor.execute(this::updateQCItem);
+            } else {
+                unregisterQCCallback(this);
+            }
+        }
+    }
+
+    @WorkerThread
+    private void updateQCItem() {
+        try {
+            QCItem item = bind();
+            mContext.getMainExecutor().execute(() -> onQCItemUpdated(item));
+        } catch (Exception e) {
+            Log.d(TAG, "Error fetching QCItem", e);
+        }
+    }
+
+    private QCItem bind() {
+        try (ContentProviderClient provider = getClient()) {
+            if (provider == null) {
+                return null;
+            }
+            Bundle extras = new Bundle();
+            extras.putParcelable(EXTRA_URI, mUri);
+            Bundle res = provider.call(METHOD_BIND, /* arg= */ null, extras);
+            if (res == null) {
+                return null;
+            }
+            res.setDefusable(true);
+            res.setClassLoader(QCItem.class.getClassLoader());
+            Parcelable parcelable = res.getParcelable(EXTRA_ITEM);
+            if (parcelable instanceof QCItem) {
+                return (QCItem) parcelable;
+            }
+            return null;
+        } catch (RemoteException e) {
+            Log.d(TAG, "Error binding QCItem", e);
+            return null;
+        }
+    }
+
+    private void subscribe() {
+        try (ContentProviderClient client = getClient()) {
+            if (client == null) {
+                return;
+            }
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_URI, mUri);
+            try {
+                client.call(METHOD_SUBSCRIBE, /* arg= */ null, b);
+            } catch (Exception e) {
+                Log.d(TAG, "Error subscribing to QCItem", e);
+            }
+        }
+    }
+
+    private void unsubscribe()  {
+        try (ContentProviderClient client = getClient()) {
+            if (client == null) {
+                return;
+            }
+            Bundle b = new Bundle();
+            b.putParcelable(EXTRA_URI, mUri);
+            try {
+                client.call(METHOD_UNSUBSCRIBE, /* arg= */ null, b);
+            } catch (Exception e) {
+                Log.d(TAG, "Error unsubscribing from QCItem", e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    ContentProviderClient getClient() {
+        ContentProviderClient client = mContext.getContentResolver()
+                .acquireContentProviderClient(mUri);
+        if (client == null) {
+            return null;
+        }
+        client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
+        return client;
+    }
+
+    private void registerQCCallback(@NonNull Executor executor, @NonNull QCItemCallback callback) {
+        getObserver(callback, new QCObserver(mUri, executor, callback)).startObserving();
+    }
+
+    private void unregisterQCCallback(@NonNull QCItemCallback callback) {
+        synchronized (mObserverLookup) {
+            QCObserver observer = mObserverLookup.remove(new Pair<>(mUri, callback));
+            if (observer != null) {
+                observer.stopObserving();
+            }
+        }
+    }
+
+    private QCObserver getObserver(QCItemCallback callback, QCObserver observer) {
+        Pair<Uri, QCItemCallback> key = new Pair<>(mUri, callback);
+        synchronized (mObserverLookup) {
+            QCObserver oldObserver = mObserverLookup.put(key, observer);
+            if (oldObserver != null) {
+                oldObserver.stopObserving();
+            }
+        }
+        return observer;
+    }
+
+    private class QCObserver {
+        private final Uri mUri;
+        private final Executor mExecutor;
+        private final QCItemCallback mCallback;
+        private boolean mIsSubscribed;
+
+        private final Runnable mUpdateItem = new Runnable() {
+            @Override
+            public void run() {
+                trySubscribe();
+                QCItem item = bind();
+                mExecutor.execute(() -> mCallback.onQCItemUpdated(item));
+            }
+        };
+
+        private final ContentObserver mObserver = new ContentObserver(
+                new Handler(Looper.getMainLooper())) {
+            @Override
+            public void onChange(boolean selfChange) {
+                android.os.AsyncTask.execute(mUpdateItem);
+            }
+        };
+
+        QCObserver(Uri uri, Executor executor, QCItemCallback callback) {
+            mUri = uri;
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        void startObserving() {
+            ContentProviderClient provider =
+                    mContext.getContentResolver().acquireContentProviderClient(mUri);
+            if (provider != null) {
+                provider.close();
+                mContext.getContentResolver().registerContentObserver(
+                        mUri, /* notifyForDescendants= */ true, mObserver);
+                trySubscribe();
+            }
+        }
+
+        void trySubscribe() {
+            if (!mIsSubscribed) {
+                subscribe();
+                mIsSubscribed = true;
+            }
+        }
+
+        void stopObserving() {
+            mContext.getContentResolver().unregisterContentObserver(mObserver);
+            if (mIsSubscribed) {
+                unsubscribe();
+                mIsSubscribed = false;
+            }
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/provider/BaseLocalQCProvider.java b/car-qc-lib/src/com/android/car/qc/provider/BaseLocalQCProvider.java
new file mode 100644
index 0000000..764d2a3
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/provider/BaseLocalQCProvider.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.provider;
+
+import android.content.Context;
+
+import com.android.car.qc.QCItem;
+
+/**
+ * Base class for local Quick Control providers.
+ */
+public abstract class BaseLocalQCProvider {
+
+    /**
+     * Callback to be executed when the QCItem updates.
+     */
+    public interface Notifier {
+        /**
+         * Called when the QCItem has been updated.
+         */
+        default void notifyUpdate() {
+        }
+    }
+
+    private Notifier mNotifier;
+    private boolean mIsListening;
+    protected final Context mContext;
+
+    public BaseLocalQCProvider(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Set the notifier that should be called when the QCItem updates.
+     */
+    public void setNotifier(Notifier notifier) {
+        mNotifier = notifier;
+    }
+
+    /**
+     * Update whether or not the provider should be listening for live updates.
+     */
+    public void shouldListen(boolean listen) {
+        if (mIsListening == listen) {
+            return;
+        }
+        mIsListening = listen;
+        if (listen) {
+            onSubscribed();
+        } else {
+            onUnsubscribed();
+        }
+    }
+
+    /**
+     * Method to create and return a {@link QCItem}.
+     */
+    public abstract QCItem getQCItem();
+
+    /**
+     * Called to inform the provider that it has been subscribed to.
+     */
+    protected void onSubscribed() {
+    }
+
+    /**
+     * Called to inform the provider that it has been unsubscribed from.
+     */
+    protected void onUnsubscribed() {
+    }
+
+    /**
+     * Called to inform the provider that it is being destroyed.
+     */
+    public void onDestroy() {
+    }
+
+    protected void notifyChange() {
+        if (mNotifier != null) {
+            mNotifier.notifyUpdate();
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java b/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
new file mode 100644
index 0000000..61db361
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.StrictMode;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.qc.QCItem;
+
+import java.util.Set;
+
+/**
+ * Base Quick Controls provider implementation.
+ */
+public abstract class BaseQCProvider extends ContentProvider {
+    public static final String METHOD_BIND = "QC_METHOD_BIND";
+    public static final String METHOD_SUBSCRIBE = "QC_METHOD_SUBSCRIBE";
+    public static final String METHOD_UNSUBSCRIBE = "QC_METHOD_UNSUBSCRIBE";
+    public static final String METHOD_DESTROY = "QC_METHOD_DESTROY";
+    public static final String EXTRA_URI = "QC_EXTRA_URI";
+    public static final String EXTRA_ITEM = "QC_EXTRA_ITEM";
+
+    private static final String TAG = "BaseQCProvider";
+    private static final long QC_ANR_TIMEOUT = 3000L;
+    private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());
+    private String mCallbackMethod;
+    private final Runnable mAnr = () -> {
+        Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+        Log.e(TAG, "Timed out while handling QC method " + mCallbackMethod);
+    };
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        enforceCallingPermissions();
+
+        Uri uri = getUriWithoutUserId(validateIncomingUriOrNull(
+                extras.getParcelable(EXTRA_URI)));
+        switch(method) {
+            case METHOD_BIND:
+                QCItem item = handleBind(uri);
+                Bundle b = new Bundle();
+                b.putParcelable(EXTRA_ITEM, item);
+                return b;
+            case METHOD_SUBSCRIBE:
+                handleSubscribe(uri);
+                break;
+            case METHOD_UNSUBSCRIBE:
+                handleUnsubscribe(uri);
+                break;
+            case METHOD_DESTROY:
+                handleDestroy(uri);
+                break;
+        }
+        return super.call(method, arg, extras);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    /**
+     * Method to create and return a {@link QCItem}.
+     *
+     * onBind is expected to return as quickly as possible. Therefore, no network or other IO
+     * will be allowed. Any loading that needs to be done should happen in the background and
+     * should then notify the content resolver of the change when ready to provide the
+     * complete data in onBind.
+     */
+    @Nullable
+    protected QCItem onBind(@NonNull Uri uri) {
+        return null;
+    }
+
+    /**
+     * Called to inform an app that an item has been subscribed to.
+     *
+     * Subscribing is a way that a host can notify apps of which QCItems they would like to
+     * receive updates for. The providing apps are expected to keep the content up to date
+     * and notify of change via the content resolver.
+     */
+    protected void onSubscribed(@NonNull Uri uri) {
+    }
+
+    /**
+     * Called to inform an app that an item has been unsubscribed from.
+     *
+     * This is used to notify providing apps that a host is no longer listening
+     * to updates, so any background processes and/or listeners should be removed.
+     */
+    protected void onUnsubscribed(@NonNull Uri uri) {
+    }
+
+    /**
+     * Called to inform an app that an item is being destroyed.
+     *
+     * This is used to notify providing apps that a host is no longer going to use this QCItem
+     * instance, so the relevant elements should be cleaned up.
+     */
+    protected void onDestroy(@NonNull Uri uri) {
+    }
+
+    /**
+     * Returns a Set of packages that are allowed to call this provider.
+     */
+    @NonNull
+    protected abstract Set<String> getAllowlistedPackages();
+
+    private QCItem handleBind(Uri uri) {
+        mCallbackMethod = "handleBind";
+        MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
+        try {
+            return onBindStrict(uri);
+        } finally {
+            MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
+        }
+    }
+
+    private QCItem onBindStrict(@NonNull Uri uri) {
+        StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+        try {
+            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+                    .detectAll()
+                    .penaltyDeath()
+                    .build());
+            return onBind(uri);
+        } finally {
+            StrictMode.setThreadPolicy(oldPolicy);
+        }
+    }
+
+    private void handleSubscribe(@NonNull Uri uri) {
+        mCallbackMethod = "handleSubscribe";
+        MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
+        try {
+            onSubscribed(uri);
+        } finally {
+            MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
+        }
+    }
+
+    private void handleUnsubscribe(@NonNull Uri uri) {
+        mCallbackMethod = "handleUnsubscribe";
+        MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
+        try {
+            onUnsubscribed(uri);
+        } finally {
+            MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
+        }
+    }
+
+    private void handleDestroy(@NonNull Uri uri) {
+        mCallbackMethod = "handleDestroy";
+        MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
+        try {
+            onDestroy(uri);
+        } finally {
+            MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
+        }
+    }
+
+    private Uri validateIncomingUriOrNull(Uri uri) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri cannot be null");
+        }
+        return validateIncomingUri(uri);
+    }
+
+    private void enforceCallingPermissions() {
+        String callingPackage = getCallingPackage();
+        if (callingPackage == null) {
+            throw new IllegalArgumentException("Calling package cannot be null");
+        }
+        if (!getAllowlistedPackages().contains(callingPackage)) {
+            throw new SecurityException(
+                    String.format("%s is not permitted to access provider: %s", callingPackage,
+                            getClass().getName()));
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCListView.java b/car-qc-lib/src/com/android/car/qc/view/QCListView.java
new file mode 100644
index 0000000..9aba976
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCListView.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.android.car.qc.view.QCView.QCActionListener;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import androidx.lifecycle.Observer;
+
+import com.android.car.qc.QCItem;
+import com.android.car.qc.QCList;
+
+/**
+ * Quick Controls view for {@link QCList} instances.
+ */
+public class QCListView extends LinearLayout implements Observer<QCItem> {
+
+    private QCActionListener mActionListener;
+
+    public QCListView(Context context) {
+        super(context);
+        init();
+    }
+
+    public QCListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public QCListView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    public QCListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init();
+    }
+
+    private void init() {
+        setOrientation(VERTICAL);
+    }
+
+    /**
+     * Set the view's {@link QCActionListener}. This listener will propagate to all QCRows.
+     */
+    public void setActionListener(QCActionListener listener) {
+        mActionListener = listener;
+        for (int i = 0; i < getChildCount(); i++) {
+            QCRowView view = (QCRowView) getChildAt(i);
+            view.setActionListener(mActionListener);
+        }
+    }
+
+    @Override
+    public void onChanged(QCItem qcItem) {
+        if (qcItem == null) {
+            removeAllViews();
+            return;
+        }
+        if (!qcItem.getType().equals(QCItem.QC_TYPE_LIST)) {
+            throw new IllegalArgumentException("Expected QCList type for QCListView but got "
+                    + qcItem.getType());
+        }
+        QCList qcList = (QCList) qcItem;
+        int rowCount = qcList.getRows().size();
+        for (int i = 0; i < rowCount; i++) {
+            if (getChildAt(i) != null) {
+                QCRowView view = (QCRowView) getChildAt(i);
+                view.setRow(qcList.getRows().get(i));
+                view.setActionListener(mActionListener);
+            } else {
+                QCRowView view = new QCRowView(getContext());
+                view.setRow(qcList.getRows().get(i));
+                view.setActionListener(mActionListener);
+                addView(view);
+            }
+        }
+        if (getChildCount() > rowCount) {
+            // remove extra rows
+            removeViews(rowCount, getChildCount() - rowCount);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
new file mode 100644
index 0000000..1e10e4b
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.android.car.qc.QCItem.QC_ACTION_SLIDER_VALUE;
+import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
+import static com.android.car.qc.view.QCView.QCActionListener;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.qc.QCActionItem;
+import com.android.car.qc.QCItem;
+import com.android.car.qc.QCRow;
+import com.android.car.qc.QCSlider;
+import com.android.car.qc.R;
+import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.DirectManipulationHelper;
+import com.android.car.ui.uxr.DrawableStateToggleButton;
+
+/**
+ * Quick Controls view for {@link QCRow} instances.
+ */
+public class QCRowView extends FrameLayout {
+    private static final String TAG = "QCRowView";
+
+    private LayoutInflater mLayoutInflater;
+    private BidiFormatter mBidiFormatter;
+    private View mContentView;
+    private TextView mTitle;
+    private TextView mSubtitle;
+    private ImageView mStartIcon;
+    @ColorInt
+    private int mStartIconTint;
+    private LinearLayout mStartItemsContainer;
+    private LinearLayout mEndItemsContainer;
+    private LinearLayout mSeekBarContainer;
+    @Nullable
+    private QCSlider mQCSlider;
+    private QCSeekBarView mSeekBar;
+    private QCActionListener mActionListener;
+    private boolean mInDirectManipulationMode;
+
+    private QCSeekbarChangeListener mSeekbarChangeListener;
+    private final View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
+        @Override
+        public boolean onKey(View v, int keyCode, KeyEvent event) {
+            if (mSeekBar == null || (!mSeekBar.isEnabled()
+                    && !mSeekBar.isClickableWhileDisabled())) {
+                return false;
+            }
+            // Consume nudge events in direct manipulation mode.
+            if (mInDirectManipulationMode
+                    && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
+                    || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+                    || keyCode == KeyEvent.KEYCODE_DPAD_UP
+                    || keyCode == KeyEvent.KEYCODE_DPAD_DOWN)) {
+                return true;
+            }
+
+            // Handle events to enter or exit direct manipulation mode.
+            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+                if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                    if (mQCSlider != null) {
+                        if (mQCSlider.isEnabled()) {
+                            setInDirectManipulationMode(v, mSeekBar, !mInDirectManipulationMode);
+                        } else {
+                            fireAction(mQCSlider, new Intent());
+                        }
+                    }
+                }
+                return true;
+            }
+            if (keyCode == KeyEvent.KEYCODE_BACK) {
+                if (mInDirectManipulationMode) {
+                    if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                        setInDirectManipulationMode(v, mSeekBar, false);
+                    }
+                    return true;
+                }
+            }
+
+            // Don't propagate confirm keys to the SeekBar to prevent a ripple effect on the thumb.
+            if (KeyEvent.isConfirmKey(keyCode)) {
+                return false;
+            }
+
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                return mSeekBar.onKeyDown(keyCode, event);
+            } else {
+                return mSeekBar.onKeyUp(keyCode, event);
+            }
+        }
+    };
+
+    private final View.OnFocusChangeListener mSeekBarFocusChangeListener =
+            (v, hasFocus) -> {
+                if (!hasFocus && mInDirectManipulationMode && mSeekBar != null) {
+                    setInDirectManipulationMode(v, mSeekBar, false);
+                }
+            };
+
+    private final View.OnGenericMotionListener mSeekBarScrollListener =
+            (v, event) -> {
+                if (!mInDirectManipulationMode || mSeekBar == null) {
+                    return false;
+                }
+                int adjustment = Math.round(event.getAxisValue(MotionEvent.AXIS_SCROLL));
+                if (adjustment == 0) {
+                    return false;
+                }
+                int count = Math.abs(adjustment);
+                int keyCode =
+                        adjustment < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT;
+                KeyEvent downEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
+                        KeyEvent.ACTION_DOWN, keyCode, /* repeat= */ 0);
+                KeyEvent upEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
+                        KeyEvent.ACTION_UP, keyCode, /* repeat= */ 0);
+                for (int i = 0; i < count; i++) {
+                    mSeekBar.onKeyDown(keyCode, downEvent);
+                    mSeekBar.onKeyUp(keyCode, upEvent);
+                }
+                return true;
+            };
+
+    QCRowView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    QCRowView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    QCRowView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    QCRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mLayoutInflater = LayoutInflater.from(context);
+        mBidiFormatter = BidiFormatter.getInstance();
+        mLayoutInflater.inflate(R.layout.qc_row_view, /* root= */ this);
+        mContentView = findViewById(R.id.qc_row_content);
+        mTitle = findViewById(R.id.qc_title);
+        mSubtitle = findViewById(R.id.qc_summary);
+        mStartIcon = findViewById(R.id.qc_icon);
+        mStartItemsContainer = findViewById(R.id.qc_row_start_items);
+        mEndItemsContainer = findViewById(R.id.qc_row_end_items);
+        mSeekBarContainer = findViewById(R.id.qc_seekbar_wrapper);
+        mSeekBar = findViewById(R.id.seekbar);
+    }
+
+    void setActionListener(QCActionListener listener) {
+        mActionListener = listener;
+    }
+
+    void setRow(QCRow row) {
+        if (row == null) {
+            setVisibility(GONE);
+            return;
+        }
+        setVisibility(VISIBLE);
+        CarUiUtils.makeAllViewsEnabled(mContentView, row.isEnabled());
+        if (!row.isEnabled()) {
+            if (row.isClickableWhileDisabled() && (row.getDisabledClickAction() != null
+                    || row.getDisabledClickActionHandler() != null)) {
+                mContentView.setOnClickListener(v -> {
+                    fireAction(row, /* intent= */ null);
+                });
+            }
+        } else if (row.getPrimaryAction() != null || row.getActionHandler() != null) {
+            mContentView.setOnClickListener(v -> {
+                fireAction(row, /* intent= */ null);
+            });
+        }
+        if (!TextUtils.isEmpty(row.getTitle())) {
+            mTitle.setVisibility(VISIBLE);
+            mTitle.setText(
+                    mBidiFormatter.unicodeWrap(row.getTitle(), TextDirectionHeuristics.LOCALE));
+        } else {
+            mTitle.setVisibility(GONE);
+        }
+        if (!TextUtils.isEmpty(row.getSubtitle())) {
+            mSubtitle.setVisibility(VISIBLE);
+            mSubtitle.setText(
+                    mBidiFormatter.unicodeWrap(row.getSubtitle(), TextDirectionHeuristics.LOCALE));
+        } else {
+            mSubtitle.setVisibility(GONE);
+        }
+        if (row.getStartIcon() != null) {
+            mStartIcon.setVisibility(VISIBLE);
+            Drawable drawable = row.getStartIcon().loadDrawable(getContext());
+            if (drawable != null && row.isStartIconTintable()) {
+                if (mStartIconTint == 0) {
+                    mStartIconTint = getContext().getColor(R.color.qc_start_icon_color);
+                }
+                drawable.setTint(mStartIconTint);
+            }
+            mStartIcon.setImageDrawable(drawable);
+        } else {
+            mStartIcon.setImageDrawable(null);
+            mStartIcon.setVisibility(GONE);
+        }
+        QCSlider slider = row.getSlider();
+        if (slider != null) {
+            mSeekBarContainer.setVisibility(View.VISIBLE);
+            initSlider(slider);
+        } else {
+            mSeekBarContainer.setVisibility(View.GONE);
+            mQCSlider = null;
+        }
+
+        int startItemCount = row.getStartItems().size();
+        for (int i = 0; i < startItemCount; i++) {
+            QCActionItem action = row.getStartItems().get(i);
+            initActionItem(mStartItemsContainer, mStartItemsContainer.getChildAt(i), action);
+        }
+        if (mStartItemsContainer.getChildCount() > startItemCount) {
+            // remove extra items
+            mStartItemsContainer.removeViews(startItemCount,
+                    mStartItemsContainer.getChildCount() - startItemCount);
+        }
+        if (startItemCount == 0) {
+            mStartItemsContainer.setVisibility(View.GONE);
+        } else {
+            mStartItemsContainer.setVisibility(View.VISIBLE);
+        }
+
+        int endItemCount = row.getEndItems().size();
+        for (int i = 0; i < endItemCount; i++) {
+            QCActionItem action = row.getEndItems().get(i);
+            initActionItem(mEndItemsContainer, mEndItemsContainer.getChildAt(i), action);
+        }
+        if (mEndItemsContainer.getChildCount() > endItemCount) {
+            // remove extra items
+            mEndItemsContainer.removeViews(endItemCount,
+                    mEndItemsContainer.getChildCount() - endItemCount);
+        }
+        if (endItemCount == 0) {
+            mEndItemsContainer.setVisibility(View.GONE);
+        } else {
+            mEndItemsContainer.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private void initActionItem(@NonNull ViewGroup root, @Nullable View actionView,
+            @NonNull QCActionItem action) {
+        if (action.getType().equals(QC_TYPE_ACTION_SWITCH)) {
+            initSwitchView(action, root, actionView);
+        } else {
+            initToggleView(action, root, actionView);
+        }
+    }
+
+    private void initSwitchView(QCActionItem action, ViewGroup root, View actionView) {
+        Switch switchView = actionView == null ? null : actionView.findViewById(
+                android.R.id.switch_widget);
+        if (switchView == null) {
+            actionView = createActionView(root, actionView, R.layout.qc_action_switch);
+            switchView = actionView.requireViewById(android.R.id.switch_widget);
+        }
+        CarUiUtils.makeAllViewsEnabled(switchView, action.isEnabled());
+
+        boolean shouldEnableView =
+                (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable();
+        switchView.setOnCheckedChangeListener(null);
+        switchView.setEnabled(shouldEnableView);
+        switchView.setChecked(action.isChecked());
+        switchView.setOnTouchListener((v, event) -> {
+            if (!action.isEnabled()) {
+                if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                    fireAction(action, new Intent());
+                }
+                return true;
+            }
+            return false;
+        });
+        switchView.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> {
+                    Intent intent = new Intent();
+                    intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
+                    fireAction(action, intent);
+                });
+    }
+
+    private void initToggleView(QCActionItem action, ViewGroup root, View actionView) {
+        DrawableStateToggleButton tmpToggleButton =
+                actionView == null ? null : actionView.findViewById(R.id.qc_toggle_button);
+        if (tmpToggleButton == null) {
+            actionView = createActionView(root, actionView, R.layout.qc_action_toggle);
+            tmpToggleButton = actionView.requireViewById(R.id.qc_toggle_button);
+        }
+        DrawableStateToggleButton toggleButton = tmpToggleButton; // must be effectively final
+        boolean shouldEnableView =
+                (action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable();
+        toggleButton.setText(null);
+        toggleButton.setTextOn(null);
+        toggleButton.setTextOff(null);
+        toggleButton.setOnCheckedChangeListener(null);
+        Drawable icon = QCViewUtils.getInstance(mContext).getToggleIcon(
+                action.getIcon(), action.isAvailable());
+        toggleButton.setButtonDrawable(icon);
+        toggleButton.setChecked(action.isChecked());
+        toggleButton.setEnabled(shouldEnableView);
+        setToggleButtonDrawableState(toggleButton, action.isEnabled(), action.isAvailable());
+        toggleButton.setOnTouchListener((v, event) -> {
+            if (!action.isEnabled()) {
+                if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                    fireAction(action, new Intent());
+                }
+                return true;
+            }
+            return false;
+        });
+        toggleButton.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> {
+                    Intent intent = new Intent();
+                    intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
+                    fireAction(action, intent);
+                });
+    }
+
+    private void setToggleButtonDrawableState(DrawableStateToggleButton view,
+            boolean enabled, boolean available) {
+        int[] statesToAdd = null;
+        int[] statesToRemove = null;
+        if (enabled) {
+            if (!available) {
+                statesToAdd =
+                        new int[]{android.R.attr.state_enabled, R.attr.state_toggle_unavailable};
+            } else {
+                statesToAdd = new int[]{android.R.attr.state_enabled};
+                statesToRemove = new int[]{R.attr.state_toggle_unavailable};
+            }
+        } else {
+            if (available) {
+                statesToRemove =
+                        new int[]{android.R.attr.state_enabled, R.attr.state_toggle_unavailable};
+            } else {
+                statesToAdd = new int[]{R.attr.state_toggle_unavailable};
+                statesToRemove = new int[]{android.R.attr.state_enabled};
+            }
+        }
+        CarUiUtils.applyDrawableStatesToAllViews(view, statesToAdd, statesToRemove);
+    }
+
+    @NonNull
+    private View createActionView(@NonNull ViewGroup root, @Nullable View actionView,
+            @LayoutRes int resId) {
+        if (actionView != null) {
+            // remove current action view
+            root.removeView(actionView);
+        }
+        actionView = mLayoutInflater.inflate(resId, /* root= */ null);
+        root.addView(actionView);
+        return actionView;
+    }
+
+    private void initSlider(QCSlider slider) {
+        mQCSlider = slider;
+        mSeekBar.setOnSeekBarChangeListener(null);
+        mSeekBar.setMin(slider.getMin());
+        mSeekBar.setMax(slider.getMax());
+        mSeekBar.setProgress(slider.getValue());
+        mSeekBar.setEnabled(slider.isEnabled());
+        mSeekBar.setClickableWhileDisabled(slider.isClickableWhileDisabled());
+        mSeekBar.setDisabledClickListener(seekBar -> fireAction(slider, new Intent()));
+        if (!slider.isEnabled() && mInDirectManipulationMode) {
+            setInDirectManipulationMode(mSeekBarContainer, mSeekBar, false);
+        }
+        if (mSeekbarChangeListener == null) {
+            mSeekbarChangeListener = new QCSeekbarChangeListener();
+        }
+        mSeekbarChangeListener.setSlider(slider);
+        mSeekBar.setOnSeekBarChangeListener(mSeekbarChangeListener);
+        // set up rotary support
+        mSeekBarContainer.setOnKeyListener(mSeekBarKeyListener);
+        mSeekBarContainer.setOnFocusChangeListener(mSeekBarFocusChangeListener);
+        mSeekBarContainer.setOnGenericMotionListener(mSeekBarScrollListener);
+    }
+
+    private void setInDirectManipulationMode(View view, SeekBar seekbar, boolean enable) {
+        mInDirectManipulationMode = enable;
+        DirectManipulationHelper.enableDirectManipulationMode(seekbar, enable);
+        view.setSelected(enable);
+        seekbar.setSelected(enable);
+    }
+
+    private void fireAction(QCItem item, Intent intent) {
+        if (!item.isEnabled()) {
+            if (item.getDisabledClickAction() != null) {
+                try {
+                    item.getDisabledClickAction().send(getContext(), 0, intent);
+                    if (mActionListener != null) {
+                        mActionListener.onQCAction(item, item.getDisabledClickAction());
+                    }
+                } catch (PendingIntent.CanceledException e) {
+                    Log.d(TAG, "Error sending intent", e);
+                }
+            } else if (item.getDisabledClickActionHandler() != null) {
+                item.getDisabledClickActionHandler().onAction(item, getContext(), intent);
+                if (mActionListener != null) {
+                    mActionListener.onQCAction(item, item.getDisabledClickActionHandler());
+                }
+            }
+            return;
+        }
+
+        if (item.getPrimaryAction() != null) {
+            try {
+                item.getPrimaryAction().send(getContext(), 0, intent);
+                if (mActionListener != null) {
+                    mActionListener.onQCAction(item, item.getPrimaryAction());
+                }
+            } catch (PendingIntent.CanceledException e) {
+                Log.d(TAG, "Error sending intent", e);
+            }
+        } else if (item.getActionHandler() != null) {
+            item.getActionHandler().onAction(item, getContext(), intent);
+            if (mActionListener != null) {
+                mActionListener.onQCAction(item, item.getActionHandler());
+            }
+        }
+    }
+
+    private class QCSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener {
+        // Interval of updates (in ms) sent in response to seekbar moving.
+        private static final int SLIDER_UPDATE_INTERVAL = 200;
+
+        private final Handler mSliderUpdateHandler;
+        private QCSlider mSlider;
+        private int mCurrSliderValue;
+        private boolean mSliderUpdaterRunning;
+        private long mLastSentSliderUpdate;
+        private final Runnable mSliderUpdater = () -> {
+            sendSliderValue();
+            mSliderUpdaterRunning = false;
+        };
+
+        QCSeekbarChangeListener() {
+            mSliderUpdateHandler = new Handler();
+        }
+
+        void setSlider(QCSlider slider) {
+            mSlider = slider;
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            mCurrSliderValue = progress;
+            long now = System.currentTimeMillis();
+            if (mLastSentSliderUpdate != 0
+                    && now - mLastSentSliderUpdate > SLIDER_UPDATE_INTERVAL) {
+                mSliderUpdaterRunning = false;
+                mSliderUpdateHandler.removeCallbacks(mSliderUpdater);
+                sendSliderValue();
+            } else if (!mSliderUpdaterRunning) {
+                mSliderUpdaterRunning = true;
+                mSliderUpdateHandler.postDelayed(mSliderUpdater, SLIDER_UPDATE_INTERVAL);
+            }
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            if (mSliderUpdaterRunning) {
+                mSliderUpdaterRunning = false;
+                mSliderUpdateHandler.removeCallbacks(mSliderUpdater);
+            }
+            mCurrSliderValue = seekBar.getProgress();
+            sendSliderValue();
+        }
+
+        private void sendSliderValue() {
+            if (mSlider == null) {
+                return;
+            }
+            mLastSentSliderUpdate = System.currentTimeMillis();
+            Intent intent = new Intent();
+            intent.putExtra(QC_ACTION_SLIDER_VALUE, mCurrSliderValue);
+            fireAction(mSlider, intent);
+        }
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java b/car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
new file mode 100644
index 0000000..b13f784
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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.car.qc.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.SeekBar;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.ui.uxr.DrawableStateSeekBar;
+
+import java.util.function.Consumer;
+
+/**
+ * A {@link SeekBar} specifically for Quick Controls that allows for a disabled click action
+ * to execute on {@link MotionEvent.ACTION_UP}.
+ */
+public class QCSeekBarView extends DrawableStateSeekBar {
+    private boolean mClickableWhileDisabled;
+    private Consumer<SeekBar> mDisabledClickListener;
+
+    public QCSeekBarView(Context context) {
+        super(context);
+    }
+
+    public QCSeekBarView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // AbsSeekBar will ignore all touch events if not enabled. If this SeekBar should be
+        // clickable while disabled, the touch event will be handled here.
+        if (!isEnabled() && mClickableWhileDisabled) {
+            if (event.getAction() == MotionEvent.ACTION_UP && mDisabledClickListener != null) {
+                mDisabledClickListener.accept(this);
+            }
+            return true;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    public void setClickableWhileDisabled(boolean clickable) {
+        mClickableWhileDisabled = clickable;
+    }
+
+    public void setDisabledClickListener(@Nullable Consumer<SeekBar> disabledClickListener) {
+        mDisabledClickListener = disabledClickListener;
+    }
+
+    public boolean isClickableWhileDisabled() {
+        return mClickableWhileDisabled;
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCTileView.java b/car-qc-lib/src/com/android/car/qc/view/QCTileView.java
new file mode 100644
index 0000000..33c0eff
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCTileView.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
+import static com.android.car.qc.view.QCView.QCActionListener;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.lifecycle.Observer;
+
+import com.android.car.qc.QCItem;
+import com.android.car.qc.QCTile;
+import com.android.car.qc.R;
+import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.uxr.DrawableStateToggleButton;
+
+/**
+ * Quick Controls view for {@link QCTile} instances.
+ */
+public class QCTileView extends FrameLayout implements Observer<QCItem> {
+    private static final String TAG = "QCTileView";
+
+    private View mTileWrapper;
+    private DrawableStateToggleButton mToggleButton;
+    private TextView mSubtitle;
+    private QCActionListener mActionListener;
+
+    public QCTileView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public QCTileView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public QCTileView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    public QCTileView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    /**
+     * Set the tile's {@link QCActionListener}.
+     */
+    public void setActionListener(QCActionListener listener) {
+        mActionListener = listener;
+    }
+
+    private void init(Context context) {
+        View.inflate(context, R.layout.qc_tile_view, /* root= */ this);
+        mTileWrapper = findViewById(R.id.qc_tile_wrapper);
+        mToggleButton = findViewById(R.id.qc_tile_toggle_button);
+        mSubtitle = findViewById(android.R.id.summary);
+        mToggleButton.setText(null);
+        mToggleButton.setTextOn(null);
+        mToggleButton.setTextOff(null);
+    }
+
+    @Override
+    public void onChanged(QCItem qcItem) {
+        if (qcItem == null) {
+            removeAllViews();
+            return;
+        }
+        if (!qcItem.getType().equals(QCItem.QC_TYPE_TILE)) {
+            throw new IllegalArgumentException("Expected QCTile type for QCTileView but got "
+                    + qcItem.getType());
+        }
+        QCTile qcTile = (QCTile) qcItem;
+        mSubtitle.setText(qcTile.getSubtitle());
+        CarUiUtils.makeAllViewsEnabled(mToggleButton, qcTile.isEnabled());
+        mToggleButton.setOnCheckedChangeListener(null);
+        mToggleButton.setChecked(qcTile.isChecked());
+        mToggleButton.setEnabled(qcTile.isEnabled() || qcTile.isClickableWhileDisabled());
+        mTileWrapper.setEnabled(
+                (qcTile.isEnabled() || qcTile.isClickableWhileDisabled()) && qcTile.isAvailable());
+        mTileWrapper.setOnClickListener(v -> {
+            if (!qcTile.isEnabled()) {
+                if (qcTile.getDisabledClickAction() != null) {
+                    try {
+                        qcTile.getDisabledClickAction().send(getContext(), 0, new Intent());
+                        if (mActionListener != null) {
+                            mActionListener.onQCAction(qcTile, qcTile.getDisabledClickAction());
+                        }
+                    } catch (PendingIntent.CanceledException e) {
+                        Log.d(TAG, "Error sending intent", e);
+                    }
+                } else if (qcTile.getDisabledClickActionHandler() != null) {
+                    qcTile.getDisabledClickActionHandler().onAction(qcTile, getContext(),
+                            new Intent());
+                    if (mActionListener != null) {
+                        mActionListener.onQCAction(qcTile, qcTile.getDisabledClickActionHandler());
+                    }
+                }
+                return;
+            }
+            mToggleButton.toggle();
+        });
+        Drawable icon = QCViewUtils.getInstance(mContext).getToggleIcon(
+                qcTile.getIcon(), qcTile.isAvailable());
+        mToggleButton.setButtonDrawable(icon);
+        mToggleButton.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> {
+                    Intent intent = new Intent();
+                    intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
+                    if (qcTile.getPrimaryAction() != null) {
+                        try {
+                            qcTile.getPrimaryAction().send(getContext(), 0, intent);
+                            if (mActionListener != null) {
+                                mActionListener.onQCAction(qcTile, qcTile.getPrimaryAction());
+                            }
+                        } catch (PendingIntent.CanceledException e) {
+                            Log.d(TAG, "Error sending intent", e);
+                        }
+                    } else if (qcTile.getActionHandler() != null) {
+                        qcTile.getActionHandler().onAction(qcTile, getContext(), intent);
+                        if (mActionListener != null) {
+                            mActionListener.onQCAction(qcTile, qcTile.getActionHandler());
+                        }
+                    }
+                });
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCView.java b/car-qc-lib/src/com/android/car/qc/view/QCView.java
new file mode 100644
index 0000000..a757354
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCView.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Observer;
+
+import com.android.car.qc.QCItem;
+
+/**
+ * Base Quick Controls View - supports {@link QCItem.QC_TYPE_TILE} and {@link QCItem.QC_TYPE_LIST}
+ */
+public class QCView extends FrameLayout implements Observer<QCItem> {
+    @QCItem.QCItemType
+    private String mType;
+    private Observer<QCItem> mChildObserver;
+    private QCActionListener mActionListener;
+
+    public QCView(Context context) {
+        super(context);
+    }
+
+    public QCView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public QCView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public QCView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /**
+     * Set the view's {@link QCActionListener}. This listener will propagate to all sub-views.
+     */
+    public void setActionListener(QCActionListener listener) {
+        mActionListener = listener;
+        if (mChildObserver instanceof QCTileView) {
+            ((QCTileView) mChildObserver).setActionListener(mActionListener);
+        } else if (mChildObserver instanceof QCListView) {
+            ((QCListView) mChildObserver).setActionListener(mActionListener);
+        }
+    }
+
+    @Override
+    public void onChanged(QCItem qcItem) {
+        if (qcItem == null) {
+            removeAllViews();
+            mChildObserver = null;
+            mType = null;
+            return;
+        }
+        if (!isValidQCItemType(qcItem)) {
+            throw new IllegalArgumentException("Expected QCTile or QCList type but got "
+                    + qcItem.getType());
+        }
+        if (qcItem.getType().equals(mType)) {
+            mChildObserver.onChanged(qcItem);
+            return;
+        }
+        removeAllViews();
+        mType = qcItem.getType();
+        if (mType.equals(QCItem.QC_TYPE_TILE)) {
+            QCTileView view = new QCTileView(getContext());
+            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                    LayoutParams.WRAP_CONTENT,
+                    LayoutParams.WRAP_CONTENT,
+                    Gravity.CENTER_HORIZONTAL);
+            view.onChanged(qcItem);
+            view.setActionListener(mActionListener);
+            addView(view, params);
+            mChildObserver = view;
+        } else {
+            QCListView view = new QCListView(getContext());
+            view.onChanged(qcItem);
+            view.setActionListener(mActionListener);
+            addView(view);
+            mChildObserver = view;
+        }
+    }
+
+    private boolean isValidQCItemType(QCItem qcItem) {
+        String type = qcItem.getType();
+        return type.equals(QCItem.QC_TYPE_TILE) || type.equals(QCItem.QC_TYPE_LIST);
+    }
+
+    /**
+     * Listener to be called when an action occurs on a QCView.
+     */
+    public interface QCActionListener {
+        /**
+         * Called when an interaction has occurred with an element in this view.
+         * @param item the specific item within the {@link QCItem} that was interacted with.
+         * @param action the action that was executed - is generally either a
+         *               {@link android.app.PendingIntent} or {@link QCItem.ActionHandler}
+         */
+        void onQCAction(@NonNull QCItem item, @NonNull Object action);
+    }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java b/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java
new file mode 100644
index 0000000..366c724
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.LayerDrawable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.qc.R;
+
+/**
+ * Utility class used by {@link QCTileView} and {@link QCRowView}
+ */
+public class QCViewUtils {
+    private static QCViewUtils sInstance;
+
+    private final Context mContext;
+    private final Drawable mDefaultToggleBackground;
+    private final Drawable mUnavailableToggleBackground;
+    private final ColorStateList mDefaultToggleIconTint;
+    @ColorInt
+    private final int mUnavailableToggleIconTint;
+    private final int mToggleForegroundIconInset;
+
+    private QCViewUtils(@NonNull Context context) {
+        mContext = context.getApplicationContext();
+        mDefaultToggleBackground = mContext.getDrawable(R.drawable.qc_toggle_background);
+        mUnavailableToggleBackground = mContext.getDrawable(
+                R.drawable.qc_toggle_unavailable_background);
+        mDefaultToggleIconTint = mContext.getColorStateList(R.color.qc_toggle_icon_fill_color);
+        mUnavailableToggleIconTint = mContext.getColor(R.color.qc_toggle_unavailable_color);
+        mToggleForegroundIconInset = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.qc_toggle_foreground_icon_inset);
+    }
+
+    /**
+     * Get an instance of {@link QCViewUtils}
+     */
+    public static QCViewUtils getInstance(@NonNull Context context) {
+        if (sInstance == null) {
+            sInstance = new QCViewUtils(context);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Create a return a Quick Control toggle icon - used for tiles and action toggles.
+     */
+    public Drawable getToggleIcon(@Nullable Icon icon, boolean available) {
+        Drawable background = available
+                ? mDefaultToggleBackground.getConstantState().newDrawable().mutate()
+                : mUnavailableToggleBackground.getConstantState().newDrawable().mutate();
+        if (icon == null) {
+            return background;
+        }
+
+        Drawable iconDrawable = icon.loadDrawable(mContext);
+        if (iconDrawable == null) {
+            return background;
+        }
+
+        if (!available) {
+            iconDrawable.setTint(mUnavailableToggleIconTint);
+        } else {
+            iconDrawable.setTintList(mDefaultToggleIconTint);
+        }
+
+        Drawable[] layers = {background, iconDrawable};
+        LayerDrawable drawable = new LayerDrawable(layers);
+        drawable.setLayerInsetRelative(/* index= */ 1, mToggleForegroundIconInset,
+                mToggleForegroundIconInset, mToggleForegroundIconInset,
+                mToggleForegroundIconInset);
+        return drawable;
+    }
+}
diff --git a/car-qc-lib/tests/unit/Android.bp b/car-qc-lib/tests/unit/Android.bp
new file mode 100644
index 0000000..b1f107a
--- /dev/null
+++ b/car-qc-lib/tests/unit/Android.bp
@@ -0,0 +1,47 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "CarQCLibUnitTests",
+
+    certificate: "platform",
+    privileged: true,
+
+    srcs: ["src/**/*.java"],
+
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+
+    static_libs: [
+        "car-qc-lib",
+        "androidx.test.core",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "androidx.test.ext.truth",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "truth-prebuilt",
+        "testng",
+    ],
+
+    jni_libs: ["libdexmakerjvmtiagent", "libstaticjvmtiagent"],
+}
diff --git a/car-qc-lib/tests/unit/AndroidManifest.xml b/car-qc-lib/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..e500c4d
--- /dev/null
+++ b/car-qc-lib/tests/unit/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.car.qc.tests.unit">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+
+        <provider
+            android:name="com.android.car.qc.testutils.AllowedTestQCProvider"
+            android:authorities="com.android.car.qc.testutils.AllowedTestQCProvider"
+            android:exported="true">
+        </provider>
+
+        <provider
+            android:name="com.android.car.qc.testutils.DeniedTestQCProvider"
+            android:authorities="com.android.car.qc.testutils.DeniedTestQCProvider"
+            android:exported="true">
+        </provider>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.car.qc.tests.unit"
+                     android:label="Quick Controls Library Unit Tests"/>
+</manifest>
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java
new file mode 100644
index 0000000..ff70540
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCActionItemTest extends QCItemTestCase<QCActionItem> {
+
+    @Test
+    public void onCreate_invalidType_throwsException() {
+        assertThrows(IllegalArgumentException.class,
+                () -> createAction("INVALID_TYPE", /* action= */ null,
+                        /* disabledAction= */ null, /* icon= */ null));
+    }
+
+    @Test
+    public void onCreateSwitch_hasCorrectType() {
+        QCActionItem action = createAction(QC_TYPE_ACTION_SWITCH, /* action= */ null,
+                /* disabledAction= */ null, /* icon= */null);
+        assertThat(action.getType()).isEqualTo(QC_TYPE_ACTION_SWITCH);
+    }
+
+    @Test
+    public void onCreateToggle_hasCorrectType() {
+        QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, /* action= */ null,
+                /* disabledAction= */ null, /* icon= */ null);
+        assertThat(action.getType()).isEqualTo(QC_TYPE_ACTION_TOGGLE);
+    }
+
+    @Test
+    public void onBundle_nullActions_noCrash() {
+        QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, /* action= */ null,
+                /* disabledAction= */ null, mDefaultIcon);
+        writeAndLoadFromBundle(action);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void onBundle_nullIcon_noCrash() {
+        QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, mDefaultAction,
+                mDefaultDisabledAction, /* icon= */ null);
+        writeAndLoadFromBundle(action);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void onBundle_switch_accurateData() {
+        QCActionItem action = createAction(QC_TYPE_ACTION_SWITCH, mDefaultAction,
+                mDefaultDisabledAction, /* icon= */ null);
+        QCActionItem newAction = writeAndLoadFromBundle(action);
+        assertThat(newAction.getType()).isEqualTo(QC_TYPE_ACTION_SWITCH);
+        assertThat(newAction.isChecked()).isTrue();
+        assertThat(newAction.isEnabled()).isTrue();
+        assertThat(newAction.isClickableWhileDisabled()).isFalse();
+        assertThat(newAction.getPrimaryAction()).isNotNull();
+        assertThat(newAction.getIcon()).isNull();
+    }
+
+    @Test
+    public void onBundle_toggle_accurateDate() {
+        QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, mDefaultAction,
+                mDefaultDisabledAction, mDefaultIcon);
+        QCActionItem newAction = writeAndLoadFromBundle(action);
+        assertThat(newAction.getType()).isEqualTo(QC_TYPE_ACTION_TOGGLE);
+        assertThat(newAction.isChecked()).isTrue();
+        assertThat(newAction.isEnabled()).isTrue();
+        assertThat(newAction.isClickableWhileDisabled()).isFalse();
+        assertThat(newAction.getPrimaryAction()).isNotNull();
+        assertThat(newAction.getIcon()).isNotNull();
+    }
+
+    private QCActionItem createAction(String type, PendingIntent action,
+            PendingIntent disabledAction, Icon icon) {
+        return new QCActionItem.Builder(type)
+                .setChecked(true)
+                .setEnabled(true)
+                .setAction(action)
+                .setDisabledClickAction(disabledAction)
+                .setIcon(icon)
+                .build();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCItemTestCase.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCItemTestCase.java
new file mode 100644
index 0000000..3481a85
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCItemTestCase.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import android.R;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+public abstract class QCItemTestCase<T extends QCItem> {
+    protected static final String BUNDLE_KEY = "BUNDLE_KEY";
+    protected static final String TEST_TITLE = "TEST TITLE";
+    protected static final String TEST_SUBTITLE = "TEST SUBTITLE";
+
+    protected final Context mContext = ApplicationProvider.getApplicationContext();
+
+    protected PendingIntent mDefaultAction = PendingIntent.getActivity(mContext,
+            /* requestCode= */ 0, new Intent(), PendingIntent.FLAG_IMMUTABLE);
+    protected PendingIntent mDefaultDisabledAction = PendingIntent.getActivity(mContext,
+            /* requestCode= */ 1, new Intent(), PendingIntent.FLAG_IMMUTABLE);
+    protected Icon mDefaultIcon = Icon.createWithResource(mContext, R.drawable.btn_star);
+
+    protected T writeAndLoadFromBundle(T item) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(BUNDLE_KEY, item);
+        return bundle.getParcelable(BUNDLE_KEY);
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCListTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCListTest.java
new file mode 100644
index 0000000..766d82c
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCListTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import static com.android.car.qc.QCItem.QC_TYPE_LIST;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class QCListTest extends QCItemTestCase<QCList> {
+
+    @Test
+    public void onCreate_hasCorrectType() {
+        QCList list = createList(Collections.emptyList());
+        assertThat(list.getType()).isEqualTo(QC_TYPE_LIST);
+    }
+
+    @Test
+    public void createFromParcel_accurateData() {
+        QCRow row = new QCRow.Builder()
+                .setTitle(TEST_TITLE)
+                .setSubtitle(TEST_SUBTITLE)
+                .setIcon(mDefaultIcon)
+                .setPrimaryAction(mDefaultAction)
+                .build();
+
+        QCList list = createList(Collections.singletonList(row));
+        QCList newList = writeAndLoadFromBundle(list);
+        assertThat(newList.getType()).isEqualTo(QC_TYPE_LIST);
+        assertThat(newList.getRows().size()).isEqualTo(1);
+    }
+
+    private QCList createList(List<QCRow> rows) {
+        QCList.Builder builder = new QCList.Builder();
+        for (QCRow row : rows) {
+            builder.addRow(row);
+        }
+        return builder.build();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCRowTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCRowTest.java
new file mode 100644
index 0000000..cd1ff7c
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCRowTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
+import static com.android.car.qc.QCItem.QC_TYPE_ROW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class QCRowTest extends QCItemTestCase<QCRow> {
+
+    @Test
+    public void onCreate_hasCorrectType() {
+        QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null);
+        assertThat(row.getType()).isEqualTo(QC_TYPE_ROW);
+    }
+
+    @Test
+    public void onBundle_nullActions_noCrash() {
+        QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, mDefaultIcon);
+        writeAndLoadFromBundle(row);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void onBundle_nullIcon_noCrash() {
+        QCRow row = createRow(mDefaultAction, mDefaultDisabledAction, /* icon= */ null);
+        writeAndLoadFromBundle(row);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void createFromParcel_accurateData() {
+        QCRow row = createRow(mDefaultAction, mDefaultDisabledAction, mDefaultIcon);
+        QCRow newRow = writeAndLoadFromBundle(row);
+        assertThat(newRow.getType()).isEqualTo(QC_TYPE_ROW);
+        assertThat(newRow.getTitle()).isEqualTo(TEST_TITLE);
+        assertThat(newRow.getSubtitle()).isEqualTo(TEST_SUBTITLE);
+        assertThat(newRow.getPrimaryAction()).isNotNull();
+        assertThat(newRow.getStartIcon()).isNotNull();
+    }
+
+    @Test
+    public void createFromParcel_accurateData_startItem() {
+        QCActionItem item = new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build();
+
+        QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null,
+                Collections.singletonList(item), Collections.emptyList(), Collections.emptyList());
+        QCRow newRow = writeAndLoadFromBundle(row);
+        assertThat(newRow.getStartItems().size()).isEqualTo(1);
+    }
+
+    @Test
+    public void createFromParcel_accurateData_endItem() {
+        QCActionItem item = new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build();
+
+        QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null,
+                Collections.emptyList(), Collections.singletonList(item), Collections.emptyList());
+        QCRow newRow = writeAndLoadFromBundle(row);
+        assertThat(newRow.getEndItems().size()).isEqualTo(1);
+    }
+
+    @Test
+    public void createFromParcel_accurateData_slider() {
+        QCSlider slider = new QCSlider.Builder().build();
+
+        QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null,
+                Collections.emptyList(), Collections.emptyList(),
+                Collections.singletonList(slider));
+        QCRow newRow = writeAndLoadFromBundle(row);
+        assertThat(newRow.getSlider()).isNotNull();
+    }
+
+    private QCRow createRow(PendingIntent action, PendingIntent disabledAction, Icon icon) {
+        return createRow(action, disabledAction, icon, Collections.emptyList(),
+                Collections.emptyList(), Collections.emptyList());
+    }
+
+    private QCRow createRow(PendingIntent action, PendingIntent disabledAction, Icon icon,
+            List<QCActionItem> startItems, List<QCActionItem> endItems, List<QCSlider> sliders) {
+        QCRow.Builder builder = new QCRow.Builder()
+                .setTitle(TEST_TITLE)
+                .setSubtitle(TEST_SUBTITLE)
+                .setIcon(icon)
+                .setPrimaryAction(action)
+                .setDisabledClickAction(disabledAction);
+        for (QCActionItem item : startItems) {
+            builder.addStartItem(item);
+        }
+        for (QCActionItem item : endItems) {
+            builder.addEndItem(item);
+        }
+        for (QCSlider slider : sliders) {
+            builder.addSlider(slider);
+        }
+        return builder.build();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCSliderTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCSliderTest.java
new file mode 100644
index 0000000..533e41d
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCSliderTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import static com.android.car.qc.QCItem.QC_TYPE_SLIDER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCSliderTest extends QCItemTestCase<QCSlider> {
+    private static final int MIN = 50;
+    private static final int MAX = 150;
+    private static final int VALUE = 75;
+
+    @Test
+    public void onCreate_hasCorrectType() {
+        QCSlider slider = createSlider(/* action= */ null, /* disabledAction= */ null);
+        assertThat(slider.getType()).isEqualTo(QC_TYPE_SLIDER);
+    }
+
+    @Test
+    public void onBundle_nullActions_noCrash() {
+        QCSlider slider = createSlider(/* action= */ null, /* disabledAction= */ null);
+        writeAndLoadFromBundle(slider);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void createFromParcel_accurateData() {
+        QCSlider slider = createSlider(mDefaultAction, mDefaultDisabledAction);
+        QCSlider newSlider = writeAndLoadFromBundle(slider);
+        assertThat(newSlider.getType()).isEqualTo(QC_TYPE_SLIDER);
+        assertThat(newSlider.getPrimaryAction()).isNotNull();
+        assertThat(newSlider.getDisabledClickAction()).isNotNull();
+        assertThat(newSlider.getMin()).isEqualTo(MIN);
+        assertThat(newSlider.getMax()).isEqualTo(MAX);
+        assertThat(newSlider.getValue()).isEqualTo(VALUE);
+    }
+
+    private QCSlider createSlider(PendingIntent action, PendingIntent disabledAction) {
+        return new QCSlider.Builder()
+                .setMin(MIN)
+                .setMax(MAX)
+                .setValue(VALUE)
+                .setInputAction(action)
+                .setDisabledClickAction(disabledAction)
+                .build();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/QCTileTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/QCTileTest.java
new file mode 100644
index 0000000..5bed094
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/QCTileTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc;
+
+import static com.android.car.qc.QCItem.QC_TYPE_TILE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCTileTest extends QCItemTestCase<QCTile> {
+
+    @Test
+    public void onCreate_hasCorrectType() {
+        QCTile tile = createTile(/* action= */ null, /* disabledAction= */ null, /* icon= */ null);
+        assertThat(tile.getType()).isEqualTo(QC_TYPE_TILE);
+    }
+
+    @Test
+    public void onBundle_nullAction_noCrash() {
+        QCTile tile = createTile(/* action= */ null, /* disabledAction= */ null, mDefaultIcon);
+        writeAndLoadFromBundle(tile);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void onBundle_nullIcon_noCrash() {
+        QCTile tile = createTile(mDefaultAction, mDefaultDisabledAction, /* icon= */ null);
+        writeAndLoadFromBundle(tile);
+        // Test passes if this doesn't crash
+    }
+
+    @Test
+    public void createFromParcel_accurateData() {
+        QCTile tile = createTile(mDefaultAction, mDefaultDisabledAction, mDefaultIcon);
+        QCTile newTile = writeAndLoadFromBundle(tile);
+        assertThat(newTile.getType()).isEqualTo(QC_TYPE_TILE);
+        assertThat(newTile.getSubtitle()).isEqualTo(TEST_SUBTITLE);
+        assertThat(newTile.isChecked()).isTrue();
+        assertThat(newTile.isEnabled()).isTrue();
+        assertThat(newTile.getPrimaryAction()).isNotNull();
+        assertThat(newTile.getDisabledClickAction()).isNotNull();
+        assertThat(newTile.getIcon()).isNotNull();
+    }
+
+    private QCTile createTile(PendingIntent action, PendingIntent disabledAction, Icon icon) {
+        return new QCTile.Builder()
+                .setSubtitle(TEST_SUBTITLE)
+                .setChecked(true)
+                .setEnabled(true)
+                .setAction(action)
+                .setDisabledClickAction(disabledAction)
+                .setIcon(icon)
+                .build();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/controller/BaseQCControllerTestCase.java b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/BaseQCControllerTestCase.java
new file mode 100644
index 0000000..095b192
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/BaseQCControllerTestCase.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+
+import androidx.lifecycle.Observer;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.car.qc.QCItem;
+import com.android.car.qc.QCTile;
+
+import org.junit.Test;
+
+public abstract class BaseQCControllerTestCase<T extends BaseQCController> {
+
+    protected final Context mContext = spy(ApplicationProvider.getApplicationContext());
+
+    protected abstract T getController();
+
+    @Test
+    public void listen_updateListeningCalled() {
+        T spiedController = spy(getController());
+        spiedController.listen(true);
+        verify(spiedController).updateListening();
+    }
+
+    @Test
+    public void addObserver_updateListeningCalled() {
+        Observer<QCItem> observer = mock(Observer.class);
+        T spiedController = spy(getController());
+        spiedController.addObserver(observer);
+        verify(spiedController).updateListening();
+    }
+
+    @Test
+    public void removeObserver_updateListeningCalled() {
+        Observer<QCItem> observer = mock(Observer.class);
+        T spiedController = spy(getController());
+        spiedController.removeObserver(observer);
+        verify(spiedController).updateListening();
+    }
+
+    @Test
+    public void onQCItemUpdated_observersNotified() {
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().onQCItemUpdated(new QCTile.Builder().build());
+        verify(observer).onChanged(any(QCItem.class));
+    }
+
+    @Test
+    public void onDestroy_cleanUpController() {
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        getController().destroy();
+        assertThat(getController().mObservers.size()).isEqualTo(0);
+        assertThat(getController().mShouldListen).isFalse();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/controller/LocalQCControllerTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/LocalQCControllerTest.java
new file mode 100644
index 0000000..17d7392
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/LocalQCControllerTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import androidx.lifecycle.Observer;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCItem;
+import com.android.car.qc.provider.BaseLocalQCProvider;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public class LocalQCControllerTest extends BaseQCControllerTestCase<LocalQCController> {
+
+    private LocalQCController mController;
+    private BaseLocalQCProvider mProvider;
+
+    @Override
+    protected LocalQCController getController() {
+        if (mController == null) {
+            mProvider = mock(BaseLocalQCProvider.class);
+            mController = new LocalQCController(mContext, mProvider);
+        }
+        return mController;
+    }
+
+    @Test
+    public void onCreate_setsProviderNotifier() {
+        getController(); // instantiate
+        verify(mProvider).setNotifier(any());
+    }
+
+    @Test
+    public void updateListening_updatesProviderListening() {
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        verify(mProvider).shouldListen(true);
+        getController().listen(false);
+        verify(mProvider).shouldListen(false);
+    }
+
+    @Test
+    public void updateListening_listen_updatesQCItem() {
+        Observer<QCItem> observer = mock(Observer.class);
+        LocalQCController spiedController = spy(getController());
+        spiedController.addObserver(observer);
+        Mockito.reset(mProvider);
+        spiedController.listen(true);
+        verify(mProvider).getQCItem();
+        verify(spiedController).onQCItemUpdated(any());
+    }
+
+    @Test
+    public void onDestroy_callsProviderDestroy() {
+        getController().destroy();
+        verify(mProvider).onDestroy();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/controller/RemoteQCControllerTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/RemoteQCControllerTest.java
new file mode 100644
index 0000000..a1db602
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/RemoteQCControllerTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.controller;
+
+import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
+import static com.android.car.qc.testutils.TestQCProvider.IS_DESTROYED_KEY;
+import static com.android.car.qc.testutils.TestQCProvider.IS_SUBSCRIBED_KEY;
+import static com.android.car.qc.testutils.TestQCProvider.KEY_DEFAULT;
+import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_DESTROYED;
+import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_SUBSCRIBED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import androidx.lifecycle.Observer;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.car.qc.QCItem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RemoteQCControllerTest extends BaseQCControllerTestCase<RemoteQCController> {
+
+    private final Uri mDefaultUri = Uri.parse(
+            "content://com.android.car.qc.testutils.AllowedTestQCProvider/" + KEY_DEFAULT);
+
+    private RemoteQCController mController;
+
+    @Override
+    protected RemoteQCController getController() {
+        if (mController == null) {
+            mController = new RemoteQCController(mContext, mDefaultUri, mContext.getMainExecutor());
+        }
+        return mController;
+    }
+
+    @Test
+    public void updateListening_listen_updatesQCItem() {
+        Observer<QCItem> observer = mock(Observer.class);
+        RemoteQCController spiedController = spy(getController());
+        spiedController.addObserver(observer);
+        spiedController.listen(true);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        verify(spiedController).onQCItemUpdated(notNull());
+    }
+
+    @Test
+    public void updateListening_listen_providerSubscribed() throws RemoteException {
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        Bundle res = getController().getClient().call(METHOD_IS_SUBSCRIBED, null, extras);
+        assertThat(res).isNotNull();
+        boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, false);
+        assertThat(isSubscribed).isTrue();
+    }
+
+    @Test
+    public void updateListening_doNotListen_providerUnsubscribed() throws RemoteException {
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        getController().listen(false);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        Bundle res = getController().getClient().call(METHOD_IS_SUBSCRIBED, null, extras);
+        assertThat(res).isNotNull();
+        boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, true);
+        assertThat(isSubscribed).isFalse();
+    }
+
+    @Test
+    public void updateListening_listen_registerContentObserver() {
+        ContentResolver resolver = mock(ContentResolver.class);
+        when(mContext.getContentResolver()).thenReturn(resolver);
+        when(resolver.acquireContentProviderClient(mDefaultUri)).thenReturn(
+                mock(ContentProviderClient.class));
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        verify(resolver).registerContentObserver(eq(mDefaultUri), eq(true),
+                any(ContentObserver.class));
+    }
+
+    @Test
+    public void updateListening_doNotListen_unregisterContentObserver() {
+        ContentResolver resolver = mock(ContentResolver.class);
+        when(mContext.getContentResolver()).thenReturn(resolver);
+        when(resolver.acquireContentProviderClient(mDefaultUri)).thenReturn(
+                mock(ContentProviderClient.class));
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        getController().listen(false);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        verify(resolver).unregisterContentObserver(any(ContentObserver.class));
+    }
+
+    @Test
+    public void onDestroy_callsProviderOnDestroy() throws RemoteException {
+        Observer<QCItem> observer = mock(Observer.class);
+        getController().addObserver(observer);
+        getController().listen(true);
+        getController().listen(false);
+        getController().destroy();
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        Bundle res = getController().getClient().call(METHOD_IS_DESTROYED, null, extras);
+        assertThat(res).isNotNull();
+        boolean isDestroyed = res.getBoolean(IS_DESTROYED_KEY, false);
+        assertThat(isDestroyed).isTrue();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseLocalQCProviderTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseLocalQCProviderTest.java
new file mode 100644
index 0000000..6defad7
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseLocalQCProviderTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCItem;
+import com.android.car.qc.QCTile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BaseLocalQCProviderTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private TestBaseLocalQCProvider mProvider;
+
+    @Before
+    public void setUp() {
+        mProvider = new TestBaseLocalQCProvider(mContext);
+    }
+
+    @Test
+    public void getQCItem_returnsItem() {
+        QCItem item = mProvider.getQCItem();
+        assertThat(item).isNotNull();
+        assertThat(item instanceof QCTile).isTrue();
+    }
+
+    @Test
+    public void listen_callsOnSubscribed() {
+        mProvider.shouldListen(true);
+        assertThat(mProvider.isSubscribed()).isTrue();
+    }
+
+    @Test
+    public void stopListening_callsOnUnsubscribed() {
+        mProvider.shouldListen(true);
+        mProvider.shouldListen(false);
+        assertThat(mProvider.isSubscribed()).isFalse();
+    }
+
+    @Test
+    public void notifyChange_updateNotified() {
+        BaseLocalQCProvider.Notifier notifier = mock(BaseLocalQCProvider.Notifier.class);
+        mProvider.setNotifier(notifier);
+        mProvider.notifyChange();
+        verify(notifier).notifyUpdate();
+    }
+
+    private static class TestBaseLocalQCProvider extends BaseLocalQCProvider {
+
+        private boolean mIsSubscribed;
+
+        TestBaseLocalQCProvider(Context context) {
+            super(context);
+        }
+
+        @Override
+        public QCItem getQCItem() {
+            return new QCTile.Builder().build();
+        }
+
+        @Override
+        protected void onSubscribed() {
+            mIsSubscribed = true;
+        }
+
+        @Override
+        protected void onUnsubscribed() {
+            mIsSubscribed = false;
+        }
+
+        boolean isSubscribed() {
+            return mIsSubscribed;
+        }
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseQCProviderTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseQCProviderTest.java
new file mode 100644
index 0000000..30607fa
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseQCProviderTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.provider;
+
+import static com.android.car.qc.provider.BaseQCProvider.EXTRA_ITEM;
+import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_BIND;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_DESTROY;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_SUBSCRIBE;
+import static com.android.car.qc.provider.BaseQCProvider.METHOD_UNSUBSCRIBE;
+import static com.android.car.qc.testutils.TestQCProvider.IS_DESTROYED_KEY;
+import static com.android.car.qc.testutils.TestQCProvider.IS_SUBSCRIBED_KEY;
+import static com.android.car.qc.testutils.TestQCProvider.KEY_DEFAULT;
+import static com.android.car.qc.testutils.TestQCProvider.KEY_SLOW;
+import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_DESTROYED;
+import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_SUBSCRIBED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCItem;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BaseQCProviderTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final Uri mDefaultUri = Uri.parse(
+            "content://com.android.car.qc.testutils.AllowedTestQCProvider/" + KEY_DEFAULT);
+    private final Uri mSlowUri =
+            Uri.parse("content://com.android.car.qc.testutils.AllowedTestQCProvider/" + KEY_SLOW);
+    private final Uri mDeniedUri =
+            Uri.parse("content://com.android.car.qc.testutils.DeniedTestQCProvider");
+
+    @Test
+    public void callOnBind_allowed_returnsItem() throws RemoteException {
+        ContentProviderClient provider = getClient(mDefaultUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        Bundle res = provider.call(METHOD_BIND, null, extras);
+        assertThat(res).isNotNull();
+        res.setClassLoader(QCItem.class.getClassLoader());
+        Parcelable parcelable = res.getParcelable(EXTRA_ITEM);
+        assertThat(parcelable).isNotNull();
+        assertThat(parcelable instanceof QCItem).isTrue();
+    }
+
+    @Test
+    public void callOnBind_noUri_throwsIllegalArgumentException() throws RemoteException {
+        ContentProviderClient provider = getClient(mDefaultUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        assertThrows(IllegalArgumentException.class,
+                () -> provider.call(METHOD_BIND, null, extras));
+    }
+
+    @Test
+    public void callOnBind_slowOperation_throwsRuntimeException() {
+        ContentProviderClient provider = getClient(mSlowUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mSlowUri);
+        assertThrows(RuntimeException.class,
+                () -> provider.call(METHOD_BIND, null, extras));
+    }
+
+    @Test
+    public void callOnBind_notAllowed_throwsSecurityException() {
+        ContentProviderClient provider = getClient(mDeniedUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDeniedUri);
+        assertThrows(SecurityException.class,
+                () -> provider.call(METHOD_BIND, null, extras));
+    }
+
+    @Test
+    public void callOnSubscribed_isSubscribed() throws RemoteException {
+        ContentProviderClient provider = getClient(mDefaultUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        provider.call(METHOD_SUBSCRIBE, null, extras);
+
+        Bundle res = provider.call(METHOD_IS_SUBSCRIBED, null, extras);
+        assertThat(res).isNotNull();
+        boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, false);
+        assertThat(isSubscribed).isTrue();
+    }
+
+    @Test
+    public void callOnUnsubscribed_isUnsubscribed() throws RemoteException {
+        ContentProviderClient provider = getClient(mDefaultUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        provider.call(METHOD_SUBSCRIBE, null, extras);
+        provider.call(METHOD_UNSUBSCRIBE, null, extras);
+
+        Bundle res = provider.call(METHOD_IS_SUBSCRIBED, null, extras);
+        assertThat(res).isNotNull();
+        boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, true);
+        assertThat(isSubscribed).isFalse();
+    }
+
+    @Test
+    public void callDestroy_isDestroyed() throws RemoteException {
+        ContentProviderClient provider = getClient(mDefaultUri);
+        assertThat(provider).isNotNull();
+        Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_URI, mDefaultUri);
+        provider.call(METHOD_SUBSCRIBE, null, extras);
+        provider.call(METHOD_UNSUBSCRIBE, null, extras);
+        provider.call(METHOD_DESTROY, null, extras);
+
+        Bundle res = provider.call(METHOD_IS_DESTROYED, null, extras);
+        assertThat(res).isNotNull();
+        boolean isDestroyed = res.getBoolean(IS_DESTROYED_KEY, false);
+        assertThat(isDestroyed).isTrue();
+    }
+
+    private ContentProviderClient getClient(Uri uri) {
+        return mContext.getContentResolver().acquireContentProviderClient(uri);
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/AllowedTestQCProvider.java b/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/AllowedTestQCProvider.java
new file mode 100644
index 0000000..d3cbf87
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/AllowedTestQCProvider.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.testutils;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class AllowedTestQCProvider extends TestQCProvider {
+    @Override
+    protected Set<String> getAllowlistedPackages() {
+        Set<String> allowlist = new HashSet<>();
+        allowlist.add("com.android.car.qc.tests.unit");
+        return allowlist;
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/DeniedTestQCProvider.java b/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/DeniedTestQCProvider.java
new file mode 100644
index 0000000..a9c56ce
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/DeniedTestQCProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.testutils;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DeniedTestQCProvider extends TestQCProvider {
+    @Override
+    protected Set<String> getAllowlistedPackages() {
+        return new HashSet<>();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/TestQCProvider.java b/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/TestQCProvider.java
new file mode 100644
index 0000000..8248832
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/testutils/TestQCProvider.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.testutils;
+
+import android.R;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import com.android.car.qc.QCItem;
+import com.android.car.qc.QCTile;
+import com.android.car.qc.provider.BaseQCProvider;
+
+import java.io.ByteArrayOutputStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class TestQCProvider extends BaseQCProvider {
+
+    public static final String METHOD_IS_SUBSCRIBED = "METHOD_IS_SUBSCRIBED";
+    public static final String IS_SUBSCRIBED_KEY = "IS_SUBSCRIBED";
+    public static final String METHOD_IS_DESTROYED = "METHOD_IS_DESTROYED";
+    public static final String IS_DESTROYED_KEY = "IS_DESTROYED";
+
+    public static final String KEY_DEFAULT = "DEFAULT";
+    public static final String KEY_SLOW = "SLOW";
+
+    private final Set<Uri> mSubscribedUris = new HashSet<>();
+    private final Set<Uri> mDestroyedUris = new HashSet<>();
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        if (METHOD_IS_SUBSCRIBED.equals(method)) {
+            Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_URI));
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(IS_SUBSCRIBED_KEY, mSubscribedUris.contains(uri));
+            return bundle;
+        }
+        if (METHOD_IS_DESTROYED.equals(method)) {
+            Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_URI));
+            Bundle bundle = new Bundle();
+            bundle.putBoolean(IS_DESTROYED_KEY, mDestroyedUris.contains(uri));
+            return bundle;
+        }
+        return super.call(method, arg, extras);
+    }
+
+    @Override
+    protected QCItem onBind(@NonNull Uri uri) {
+        List<String> pathSegments = uri.getPathSegments();
+        String key = pathSegments.get(0);
+
+        if (KEY_DEFAULT.equals(key)) {
+            return new QCTile.Builder()
+                    .setIcon(Icon.createWithResource(getContext(), R.drawable.btn_star))
+                    .build();
+        } else if (KEY_SLOW.equals(key)) {
+            // perform a slow operation that should trigger the strict thread policy
+            Drawable d = getContext().getDrawable(R.drawable.btn_star);
+            Bitmap bitmap = drawableToBitmap(d);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
+            byte[] b = baos.toByteArray();
+            Icon icon = Icon.createWithData(b, 0, b.length);
+            return new QCTile.Builder()
+                    .setIcon(icon)
+                    .build();
+        }
+        return null;
+    }
+
+    @Override
+    protected void onSubscribed(@NonNull Uri uri) {
+        mSubscribedUris.add(uri);
+    }
+
+    @Override
+    protected void onUnsubscribed(@NonNull Uri uri) {
+        mSubscribedUris.remove(uri);
+    }
+
+    @Override
+    protected void onDestroy(@NonNull Uri uri) {
+        mDestroyedUris.add(uri);
+    }
+
+    private static Bitmap drawableToBitmap(Drawable drawable) {
+
+        if (drawable instanceof BitmapDrawable) {
+            return ((BitmapDrawable) drawable).getBitmap();
+        }
+
+        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+
+        return bitmap;
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCListViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCListViewTest.java
new file mode 100644
index 0000000..a1065e8
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCListViewTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCList;
+import com.android.car.qc.QCRow;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCListViewTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private QCListView mView;
+
+    @Before
+    public void setUp() {
+        mView = new QCListView(mContext);
+    }
+
+    @Test
+    public void onChanged_null_noViews() {
+        mView.onChanged(null);
+        assertThat(mView.getChildCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void onChanged_invalidType_throwsIllegalArgumentException() {
+        QCRow row = new QCRow.Builder().build();
+        assertThrows(IllegalArgumentException.class,
+                () -> mView.onChanged(row));
+    }
+
+    @Test
+    public void onChanged_createsRows() {
+        QCList list = new QCList.Builder()
+                .addRow(new QCRow.Builder().build())
+                .addRow(new QCRow.Builder().build())
+                .build();
+        mView.onChanged(list);
+        assertThat(mView.getChildCount()).isEqualTo(2);
+        assertThat(mView.getChildAt(0) instanceof QCRowView).isTrue();
+        assertThat(mView.getChildAt(1) instanceof QCRowView).isTrue();
+    }
+
+    @Test
+    public void onChanged_decreasedRowCount_removesExtraRows() {
+        QCList list = new QCList.Builder()
+                .addRow(new QCRow.Builder().build())
+                .addRow(new QCRow.Builder().build())
+                .build();
+        mView.onChanged(list);
+        assertThat(mView.getChildCount()).isEqualTo(2);
+        list = new QCList.Builder()
+                .addRow(new QCRow.Builder().build())
+                .build();
+        mView.onChanged(list);
+        assertThat(mView.getChildCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void setActionListener_setsOnChildView() {
+        QCList list = new QCList.Builder()
+                .addRow(new QCRow.Builder().build())
+                .addRow(new QCRow.Builder().build())
+                .build();
+        mView.onChanged(list);
+        assertThat(mView.getChildCount()).isEqualTo(2);
+        QCRowView row1 = (QCRowView) mView.getChildAt(0);
+        QCRowView row2 = (QCRowView) mView.getChildAt(1);
+        ExtendedMockito.spyOn(row1);
+        ExtendedMockito.spyOn(row2);
+        QCView.QCActionListener listener = mock(QCView.QCActionListener.class);
+        mView.setActionListener(listener);
+        ExtendedMockito.verify(row1).setActionListener(listener);
+        ExtendedMockito.verify(row2).setActionListener(listener);
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java
new file mode 100644
index 0000000..647317a
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCActionItem;
+import com.android.car.qc.QCRow;
+import com.android.car.qc.QCSlider;
+import com.android.car.qc.R;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCRowViewTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private QCRowView mView;
+
+    @Before
+    public void setUp() {
+        mView = new QCRowView(mContext);
+    }
+
+    @Test
+    public void setRow_null_notVisible() {
+        mView.setRow(null);
+        assertThat(mView.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void setRow_notNull_visible() {
+        QCRow row = new QCRow.Builder().build();
+        mView.setRow(row);
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void setRow_setsTitle() {
+        String title = "TEST_TITLE";
+        QCRow row = new QCRow.Builder().setTitle(title).build();
+        mView.setRow(row);
+        TextView titleView = mView.findViewById(R.id.qc_title);
+        assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(titleView.getText().toString()).isEqualTo(title);
+    }
+
+    @Test
+    public void setRow_setsSubtitle() {
+        String subtitle = "TEST_TITLE";
+        QCRow row = new QCRow.Builder().setSubtitle(subtitle).build();
+        mView.setRow(row);
+        TextView subtitleView = mView.findViewById(R.id.qc_summary);
+        assertThat(subtitleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(subtitleView.getText().toString()).isEqualTo(subtitle);
+    }
+
+    @Test
+    public void setRow_setsIcon() {
+        Icon icon = Icon.createWithResource(mContext, android.R.drawable.btn_star);
+        QCRow row = new QCRow.Builder().setIcon(icon).build();
+        mView.setRow(row);
+        ImageView iconView = mView.findViewById(R.id.qc_icon);
+        assertThat(iconView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(iconView.getDrawable()).isNotNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void setRow_createsStartItems() {
+        QCRow row = new QCRow.Builder()
+                .addStartItem(new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build())
+                .addStartItem(new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE).build())
+                .build();
+        mView.setRow(row);
+        LinearLayout startContainer = mView.findViewById(R.id.qc_row_start_items);
+        assertThat(startContainer.getChildCount()).isEqualTo(2);
+        assertThat((View) startContainer.getChildAt(0).findViewById(
+                android.R.id.switch_widget)).isNotNull();
+        assertThat((View) startContainer.getChildAt(1).findViewById(
+                R.id.qc_toggle_button)).isNotNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void setRow_createsEndItems() {
+        QCRow row = new QCRow.Builder()
+                .addEndItem(new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build())
+                .addEndItem(new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE).build())
+                .build();
+        mView.setRow(row);
+        LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
+        assertThat(endContainer.getChildCount()).isEqualTo(2);
+        assertThat((View) endContainer.getChildAt(0).findViewById(
+                android.R.id.switch_widget)).isNotNull();
+        assertThat((View) endContainer.getChildAt(1).findViewById(
+                R.id.qc_toggle_button)).isNotNull();
+    }
+
+    @Test
+    public void setRow_noSlider_sliderViewNotVisible() {
+        QCRow row = new QCRow.Builder().build();
+        mView.setRow(row);
+        LinearLayout sliderContainer = mView.findViewById(R.id.qc_seekbar_wrapper);
+        assertThat(sliderContainer.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    @UiThreadTest
+    public void setRow_hasSlider_sliderViewVisible() {
+        QCRow row = new QCRow.Builder()
+                .addSlider(new QCSlider.Builder().build())
+                .build();
+        mView.setRow(row);
+        LinearLayout sliderContainer = mView.findViewById(R.id.qc_seekbar_wrapper);
+        assertThat(sliderContainer.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void onRowClick_firesAction() throws PendingIntent.CanceledException {
+        PendingIntent action = mock(PendingIntent.class);
+        QCRow row = new QCRow.Builder().setPrimaryAction(action).build();
+        mView.setRow(row);
+        mView.findViewById(R.id.qc_row_content).performClick();
+        verify(action).send(any(Context.class), anyInt(), eq(null));
+    }
+
+    @Test
+    public void onSwitchClick_firesAction() throws PendingIntent.CanceledException {
+        PendingIntent action = mock(PendingIntent.class);
+        QCRow row = new QCRow.Builder()
+                .addEndItem(
+                        new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).setAction(action).build())
+                .build();
+        mView.setRow(row);
+        LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
+        assertThat(endContainer.getChildCount()).isEqualTo(1);
+        endContainer.getChildAt(0).performClick();
+        verify(action).send(any(Context.class), anyInt(), any(Intent.class));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onToggleClick_firesAction() throws PendingIntent.CanceledException {
+        PendingIntent action = mock(PendingIntent.class);
+        QCRow row = new QCRow.Builder()
+                .addEndItem(
+                        new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE).setAction(action).build())
+                .build();
+        mView.setRow(row);
+        LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
+        assertThat(endContainer.getChildCount()).isEqualTo(1);
+        endContainer.getChildAt(0).performClick();
+        verify(action).send(any(Context.class), anyInt(), any(Intent.class));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onSliderChange_firesAction() throws PendingIntent.CanceledException {
+        PendingIntent action = mock(PendingIntent.class);
+        QCRow row = new QCRow.Builder()
+                .addSlider(new QCSlider.Builder().setInputAction(action).build())
+                .build();
+        mView.setRow(row);
+        SeekBar seekBar = mView.findViewById(R.id.seekbar);
+        seekBar.setProgress(50);
+        MotionEvent motionEvent = ExtendedMockito.mock(MotionEvent.class);
+        ExtendedMockito.when(motionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
+        seekBar.onTouchEvent(motionEvent);
+        verify(action).send(any(Context.class), anyInt(), any(Intent.class));
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java
new file mode 100644
index 0000000..9adbd3b
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 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.car.qc.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.widget.SeekBar;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class QCSeekBarViewTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final QCSeekBarView mView = new QCSeekBarView(mContext);
+
+    @Mock
+    private MotionEvent mMotionEvent;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
+    }
+
+    @Test
+    public void enabled_standardTouchEvent() {
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+    }
+
+    @Test
+    public void disabled_standardTouchEvent() {
+        mView.setEnabled(false);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isFalse();
+    }
+
+    @Test
+    public void clickableWhileDisabled_customTouchEvent() {
+        mView.setEnabled(false);
+        mView.setClickableWhileDisabled(true);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+    }
+
+    @Test
+    public void clickableWhileDisabled_actionDown_doesNotTriggerDisabledClickListener() {
+        AtomicBoolean called = new AtomicBoolean(false);
+        Consumer<SeekBar> disabledClickListener = seekBar -> called.set(true);
+        mView.setEnabled(false);
+        mView.setClickableWhileDisabled(true);
+        mView.setDisabledClickListener(disabledClickListener);
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+        assertThat(called.get()).isFalse();
+    }
+
+    @Test
+    public void clickableWhileDisabled_actionUp_triggersDisabledClickListener() {
+        AtomicBoolean called = new AtomicBoolean(false);
+        Consumer<SeekBar> disabledClickListener = seekBar -> called.set(true);
+        mView.setEnabled(false);
+        mView.setClickableWhileDisabled(true);
+        mView.setDisabledClickListener(disabledClickListener);
+
+        assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
+        assertThat(called.get()).isTrue();
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCTileViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCTileViewTest.java
new file mode 100644
index 0000000..e900441
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCTileViewTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.LayerDrawable;
+import android.widget.TextView;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCRow;
+import com.android.car.qc.QCTile;
+import com.android.car.qc.R;
+import com.android.car.ui.uxr.DrawableStateToggleButton;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCTileViewTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private QCTileView mView;
+
+    @Before
+    public void setUp() {
+        mView = new QCTileView(mContext);
+    }
+
+    @Test
+    public void onChanged_null_noViews() {
+        mView.onChanged(null);
+        assertThat(mView.getChildCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void onChanged_invalidType_throwsIllegalArgumentException() {
+        QCRow row = new QCRow.Builder().build();
+        assertThrows(IllegalArgumentException.class,
+                () -> mView.onChanged(row));
+    }
+
+    @Test
+    @UiThreadTest
+    public void onChanged_setsSubtitleView() {
+        String subtitle = "TEST_SUBTITLE";
+        QCTile tile = new QCTile.Builder().setSubtitle(subtitle).build();
+        mView.onChanged(tile);
+        TextView subtitleView = mView.findViewById(android.R.id.summary);
+        assertThat(subtitleView.getText().toString()).isEqualTo(subtitle);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onChanged_setsButtonState() {
+        QCTile tile = new QCTile.Builder().setChecked(true).setEnabled(true).build();
+        mView.onChanged(tile);
+        DrawableStateToggleButton button = mView.findViewById(R.id.qc_tile_toggle_button);
+        assertThat(button.isEnabled()).isTrue();
+        assertThat(button.isChecked()).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void onChanged_setsIcon() {
+        Icon icon = Icon.createWithResource(mContext, android.R.drawable.btn_star);
+        QCTile tile = new QCTile.Builder().setIcon(icon).build();
+        mView.onChanged(tile);
+        DrawableStateToggleButton button = mView.findViewById(R.id.qc_tile_toggle_button);
+        Drawable buttonDrawable = button.getButtonDrawable();
+        assertThat(buttonDrawable).isNotNull();
+        assertThat(buttonDrawable instanceof LayerDrawable).isTrue();
+        assertThat(((LayerDrawable) buttonDrawable).getNumberOfLayers()).isEqualTo(2);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onClick_firesAction() throws PendingIntent.CanceledException {
+        PendingIntent action = mock(PendingIntent.class);
+        QCTile tile = new QCTile.Builder().setChecked(false).setAction(action).build();
+        mView.onChanged(tile);
+        mView.findViewById(R.id.qc_tile_wrapper).performClick();
+        DrawableStateToggleButton button = mView.findViewById(R.id.qc_tile_toggle_button);
+        assertThat(button.isChecked()).isTrue();
+        verify(action).send(any(Context.class), anyInt(), any(Intent.class));
+    }
+}
diff --git a/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCViewTest.java b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCViewTest.java
new file mode 100644
index 0000000..9d3d9d7
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/view/QCViewTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.qc.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.qc.QCList;
+import com.android.car.qc.QCRow;
+import com.android.car.qc.QCTile;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class QCViewTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private QCView mView;
+
+    @Before
+    public void setUp() {
+        mView = new QCView(mContext);
+    }
+
+    @Test
+    public void onChanged_null_noViews() {
+        mView.onChanged(null);
+        assertThat(mView.getChildCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void onChanged_invalidType_throwsIllegalArgumentException() {
+        QCRow row = new QCRow.Builder().build();
+        assertThrows(IllegalArgumentException.class,
+                () -> mView.onChanged(row));
+    }
+
+    @Test
+    public void onChanged_list_createsListView() {
+        QCList list = new QCList.Builder().build();
+        mView.onChanged(list);
+        assertThat(mView.getChildCount()).isEqualTo(1);
+        assertThat(mView.getChildAt(0) instanceof QCListView).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void onChanged_tile_createsTileView() {
+        QCTile tile = new QCTile.Builder().build();
+        mView.onChanged(tile);
+        assertThat(mView.getChildCount()).isEqualTo(1);
+        assertThat(mView.getChildAt(0) instanceof QCTileView).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void onChanged_alreadyHasView_callsOnChanged() {
+        QCTile tile = new QCTile.Builder().build();
+        mView.onChanged(tile);
+        assertThat(mView.getChildCount()).isEqualTo(1);
+        assertThat(mView.getChildAt(0) instanceof QCTileView).isTrue();
+        QCTileView tileView = (QCTileView) mView.getChildAt(0);
+        ExtendedMockito.spyOn(tileView);
+        mView.onChanged(tile);
+        verify(tileView).onChanged(tile);
+    }
+
+    @Test
+    @UiThreadTest
+    public void setActionListener_setsOnChildView() {
+        QCTile tile = new QCTile.Builder().build();
+        mView.onChanged(tile);
+        assertThat(mView.getChildCount()).isEqualTo(1);
+        assertThat(mView.getChildAt(0) instanceof QCTileView).isTrue();
+        QCTileView tileView = (QCTileView) mView.getChildAt(0);
+        ExtendedMockito.spyOn(tileView);
+        QCView.QCActionListener listener = mock(QCView.QCActionListener.class);
+        mView.setActionListener(listener);
+        ExtendedMockito.verify(tileView).setActionListener(listener);
+    }
+}