diff options
author | Baligh Uddin <baligh@google.com> | 2022-03-09 14:47:16 +0000 |
---|---|---|
committer | Baligh Uddin <baligh@google.com> | 2022-03-09 14:47:16 +0000 |
commit | 947f53d11f959c867db2eb6aea7253b74067c356 (patch) | |
tree | 38bb886c03d63152125c0e86d6729af706931ccf | |
parent | 59bcf3c9997986c8b66fcfc2b10eb6871f476387 (diff) | |
parent | cf5bfbcac61fef1c13cbcfd738bd38b4bd248838 (diff) | |
download | systemlibs-947f53d11f959c867db2eb6aea7253b74067c356.tar.gz |
Merge history of car-libs into packages/apps/Car/systemlibs
BUG: 196593308
Test: TH
Change-Id: Ie5d62febd5e69d6541c2f2638a1193624f4b3d14
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 @@ -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 Binary files differnew file mode 100755 index 0000000..8f8aeaf --- /dev/null +++ b/androidx-car/androidx-car-resources.aar 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() |