summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBaligh Uddin <baligh@google.com>2022-03-09 14:47:16 +0000
committerBaligh Uddin <baligh@google.com>2022-03-09 14:47:16 +0000
commit947f53d11f959c867db2eb6aea7253b74067c356 (patch)
tree38bb886c03d63152125c0e86d6729af706931ccf
parent59bcf3c9997986c8b66fcfc2b10eb6871f476387 (diff)
parentcf5bfbcac61fef1c13cbcfd738bd38b4bd248838 (diff)
downloadsystemlibs-947f53d11f959c867db2eb6aea7253b74067c356.tar.gz
Merge history of car-libs into packages/apps/Car/systemlibs
BUG: 196593308 Test: TH Change-Id: Ie5d62febd5e69d6541c2f2638a1193624f4b3d14
-rw-r--r--.gitignore22
-rw-r--r--OWNERS14
-rw-r--r--PREUPLOAD.cfg7
-rw-r--r--androidx-car/Android.bp50
-rw-r--r--androidx-car/AndroidManifest.xml24
-rwxr-xr-xandroidx-car/androidx-car-resources.aarbin0 -> 177967 bytes
-rw-r--r--car-assist-client-lib/Android.bp37
-rw-r--r--car-assist-client-lib/AndroidManifest.xml19
-rw-r--r--car-assist-client-lib/OWNERS3
-rw-r--r--car-assist-client-lib/res/values-af/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-am/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ar/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-as/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-az/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-b+sr+Latn/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-be/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-bg/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-bn/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-bs/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ca/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-cs/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-da/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-de/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-el/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-en-rAU/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-en-rCA/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-en-rGB/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-en-rIN/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-en-rXC/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-es-rUS/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-es/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-et/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-eu/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-fa/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-fi/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-fr-rCA/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-fr/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-gl/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-gu/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-hi/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-hr/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-hu/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-hy/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-in/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-is/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-it/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-iw/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ja/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ka/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-kk/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-km/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-kn/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ko/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ky/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-lo/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-lt/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-lv/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-mk/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ml/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-mn/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-mr/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ms/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-my/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-nb/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ne/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-nl/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-or/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-pa/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-pl/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-pt-rPT/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-pt/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ro/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ru/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-si/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-sk/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-sl/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-sq/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-sr/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-sv/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-sw/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ta/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-te/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-th/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-tl/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-tr/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-uk/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-ur/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-uz/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-vi/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-zh-rCN/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-zh-rHK/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-zh-rTW/strings.xml21
-rw-r--r--car-assist-client-lib/res/values-zu/strings.xml21
-rw-r--r--car-assist-client-lib/res/values/config.xml20
-rw-r--r--car-assist-client-lib/res/values/strings.xml22
-rw-r--r--car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java78
-rw-r--r--car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java466
-rw-r--r--car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java256
-rw-r--r--car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java100
-rw-r--r--car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java89
-rw-r--r--car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java399
-rw-r--r--car-broadcastradio-support/Android.bp37
-rw-r--r--car-broadcastradio-support/AndroidManifest.xml21
-rw-r--r--car-broadcastradio-support/OWNERS3
-rw-r--r--car-broadcastradio-support/res/values-af/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-am/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ar/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-as/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-az/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-b+sr+Latn/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-be/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-bg/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-bn/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-bs/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ca/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-cs/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-da/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-de/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-el/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-en-rAU/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-en-rCA/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-en-rGB/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-en-rIN/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-en-rXC/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-es-rUS/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-es/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-et/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-eu/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-fa/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-fi/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-fr-rCA/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-fr/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-gl/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-gu/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-hi/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-hr/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-hu/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-hy/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-in/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-is/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-it/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-iw/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ja/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ka/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-kk/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-km/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-kn/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ko/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ky/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-lo/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-lt/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-lv/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-mk/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ml/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-mn/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-mr/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ms/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-my/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-nb/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ne/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-nl/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-or/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-pa/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-pl/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-pt-rPT/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-pt/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ro/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ru/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-si/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-sk/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-sl/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-sq/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-sr/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-sv/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-sw/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ta/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-te/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-th/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-tl/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-tr/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-uk/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-ur/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-uz/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-vi/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-zh-rCN/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-zh-rHK/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-zh-rTW/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values-zu/strings.xml24
-rw-r--r--car-broadcastradio-support/res/values/strings.xml31
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.aidl18
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/Program.java113
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/media/BrowseTree.java505
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ImageResolver.java33
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramInfoExt.java173
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/ProgramSelectorExt.java486
-rw-r--r--car-broadcastradio-support/src/com/android/car/broadcastradio/support/platform/RadioMetadataExt.java60
-rw-r--r--car-qc-lib/Android.bp31
-rw-r--r--car-qc-lib/AndroidManifest.xml19
-rw-r--r--car-qc-lib/OWNERS8
-rw-r--r--car-qc-lib/PREUPLOAD.cfg7
-rw-r--r--car-qc-lib/res/color/qc_toggle_background_color.xml27
-rw-r--r--car-qc-lib/res/color/qc_toggle_icon_fill_color.xml27
-rw-r--r--car-qc-lib/res/drawable/qc_row_action_divider.xml21
-rw-r--r--car-qc-lib/res/drawable/qc_seekbar_wrapper_background.xml27
-rw-r--r--car-qc-lib/res/drawable/qc_toggle_background.xml29
-rw-r--r--car-qc-lib/res/drawable/qc_toggle_button_background.xml34
-rw-r--r--car-qc-lib/res/drawable/qc_toggle_rotary_background.xml34
-rw-r--r--car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml31
-rw-r--r--car-qc-lib/res/layout/qc_action_switch.xml21
-rw-r--r--car-qc-lib/res/layout/qc_action_toggle.xml25
-rw-r--r--car-qc-lib/res/layout/qc_row_view.xml145
-rw-r--r--car-qc-lib/res/layout/qc_tile_view.xml41
-rw-r--r--car-qc-lib/res/values/attrs.xml19
-rw-r--r--car-qc-lib/res/values/colors.xml22
-rw-r--r--car-qc-lib/res/values/dimens.xml35
-rw-r--r--car-qc-lib/res/values/styles.xml39
-rw-r--r--car-qc-lib/src/com/android/car/qc/QCActionItem.java210
-rw-r--r--car-qc-lib/src/com/android/car/qc/QCItem.java154
-rw-r--r--car-qc-lib/src/com/android/car/qc/QCList.java106
-rw-r--r--car-qc-lib/src/com/android/car/qc/QCRow.java315
-rw-r--r--car-qc-lib/src/com/android/car/qc/QCSlider.java189
-rw-r--r--car-qc-lib/src/com/android/car/qc/QCTile.java222
-rw-r--r--car-qc-lib/src/com/android/car/qc/controller/BaseQCController.java96
-rw-r--r--car-qc-lib/src/com/android/car/qc/controller/LocalQCController.java70
-rw-r--r--car-qc-lib/src/com/android/car/qc/controller/QCItemCallback.java33
-rw-r--r--car-qc-lib/src/com/android/car/qc/controller/RemoteQCController.java278
-rw-r--r--car-qc-lib/src/com/android/car/qc/provider/BaseLocalQCProvider.java97
-rw-r--r--car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java231
-rw-r--r--car-qc-lib/src/com/android/car/qc/view/QCListView.java101
-rw-r--r--car-qc-lib/src/com/android/car/qc/view/QCRowView.java533
-rw-r--r--car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java78
-rw-r--r--car-qc-lib/src/com/android/car/qc/view/QCTileView.java152
-rw-r--r--car-qc-lib/src/com/android/car/qc/view/QCView.java120
-rw-r--r--car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java95
-rw-r--r--car-qc-lib/tests/unit/Android.bp47
-rw-r--r--car-qc-lib/tests/unit/AndroidManifest.xml41
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/QCActionItemTest.java110
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/QCItemTestCase.java46
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/QCListTest.java62
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/QCRowTest.java124
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/QCSliderTest.java70
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/QCTileTest.java77
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/controller/BaseQCControllerTestCase.java82
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/controller/LocalQCControllerTest.java92
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/controller/RemoteQCControllerTest.java163
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseLocalQCProviderTest.java102
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/provider/BaseQCProviderTest.java153
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/testutils/AllowedTestQCProvider.java29
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/testutils/DeniedTestQCProvider.java27
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/testutils/TestQCProvider.java122
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/view/QCListViewTest.java105
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/view/QCRowViewTest.java213
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/view/QCSeekBarViewTest.java97
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/view/QCTileViewTest.java116
-rw-r--r--car-qc-lib/tests/unit/src/com/android/car/qc/view/QCViewTest.java107
-rwxr-xr-xtools/go_rotary.sh74
-rw-r--r--tools/rro/README18
-rwxr-xr-xtools/rro/generate-overlayable.py79
-rwxr-xr-xtools/rro/generate-overlays.py80
-rw-r--r--tools/rro/resource_utils.py206
-rwxr-xr-xtools/rro/verify-overlayable.py55
261 files changed, 13085 insertions, 0 deletions
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..38f9800
--- /dev/null
+++ b/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/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..a49d23b
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/BaseQCController.java
@@ -0,0 +1,96 @@
+/*
+ * 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();
+ }
+
+ /**
+ * Perform a single retrieval from the provider (without subscribing to live updates).
+ */
+ public abstract void bind();
+
+ /**
+ * 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..a946b9d
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/LocalQCController.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.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
+ public void bind() {
+ onQCItemUpdated(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..634bbb5
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/controller/RemoteQCController.java
@@ -0,0 +1,278 @@
+/*
+ * 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
+ public void bind() {
+ mBackgroundExecutor.execute(this::updateQCItem);
+ }
+
+ @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 = getQCItem();
+ mContext.getMainExecutor().execute(() -> onQCItemUpdated(item));
+ } catch (Exception e) {
+ Log.d(TAG, "Error fetching QCItem", e);
+ }
+ }
+
+ private QCItem getQCItem() {
+ 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 = getQCItem();
+ 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..5226ee7
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/LocalQCControllerTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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 onBind_updatesQCItem() {
+ Observer<QCItem> observer = mock(Observer.class);
+ LocalQCController spiedController = spy(getController());
+ spiedController.addObserver(observer);
+ Mockito.reset(mProvider);
+ spiedController.bind();
+ verify(mProvider).getQCItem();
+ verify(spiedController).onQCItemUpdated(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..55f0a9c
--- /dev/null
+++ b/car-qc-lib/tests/unit/src/com/android/car/qc/controller/RemoteQCControllerTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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 onBind_updatesQCItem() {
+ Observer<QCItem> observer = mock(Observer.class);
+ RemoteQCController spiedController = spy(getController());
+ spiedController.addObserver(observer);
+ spiedController.bind();
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ verify(spiedController).onQCItemUpdated(notNull());
+ }
+
+ @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);
+ }
+}
diff --git a/tools/go_rotary.sh b/tools/go_rotary.sh
new file mode 100755
index 0000000..b3b28d3
--- /dev/null
+++ b/tools/go_rotary.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+# 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.
+
+TMP_OUTDIR="/tmp/rotary"
+ME=`basename "$0"`
+
+function help {
+ echo "A simple helper script that runs the Trade Federation unit tests"
+ echo "to print this message: packages/apps/Car/tests/tools/$ME"
+ echo "to build: packages/apps/Car/tests/tools/$ME b"
+ echo "to install: packages/apps/Car/tests/tools/$ME i"
+ echo "to run only: packages/apps/Car/tests/tools/$ME r"
+ echo "the apks and jar are in $TMP_OUTDIR"
+}
+
+function build {
+ echo
+ echo "Building the apks"
+ . build/envsetup.sh ; lunch aosp_car_x86-userdebug; make CarRotaryController RotaryPlayground android.car -j32
+ ANDROID_OUT=$ANDROID_BUILD_TOP/out
+ rm -r $TMP_OUTDIR
+ mkdir -p $TMP_OUTDIR
+ cp $ANDROID_PRODUCT_OUT/system/app/CarRotaryController/CarRotaryController.apk $TMP_OUTDIR
+ cp $ANDROID_PRODUCT_OUT/system/app/RotaryPlayground/RotaryPlayground.apk $TMP_OUTDIR
+ cp $ANDROID_OUT/target/common/obj/JAVA_LIBRARIES/android.car_intermediates/classes.jar $TMP_OUTDIR/android.car.jar
+}
+
+function install {
+ echo
+ echo "Installing the apks"
+ adb install -g $TMP_OUTDIR/CarRotaryController.apk
+ adb install -g $TMP_OUTDIR/RotaryPlayground.apk
+}
+
+function run {
+ echo
+ echo "Starting Rotary service and playground app"
+ adb shell settings put secure enabled_accessibility_services com.android.car.rotary/com.android.car.rotary.RotaryService
+ adb shell am start -n com.android.car.rotaryplayground/com.android.car.rotaryplayground.RotaryActivity
+}
+
+ACTION=$1
+
+if [[ $ACTION == "b" ]]; then
+ SECONDS=0
+ build
+ echo "Build time: $SECONDS sec."
+ ACTION="i"
+fi
+
+if [[ $ACTION == "i" ]]; then
+ install
+ ACTION="r"
+fi
+
+if [[ $ACTION == "r" ]]; then
+ run
+ exit
+fi
+
+help
diff --git a/tools/rro/README b/tools/rro/README
new file mode 100644
index 0000000..2678b1e
--- /dev/null
+++ b/tools/rro/README
@@ -0,0 +1,18 @@
+These scripts are used to generate and verify overlayable.xml files.
+
+Sample invocations (Media Center as an example).
+
+To generate:
+PROJECT_TOP=$ANDROID_BUILD_TOP/packages/apps/Car/Media
+python $ANDROID_BUILD_TOP/packages/apps/Car/libs/tools/rro/generate-overlayable.py \
+ -n CarMediaApp \
+ -r $PROJECT_TOP/res \
+ -e $PROJECT_TOP/res/values/overlayable.xml $PROJECT_TOP/res/xml/automotive_app_desc.xml \
+ -o $PROJECT_TOP/res/values/overlayable.xml
+
+To verify:
+PROJECT_TOP=$ANDROID_BUILD_TOP/packages/apps/Car/Media
+python $ANDROID_BUILD_TOP/packages/apps/Car/libs/tools/rro/verify-overlayable.py \
+ -r $PROJECT_TOP/res \
+ -e $PROJECT_TOP/res/values/overlayable.xml $PROJECT_TOP/res/xml/automotive_app_desc.xml \
+ -o $PROJECT_TOP/res/values/overlayable.xml
diff --git a/tools/rro/generate-overlayable.py b/tools/rro/generate-overlayable.py
new file mode 100755
index 0000000..2f76a2d
--- /dev/null
+++ b/tools/rro/generate-overlayable.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# 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.
+
+import argparse
+import sys
+from resource_utils import get_all_resources, Resource
+from datetime import datetime
+import lxml.etree as etree
+if sys.version_info[0] != 3:
+ print("Must use python 3")
+ sys.exit(1)
+
+COPYRIGHT_STR = """ Copyright (C) %s The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.""" % (datetime.today().strftime("%Y"))
+
+AUTOGENERATION_NOTICE_STR = """
+THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY.
+REGENERATE USING packages/apps/Car/libs/tools/rro/generate-overlayable.py
+"""
+
+"""
+Script used to update the 'overlayable.xml' file.
+"""
+def main():
+ parser = argparse.ArgumentParser(description='Generate overlayable.xml.')
+ optional_args = parser.add_argument_group('optional arguments')
+ optional_args.add_argument('-t', '--policyType', default='system|product|signature', help='Policy type for the overlay - delimited by |')
+ optional_args.add_argument('-e', '--excludeFiles', nargs='*', help='File paths (absolute or relative to cwd) that should be excluded when generating overlayable.xml')
+ optional_args.add_argument('-o', '--outputFile', default='', help='Output file path (absolute or relative to cwd). If empty, output to stdout')
+ required_args = parser.add_argument_group('required arguments')
+ required_args.add_argument('-n', '--targetName', help='Overlayable name for the overlay.', required=True)
+ required_args.add_argument('-r', '--resourcePath', help='Path to resource directory (absolute or relative to cwd)', required=True)
+ args = parser.parse_args()
+
+ resources = get_all_resources(args.resourcePath, args.excludeFiles)
+ generate_overlayable_file(resources, args.targetName, args.policyType, args.outputFile)
+
+def generate_overlayable_file(resources, target_name, policy_type, output_file):
+ resources = sorted(resources, key=lambda x: x.type + x.name)
+ root = etree.Element('resources')
+ root.addprevious(etree.Comment(COPYRIGHT_STR))
+ root.addprevious(etree.Comment(AUTOGENERATION_NOTICE_STR))
+ overlayable = etree.SubElement(root, 'overlayable')
+ overlayable.set('name', target_name)
+ policy = etree.SubElement(overlayable, 'policy')
+ policy.set('type', policy_type)
+ for resource in resources:
+ item = etree.SubElement(policy, 'item')
+ item.set('type', resource.type)
+ item.set('name', resource.name)
+ data = etree.ElementTree(root)
+ if not output_file:
+ print(etree.tostring(data, pretty_print=True, xml_declaration=True).decode())
+ else:
+ with open(output_file, 'wb') as f:
+ data.write(f, pretty_print=True, xml_declaration=True, encoding='utf-8')
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/rro/generate-overlays.py b/tools/rro/generate-overlays.py
new file mode 100755
index 0000000..2479869
--- /dev/null
+++ b/tools/rro/generate-overlays.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+# 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.
+
+import argparse
+import sys
+from resource_utils import get_all_resources, get_androidx_resources, Resource
+from datetime import datetime
+import lxml.etree as etree
+if sys.version_info[0] != 3:
+ print("Must use python 3")
+ sys.exit(1)
+
+COPYRIGHT_STR = """ Copyright (C) %s The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.""" % (datetime.today().strftime("%Y"))
+
+AUTOGENERATION_NOTICE_STR = """
+THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY.
+REGENERATE USING packages/apps/Car/libs/tools/rro/generate-overlays.py
+"""
+
+"""
+Script used to update the 'overlayable.xml' file.
+"""
+def main():
+ parser = argparse.ArgumentParser(description="Generate overlayable.xml. This script assumes that all the RRO resources have the exact same name as the app resource they're overlaying.")
+ parser.add_argument('-o', '--outputFile', default='', help='Output file path. If empty, output to stdout')
+ parser.add_argument('-a', '--appResources', help="Path to the app's resource folder. If given, will be used to exclude any rro-specific resources from overlays.xml.")
+ required_args = parser.add_argument_group('Required arguments')
+ required_args.add_argument('-r', '--resourcePath', help="Path to the RRO's resource directory", required=True)
+ args = parser.parse_args()
+
+ resources = get_all_resources(args.resourcePath)
+ try:
+ resources.remove(Resource('overlays', 'xml'))
+ except KeyError:
+ pass
+
+ if args.appResources:
+ resources = resources.intersection(
+ get_all_resources(args.appResources).union(get_androidx_resources()))
+ generate_overlays_file(resources, args.outputFile)
+
+def generate_overlays_file(resources, output_file):
+ resources = sorted(resources, key=lambda x: x.type + x.name)
+ root = etree.Element('overlay')
+ root.addprevious(etree.Comment(COPYRIGHT_STR))
+ root.addprevious(etree.Comment(AUTOGENERATION_NOTICE_STR))
+ for resource in resources:
+ item = etree.SubElement(root, 'item')
+ item.set('target', f'{resource.type}/{resource.name}')
+ item.set('value', f'@{resource.type}/{resource.name}')
+ rootTree = etree.ElementTree(root)
+ if not output_file:
+ print(etree.tostring(rootTree, pretty_print=True, xml_declaration=True).decode())
+ else:
+ with open(output_file, 'wb') as f:
+ rootTree.write(f, pretty_print=True, xml_declaration=True, encoding='utf-8')
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/rro/resource_utils.py b/tools/rro/resource_utils.py
new file mode 100644
index 0000000..5f35e1a
--- /dev/null
+++ b/tools/rro/resource_utils.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+# 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.
+
+import os
+import re
+import lxml.etree as etree
+
+class ResourceLocation:
+ def __init__(self, file, line=None):
+ self.file = file
+ self.line = line
+ def __str__(self):
+ if self.line is not None:
+ return self.file + ':' + str(self.line)
+ else:
+ return self.file
+
+class Resource:
+ def __init__(self, name, type, location=None):
+ self.name = name
+ self.type = type
+ self.locations = []
+ if location is not None:
+ self.locations.append(location)
+ def __eq__(self, other):
+ if isinstance(other, _Grab):
+ return other == self
+ return self.name == other.name and self.type == other.type
+ def __ne__(self, other):
+ if isinstance(other, _Grab):
+ return other != self
+ return self.name != other.name or self.type != other.type
+ def __hash__(self):
+ return hash((self.name, self.type))
+ def __str__(self):
+ result = ''
+ for location in self.locations:
+ result += str(location) + ': '
+ result += '<'+self.type+' name="'+self.name+'"'
+ return result + '>'
+ def __repr__(self):
+ return str(self)
+
+def get_all_resources(resDir, excluded_resource_files=[]):
+ excluded_resource_files = [os.path.abspath(file) for file in excluded_resource_files]
+ allResDirs = [f for f in os.listdir(resDir) if os.path.isdir(os.path.join(resDir, f))]
+ valuesDirs = [f for f in allResDirs if f.startswith('values')]
+ fileDirs = [f for f in allResDirs if not f.startswith('values')]
+ resources = set()
+ # Get the filenames of the all the files in all the fileDirs
+ for dir in fileDirs:
+ type = dir.split('-')[0]
+ for file in os.listdir(os.path.join(resDir, dir)):
+ filePath = os.path.abspath(os.path.join(resDir, dir, file))
+ if file.endswith('.xml') and filePath not in excluded_resource_files:
+ add_resource_to_set(resources,
+ Resource(file[:-4], type,
+ ResourceLocation(os.path.join(resDir, dir, file))))
+ if dir.startswith("layout"):
+ for resource in get_ids_from_layout_file(os.path.join(resDir, dir, file)):
+ add_resource_to_set(resources, resource)
+ for dir in valuesDirs:
+ for file in os.listdir(os.path.join(resDir, dir)):
+ filePath = os.path.abspath(os.path.join(resDir, dir, file))
+ if file.endswith('.xml') and filePath not in excluded_resource_files:
+ for resource in get_resources_from_single_file(os.path.join(resDir, dir, file),
+ dir != "values"):
+ add_resource_to_set(resources, resource)
+ return resources
+
+def get_ids_from_layout_file(filename):
+ result = set()
+ with open(filename, 'r') as file:
+ r = re.compile("@\+id/([a-zA-Z0-9_]+)")
+ for i in r.findall(file.read()):
+ add_resource_to_set(result, Resource(i, 'id', ResourceLocation(filename)))
+ return result
+
+def get_resources_from_single_file(filename, ignore_strings=False):
+ doc = etree.parse(filename)
+ root = doc.getroot()
+ result = set()
+ for resource in root:
+ if resource.tag is etree.Comment:
+ continue
+ if resource.tag == 'declare-styleable':
+ for attr in resource:
+ resName = attr.get('name')
+ # Skip resources beginning with 'android:' as they are part of the framework
+ # resources. This script finds only the app's resources.
+ if resName is None or resName.startswith('android:'):
+ continue
+ resType = "attr"
+ add_resource_to_set(result, Resource(resName, resType, ResourceLocation(filename, attr.sourceline)))
+ continue
+ resName = resource.get('name')
+ resType = resource.tag
+ if resType == "string-array" or resType == "integer-array":
+ resType = "array"
+ if resource.tag == 'item' or resource.tag == 'public':
+ resType = resource.get('type')
+ if (resType == 'string' or resType == 'plurals') and ignore_strings:
+ continue
+ if resType == 'overlayable':
+ for policy in resource:
+ for overlayable in policy:
+ resName = overlayable.get('name')
+ resType = overlayable.get('type')
+ add_resource_to_set(result, Resource(resName, resType,
+ ResourceLocation(filename, resource.sourceline)))
+ else:
+ add_resource_to_set(result, Resource(resName, resType,
+ ResourceLocation(filename, resource.sourceline)))
+ return result
+
+# Used to get objects out of sets
+class _Grab:
+ def __init__(self, value):
+ self.search_value = value
+ def __hash__(self):
+ return hash(self.search_value)
+ def __eq__(self, other):
+ if self.search_value == other:
+ self.actual_value = other
+ return True
+ return False
+
+def add_resource_to_set(resourceset, resource):
+ if (resource.name == None):
+ return
+ grabber = _Grab(resource)
+ if grabber in resourceset:
+ grabber.actual_value.locations.extend(resource.locations)
+ else:
+ resourceset.update([resource])
+
+def merge_resources(set1, set2):
+ for resource in set2:
+ add_resource_to_set(set1, resource)
+
+def get_androidx_resources():
+ # source: https://android.googlesource.com/platform/frameworks/opt/sherpa/+/studio-3.0/constraintlayout/src/main/res/values/attrs.xml
+ resources = set()
+ add_resource_to_set(resources, Resource('layout_optimizationLevel', 'attr'))
+ add_resource_to_set(resources, Resource('constraintSet', 'attr'))
+ add_resource_to_set(resources, Resource('barrierDirection', 'attr'))
+ add_resource_to_set(resources, Resource('constraint_referenced_ids', 'attr'))
+ add_resource_to_set(resources, Resource('chainUseRtl', 'attr'))
+ add_resource_to_set(resources, Resource('title', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintGuide_begin', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintGuide_end', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintGuide_percent', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintLeft_toLeftOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintLeft_toRightOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintRight_toLeftOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintRight_toRightOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintTop_toTopOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintTop_toBottomOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintBottom_toTopOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintBottom_toBottomOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintBaseline_toBaselineOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintStart_toEndOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintStart_toStartOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintEnd_toStartOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintEnd_toEndOf', 'attr'))
+ add_resource_to_set(resources, Resource('layout_goneMarginLeft', 'attr'))
+ add_resource_to_set(resources, Resource('layout_goneMarginTop', 'attr'))
+ add_resource_to_set(resources, Resource('layout_goneMarginRight', 'attr'))
+ add_resource_to_set(resources, Resource('layout_goneMarginBottom', 'attr'))
+ add_resource_to_set(resources, Resource('layout_goneMarginStart', 'attr'))
+ add_resource_to_set(resources, Resource('layout_goneMarginEnd', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHorizontal_bias', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintVertical_bias', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintWidth_default', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHeight_default', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintWidth_min', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintWidth_max', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintWidth_percent', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHeight_min', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHeight_max', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHeight_percent', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintLeft_creator', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintTop_creator', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintRight_creator', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintBottom_creator', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintBaseline_creator', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintDimensionRatio', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHorizontal_weight', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintVertical_weight', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintHorizontal_chainStyle', 'attr'))
+ add_resource_to_set(resources, Resource('layout_constraintVertical_chainStyle', 'attr'))
+ add_resource_to_set(resources, Resource('layout_editor_absoluteX', 'attr'))
+ add_resource_to_set(resources, Resource('layout_editor_absoluteY', 'attr'))
+ return resources
diff --git a/tools/rro/verify-overlayable.py b/tools/rro/verify-overlayable.py
new file mode 100755
index 0000000..05feb51
--- /dev/null
+++ b/tools/rro/verify-overlayable.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+# 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.
+
+import argparse
+import sys
+from resource_utils import get_all_resources, get_resources_from_single_file, Resource
+
+if sys.version_info[0] != 3:
+ print("Must use python 3")
+ sys.exit(1)
+
+"""
+Script used to verify the 'overlayable.xml' file.
+"""
+def main():
+ parser = argparse.ArgumentParser(description='Verify overlayable.xml.')
+ optional_args = parser.add_argument_group('optional arguments')
+ optional_args.add_argument('-e', '--excludeFiles', nargs='*', help='File paths (absolute or relative to cwd) that should be excluded when generating overlayable.xml')
+ required_args = parser.add_argument_group('required arguments')
+ required_args.add_argument('-r', '--resourcePath', help='Path to resource directory (absolute or relative to cwd)', required=True)
+ required_args.add_argument('-o', '--overlayableFilePath', help='Filepath to overlayable.xml (absolute or relative to cwd).', required=True)
+ args = parser.parse_args()
+
+ resources = get_all_resources(args.resourcePath, args.excludeFiles)
+ old_mapping = get_resources_from_single_file(args.overlayableFilePath)
+ compare_resources(old_mapping, resources, args.overlayableFilePath)
+
+def compare_resources(old_mapping, new_mapping, res_public_file):
+ removed = old_mapping.difference(new_mapping)
+ added = new_mapping.difference(old_mapping)
+ if len(removed) > 0:
+ print('Resources removed:\n' + '\n'.join(map(lambda x: str(x), removed)))
+ if len(added) > 0:
+ print('Resources added:\n' + '\n'.join(map(lambda x: str(x), added)))
+ if len(added) + len(removed) > 0:
+ print("Some resource have been modified. If this is intentional please " +
+ "run 'python3 generate-overlayable.py' again and submit the new %s" % res_public_file)
+ print("Some projects may include $PROJECT_TOP/tools/generate-overlayable.sh which calls " +
+ "the above command with the appropriate command line arguments")
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()