diff options
153 files changed, 4952 insertions, 193 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1c01fc78e..5206504a8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -99,7 +99,8 @@ android:name="com.android.providers.media.photopicker.RemoteVideoPreviewProvider" android:process=":PhotoPicker" android:authorities="com.android.providers.media.remote_video_preview" - android:exported="false" /> + android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS" + android:exported="true" /> <!-- Don't initialise WorkManager by default at startup --> <provider diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt index 7dec7f604..628c65ed0 100644 --- a/apex/framework/api/current.txt +++ b/apex/framework/api/current.txt @@ -151,6 +151,7 @@ package android.provider { field public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel"; field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title"; field public static final String EXTRA_OUTPUT = "output"; + field @FlaggedApi("com.android.providers.media.flags.picker_pre_selection") public static final String EXTRA_PICKER_PRE_SELECTION_URIS = "android.provider.extra.PICKER_PRE_SELECTION_URIS"; field @FlaggedApi("com.android.providers.media.flags.picker_accent_color") public static final String EXTRA_PICK_IMAGES_ACCENT_COLOR = "android.provider.extra.PICK_IMAGES_ACCENT_COLOR"; field @FlaggedApi("com.android.providers.media.flags.pick_ordered_images") public static final String EXTRA_PICK_IMAGES_IN_ORDER = "android.provider.extra.PICK_IMAGES_IN_ORDER"; field @FlaggedApi("com.android.providers.media.flags.picker_default_tab") public static final String EXTRA_PICK_IMAGES_LAUNCH_TAB = "android.provider.extra.PICK_IMAGES_LAUNCH_TAB"; diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java index 2f7796163..7cb885bf7 100644 --- a/apex/framework/java/android/provider/MediaStore.java +++ b/apex/framework/java/android/provider/MediaStore.java @@ -1008,6 +1008,33 @@ public final class MediaStore { "android.provider.extra.MEDIA_CAPABILITIES_UID"; /** + * The name of an optional intent-extra used to specify URIs for pre-selection in photo picker + * opened with {@link MediaStore#ACTION_PICK_IMAGES} in multi-select mode. + * + * <p>Only MediaStore content URI(s) of the item(s) received as a result of + * {@link MediaStore#ACTION_PICK_IMAGES} action are accepted. The value of this intent-extra + * should be an ArrayList of type parcelables. Default value is null. Maximum number of URIs + * that can be accepted is limited by the value passed in + * {@link MediaStore#EXTRA_PICK_IMAGES_MAX} as part of the {@link MediaStore#ACTION_PICK_IMAGES} + * intent. In case the count of input URIs is greater than the limit then + * {@code IllegalArgumentException} is thrown.</p> + * + * <p>The provided list will be checked for permissions and authority. Any URI that is + * inaccessible, doesn't match the current authorities(local or cloud) or is invalid will be + * filtered out.</p> + * + * <p>The items corresponding to the URIs will appear selected when the photo picker is opened. + * In the case of {@link MediaStore#EXTRA_PICK_IMAGES_IN_ORDER} the chronological order of the + * input list will be used for ordered selection of the pre-selected items.</p> + * + * <p>This is not a mechanism to revoke permissions for items, i.e. de-selection of a + * pre-selected item by the user will not result in revocation of the grant.</p> + */ + @FlaggedApi("com.android.providers.media.flags.picker_pre_selection") + public static final String EXTRA_PICKER_PRE_SELECTION_URIS = + "android.provider.extra.PICKER_PRE_SELECTION_URIS"; + + /** * Flag used to set file mode in bundle for opening a document. * * @hide diff --git a/photopicker/Android.bp b/photopicker/Android.bp index c79505a2b..a26651966 100644 --- a/photopicker/Android.bp +++ b/photopicker/Android.bp @@ -20,6 +20,7 @@ android_library { "androidx.compose.foundation_foundation", "androidx.compose.material3_material3", "androidx.compose.material3_material3-window-size-class", + "androidx.compose.material_material-icons-extended", "androidx.compose.runtime_runtime", "androidx.compose.ui_ui", "androidx.core_core-ktx", @@ -64,6 +65,10 @@ android_app { static_libs: [ "PhotopickerLib", ], + optimize: { + // Needed for removing unused icons from material-icons-extended + shrink_resources: true, + }, plugins: [], kotlincflags: ["-Xjvm-default=all"], certificate: "media", diff --git a/photopicker/res/values-af/feature_preview_strings.xml b/photopicker/res/values-af/feature_preview_strings.xml new file mode 100644 index 000000000..5a47c1e29 --- /dev/null +++ b/photopicker/res/values-af/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Kies"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Ontkies"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Voorbeskou"</string> +</resources> diff --git a/photopicker/res/values-am/feature_preview_strings.xml b/photopicker/res/values-am/feature_preview_strings.xml new file mode 100644 index 000000000..2d1ca0eb1 --- /dev/null +++ b/photopicker/res/values-am/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ምረጥ"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"አትምረጥ"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ቅድመ-ዕይታ"</string> +</resources> diff --git a/photopicker/res/values-ar/feature_preview_strings.xml b/photopicker/res/values-ar/feature_preview_strings.xml new file mode 100644 index 000000000..d5ebc7340 --- /dev/null +++ b/photopicker/res/values-ar/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"اختيار"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"إلغاء الاختيار"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"معاينة"</string> +</resources> diff --git a/photopicker/res/values-as/feature_preview_strings.xml b/photopicker/res/values-as/feature_preview_strings.xml new file mode 100644 index 000000000..7d0a0f37b --- /dev/null +++ b/photopicker/res/values-as/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"বাছনি কৰক"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"বাছনিৰ পৰা আঁতৰাওক"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"পূৰ্বদৰ্শন কৰক"</string> +</resources> diff --git a/photopicker/res/values-az/feature_preview_strings.xml b/photopicker/res/values-az/feature_preview_strings.xml new file mode 100644 index 000000000..deea70501 --- /dev/null +++ b/photopicker/res/values-az/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Seçin"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Seçimi ləğv edin"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Önizləmə"</string> +</resources> diff --git a/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml b/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml new file mode 100644 index 000000000..9d01762e0 --- /dev/null +++ b/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Izaberi"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Opozovi izbor"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string> +</resources> diff --git a/photopicker/res/values-be/core_strings.xml b/photopicker/res/values-be/core_strings.xml index 43ac3fe01..b8ac4ee4d 100644 --- a/photopicker/res/values-be/core_strings.xml +++ b/photopicker/res/values-be/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Медыяфайл"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Выбрана"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Дадаць <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фота"</string> </resources> diff --git a/photopicker/res/values-be/feature_preview_strings.xml b/photopicker/res/values-be/feature_preview_strings.xml new file mode 100644 index 000000000..325fd1efb --- /dev/null +++ b/photopicker/res/values-be/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Выбраць"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Скасаваць выбар"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Перадпрагляд"</string> +</resources> diff --git a/photopicker/res/values-bg/core_strings.xml b/photopicker/res/values-bg/core_strings.xml index 41f6098a1..2178fcee8 100644 --- a/photopicker/res/values-bg/core_strings.xml +++ b/photopicker/res/values-bg/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Мултимедия"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Избрано"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Добавяне на <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Снимки"</string> </resources> diff --git a/photopicker/res/values-bg/feature_preview_strings.xml b/photopicker/res/values-bg/feature_preview_strings.xml new file mode 100644 index 000000000..106c00a50 --- /dev/null +++ b/photopicker/res/values-bg/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Избор"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Премахване на избора"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Визуализация"</string> +</resources> diff --git a/photopicker/res/values-bn/core_strings.xml b/photopicker/res/values-bn/core_strings.xml index aaeb66746..e5063c718 100644 --- a/photopicker/res/values-bn/core_strings.xml +++ b/photopicker/res/values-bn/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"মিডিয়া"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"বেছে নেওয়া হয়েছে"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>টি যোগ করুন"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ফটো"</string> </resources> diff --git a/photopicker/res/values-bn/feature_preview_strings.xml b/photopicker/res/values-bn/feature_preview_strings.xml new file mode 100644 index 000000000..cf837e6fa --- /dev/null +++ b/photopicker/res/values-bn/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"বেছে নিন"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"বাদ দিন"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"প্রিভিউ দেখুন"</string> +</resources> diff --git a/photopicker/res/values-bs/core_strings.xml b/photopicker/res/values-bs/core_strings.xml index b399cfc0a..a48fcc97d 100644 --- a/photopicker/res/values-bs/core_strings.xml +++ b/photopicker/res/values-bs/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Mediji"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Odabrano"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string> </resources> diff --git a/photopicker/res/values-bs/feature_preview_strings.xml b/photopicker/res/values-bs/feature_preview_strings.xml new file mode 100644 index 000000000..a54521c29 --- /dev/null +++ b/photopicker/res/values-bs/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Odaberi"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Poništi odabir"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string> +</resources> diff --git a/photopicker/res/values-ca/feature_preview_strings.xml b/photopicker/res/values-ca/feature_preview_strings.xml new file mode 100644 index 000000000..69d2d70fa --- /dev/null +++ b/photopicker/res/values-ca/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecciona"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desselecciona"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previsualitza"</string> +</resources> diff --git a/photopicker/res/values-cs/feature_preview_strings.xml b/photopicker/res/values-cs/feature_preview_strings.xml new file mode 100644 index 000000000..805ea3f3a --- /dev/null +++ b/photopicker/res/values-cs/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Vybrat"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Zrušit výběr"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Zobrazit náhled"</string> +</resources> diff --git a/photopicker/res/values-da/core_strings.xml b/photopicker/res/values-da/core_strings.xml index 0d72f5497..04097a9a6 100644 --- a/photopicker/res/values-da/core_strings.xml +++ b/photopicker/res/values-da/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Medier"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Valgt"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Tilføj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-da/feature_preview_strings.xml b/photopicker/res/values-da/feature_preview_strings.xml new file mode 100644 index 000000000..65b723771 --- /dev/null +++ b/photopicker/res/values-da/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Vælg"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Fravælg"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Forhåndsvisning"</string> +</resources> diff --git a/photopicker/res/values-de/core_strings.xml b/photopicker/res/values-de/core_strings.xml index efababbbb..327005238 100644 --- a/photopicker/res/values-de/core_strings.xml +++ b/photopicker/res/values-de/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Medium"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Ausgewählt"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> hinzufügen"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-de/feature_preview_strings.xml b/photopicker/res/values-de/feature_preview_strings.xml new file mode 100644 index 000000000..4aa8e5691 --- /dev/null +++ b/photopicker/res/values-de/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Auswählen"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Auswahl aufheben"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vorschau anzeigen"</string> +</resources> diff --git a/photopicker/res/values-el/feature_preview_strings.xml b/photopicker/res/values-el/feature_preview_strings.xml new file mode 100644 index 000000000..f75d47c3f --- /dev/null +++ b/photopicker/res/values-el/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Επιλογή"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Αποεπιλογή"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Προεπισκόπηση"</string> +</resources> diff --git a/photopicker/res/values-en-rAU/feature_preview_strings.xml b/photopicker/res/values-en-rAU/feature_preview_strings.xml new file mode 100644 index 000000000..831b68b06 --- /dev/null +++ b/photopicker/res/values-en-rAU/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> +</resources> diff --git a/photopicker/res/values-en-rCA/feature_preview_strings.xml b/photopicker/res/values-en-rCA/feature_preview_strings.xml new file mode 100644 index 000000000..831b68b06 --- /dev/null +++ b/photopicker/res/values-en-rCA/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> +</resources> diff --git a/photopicker/res/values-en-rGB/feature_preview_strings.xml b/photopicker/res/values-en-rGB/feature_preview_strings.xml new file mode 100644 index 000000000..831b68b06 --- /dev/null +++ b/photopicker/res/values-en-rGB/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> +</resources> diff --git a/photopicker/res/values-en-rIN/feature_preview_strings.xml b/photopicker/res/values-en-rIN/feature_preview_strings.xml new file mode 100644 index 000000000..831b68b06 --- /dev/null +++ b/photopicker/res/values-en-rIN/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> +</resources> diff --git a/photopicker/res/values-en-rXC/feature_preview_strings.xml b/photopicker/res/values-en-rXC/feature_preview_strings.xml new file mode 100644 index 000000000..06d7203d5 --- /dev/null +++ b/photopicker/res/values-en-rXC/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> +</resources> diff --git a/photopicker/res/values-es-rUS/core_strings.xml b/photopicker/res/values-es-rUS/core_strings.xml index 35b330f31..5c4638cef 100644 --- a/photopicker/res/values-es-rUS/core_strings.xml +++ b/photopicker/res/values-es-rUS/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Contenido multimedia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Seleccionado"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Agregar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-es-rUS/feature_preview_strings.xml b/photopicker/res/values-es-rUS/feature_preview_strings.xml new file mode 100644 index 000000000..6218b7748 --- /dev/null +++ b/photopicker/res/values-es-rUS/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleccionar"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Anular selección"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vista previa"</string> +</resources> diff --git a/photopicker/res/values-es/core_strings.xml b/photopicker/res/values-es/core_strings.xml index f7090d709..76bad3437 100644 --- a/photopicker/res/values-es/core_strings.xml +++ b/photopicker/res/values-es/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Contenido multimedia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Seleccionado"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Añadir <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-es/feature_preview_strings.xml b/photopicker/res/values-es/feature_preview_strings.xml new file mode 100644 index 000000000..426af3f85 --- /dev/null +++ b/photopicker/res/values-es/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleccionar"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vista previa"</string> +</resources> diff --git a/photopicker/res/values-et/core_strings.xml b/photopicker/res/values-et/core_strings.xml index e13d0b3b8..d8c463c44 100644 --- a/photopicker/res/values-et/core_strings.xml +++ b/photopicker/res/values-et/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Meedia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Valitud"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Lisa <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotod"</string> </resources> diff --git a/photopicker/res/values-et/feature_preview_strings.xml b/photopicker/res/values-et/feature_preview_strings.xml new file mode 100644 index 000000000..ddca7f020 --- /dev/null +++ b/photopicker/res/values-et/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Vali"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Tühista valik"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Eelvaade"</string> +</resources> diff --git a/photopicker/res/values-eu/core_strings.xml b/photopicker/res/values-eu/core_strings.xml index d363dc481..31e934c4a 100644 --- a/photopicker/res/values-eu/core_strings.xml +++ b/photopicker/res/values-eu/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Multimedia-edukia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Hautatuta"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Gehitu <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Argazkiak"</string> </resources> diff --git a/photopicker/res/values-eu/feature_preview_strings.xml b/photopicker/res/values-eu/feature_preview_strings.xml new file mode 100644 index 000000000..bc265ecc1 --- /dev/null +++ b/photopicker/res/values-eu/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Hautatu"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desautatu"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Aurreikusi"</string> +</resources> diff --git a/photopicker/res/values-fa/feature_preview_strings.xml b/photopicker/res/values-fa/feature_preview_strings.xml new file mode 100644 index 000000000..07c8f60aa --- /dev/null +++ b/photopicker/res/values-fa/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"انتخاب کردن"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"لغو انتخاب کردن"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"پیشنمایش"</string> +</resources> diff --git a/photopicker/res/values-fi/core_strings.xml b/photopicker/res/values-fi/core_strings.xml index e4b5f11a9..6559cc5f6 100644 --- a/photopicker/res/values-fi/core_strings.xml +++ b/photopicker/res/values-fi/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Valittu"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Lisää <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Kuvat"</string> </resources> diff --git a/photopicker/res/values-fi/feature_preview_strings.xml b/photopicker/res/values-fi/feature_preview_strings.xml new file mode 100644 index 000000000..508cd68dc --- /dev/null +++ b/photopicker/res/values-fi/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Valitse"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Poista valinta"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Esikatsele"</string> +</resources> diff --git a/photopicker/res/values-fr-rCA/feature_preview_strings.xml b/photopicker/res/values-fr-rCA/feature_preview_strings.xml new file mode 100644 index 000000000..101a51e34 --- /dev/null +++ b/photopicker/res/values-fr-rCA/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Sélectionner"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Désélectionner"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Aperçu"</string> +</resources> diff --git a/photopicker/res/values-fr/feature_preview_strings.xml b/photopicker/res/values-fr/feature_preview_strings.xml new file mode 100644 index 000000000..62c480ab5 --- /dev/null +++ b/photopicker/res/values-fr/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Sélectionner"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Désélectionner"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Prévisualiser"</string> +</resources> diff --git a/photopicker/res/values-gl/core_strings.xml b/photopicker/res/values-gl/core_strings.xml index a532ba46d..faf034bf7 100644 --- a/photopicker/res/values-gl/core_strings.xml +++ b/photopicker/res/values-gl/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Multimedia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Elemento seleccionado"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Engadir <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-gl/feature_preview_strings.xml b/photopicker/res/values-gl/feature_preview_strings.xml new file mode 100644 index 000000000..c9632f861 --- /dev/null +++ b/photopicker/res/values-gl/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleccionar"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Anular selección"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previsualizar"</string> +</resources> diff --git a/photopicker/res/values-gu/core_strings.xml b/photopicker/res/values-gu/core_strings.xml index 943251f38..f57db6423 100644 --- a/photopicker/res/values-gu/core_strings.xml +++ b/photopicker/res/values-gu/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"મીડિયા"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"પસંદ કર્યું છે"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ઉમેરો"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> </resources> diff --git a/photopicker/res/values-gu/feature_preview_strings.xml b/photopicker/res/values-gu/feature_preview_strings.xml new file mode 100644 index 000000000..debb55472 --- /dev/null +++ b/photopicker/res/values-gu/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"પસંદ કરો"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"નાપસંદ કરો"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"પ્રીવ્યૂ કરો"</string> +</resources> diff --git a/photopicker/res/values-hi/core_strings.xml b/photopicker/res/values-hi/core_strings.xml index 0b043e164..33c98706d 100644 --- a/photopicker/res/values-hi/core_strings.xml +++ b/photopicker/res/values-hi/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"मीडिया"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"चुना गया"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> जोड़ें"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"फ़ोटो"</string> </resources> diff --git a/photopicker/res/values-hi/feature_preview_strings.xml b/photopicker/res/values-hi/feature_preview_strings.xml new file mode 100644 index 000000000..4587e9131 --- /dev/null +++ b/photopicker/res/values-hi/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"चुनें"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"चुने हुए को हटाएं"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"झलक"</string> +</resources> diff --git a/photopicker/res/values-hr/core_strings.xml b/photopicker/res/values-hr/core_strings.xml index 0c1899080..bce037a4a 100644 --- a/photopicker/res/values-hr/core_strings.xml +++ b/photopicker/res/values-hr/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Mediji"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Odabrano"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string> </resources> diff --git a/photopicker/res/values-hr/feature_preview_strings.xml b/photopicker/res/values-hr/feature_preview_strings.xml new file mode 100644 index 000000000..a54521c29 --- /dev/null +++ b/photopicker/res/values-hr/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Odaberi"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Poništi odabir"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string> +</resources> diff --git a/photopicker/res/values-hu/feature_preview_strings.xml b/photopicker/res/values-hu/feature_preview_strings.xml new file mode 100644 index 000000000..e3a4a84be --- /dev/null +++ b/photopicker/res/values-hu/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Kijelölés"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Kijelölés megszüntetése"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Előnézet"</string> +</resources> diff --git a/photopicker/res/values-hy/core_strings.xml b/photopicker/res/values-hy/core_strings.xml index 7de23f7f8..696033f5d 100644 --- a/photopicker/res/values-hy/core_strings.xml +++ b/photopicker/res/values-hy/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Մեդիա"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Ընտրված է"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Ավելացնել <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Լուսանկարներ"</string> </resources> diff --git a/photopicker/res/values-hy/feature_preview_strings.xml b/photopicker/res/values-hy/feature_preview_strings.xml new file mode 100644 index 000000000..2af22f262 --- /dev/null +++ b/photopicker/res/values-hy/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Ընտրել"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Չեղարկել ընտրությունը"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Նախադիտել"</string> +</resources> diff --git a/photopicker/res/values-in/feature_preview_strings.xml b/photopicker/res/values-in/feature_preview_strings.xml new file mode 100644 index 000000000..50fb82da8 --- /dev/null +++ b/photopicker/res/values-in/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Pilih"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Batalkan pilihan"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pratinjau"</string> +</resources> diff --git a/photopicker/res/values-is/feature_preview_strings.xml b/photopicker/res/values-is/feature_preview_strings.xml new file mode 100644 index 000000000..4e56a7a4c --- /dev/null +++ b/photopicker/res/values-is/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Velja"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Afvelja"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Forskoða"</string> +</resources> diff --git a/photopicker/res/values-it/feature_preview_strings.xml b/photopicker/res/values-it/feature_preview_strings.xml new file mode 100644 index 000000000..1cd965268 --- /dev/null +++ b/photopicker/res/values-it/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleziona"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deseleziona"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Anteprima"</string> +</resources> diff --git a/photopicker/res/values-iw/feature_preview_strings.xml b/photopicker/res/values-iw/feature_preview_strings.xml new file mode 100644 index 000000000..5302972d5 --- /dev/null +++ b/photopicker/res/values-iw/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"בחירה"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ביטול הבחירה"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"תצוגה מקדימה"</string> +</resources> diff --git a/photopicker/res/values-ja/feature_preview_strings.xml b/photopicker/res/values-ja/feature_preview_strings.xml new file mode 100644 index 000000000..06409c554 --- /dev/null +++ b/photopicker/res/values-ja/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"選択"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"選択を解除"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"プレビュー"</string> +</resources> diff --git a/photopicker/res/values-ka/feature_preview_strings.xml b/photopicker/res/values-ka/feature_preview_strings.xml new file mode 100644 index 000000000..b125bbb22 --- /dev/null +++ b/photopicker/res/values-ka/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"არჩევა"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"არჩევის გაუქმება"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"გადახედვა"</string> +</resources> diff --git a/photopicker/res/values-kk/core_strings.xml b/photopicker/res/values-kk/core_strings.xml index b8daa97f9..dc804233b 100644 --- a/photopicker/res/values-kk/core_strings.xml +++ b/photopicker/res/values-kk/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Meдиа"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Таңдалды"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> қосу"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> </resources> diff --git a/photopicker/res/values-kk/feature_preview_strings.xml b/photopicker/res/values-kk/feature_preview_strings.xml new file mode 100644 index 000000000..525bae620 --- /dev/null +++ b/photopicker/res/values-kk/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Таңдау"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Таңдаудан алу"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Алдын ала көру"</string> +</resources> diff --git a/photopicker/res/values-km/feature_preview_strings.xml b/photopicker/res/values-km/feature_preview_strings.xml new file mode 100644 index 000000000..7c7bf8fbe --- /dev/null +++ b/photopicker/res/values-km/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ជ្រើសរើស"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ដកការជ្រើសរើស"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"មើលសាកល្បង"</string> +</resources> diff --git a/photopicker/res/values-kn/feature_preview_strings.xml b/photopicker/res/values-kn/feature_preview_strings.xml new file mode 100644 index 000000000..caaa12a20 --- /dev/null +++ b/photopicker/res/values-kn/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ಆಯ್ಕೆ ಮಾಡಿ"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ಆಯ್ಕೆ ರದ್ದುಮಾಡಿ"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ಪೂರ್ವವೀಕ್ಷಣೆ"</string> +</resources> diff --git a/photopicker/res/values-ko/core_strings.xml b/photopicker/res/values-ko/core_strings.xml index 41672d630..69bbaa275 100644 --- a/photopicker/res/values-ko/core_strings.xml +++ b/photopicker/res/values-ko/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"미디어"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"선택됨"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> 추가"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"사진"</string> </resources> diff --git a/photopicker/res/values-ko/feature_preview_strings.xml b/photopicker/res/values-ko/feature_preview_strings.xml new file mode 100644 index 000000000..baaf7e0b9 --- /dev/null +++ b/photopicker/res/values-ko/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"선택"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"선택 해제"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"미리보기"</string> +</resources> diff --git a/photopicker/res/values-ky/core_strings.xml b/photopicker/res/values-ky/core_strings.xml index be5771936..2a4ed25cb 100644 --- a/photopicker/res/values-ky/core_strings.xml +++ b/photopicker/res/values-ky/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Медиа"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Тандалды"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> кошуу"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Сүрөттөр"</string> </resources> diff --git a/photopicker/res/values-ky/feature_preview_strings.xml b/photopicker/res/values-ky/feature_preview_strings.xml new file mode 100644 index 000000000..ed4480f33 --- /dev/null +++ b/photopicker/res/values-ky/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Тандоо"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Тандоодон чыгаруу"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Алдын ала көрүү"</string> +</resources> diff --git a/photopicker/res/values-lo/feature_preview_strings.xml b/photopicker/res/values-lo/feature_preview_strings.xml new file mode 100644 index 000000000..909dbd44e --- /dev/null +++ b/photopicker/res/values-lo/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ເລືອກ"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ເຊົາເລືອກ"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ຕົວຢ່າງ"</string> +</resources> diff --git a/photopicker/res/values-lt/feature_preview_strings.xml b/photopicker/res/values-lt/feature_preview_strings.xml new file mode 100644 index 000000000..543ea6156 --- /dev/null +++ b/photopicker/res/values-lt/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Pasirinkti"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Panaikinti pasirinkimą"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Peržiūrėti"</string> +</resources> diff --git a/photopicker/res/values-lv/feature_preview_strings.xml b/photopicker/res/values-lv/feature_preview_strings.xml new file mode 100644 index 000000000..90bc81cf1 --- /dev/null +++ b/photopicker/res/values-lv/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Atlasīt"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Noņemt atlasi"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Priekšskatīt"</string> +</resources> diff --git a/photopicker/res/values-mk/core_strings.xml b/photopicker/res/values-mk/core_strings.xml index c4a3d8401..1228652e0 100644 --- a/photopicker/res/values-mk/core_strings.xml +++ b/photopicker/res/values-mk/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Аудиовизуелни содржини"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Избрано"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Додајте „<xliff:g id="COUNT">(%1$s)</xliff:g>“"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фотографии"</string> </resources> diff --git a/photopicker/res/values-mk/feature_preview_strings.xml b/photopicker/res/values-mk/feature_preview_strings.xml new file mode 100644 index 000000000..1e81ee9b3 --- /dev/null +++ b/photopicker/res/values-mk/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Избери"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Поништи го изборот"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Прикажи"</string> +</resources> diff --git a/photopicker/res/values-ml/feature_preview_strings.xml b/photopicker/res/values-ml/feature_preview_strings.xml new file mode 100644 index 000000000..852726050 --- /dev/null +++ b/photopicker/res/values-ml/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"തിരഞ്ഞെടുക്കുക"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"തിരഞ്ഞെടുത്തത് മാറ്റുക"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"പ്രിവ്യൂ ചെയ്യുക"</string> +</resources> diff --git a/photopicker/res/values-mn/core_strings.xml b/photopicker/res/values-mn/core_strings.xml index d6e400cae..276403ddb 100644 --- a/photopicker/res/values-mn/core_strings.xml +++ b/photopicker/res/values-mn/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Медиа"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Сонгосон"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>-г нэмэх"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Зураг"</string> </resources> diff --git a/photopicker/res/values-mn/feature_preview_strings.xml b/photopicker/res/values-mn/feature_preview_strings.xml new file mode 100644 index 000000000..a99a515a2 --- /dev/null +++ b/photopicker/res/values-mn/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Сонгох"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Сонголтыг цуцлах"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Урьдчилан үзэх"</string> +</resources> diff --git a/photopicker/res/values-mr/feature_preview_strings.xml b/photopicker/res/values-mr/feature_preview_strings.xml new file mode 100644 index 000000000..d79b17531 --- /dev/null +++ b/photopicker/res/values-mr/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"निवडा"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"निवड रद्द करा"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"पूर्वावलोकन करा"</string> +</resources> diff --git a/photopicker/res/values-ms/feature_preview_strings.xml b/photopicker/res/values-ms/feature_preview_strings.xml new file mode 100644 index 000000000..dad1858c8 --- /dev/null +++ b/photopicker/res/values-ms/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Pilih"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Nyahpilih"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pratonton"</string> +</resources> diff --git a/photopicker/res/values-my/core_strings.xml b/photopicker/res/values-my/core_strings.xml index 25f9fb609..30da21934 100644 --- a/photopicker/res/values-my/core_strings.xml +++ b/photopicker/res/values-my/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"မီဒီယာ"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"ရွေးထားသည်"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ပုံ ထည့်ရန်"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ဓာတ်ပုံများ"</string> </resources> diff --git a/photopicker/res/values-my/feature_preview_strings.xml b/photopicker/res/values-my/feature_preview_strings.xml new file mode 100644 index 000000000..4612fd1e8 --- /dev/null +++ b/photopicker/res/values-my/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ရွေးရန်"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"မရွေးတော့ရန်"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"အစမ်းကြည့်ရှုရန်"</string> +</resources> diff --git a/photopicker/res/values-nb/feature_preview_strings.xml b/photopicker/res/values-nb/feature_preview_strings.xml new file mode 100644 index 000000000..6343fd8d7 --- /dev/null +++ b/photopicker/res/values-nb/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Merk"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Fjern merkingen"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Se forhåndsvisning"</string> +</resources> diff --git a/photopicker/res/values-ne/feature_preview_strings.xml b/photopicker/res/values-ne/feature_preview_strings.xml new file mode 100644 index 000000000..656e9e900 --- /dev/null +++ b/photopicker/res/values-ne/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"चयन गर्नुहोस्"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"चयन रद्द गर्नुहोस्"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"प्रिभ्यू गर्नुहोस्"</string> +</resources> diff --git a/photopicker/res/values-nl/core_strings.xml b/photopicker/res/values-nl/core_strings.xml index 47c0e83e6..2b556d6c8 100644 --- a/photopicker/res/values-nl/core_strings.xml +++ b/photopicker/res/values-nl/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Geselecteerd"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> toevoegen"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto\'s"</string> </resources> diff --git a/photopicker/res/values-nl/feature_preview_strings.xml b/photopicker/res/values-nl/feature_preview_strings.xml new file mode 100644 index 000000000..c05927e10 --- /dev/null +++ b/photopicker/res/values-nl/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecteren"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselecteren"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Voorbeeld"</string> +</resources> diff --git a/photopicker/res/values-or/core_strings.xml b/photopicker/res/values-or/core_strings.xml index 83e33b13a..bf7abb2a9 100644 --- a/photopicker/res/values-or/core_strings.xml +++ b/photopicker/res/values-or/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"ମିଡିଆ"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"ଚୟନ କରାଯାଇଛି"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ଯୋଗ କରନ୍ତୁ"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> </resources> diff --git a/photopicker/res/values-or/feature_preview_strings.xml b/photopicker/res/values-or/feature_preview_strings.xml new file mode 100644 index 000000000..f49a5e443 --- /dev/null +++ b/photopicker/res/values-or/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ଚୟନ କରନ୍ତୁ"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ଅଚୟନ କରନ୍ତୁ"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ପ୍ରିଭ୍ୟୁ"</string> +</resources> diff --git a/photopicker/res/values-pa/feature_preview_strings.xml b/photopicker/res/values-pa/feature_preview_strings.xml new file mode 100644 index 000000000..a44d37475 --- /dev/null +++ b/photopicker/res/values-pa/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ਚੁਣੋ"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ਅਣ-ਚੁਣਿਆ ਕਰੋ"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ਪੂਰਵ-ਝਲਕ ਦੇਖੋ"</string> +</resources> diff --git a/photopicker/res/values-pl/feature_preview_strings.xml b/photopicker/res/values-pl/feature_preview_strings.xml new file mode 100644 index 000000000..14971ceac --- /dev/null +++ b/photopicker/res/values-pl/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Zaznacz"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Odznacz"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Podgląd"</string> +</resources> diff --git a/photopicker/res/values-pt-rBR/core_strings.xml b/photopicker/res/values-pt-rBR/core_strings.xml index 0b6e1cc16..4c74e0ea7 100644 --- a/photopicker/res/values-pt-rBR/core_strings.xml +++ b/photopicker/res/values-pt-rBR/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Mídia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selecionado"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adicionar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-pt-rBR/feature_preview_strings.xml b/photopicker/res/values-pt-rBR/feature_preview_strings.xml new file mode 100644 index 000000000..8a6228db1 --- /dev/null +++ b/photopicker/res/values-pt-rBR/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecionar"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Visualizar"</string> +</resources> diff --git a/photopicker/res/values-pt-rPT/feature_preview_strings.xml b/photopicker/res/values-pt-rPT/feature_preview_strings.xml new file mode 100644 index 000000000..b36bbf727 --- /dev/null +++ b/photopicker/res/values-pt-rPT/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecionar"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pré-visualizar"</string> +</resources> diff --git a/photopicker/res/values-pt/core_strings.xml b/photopicker/res/values-pt/core_strings.xml index 0b6e1cc16..4c74e0ea7 100644 --- a/photopicker/res/values-pt/core_strings.xml +++ b/photopicker/res/values-pt/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Mídia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selecionado"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adicionar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> </resources> diff --git a/photopicker/res/values-pt/feature_preview_strings.xml b/photopicker/res/values-pt/feature_preview_strings.xml new file mode 100644 index 000000000..8a6228db1 --- /dev/null +++ b/photopicker/res/values-pt/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecionar"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Visualizar"</string> +</resources> diff --git a/photopicker/res/values-ro/core_strings.xml b/photopicker/res/values-ro/core_strings.xml index b41616398..db64d06dc 100644 --- a/photopicker/res/values-ro/core_strings.xml +++ b/photopicker/res/values-ro/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selectat"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adaugă <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografii"</string> </resources> diff --git a/photopicker/res/values-ro/feature_preview_strings.xml b/photopicker/res/values-ro/feature_preview_strings.xml new file mode 100644 index 000000000..43e60afd3 --- /dev/null +++ b/photopicker/res/values-ro/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Selectează"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Debifează"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previzualizează"</string> +</resources> diff --git a/photopicker/res/values-ru/feature_preview_strings.xml b/photopicker/res/values-ru/feature_preview_strings.xml new file mode 100644 index 000000000..486c594dc --- /dev/null +++ b/photopicker/res/values-ru/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Выбрать"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Отменить выбор"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Посмотреть"</string> +</resources> diff --git a/photopicker/res/values-si/feature_preview_strings.xml b/photopicker/res/values-si/feature_preview_strings.xml new file mode 100644 index 000000000..2e48fa7c6 --- /dev/null +++ b/photopicker/res/values-si/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"තෝරන්න"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"නොතෝරන්න"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"පෙරදසුන"</string> +</resources> diff --git a/photopicker/res/values-sk/feature_preview_strings.xml b/photopicker/res/values-sk/feature_preview_strings.xml new file mode 100644 index 000000000..31ecff02e --- /dev/null +++ b/photopicker/res/values-sk/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Vybrať"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Zrušiť výber"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Zobraziť ukážku"</string> +</resources> diff --git a/photopicker/res/values-sl/core_strings.xml b/photopicker/res/values-sl/core_strings.xml index 94c90e154..6c3b8248e 100644 --- a/photopicker/res/values-sl/core_strings.xml +++ b/photopicker/res/values-sl/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Predstavnost"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Izbrano"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string> </resources> diff --git a/photopicker/res/values-sl/feature_preview_strings.xml b/photopicker/res/values-sl/feature_preview_strings.xml new file mode 100644 index 000000000..514a17551 --- /dev/null +++ b/photopicker/res/values-sl/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Izberi"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Počisti izbiro"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Predogled"</string> +</resources> diff --git a/photopicker/res/values-sq/core_strings.xml b/photopicker/res/values-sq/core_strings.xml index 1bc0ad203..9467555ac 100644 --- a/photopicker/res/values-sq/core_strings.xml +++ b/photopicker/res/values-sq/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Zgjedhur"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Shto \"<xliff:g id="COUNT">(%1$s)</xliff:g>\""</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografitë"</string> </resources> diff --git a/photopicker/res/values-sq/feature_preview_strings.xml b/photopicker/res/values-sq/feature_preview_strings.xml new file mode 100644 index 000000000..d1edd833e --- /dev/null +++ b/photopicker/res/values-sq/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Zgjidh"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Hiq përzgjedhjen"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Shiko paraprakisht"</string> +</resources> diff --git a/photopicker/res/values-sr/feature_preview_strings.xml b/photopicker/res/values-sr/feature_preview_strings.xml new file mode 100644 index 000000000..b20ec8205 --- /dev/null +++ b/photopicker/res/values-sr/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Изабери"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Опозови избор"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Преглед"</string> +</resources> diff --git a/photopicker/res/values-sv/core_strings.xml b/photopicker/res/values-sv/core_strings.xml index dfa7a7ea6..1ce74aa0b 100644 --- a/photopicker/res/values-sv/core_strings.xml +++ b/photopicker/res/values-sv/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Markerat"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Lägg till <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string> </resources> diff --git a/photopicker/res/values-sv/feature_preview_strings.xml b/photopicker/res/values-sv/feature_preview_strings.xml new file mode 100644 index 000000000..8de5fa360 --- /dev/null +++ b/photopicker/res/values-sv/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Markera"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Avmarkera"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Förhandsgranska"</string> +</resources> diff --git a/photopicker/res/values-sw/feature_preview_strings.xml b/photopicker/res/values-sw/feature_preview_strings.xml new file mode 100644 index 000000000..fd43b06a9 --- /dev/null +++ b/photopicker/res/values-sw/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Chagua"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Acha kuchagua"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Kagua kwanza"</string> +</resources> diff --git a/photopicker/res/values-ta/core_strings.xml b/photopicker/res/values-ta/core_strings.xml index 82cedb889..8f3624530 100644 --- a/photopicker/res/values-ta/core_strings.xml +++ b/photopicker/res/values-ta/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"மீடியா"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"தேர்ந்தெடுக்கப்பட்டது"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>ஐச் சேர்"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> </resources> diff --git a/photopicker/res/values-ta/feature_preview_strings.xml b/photopicker/res/values-ta/feature_preview_strings.xml new file mode 100644 index 000000000..c961ff74c --- /dev/null +++ b/photopicker/res/values-ta/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"தேர்ந்தெடு"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"தேர்வு நீக்கு"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"மாதிரிக்காட்சி"</string> +</resources> diff --git a/photopicker/res/values-te/feature_preview_strings.xml b/photopicker/res/values-te/feature_preview_strings.xml new file mode 100644 index 000000000..ddf78e54b --- /dev/null +++ b/photopicker/res/values-te/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"ఎంచుకోండి"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ఎంపికను తొలగించండి"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ప్రివ్యూ చూడండి"</string> +</resources> diff --git a/photopicker/res/values-th/feature_preview_strings.xml b/photopicker/res/values-th/feature_preview_strings.xml new file mode 100644 index 000000000..2fc7d68ca --- /dev/null +++ b/photopicker/res/values-th/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"เลือก"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ยกเลิกการเลือก"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ตัวอย่าง"</string> +</resources> diff --git a/photopicker/res/values-tl/core_strings.xml b/photopicker/res/values-tl/core_strings.xml index 66ac78649..8f73db589 100644 --- a/photopicker/res/values-tl/core_strings.xml +++ b/photopicker/res/values-tl/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Napili"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Idagdag ang <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> </resources> diff --git a/photopicker/res/values-tl/feature_preview_strings.xml b/photopicker/res/values-tl/feature_preview_strings.xml new file mode 100644 index 000000000..a32088ec0 --- /dev/null +++ b/photopicker/res/values-tl/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Piliin"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"I-deselect"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"I-preview"</string> +</resources> diff --git a/photopicker/res/values-tr/core_strings.xml b/photopicker/res/values-tr/core_strings.xml index ff801c283..5d81bdd4c 100644 --- a/photopicker/res/values-tr/core_strings.xml +++ b/photopicker/res/values-tr/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Medya"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Seçili"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> tane ekle"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotoğraflar"</string> </resources> diff --git a/photopicker/res/values-tr/feature_preview_strings.xml b/photopicker/res/values-tr/feature_preview_strings.xml new file mode 100644 index 000000000..1e1ac53a9 --- /dev/null +++ b/photopicker/res/values-tr/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Seç"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Seçimi kaldır"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Önizle"</string> +</resources> diff --git a/photopicker/res/values-uk/core_strings.xml b/photopicker/res/values-uk/core_strings.xml index b2c067a25..d69a6bea0 100644 --- a/photopicker/res/values-uk/core_strings.xml +++ b/photopicker/res/values-uk/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Медіа"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Вибрано"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Додати <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фото"</string> </resources> diff --git a/photopicker/res/values-uk/feature_preview_strings.xml b/photopicker/res/values-uk/feature_preview_strings.xml new file mode 100644 index 000000000..725a39699 --- /dev/null +++ b/photopicker/res/values-uk/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Вибрати"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Не вибирати"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Переглянути"</string> +</resources> diff --git a/photopicker/res/values-ur/feature_preview_strings.xml b/photopicker/res/values-ur/feature_preview_strings.xml new file mode 100644 index 000000000..db09ace84 --- /dev/null +++ b/photopicker/res/values-ur/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"منتخب کریں"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"غیر منتخب کریں"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"پیش منظر دیکھیں"</string> +</resources> diff --git a/photopicker/res/values-uz/feature_preview_strings.xml b/photopicker/res/values-uz/feature_preview_strings.xml new file mode 100644 index 000000000..4bec0eb03 --- /dev/null +++ b/photopicker/res/values-uz/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Tanlash"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Tanlovni bekor qilish"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Razm solish"</string> +</resources> diff --git a/photopicker/res/values-vi/core_strings.xml b/photopicker/res/values-vi/core_strings.xml index 61f64c0aa..eb987119b 100644 --- a/photopicker/res/values-vi/core_strings.xml +++ b/photopicker/res/values-vi/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"Nội dung nghe nhìn"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Đã chọn"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"Thêm <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Ảnh"</string> </resources> diff --git a/photopicker/res/values-vi/feature_preview_strings.xml b/photopicker/res/values-vi/feature_preview_strings.xml new file mode 100644 index 000000000..c1d24369b --- /dev/null +++ b/photopicker/res/values-vi/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Chọn"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Bỏ chọn"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Xem trước"</string> +</resources> diff --git a/photopicker/res/values-zh-rCN/core_strings.xml b/photopicker/res/values-zh-rCN/core_strings.xml index e0df9b5cc..c8a24e257 100644 --- a/photopicker/res/values-zh-rCN/core_strings.xml +++ b/photopicker/res/values-zh-rCN/core_strings.xml @@ -21,6 +21,5 @@ <string name="photopicker_media_item" msgid="3592234718212377636">"媒体"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"已选择"</string> <string name="photopicker_add_button_label" msgid="6805332693977632142">"添加了 <xliff:g id="COUNT">(%1$s)</xliff:g> 张"</string> - <!-- no translation found for photopicker_photos_nav_button_label (8716403708343738371) --> - <skip /> + <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"照片"</string> </resources> diff --git a/photopicker/res/values-zh-rCN/feature_preview_strings.xml b/photopicker/res/values-zh-rCN/feature_preview_strings.xml new file mode 100644 index 000000000..c52f03432 --- /dev/null +++ b/photopicker/res/values-zh-rCN/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"选择"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"取消选择"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"预览"</string> +</resources> diff --git a/photopicker/res/values-zh-rHK/feature_preview_strings.xml b/photopicker/res/values-zh-rHK/feature_preview_strings.xml new file mode 100644 index 000000000..251a0163f --- /dev/null +++ b/photopicker/res/values-zh-rHK/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"選取"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"取消選取"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"預覽"</string> +</resources> diff --git a/photopicker/res/values-zh-rTW/feature_preview_strings.xml b/photopicker/res/values-zh-rTW/feature_preview_strings.xml new file mode 100644 index 000000000..251a0163f --- /dev/null +++ b/photopicker/res/values-zh-rTW/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"選取"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"取消選取"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"預覽"</string> +</resources> diff --git a/photopicker/res/values-zu/feature_preview_strings.xml b/photopicker/res/values-zu/feature_preview_strings.xml new file mode 100644 index 000000000..f63ef3c37 --- /dev/null +++ b/photopicker/res/values-zu/feature_preview_strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="photopicker_select_button_label" msgid="770981849239352214">"Khetha"</string> + <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Susa ukukhetha"</string> + <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Ukuhlola kuqala"</string> +</resources> diff --git a/photopicker/res/values/feature_preview_strings.xml b/photopicker/res/values/feature_preview_strings.xml index f6db6c4ce..19b94f1b2 100644 --- a/photopicker/res/values/feature_preview_strings.xml +++ b/photopicker/res/values/feature_preview_strings.xml @@ -25,5 +25,28 @@ <!-- Button label for the "Preview" button in the selection bar --> <string name="photopicker_preview_button_label" translation_description="Button label to navigate to a screen and preview the selected media.">Preview</string> + <!-- Dialog title for the Preview video error dialog. --> + <string name="photopicker_preview_dialog_error_title" translation_description="Dialog title for when a video cannot be played.">Trouble playing video</string> + + <!-- Dialog message for the Preview video error dialog. --> + <string name="photopicker_preview_dialog_error_message" translation_description="Dialog message for when a video cannot be played due to an internet connection issue.">Check your internet connection and try again</string> + + <!-- Dialog message for the Preview video error dialog. --> + <string name="photopicker_preview_dialog_error_retry_button_label" translation_description="Button label to allow a user to attempt to play a video again after an error.">Retry</string> + + <!-- Snackbar message for Preview video errors --> + <string name="photopicker_preview_video_error_snackbar" translation_description="Snackbar message shown to the user when the requested video cannot be played.">Cannot play video</string> + + <!-- A11y description for the Play button --> + <string name="photopicker_video_play_button_description" translation_description="Accessibility description for a video player Play button">Play</string> + + <!-- A11y description for the Pause button --> + <string name="photopicker_video_pause_button_description" translation_description="Accessibility description for a video player Pause button">Pause</string> + + <!-- A11y description for the Mute button --> + <string name="photopicker_video_mute_button_description" translation_description="Accessibility description for a video player volume Mute button">Mute Volume</string> + + <!-- A11y description for the Unmute button --> + <string name="photopicker_video_unmute_button_description" translation_description="Accessibility description for a video player volume Unmute button">Unmute Volume</string> </resources> diff --git a/photopicker/src/com/android/photopicker/features/preview/Preview.kt b/photopicker/src/com/android/photopicker/features/preview/Preview.kt index cf2e68e0e..ede6aa174 100644 --- a/photopicker/src/com/android/photopicker/features/preview/Preview.kt +++ b/photopicker/src/com/android/photopicker/features/preview/Preview.kt @@ -42,6 +42,8 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -72,7 +74,7 @@ private val MEASUREMENT_SELECTION_BUTTON_MIN_WIDTH = 150.dp private val MEASUREMENT_SELECTION_BAR_PADDING = 12.dp /** - * Entrypoint for the [PhotopickerDestinations.PREVIEW_SELECTION] route. + * Entry point for the [PhotopickerDestinations.PREVIEW_SELECTION] route. * * This composable will snapshot the current selection when created so that photos are not removed * from the list of preview-able photos. @@ -103,7 +105,7 @@ fun PreviewSelection(viewModel: PreviewViewModel = hiltViewModel()) { } /** - * Entrypoint for the [PhotopickerDestinations.PREVIEW_MEDIA] route. + * Entry point for the [PhotopickerDestinations.PREVIEW_MEDIA] route. * * @param previewItemFlow - A [StateFlow] from the navBackStackEntry savedStateHandler which uses * the [PreviewFeature.PREVIEW_MEDIA_KEY] to retrieve the passed [Media] item to preview. @@ -114,16 +116,23 @@ fun PreviewMedia( ) { val media by previewItemFlow.collectAsStateWithLifecycle() val selection by LocalSelection.current.flow.collectAsStateWithLifecycle() - // create a local variable for the when block so the compiler - // doesn't complain about the delegate. + // create a local variable for the when block so the compiler doesn't complain about the + // delegate. val localMedia = media Box { Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) { - when (localMedia) { - is Media.Image -> ImageUi(localMedia) - is Media.Video -> VideoUi(localMedia) - null -> {} + Box( + modifier = Modifier.padding(vertical = 50.dp), + contentAlignment = Alignment.Center + ) { + // Preview session state to keep track if the video player's audio is muted. + var audioIsMuted by remember { mutableStateOf(true) } + when (localMedia) { + is Media.Image -> ImageUi(localMedia) + is Media.Video -> VideoUi(localMedia, audioIsMuted, { audioIsMuted = it }) + null -> {} + } } } @@ -162,33 +171,43 @@ fun PreviewMedia( */ @Composable private fun Preview(selection: Set<Media>) { + val viewModel: PreviewViewModel = hiltViewModel() val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle() val events = LocalEvents.current val scope = rememberCoroutineScope() + // Preview session state to keep track if the video player's audio is muted. + var audioIsMuted by remember { mutableStateOf(true) } + // Page count equal to size of selection val state = rememberPagerState { selection.size } Box(modifier = Modifier.fillMaxSize()) { - HorizontalPager(state = state, modifier = Modifier.fillMaxSize()) { page -> + HorizontalPager( + state = state, + modifier = Modifier.fillMaxSize(), + ) { page -> val media = selection.elementAt(page) when (media) { is Media.Image -> ImageUi(media) - is Media.Video -> VideoUi(media) + is Media.Video -> VideoUi(media, audioIsMuted, { audioIsMuted = it }) } } // Bottom row of action buttons Row( - modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth().padding(12.dp), + modifier = + Modifier.align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(MEASUREMENT_SELECTION_BAR_PADDING), horizontalArrangement = Arrangement.SpaceBetween, ) { FilledTonalButton( modifier = Modifier.widthIn( - // Apply a min width to prevent the button resizing when the label changes. - min = MEASUREMENT_SELECTION_BUTTON_MIN_WIDTH + // Apply a min width to prevent the button re-sizing when the label changes. + min = MEASUREMENT_SELECTION_BUTTON_MIN_WIDTH, ), onClick = { viewModel.toggleInSelection(selection.elementAt(state.currentPage)) }, ) { @@ -226,17 +245,6 @@ private fun Preview(selection: Set<Media>) { } /** - * Composable that displays [Media.Video] - * - * @param video - */ -@Composable -private fun VideoUi(@Suppress("UNUSED_PARAMETER") video: Media.Video) { - // TODO(b/323833427): Implement remote video preview. - Text("Videos coming soon!") -} - -/** * Composable that loads a [Media.Image] in [Resolution.FULL] for the user to preview. * * @param image diff --git a/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt b/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt index 20b14d362..dbdb03675 100644 --- a/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt +++ b/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt @@ -16,22 +16,46 @@ package com.android.photopicker.features.preview +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.net.Uri +import android.os.Bundle +import android.os.RemoteException +import android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK +import android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER +import android.provider.ICloudMediaSurfaceController +import android.provider.ICloudMediaSurfaceStateChangedCallback +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.core.os.bundleOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.photopicker.core.selection.Selection +import com.android.photopicker.core.user.UserMonitor import com.android.photopicker.data.model.Media import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** * The view model for the Preview routes. * - * This view model manages snapshotting the session's selection so that items can observe a slice of + * This view model manages snapshots of the session's selection so that items can observe a slice of * state rather than the mutable selection state. + * + * Additionally, [RemoteSurfaceController] are created and held for re-use in the scope of this view + * model. The view model handles the [ICloudMediaSurfaceStateChangedCallback] for each controller, + * and stores the information for the UI to obtain via exported flows. */ @HiltViewModel class PreviewViewModel @@ -39,8 +63,18 @@ class PreviewViewModel constructor( private val scopeOverride: CoroutineScope?, private val selection: Selection<Media>, + private val userMonitor: UserMonitor, ) : ViewModel() { + companion object { + val TAG: String = PreviewFeature.TAG + + // These are the authority strings for [CloudMediaProvider]-s for local on device files. + private val PHOTOPICKER_PROVIDER_AUTHORITY = "com.android.providers.media.photopicker" + private val REMOTE_PREVIEW_PROVIDER_AUTHORITY = + "com.android.providers.media.remote_video_preview" + } + // Check if a scope override was injected before using the default [viewModelScope] private val scope: CoroutineScope = if (scopeOverride == null) { @@ -68,4 +102,174 @@ constructor( fun toggleInSelection(media: Media) { scope.launch { selection.toggle(media) } } + + /** + * Holds any cached [RemotePreviewControllerInfo] to avoid re-creating + * [RemoteSurfaceController]-s that already exist during a preview session. + */ + val controllers: HashMap<String, RemotePreviewControllerInfo> = HashMap() + + /** + * A flow that all [ICloudMediaSurfaceStateChangedCallback] push their [setPlaybackState] + * updates to. This flow is later filtered to a specific (authority + surfaceId) pairing for + * providing the playback state updates to the UI composables to collect. + * + * A shared flow is used here to ensure that all emissions are delivered since a StateFlow will + * conflate deliveries to slow receivers (sometimes the UI is slow to pull emissions) to this + * flow since they happen in quick succession, and this will avoid dropping any. + * + * See [getPlaybackInfoForPlayer] where this flow is filtered. + */ + private val _playbackInfo = MutableSharedFlow<PlaybackInfo>() + + /** + * Creates a [Flow<PlaybackInfo>] for the provided player configuration. This just siphons the + * larger [playbackInfo] flow that all of the [ICloudMediaSurfaceStateChangedCallback]-s push + * their updates to. + * + * The larger flow is filtered for updates related to the requested video session. (surfaceId + + * authority) + */ + fun getPlaybackInfoForPlayer(surfaceId: Int, video: Media.Video): Flow<PlaybackInfo> { + return _playbackInfo.filter { it.surfaceId == surfaceId && it.authority == video.authority } + } + + /** @return the active user's [ContentResolver]. */ + fun getContentResolverForCurrentUser(): ContentResolver { + return userMonitor.userStatus.value.activeContentResolver + } + + /** + * Obtains an instance of [RemoteSurfaceController] for the requested authority. Attempts to + * re-use any controllers that have previously been fetched, and additionally, generates a + * [RemotePreviewControllerInfo] for the requested authority and holds it in [controllers] for + * future re-use. + * + * @return A [RemoteSurfaceController] for [authority] + */ + fun getControllerForAuthority( + authority: String, + ): RemoteSurfaceController { + + if (controllers.containsKey(authority)) { + Log.d(TAG, "Existing controller found, re-using for $authority") + return controllers.getValue(authority).controller + } + + Log.d(TAG, "Creating controller for authority: $authority") + + val callback = buildSurfaceStateChangedCallback(authority) + + // For local photos which use the PhotopickerProvider, the remote video preview + // functionality is actually delegated to the mediaprovider:Photopicker process + // and is run out of the RemoteVideoPreviewProvider, so for the purposes of + // acquiring a [ContentProviderClient], use a different authority. + val clientAuthority = + when (authority) { + PHOTOPICKER_PROVIDER_AUTHORITY -> REMOTE_PREVIEW_PROVIDER_AUTHORITY + else -> authority + } + + // Acquire a [ContentProviderClient] that can be retained as long as the [PreviewViewModel] + // is active. This creates a binding between the current process that is running Photopicker + // and the remote process that is rendering video and prevents the remote process from being + // killed by the OS. This client is held onto until the [PreviewViewModel] is cleared when + // the Preview route is navigated away from. (The PreviewViewModel is bound to the + // navigation backStackEntry). + val remoteClient = + getContentResolverForCurrentUser().acquireContentProviderClient(clientAuthority) + // TODO: b/323833427 Navigate back to the main grid when a controller cannot be obtained. + checkNotNull(remoteClient) { "Unable to get a client for $clientAuthority" } + + // Don't reuse the remote client from above since it may not be the right provider for + // local files. Instead, assemble a new URI, and call the correct provider via + // [ContentResolver#call] + val uri: Uri = + Uri.Builder() + .apply { + scheme(ContentResolver.SCHEME_CONTENT) + authority(authority) + } + .build() + + val extras = + bundleOf( + EXTRA_LOOPING_PLAYBACK_ENABLED to true, + EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED to true, + EXTRA_SURFACE_STATE_CALLBACK to callback + ) + + val controllerBundle: Bundle? = + getContentResolverForCurrentUser() + .call( + /*uri=*/ uri, + /*method=*/ METHOD_CREATE_SURFACE_CONTROLLER, + /*arg=*/ null, + /*extras=*/ extras, + ) + checkNotNull(controllerBundle) { "No controller was returned for RemoteVideoPreview" } + + val binder = controllerBundle.getBinder(EXTRA_SURFACE_CONTROLLER) + + // Produce the [RemotePreviewControllerInfo] and save it for future re-use. + val controllerInfo = + RemotePreviewControllerInfo( + authority = authority, + client = remoteClient, + controller = + RemoteSurfaceController(ICloudMediaSurfaceController.Stub.asInterface(binder)), + ) + controllers.put(authority, controllerInfo) + + return controllerInfo.controller + } + + /** + * When this ViewModel is cleared, close any held [ContentProviderClient]s that are retained for + * video rendering. + */ + override fun onCleared() { + // When the view model is cleared then it is safe to assume the preview route is no longer + // active, and any [ContentProviderClient] that are being held to support remote video + // preview can now be closed. + for ((_, controllerInfo) in controllers) { + + try { + controllerInfo.controller.onDestroy() + } catch (e: RemoteException) { + Log.d(TAG, "Failed to destroy surface controller.", e) + } + + controllerInfo.client.close() + } + } + + /** + * Constructs a [ICloudMediaSurfaceStateChangedCallback] for the provided authority. + * + * @param authority The authority this callback will assign to its PlaybackInfo emissions. + * @return A [ICloudMediaSurfaceStateChangedCallback] bound to the provided authority. + */ + private fun buildSurfaceStateChangedCallback( + authority: String + ): ICloudMediaSurfaceStateChangedCallback.Stub { + return object : ICloudMediaSurfaceStateChangedCallback.Stub() { + override fun setPlaybackState( + surfaceId: Int, + playbackState: Int, + playbackStateInfo: Bundle? + ) { + scope.launch { + _playbackInfo.emit( + PlaybackInfo( + state = PlaybackState.fromStateInt(playbackState), + surfaceId = surfaceId, + authority = authority, + playbackStateInfo = playbackStateInfo, + ) + ) + } + } + } + } } diff --git a/photopicker/src/com/android/photopicker/features/preview/video/PlaybackInfo.kt b/photopicker/src/com/android/photopicker/features/preview/video/PlaybackInfo.kt new file mode 100644 index 000000000..609c3fcbb --- /dev/null +++ b/photopicker/src/com/android/photopicker/features/preview/video/PlaybackInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photopicker.features.preview + +import android.os.Bundle + +/** + * Data class wrapper around a PlaybackState callback from a Remote preview controller. + * + * @property state [PlaybackState] enum that represents the returned state int + * @property surfaceId the relevant surfaceId this PlaybackInfo refers to + * @property authority the authority of the surfaceId + * @property playbackStateInfo the (optionally) included Bundle in the state info. + */ +data class PlaybackInfo( + val state: PlaybackState, + val surfaceId: Int, + val authority: String, + val playbackStateInfo: Bundle? = null, +) diff --git a/photopicker/src/com/android/photopicker/features/preview/video/PlaybackState.kt b/photopicker/src/com/android/photopicker/features/preview/video/PlaybackState.kt new file mode 100644 index 000000000..2f011907f --- /dev/null +++ b/photopicker/src/com/android/photopicker/features/preview/video/PlaybackState.kt @@ -0,0 +1,54 @@ +/* + * 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.photopicker.features.preview + +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED + +/** + * Wrapper enum around the [CloudMediaProvider.CloudMediaSurfaceStateChangedCallback] state + * integers. + * + * @property state the underlying value as defined by the API. + */ +enum class PlaybackState(val state: Int) { + UNKNOWN(-1), + BUFFERING(PLAYBACK_STATE_BUFFERING), + READY(PLAYBACK_STATE_READY), + STARTED(PLAYBACK_STATE_STARTED), + PAUSED(PLAYBACK_STATE_PAUSED), + COMPLETED(PLAYBACK_STATE_COMPLETED), + MEDIA_SIZE_CHANGED(PLAYBACK_STATE_MEDIA_SIZE_CHANGED), + ERROR_RETRIABLE_FAILURE(PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE), + ERROR_PERMANENT_FAILURE(PLAYBACK_STATE_ERROR_PERMANENT_FAILURE); + + companion object { + /** + * @return Converts a [CloudMediaSurfaceStateChangedCallback] state int into the enum, or + * UNKNOWN if the value is not valid. + */ + fun fromStateInt(value: Int): PlaybackState { + return PlaybackState.entries.find { it.state == value } ?: UNKNOWN + } + } +} diff --git a/photopicker/src/com/android/photopicker/features/preview/video/RemotePreviewControllerInfo.kt b/photopicker/src/com/android/photopicker/features/preview/video/RemotePreviewControllerInfo.kt new file mode 100644 index 000000000..df7f21c83 --- /dev/null +++ b/photopicker/src/com/android/photopicker/features/preview/video/RemotePreviewControllerInfo.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photopicker.features.preview + +import android.content.ContentProviderClient + +/** + * Container that holds a [RemoteSurfaceController], and it's corresponding [ContentProviderClient]. + * + * @property authority The authority of the client and controller. + * @property client an active [ContentProviderClient] that is held until video playback is finished + * to prevent the remote rendering process from being frozen by the OS. + * @property controller [RemoteSurfaceController] from the [CloudMediaProvider] + */ +data class RemotePreviewControllerInfo( + val authority: String, + val client: ContentProviderClient, + val controller: RemoteSurfaceController, +) diff --git a/photopicker/src/com/android/photopicker/features/preview/video/RemoteSurfaceController.kt b/photopicker/src/com/android/photopicker/features/preview/video/RemoteSurfaceController.kt new file mode 100644 index 000000000..9a04b14ef --- /dev/null +++ b/photopicker/src/com/android/photopicker/features/preview/video/RemoteSurfaceController.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photopicker.features.preview + +import android.os.Bundle +import android.provider.ICloudMediaSurfaceController +import android.view.Surface + +/** + * A wrapper class around [ICloudMediaSurfaceController]. + * + * This class just proxies all calls through to the wrapped controller. + * Additionally, this Controller provides methods for the UI for obtaining + * surfaceId's and manages calling the [onPlayerCreate] and [onPlayerRelease] + * based on the number of active surfaces. + * + * @property wrapped the [ICloudMediaSurfaceController] to wrap. + */ +class RemoteSurfaceController(private val wrapped: ICloudMediaSurfaceController) : + ICloudMediaSurfaceController.Stub() { + + /** The next unique surface id for this controller */ + private var nextSurfaceId: Int = 0 + + /** + * The total number of active surfaces for this controller. + * This count is increased during [onSurfaceCreated] and decreased + * during [onSurfaceDestroyed]. + */ + private var activeSurfaceCount: Int = 0 + + /** Get a new surfaceId for a new player surface */ + fun getNextSurfaceId(): Int { + return ++nextSurfaceId + } + + /** + * Pass through of the Surface's [onSurfaceChanged] lifecycle. + * + * Additionally, this method manages creating the player if it is required. + */ + override fun onSurfaceCreated(surfaceId: Int, surface: Surface, mediaId: String) { + + if (activeSurfaceCount == 0) { + // If this is the first surface being created for this controller, + // the player needs to be initialized. + onPlayerCreate() + } + + activeSurfaceCount++ + wrapped.onSurfaceCreated(surfaceId, surface, mediaId) + } + + /** + * Pass through of the Surface's [onSurfaceChanged] lifecycle. + */ + override fun onSurfaceChanged(surfaceId: Int, format: Int, width: Int, height: Int) { + wrapped.onSurfaceChanged(surfaceId, format, width, height) + } + + /** + * Pass through of the Surface's [onSurfaceDestroyed] lifecycle. + */ + override fun onSurfaceDestroyed(surfaceId: Int) { + wrapped.onSurfaceDestroyed(surfaceId) + + if (--activeSurfaceCount == 0) { + // If there are no active surfaces left, release the player. + onPlayerRelease() + } + } + + override fun onMediaPlay(surfaceId: Int) { + wrapped.onMediaPlay(surfaceId) + } + + override fun onMediaPause(surfaceId: Int) { + wrapped.onMediaPause(surfaceId) + } + + override fun onMediaSeekTo(surfaceId: Int, timestampMillis: Long) { + wrapped.onMediaSeekTo(surfaceId, timestampMillis) + } + + override fun onConfigChange(bundle: Bundle) { + wrapped.onConfigChange(bundle) + } + + override fun onDestroy() { + wrapped.onDestroy() + } + + override fun onPlayerCreate() { + wrapped.onPlayerCreate() + } + override fun onPlayerRelease() { + wrapped.onPlayerRelease() + } +} diff --git a/photopicker/src/com/android/photopicker/features/preview/video/RetriableErrorDialog.kt b/photopicker/src/com/android/photopicker/features/preview/video/RetriableErrorDialog.kt new file mode 100644 index 000000000..5f26368a4 --- /dev/null +++ b/photopicker/src/com/android/photopicker/features/preview/video/RetriableErrorDialog.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photopicker.features.preview + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.photopicker.R + +/* Size of the spacer between dialog elements. */ +private val MEASUREMENT_ERROR_DIALOG_SPACER_SIZE = 24.dp + +/* Size of the padding around the edge of the dialog. */ +private val MEASUREMENT_ERROR_DIALOG_PADDING = 16.dp + +/** + * Creates an error dialog for the ERROR_RETRIABLE_FAILURE error state. This error state is reached + * when the remote preview provider is unable to play the video (most likely related to a connection + * issue), but the user can attempt to play the video again. + * + * @param onDismissRequest Action to take when the dialog is dismissed, most likely via a back + * navigation, or by clicking outside of the dialog. + * @param onRetry Action to take when the user clicks the "Retry" button on the dialog. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RetriableErrorDialog( + onDismissRequest: () -> Unit, + onRetry: () -> Unit, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + ) { + Surface( + modifier = Modifier.wrapContentWidth().wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column(modifier = Modifier.padding(MEASUREMENT_ERROR_DIALOG_PADDING)) { + Text( + stringResource(R.string.photopicker_preview_dialog_error_title), + style = MaterialTheme.typography.titleLarge + ) + Spacer(modifier = Modifier.height(MEASUREMENT_ERROR_DIALOG_SPACER_SIZE)) + Text( + stringResource(R.string.photopicker_preview_dialog_error_message), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(MEASUREMENT_ERROR_DIALOG_SPACER_SIZE)) + TextButton( + modifier = Modifier.align(Alignment.End), + onClick = onRetry, + ) { + Text( + stringResource(R.string.photopicker_preview_dialog_error_retry_button_label) + ) + } + } + } + } +} diff --git a/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt b/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt new file mode 100644 index 000000000..32ba6c3e2 --- /dev/null +++ b/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt @@ -0,0 +1,690 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photopicker.features.preview + +import android.content.ContentResolver.EXTRA_SIZE +import android.graphics.Point +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Bundle +import android.os.RemoteException +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED +import android.util.Log +import android.view.Surface +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.View +import android.widget.FrameLayout +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeOff +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.PauseCircle +import androidx.compose.material.icons.filled.PlayCircle +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.os.bundleOf +import androidx.hilt.navigation.compose.hiltViewModel +import com.android.photopicker.R +import com.android.photopicker.data.model.Media +import com.android.photopicker.extensions.requireSystemService +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter + +/** [AudioAttributes] to use with all VideoUi instances. */ +private val AUDIO_ATTRIBUTES = + AudioAttributes.Builder() + .apply { + setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + setUsage(AudioAttributes.USAGE_MEDIA) + } + .build() + +/** The size of the Play/Pause button in the center of the video controls */ +private val MEASUREMENT_PLAY_PAUSE_ICON_SIZE = 48.dp + +/** Padding between the edge of the screen and the Player controls box. */ +private val MEASUREMENT_PLAYER_CONTROLS_PADDING_HORIZONTAL = 8.dp +private val MEASUREMENT_PLAYER_CONTROLS_PADDING_VERTICAL = 128.dp + +/** Padding between the bottom edge of the screen and the snackbars */ +private val MEASUREMENT_SNACKBAR_BOTTOM_PADDING = 48.dp + +/** Delay in milliseconds before the player controls are faded. */ +private val TIME_MS_PLAYER_CONTROLS_FADE_DELAY = 3000L + +/** + * Builds a remote video player surface and handles the interactions with the + * [RemoteSurfaceController] for remote video playback. + * + * This composable is the entry point into creating a remote player for Photopicker video sources. + * It utilizes the remote preview functionality of [CloudMediaProvider] to expose a [Surface] to a + * remote process. + * + * @param video The video to prepare and play + * @param audioIsMuted a preview session-global of the audio mute state + * @param onRequestAudioMuteChange a callback to request a switch of the [audioIsMuted] state + * @param viewModel The current instance of the [PreviewViewModel], injected by hilt. + */ +@Composable +fun VideoUi( + video: Media.Video, + audioIsMuted: Boolean, + onRequestAudioMuteChange: (Boolean) -> Unit, + viewModel: PreviewViewModel = hiltViewModel(), +) { + + /** + * The controller is remembered based on the authority so it is efficiently re-used for videos + * from the same authority. The view model also caches surface controllers to avoid re-creating + * them. + */ + val controller = + remember(video.authority) { viewModel.getControllerForAuthority(video.authority) } + + /** Obtain a surfaceId which will identify this VideoUi's surface to the remote player. */ + val surfaceId = remember(video) { controller.getNextSurfaceId() } + + /** The visibility of the player controls for this video */ + var areControlsVisible by remember { mutableStateOf(false) } + + /** If the underlying video surface has been created */ + var surfaceCreated by remember(video) { mutableStateOf(false) } + + /** Whether the [RetriableErrorDialog] is visible. */ + var showErrorDialog by remember { mutableStateOf(false) } + + /** SnackbarHost api for launching Snackbars */ + val snackbarHostState = remember { SnackbarHostState() } + + /** Producer for [PlaybackInfo] for the current video surface */ + val playbackInfo by producePlaybackInfo(surfaceId, video) + + /** Producer for AspectRatio for the current video surface */ + val aspectRatio by produceAspectRatio(surfaceId, video) + + val context = LocalContext.current + + /** Run these effects when a new PlaybackInfo is received */ + LaunchedEffect(playbackInfo) { + when (playbackInfo.state) { + PlaybackState.READY -> { + // When the controller indicates the video is ready to be played, + // immediately request for it to begin playing. + controller.onMediaPlay(surfaceId) + } + PlaybackState.STARTED -> { + // When playback starts, show the controls to the user. + areControlsVisible = true + } + PlaybackState.ERROR_RETRIABLE_FAILURE -> { + // The remote player has indicated a retriable failure, so show the + // error dialog. + showErrorDialog = true + } + PlaybackState.ERROR_PERMANENT_FAILURE -> { + snackbarHostState.showSnackbar( + context.getString(R.string.photopicker_preview_video_error_snackbar) + ) + } + else -> {} + } + } + + // Acquire audio focus for the player, and establish a callback to change audio mute status. + val onAudioMuteToggle = + rememberAudioFocus( + video, + surfaceCreated, + audioIsMuted, + onFocusLost = { + try { + controller.onMediaPause(surfaceId) + } catch (e: RemoteException) { + Log.d(PreviewFeature.TAG, "Failed to pause media when audio focus was lost.") + } + }, + onConfigChangeRequested = { bundle -> controller.onConfigChange(bundle) }, + onRequestAudioMuteChange = onRequestAudioMuteChange, + ) + + // Finally! Now the actual VideoPlayer can be created! \0/ + // This is the top level box of the player, and all of its children are drawn on-top + // of each other. + Box { + VideoPlayer( + aspectRatio = aspectRatio, + playbackInfo = playbackInfo, + muteAudio = audioIsMuted, + areControlsVisible = areControlsVisible, + onPlayPause = { + when (playbackInfo.state) { + PlaybackState.STARTED -> controller.onMediaPause(surfaceId) + PlaybackState.PAUSED -> controller.onMediaPlay(surfaceId) + else -> {} + } + }, + onToggleAudioMute = { onAudioMuteToggle(audioIsMuted) }, + onTogglePlayerControls = { areControlsVisible = !areControlsVisible }, + onSurfaceCreated = { surface -> + controller.onSurfaceCreated(surfaceId, surface, video.mediaId) + surfaceCreated = true + }, + onSurfaceChanged = { format, width, height -> + controller.onSurfaceChanged(surfaceId, format, width, height) + }, + onSurfaceDestroyed = { controller.onSurfaceDestroyed(surfaceId) }, + ) + + // Photopicker is (generally) inside of a BottomSheet, and the preview route is inside a + // dialog, so this requires a custom [SnackbarHost] to draw on top of those elements that do + // not play nicely with snackbars. Peace was never an option. + SnackbarHost( + snackbarHostState, + modifier = + Modifier.align(Alignment.BottomCenter) + .padding(bottom = MEASUREMENT_SNACKBAR_BOTTOM_PADDING) + ) + } + + // If the Error dialog is needed, launch the dialog. + if (showErrorDialog) { + RetriableErrorDialog( + onDismissRequest = { showErrorDialog = false }, + onRetry = { + showErrorDialog = !showErrorDialog + controller.onMediaPlay(surfaceId) + }, + ) + } +} + +/** + * Composable that creates the video SurfaceView and player controls. The VideoPlayer itself is + * stateless, and handles showing loading indicators and player controls when requested by the + * parent. + * + * It hoists a number of events for the parent to handle: + * - Button/UI touch interactions + * - the underlying video surface's lifecycle events. + * + * @param aspectRatio the aspectRatio of the video to be played. (Null until it is known) + * @param playbackInfo the current PlaybackState from the remote controller + * @param muteAudio if the audio is currently muted + * @param areControlsVisible if the controls are currently visible + * @param onPlayPause Callback for the Play/Pause button + * @param onToggleAudioMute Callback for the Audio mute/unmute button + * @param onTogglePlayerControls Callback for toggling the player controls visibility + * @param onSurfaceCreated Callback for the underlying [SurfaceView] lifecycle + * @param onSurfaceChanged Callback for the underlying [SurfaceView] lifecycle + * @param onSurfaceDestroyed Callback for the underlying [SurfaceView] lifecycle + */ +@Composable +private fun VideoPlayer( + aspectRatio: Float?, + playbackInfo: PlaybackInfo, + muteAudio: Boolean, + areControlsVisible: Boolean, + onPlayPause: () -> Unit, + onToggleAudioMute: () -> Unit, + onTogglePlayerControls: () -> Unit, + onSurfaceCreated: (Surface) -> Unit, + onSurfaceChanged: (format: Int, width: Int, height: Int) -> Unit, + onSurfaceDestroyed: () -> Unit, +) { + + // Clicking anywhere on the player should toggle the visibility of the controls. + Box(Modifier.fillMaxSize().clickable { onTogglePlayerControls() }) { + val modifier = + if (aspectRatio != null) Modifier.aspectRatio(aspectRatio).align(Alignment.Center) + else Modifier.align(Alignment.Center) + VideoSurfaceView( + modifier = modifier, + playerSizeSet = aspectRatio != null, + onSurfaceCreated = onSurfaceCreated, + onSurfaceChanged = onSurfaceChanged, + onSurfaceDestroyed = onSurfaceDestroyed, + ) + + // Auto hides the controls after the delay has passed (if they are still visible). + LaunchedEffect(areControlsVisible) { + if (areControlsVisible) { + delay(TIME_MS_PLAYER_CONTROLS_FADE_DELAY) + onTogglePlayerControls() + } + } + + // Overlay the playback controls + VideoPlayerControls( + visible = areControlsVisible, + currentPlaybackState = playbackInfo.state, + onPlayPauseClicked = onPlayPause, + audioIsMuted = muteAudio, + onToggleAudioMute = onToggleAudioMute, + ) + + Box(Modifier.fillMaxSize()) { + /** Conditional UI based on the current [PlaybackInfo] */ + when (playbackInfo.state) { + PlaybackState.UNKNOWN, + PlaybackState.BUFFERING -> { + CircularProgressIndicator(Modifier.align(Alignment.Center)) + } + else -> {} + } + } + } +} + +/** + * Composes a [SurfaceView] for remote video rendering via the [CloudMediaProvider]'s remote video + * preview Binder process. + * + * The [SurfaceView] itself is wrapped inside of a compose interop [AndroidView] which wraps a + * [FrameLayout] for managing visibility, and then the [SurfaceView] itself. The SurfaceView + * attaches its own [SurfaceHolder.Callback] and hoists those events out of this composable for the + * parent to handle. + * + * @param modifier A modifier which can be used to position the SurfaceView inside of the parent. + * @param playerSizeSet Indicates the aspectRatio and size of the surface has been set by the + * parent. + * @param onSurfaceCreated Surface lifecycle callback when the underlying surface has been created. + * @param onSurfaceChanged Surface lifecycle callback when the underlying surface has been changed. + * @param onSurfaceDestroyed Surface lifecycle callback when the underlying surface has been + * destroyed. + */ +@Composable +private fun VideoSurfaceView( + modifier: Modifier = Modifier, + playerSizeSet: Boolean, + onSurfaceCreated: (Surface) -> Unit, + onSurfaceChanged: (format: Int, width: Int, height: Int) -> Unit, + onSurfaceDestroyed: () -> Unit, +) { + + /** + * [SurfaceView] is not available in compose, however the remote video preview with the cloud + * provider requires a [Surface] object passed via Binder. + * + * The SurfaceView is instead wrapped in this [AndroidView] compose inter-op and behaves like a + * normal SurfaceView. + */ + AndroidView( + /** Factory is called once on first compose, and never again */ + modifier = modifier, + factory = { context -> + + // The [FrameLayout] will manage sizing the SurfaceView since it uses a LayoutParam of + // [MATCH_PARENT] by default, it doesn't need to be explicitly set. + FrameLayout(context).apply { + + // Add a child view to the FrameLayout which is the [SurfaceView] itself. + addView( + SurfaceView(context).apply { + /** + * The SurfaceHolder callback is held by the SurfaceView itself, and is + * directly attached to this view's SurfaceHolder, so that each SurfaceView + * has its own SurfaceHolder.Callback associated with it. + */ + val surfaceCallback = + object : SurfaceHolder.Callback { + + override fun surfaceCreated(holder: SurfaceHolder) { + onSurfaceCreated(holder.getSurface()) + } + + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { + onSurfaceChanged(format, width, height) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + onSurfaceDestroyed() + } + } + + // Ensure the SurfaceView never draws outside of its parent's bounds. + setClipToOutline(true) + + getHolder().addCallback(surfaceCallback) + } + ) + + // Initially hide the view until there is a aspect ratio set to avoid any visual + // snapping to position. + setVisibility(View.INVISIBLE) + } + }, + update = { view -> + // Once the parent has indicated the size has been set, make the player visible. + if (playerSizeSet) { + view.setVisibility(View.VISIBLE) + } + }, + ) +} + +/** + * Composable which generates the Video controls UI and handles displaying / fading the controls + * when the visibility changes. + * + * @param visible Whether the controls are currently visible. + * @param currentPlaybackState the current [PlaybackInfo] of the player. + * @param onPlayPauseClicked Click handler for the Play/Pause button + * @param audioIsMuted The current audio mute state (true if muted) + * @param onToggleAudioMute Click handler for the audio mute button. + */ +@Composable +private fun VideoPlayerControls( + visible: Boolean, + currentPlaybackState: PlaybackState, + onPlayPauseClicked: () -> Unit, + audioIsMuted: Boolean, + onToggleAudioMute: () -> Unit, +) { + + AnimatedVisibility( + visible = visible, + modifier = Modifier.fillMaxSize(), + enter = fadeIn(), + exit = fadeOut(), + ) { + // Box to draw everything on top of the video surface which is underneath. + Box( + Modifier.padding( + vertical = MEASUREMENT_PLAYER_CONTROLS_PADDING_VERTICAL, + horizontal = MEASUREMENT_PLAYER_CONTROLS_PADDING_HORIZONTAL + ) + ) { + // Play / Pause button (center of the screen) + FilledTonalIconButton( + modifier = Modifier.align(Alignment.Center).size(MEASUREMENT_PLAY_PAUSE_ICON_SIZE), + onClick = { onPlayPauseClicked() }, + ) { + when (currentPlaybackState) { + PlaybackState.STARTED -> + Icon( + Icons.Filled.PauseCircle, + contentDescription = + stringResource(R.string.photopicker_video_pause_button_description), + modifier = Modifier.size(MEASUREMENT_PLAY_PAUSE_ICON_SIZE) + ) + else -> + Icon( + Icons.Filled.PlayCircle, + contentDescription = + stringResource(R.string.photopicker_video_play_button_description), + modifier = Modifier.size(MEASUREMENT_PLAY_PAUSE_ICON_SIZE) + ) + } + } + + // Mute / UnMute button (bottom right for LTR layouts) + FilledTonalIconButton( + modifier = Modifier.align(Alignment.BottomEnd), + onClick = onToggleAudioMute, + ) { + when (audioIsMuted) { + false -> + Icon( + Icons.AutoMirrored.Filled.VolumeUp, + contentDescription = + stringResource(R.string.photopicker_video_mute_button_description) + ) + true -> + Icon( + Icons.AutoMirrored.Filled.VolumeOff, + contentDescription = + stringResource(R.string.photopicker_video_unmute_button_description) + ) + } + } + } + } +} + +/** + * Acquire and remember the audio focus for the current composable context. + * + * This composable encapsulates all of the audio focus / abandon focus logic for the VideoUi. Focus + * is managed via [AudioManager] and this composable will react to changes to [audioIsMuted] and + * request (in the event video players have switched) / or abandon focus accordingly. + * + * @param video The current video being played + * @param surfaceCreated If the video surface has been created + * @param audioIsMuted if the audio is currently muted + * @param onFocusLost Callback for when the AudioManager informs the audioListener that focus has + * been lost. + * @param onConfigChangeRequested Callback for when the controller's configuration needs to be + * updated + * @param onRequestAudioMuteChange Callback to request audio mute state change + * @return Additionally, return a function which should be called to toggle the current audio mute + * status of the player. Utilizing the provided callbacks to update the controller configuration, + * this ensures the correct requests are sent to [AudioManager] before the players are unmuted / + * muted. + */ +@Composable +private fun rememberAudioFocus( + video: Media.Video, + surfaceCreated: Boolean, + audioIsMuted: Boolean, + onFocusLost: () -> Unit, + onConfigChangeRequested: (Bundle) -> Unit, + onRequestAudioMuteChange: (Boolean) -> Unit, +): (Boolean) -> Unit { + + val context = LocalContext.current + val audioManager: AudioManager = remember { context.requireSystemService() } + + /** [OnAudioFocusChangeListener] unique to this remote player (authority based) */ + val audioListener = + remember(video.authority) { + object : AudioManager.OnAudioFocusChangeListener { + override fun onAudioFocusChange(focusChange: Int) { + if ( + focusChange == AudioManager.AUDIOFOCUS_LOSS || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK + ) { + onFocusLost() + } + } + } + } + + /** [AudioFocusRequest] unique to this remote player (authority based) */ + val audioRequest = + remember(video.authority) { + AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + .apply { + setAudioAttributes(AUDIO_ATTRIBUTES) + setWillPauseWhenDucked(true) + setAcceptsDelayedFocusGain(true) + setOnAudioFocusChangeListener(audioListener) + } + .build() + } + + // Wait for the video surface to be created before setting up audio focus for the player. + // This is required because the Player may not exist yet if this is the first / only active + // surface for this controller. + if (surfaceCreated) { + + // A DisposableEffect is needed here to ensure the audio focus is abandoned + // when this composable leaves the view. Otherwise, AudioManager will continue + // to make calls to the callback which can potentially cause runtime errors, + // and audio may continue to play until the underlying video surface gets + // destroyed. + DisposableEffect(video.authority) { + + // Additionally, any time the current video's authority is different from the + // last compose, set the audio state on the current controller to match the + // session's audio state. + val bundle = + when (audioIsMuted) { + true -> bundleOf(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED to true) + false -> bundleOf(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED to false) + } + onConfigChangeRequested(bundle) + + // If the audio currently isn't muted, then request audio focus again with the new + // request to ensure callbacks are received. + if (!audioIsMuted) { + audioManager.requestAudioFocus(audioRequest) + } + + // When the composable leaves the tree, cleanup the audio request to prevent any + // audio from playing while the screen isn't being shown to the user. + onDispose { + Log.d(PreviewFeature.TAG, "Abandoning audio focus for authority $video.authority") + audioManager.abandonAudioFocusRequest(audioRequest) + } + } + } + + /** Return a function that can be used to toggle the mute status of the composable */ + return { currentlyMuted: Boolean -> + when (currentlyMuted) { + true -> { + if ( + audioManager.requestAudioFocus(audioRequest) == + AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ) { + Log.d(PreviewFeature.TAG, "Acquired audio focus to unmute player") + val bundle = bundleOf(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED to false) + onConfigChangeRequested(bundle) + onRequestAudioMuteChange(false) + } + } + false -> { + Log.d(PreviewFeature.TAG, "Abandoning audio focus and muting player") + audioManager.abandonAudioFocusRequest(audioRequest) + val bundle = bundleOf(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED to true) + onConfigChangeRequested(bundle) + onRequestAudioMuteChange(true) + } + } + } +} + +/** + * State produce for a video's [PlaybackInfo]. + * + * This producer listens to all [PlaybackState] updates for the given video and surface, and + * produces the most recent update as observable composable [State]. + * + * @param surfaceId the id of the player's surface. + * @param video the video to calculate the aspect ratio for. @viewModel an instance of + * [PreviewViewModel], this is injected by hilt. + * @return observable composable state object that yields the most recent [PlaybackInfo]. + */ +@Composable +private fun producePlaybackInfo( + surfaceId: Int, + video: Media.Video, + viewModel: PreviewViewModel = hiltViewModel() +): State<PlaybackInfo> { + + return produceState<PlaybackInfo>( + initialValue = + PlaybackInfo( + state = PlaybackState.UNKNOWN, + surfaceId, + authority = video.authority, + ), + surfaceId, + video + ) { + viewModel.getPlaybackInfoForPlayer(surfaceId, video).collect { playbackInfo -> + Log.d(PreviewFeature.TAG, "PlaybackState change received: $playbackInfo") + value = playbackInfo + } + } +} + +/** + * State producer for a video's AspectRatio. + * + * This producer listens to the controller's [PlaybackState] flow and extracts any + * [MEDIA_SIZE_CHANGED] events for the given surfaceId and video and produces the correct aspect + * ratio for the video as composable [State] + * + * @param surfaceId the id of the player's surface. + * @param video the video to calculate the aspect ratio for. @viewModel an instance of + * [PreviewViewModel], this is injected by hilt. + * @return observable composable state object that yields the correct AspectRatio + */ +@Composable +private fun produceAspectRatio( + surfaceId: Int, + video: Media.Video, + viewModel: PreviewViewModel = hiltViewModel() +): State<Float?> { + + return produceState<Float?>( + initialValue = null, + surfaceId, + video, + ) { + viewModel + .getPlaybackInfoForPlayer(surfaceId, video) + .filter { it.state == PlaybackState.MEDIA_SIZE_CHANGED } + .collect { playbackInfo -> + val size: Point? = + playbackInfo.playbackStateInfo?.getParcelable(EXTRA_SIZE, Point::class.java) + size?.let { + // AspectRatio = Width divided by height as a float + Log.d(PreviewFeature.TAG, "Media Size change received: ${size.x} x ${size.y}") + value = size.x.toFloat() / size.y.toFloat() + } + } + } +} diff --git a/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt b/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt index e6abc6aae..97403c380 100644 --- a/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt @@ -102,7 +102,7 @@ class LoadMediaTest { return Uri.EMPTY.buildUpon() .apply { scheme("content") - authority(provider.AUTHORITY) + authority(MockContentProviderWrapper.AUTHORITY) path("${CloudMediaProviderContract.URI_PATH_MEDIA}/1234") } .build() diff --git a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt index a2c0c0ab3..a0c62cc52 100644 --- a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt @@ -18,7 +18,6 @@ package com.android.photopicker.features.photogrid import android.content.ContentProvider import android.content.ContentResolver -import android.content.Context import android.content.Intent import android.provider.MediaStore import androidx.compose.ui.test.ExperimentalTestApi @@ -114,8 +113,6 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { private lateinit var provider: MockContentProviderWrapper @Mock lateinit var mockContentProvider: ContentProvider - @BindValue val context: Context = getTestableContext() - @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var events: Events diff --git a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt index ce209cc3c..e0934bf77 100644 --- a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt @@ -19,18 +19,39 @@ package com.android.photopicker.features.preview import android.content.ContentProvider import android.content.ContentResolver import android.content.Context +import android.content.pm.PackageManager import android.net.Uri +import android.os.Bundle +import android.os.UserHandle +import android.os.UserManager +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED +import android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK +import android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER +import android.provider.ICloudMediaSurfaceController +import android.provider.ICloudMediaSurfaceStateChangedCallback +import android.test.mock.MockContentResolver +import android.view.Surface import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf import com.android.photopicker.R import com.android.photopicker.core.ActivityModule import com.android.photopicker.core.ApplicationModule @@ -53,6 +74,9 @@ import com.android.photopicker.features.PhotopickerFeatureBaseTest import com.android.photopicker.inject.PhotopickerTestModule import com.android.photopicker.test.utils.MockContentProviderWrapper import com.android.photopicker.tests.HiltTestActivity +import com.android.photopicker.tests.utils.mockito.capture +import com.android.photopicker.tests.utils.mockito.mockSystemService +import com.android.photopicker.tests.utils.mockito.nonNullableEq import com.android.photopicker.tests.utils.mockito.whenever import com.google.common.truth.Truth.assertWithMessage import dagger.Module @@ -77,8 +101,15 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.isNull +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @UninstallModules( @@ -113,15 +144,23 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher /** - * PhotoGrid uses Glide for loading images, so we have to mock out the dependencies for Glide + * Preview uses Glide for loading images, so we have to mock out the dependencies for Glide * Replace the injected ContentResolver binding in [ApplicationModule] with this test value. */ @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver private lateinit var provider: MockContentProviderWrapper @Mock lateinit var mockContentProvider: ContentProvider - @BindValue val context: Context = getTestableContext() + // Needed for UserMonitor in PreviewViewModel + @Mock lateinit var mockUserManager: UserManager + @Mock lateinit var mockPackageManager: PackageManager + // Needed for Preview + lateinit var controllerProxy: ICloudMediaSurfaceController.Stub + @Mock lateinit var mockCloudMediaSurfaceController: ICloudMediaSurfaceController.Stub + @Captor lateinit var controllerBundle: ArgumentCaptor<Bundle> + + @Inject lateinit var mockContext: Context @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var events: Events @@ -130,44 +169,157 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { Media.Image( mediaId = "image_id", pickerId = 123456789L, - authority = "a", + authority = MockContentProviderWrapper.AUTHORITY, mediaSource = MediaSource.LOCAL, - mediaUri = Uri.EMPTY.buildUpon() - .apply { - scheme("content") - authority("media") - path("picker") - path("a") - path("image_id") - } - .build(), - glideLoadableUri = Uri.EMPTY.buildUpon() - .apply { - scheme("content") - authority("a") - path("image_id") - } - .build(), + mediaUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("media") + path("picker") + path("a") + path("image_id") + } + .build(), + glideLoadableUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("image_id") + } + .build(), dateTakenMillisLong = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000, sizeInBytes = 1000L, mimeType = "image/png", standardMimeTypeExtension = 1, ) + val TEST_MEDIA_VIDEO = + Media.Video( + mediaId = "video_id", + pickerId = 987654321L, + authority = MockContentProviderWrapper.AUTHORITY, + mediaSource = MediaSource.LOCAL, + mediaUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("video_id") + } + .build(), + glideLoadableUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("video_id") + } + .build(), + dateTakenMillisLong = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000, + sizeInBytes = 1000L, + mimeType = "video/mp4", + standardMimeTypeExtension = 1, + duration = 10000, + ) + @Before fun setup() { MockitoAnnotations.initMocks(this) hiltRule.inject() + // Stub for MockContentResolver constructor + whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() } + // Stub out the content resolver for Glide + val mockContentResolver = MockContentResolver(mockContext) provider = MockContentProviderWrapper(mockContentProvider) - contentResolver = ContentResolver.wrap(provider) + mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider) + contentResolver = mockContentResolver // Return a resource png so that glide actually has something to load whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) { getTestableContext().getResources().openRawResourceFd(R.drawable.android) } + + // Stubs for UserMonitor + mockSystemService(mockContext, UserManager::class.java) { mockUserManager } + whenever(mockContext.packageManager) { mockPackageManager } + whenever(mockContext.contentResolver) { contentResolver } + whenever(mockContext.packageName) { "com.android.photopicker" } + + // Recursively return the same mockContext for all user packages to keep the stubing simple. + whenever( + mockContext.createPackageContextAsUser( + anyString(), + anyInt(), + any(UserHandle::class.java) + ) + ) { + mockContext + } + + // Setup a proxy to call the mocked controller, since IBinder uses onTransact under the hood + // and that is more complicated to verify. + controllerProxy = + object : ICloudMediaSurfaceController.Stub() { + + override fun onSurfaceCreated(surfaceId: Int, surface: Surface, mediaId: String) { + mockCloudMediaSurfaceController.onSurfaceCreated(surfaceId, surface, mediaId) + } + + override fun onSurfaceChanged( + surfaceId: Int, + format: Int, + width: Int, + height: Int + ) { + mockCloudMediaSurfaceController.onSurfaceChanged( + surfaceId, + format, + width, + height + ) + } + + override fun onSurfaceDestroyed(surfaceId: Int) { + mockCloudMediaSurfaceController.onSurfaceDestroyed(surfaceId) + } + override fun onMediaPlay(surfaceId: Int) { + mockCloudMediaSurfaceController.onMediaPlay(surfaceId) + } + override fun onMediaPause(surfaceId: Int) { + mockCloudMediaSurfaceController.onMediaPause(surfaceId) + } + override fun onMediaSeekTo(surfaceId: Int, timestampMillis: Long) { + mockCloudMediaSurfaceController.onMediaSeekTo(surfaceId, timestampMillis) + } + override fun onConfigChange(bundle: Bundle) { + mockCloudMediaSurfaceController.onConfigChange(bundle) + } + override fun onDestroy() { + mockCloudMediaSurfaceController.onDestroy() + } + override fun onPlayerCreate() { + mockCloudMediaSurfaceController.onPlayerCreate() + } + override fun onPlayerRelease() { + mockCloudMediaSurfaceController.onPlayerRelease() + } + } + + whenever( + mockContentProvider.call( + /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ capture(controllerBundle), + ) + ) { + bundleOf(EXTRA_SURFACE_CONTROLLER to controllerProxy) + } } /** Ensures that the PreviewMedia route can be navigated to with an Image payload. */ @@ -208,6 +360,40 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { .isEqualTo(TEST_MEDIA_IMAGE) } + @Test + fun testNavigateToPreviewVideo() = + mainScope.runTest { + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + assertWithMessage("Expected route to be preview/media") + .that(navController.currentBackStackEntry?.destination?.route) + .isEqualTo(PhotopickerDestinations.PREVIEW_MEDIA.route) + + val previewMedia: Media? = + navController.currentBackStackEntry + ?.savedStateHandle + ?.get(PreviewFeature.PREVIEW_MEDIA_KEY) + + assertWithMessage("Expected backstack entry to have a media item") + .that(previewMedia) + .isNotNull() + + assertWithMessage("Expected media to be the selected media") + .that(previewMedia) + .isEqualTo(TEST_MEDIA_VIDEO) + } + /** Ensures that the Preview Media route can toggle the displayed item in the selection. */ @Test fun testPreviewMediaToggleSelection() = @@ -284,7 +470,7 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { // Navigate on the UI thread (similar to a click handler) composeTestRule.runOnUiThread({ navController.navigateToPreviewSelection() }) - assertWithMessage("Expected route to be preview/media") + assertWithMessage("Expected route to be preview/selection") .that(navController.currentBackStackEntry?.destination?.route) .isEqualTo(PhotopickerDestinations.PREVIEW_SELECTION.route) } @@ -402,4 +588,465 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { .that(eventsSent) .contains(Event.MediaSelectionConfirmed(FeatureToken.PREVIEW.token)) } + + /** Ensures the VideoUi creates a RemoteSurfaceController */ + @Test + fun testVideoUiCreatesRemoteSurfaceController() = + mainScope.runTest { + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + verify(mockContentProvider) + .call( + /*authority=*/ anyString(), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ any(Bundle::class.java), + ) + + val bundle = controllerBundle.getValue() + assertWithMessage("SurfaceStateChangedCallback was not provided") + .that(bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK)) + .isNotNull() + assertWithMessage("Surface controller was not looped by default") + // Default value from bundle is false so this fails if it wasn't set + .that(bundle.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false)) + .isTrue() + assertWithMessage("Surface controller was not muted by default") + // Default value from bundle is false so this fails if it wasn't set + .that(bundle.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, false)) + .isTrue() + } + + /** Ensures the VideoUi notifies of surfaceCreation */ + @Test + fun testVideoUiNotifySurfaceCreated() = + mainScope.runTest { + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + assertWithMessage("SurfaceStateChangedCallback was not provided") + .that(bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK)) + .isNotNull() + + verify(mockContentProvider) + .call( + /*authority=*/ anyString(), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ any(Bundle::class.java), + ) + + verify(mockCloudMediaSurfaceController) + .onSurfaceCreated(anyInt(), any(Surface::class.java), anyString()) + verify(mockCloudMediaSurfaceController) + .onSurfaceChanged(anyInt(), anyInt(), anyInt(), anyInt()) + verify(mockCloudMediaSurfaceController).onPlayerCreate() + } + + /** Ensures the VideoUi attempts to play videos when the controller indicates it is ready. */ + @Test + fun testVideoUiRequestsPlayWhenMediaReady() = + mainScope.runTest { + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_READY, null) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + } + + /** Ensures the VideoUi auto shows & hides the player controls. */ + @Test + fun testVideoUiShowsAndHidesPlayerControls() = + mainScope.runTest { + val resources = getTestableContext().getResources() + + val playButtonDescription = + resources.getString(R.string.photopicker_video_play_button_description) + + val pauseButtonDescription = + resources.getString(R.string.photopicker_video_pause_button_description) + + val muteButtonDescription = + resources.getString(R.string.photopicker_video_mute_button_description) + + val unmuteButtonDescription = + resources.getString(R.string.photopicker_video_unmute_button_description) + + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_READY, null) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_STARTED, null) + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Pause is the button shown once the player begins playing. + composeTestRule + .onNode(hasContentDescription(pauseButtonDescription)) + .assertIsDisplayed() + .assert(hasClickAction()) + + // Unmute is the audio button shown once the player begins playing. + composeTestRule + .onNode(hasContentDescription(unmuteButtonDescription)) + .assertIsDisplayed() + .assert(hasClickAction()) + + composeTestRule.mainClock.autoAdvance = false + // Wait enough time for the delay & the animation to end + composeTestRule.mainClock.advanceTimeBy(10_000L) + composeTestRule.waitForIdle() + + // Now the player controls should not be visible + composeTestRule + .onNode(hasContentDescription(pauseButtonDescription)) + .assertIsNotDisplayed() + composeTestRule + .onNode(hasContentDescription(unmuteButtonDescription)) + .assertIsNotDisplayed() + composeTestRule + .onNode(hasContentDescription(playButtonDescription)) + .assertIsNotDisplayed() + composeTestRule + .onNode(hasContentDescription(muteButtonDescription)) + .assertIsNotDisplayed() + } + + /** Ensures the VideoUi Play/Pause buttons work correctly. */ + @Test + fun testVideoUiPlayPauseButtonOnClick() = + mainScope.runTest { + val resources = getTestableContext().getResources() + + val playButtonDescription = + resources.getString(R.string.photopicker_video_play_button_description) + + val pauseButtonDescription = + resources.getString(R.string.photopicker_video_pause_button_description) + + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_READY, null) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_STARTED, null) + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + + clearInvocations(mockCloudMediaSurfaceController) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Pause is the button shown once the player begins playing. + composeTestRule + .onNode(hasContentDescription(pauseButtonDescription)) + .assertIsDisplayed() + .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + verify(mockCloudMediaSurfaceController).onMediaPause(anyInt()) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_PAUSED, null) + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onNode(hasContentDescription(playButtonDescription)) + .assertIsDisplayed() + .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + composeTestRule.waitForIdle() + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + } + + /** Ensures the VideoUi Mute/UnMute buttons work correctly. */ + @Test + fun testVideoUiMuteButtonOnClick() = + mainScope.runTest { + val resources = getTestableContext().getResources() + val muteButtonDescription = + resources.getString(R.string.photopicker_video_mute_button_description) + + val unmuteButtonDescription = + resources.getString(R.string.photopicker_video_unmute_button_description) + + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_READY, null) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_STARTED, null) + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + + clearInvocations(mockCloudMediaSurfaceController) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Pause is the button shown once the player begins playing. + composeTestRule + .onNode(hasContentDescription(unmuteButtonDescription)) + .assertIsDisplayed() + .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + verify(mockCloudMediaSurfaceController).onConfigChange(any(Bundle::class.java)) + + clearInvocations(mockCloudMediaSurfaceController) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onNode(hasContentDescription(muteButtonDescription)) + .assertIsDisplayed() + .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + composeTestRule.waitForIdle() + verify(mockCloudMediaSurfaceController).onConfigChange(any(Bundle::class.java)) + } + + /** Ensures the VideoUi shows an error dialog for temporary failures. */ + @Test + fun testVideoUiRetriablePlaybackError() = + mainScope.runTest { + val resources = getTestableContext().getResources() + + val retryButtonLabel = + resources.getString(R.string.photopicker_preview_dialog_error_retry_button_label) + val errorTitle = resources.getString(R.string.photopicker_preview_dialog_error_title) + val errorMessage = + resources.getString(R.string.photopicker_preview_dialog_error_message) + + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_READY, null) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_STARTED, null) + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + + clearInvocations(mockCloudMediaSurfaceController) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState( + /*surfaceId=*/ 1, + PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE, + null + ) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule.onNode(hasText(errorTitle)).assertIsDisplayed() + composeTestRule.onNode(hasText(errorMessage)).assertIsDisplayed() + composeTestRule + .onNode(hasText(retryButtonLabel)) + .assertIsDisplayed() + .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + + composeTestRule.onNode(hasText(errorTitle)).assertIsNotDisplayed() + composeTestRule.onNode(hasText(errorMessage)).assertIsNotDisplayed() + composeTestRule.onNode(hasText(retryButtonLabel)).assertIsNotDisplayed() + } + + /** Ensures the VideoUi shows a snackbar for permanent failures. */ + @Test + fun testVideoUiPermanentPlaybackError() = + mainScope.runTest { + val resources = getTestableContext().getResources() + + val errorMessage = + resources.getString(R.string.photopicker_preview_video_error_snackbar) + + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ + navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO) + }) + + composeTestRule.waitForIdle() + advanceTimeBy(100) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_READY, null) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState(/*surfaceId=*/ 1, PLAYBACK_STATE_STARTED, null) + verify(mockCloudMediaSurfaceController).onMediaPlay(anyInt()) + + clearInvocations(mockCloudMediaSurfaceController) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + callback.setPlaybackState( + /*surfaceId=*/ 1, + PLAYBACK_STATE_ERROR_PERMANENT_FAILURE, + null + ) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule.onNode(hasText(errorMessage)).assertIsDisplayed() + } } diff --git a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt index 04666ac58..c0deb58ac 100644 --- a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt @@ -16,32 +16,107 @@ package com.android.photopicker.features.preview +import android.content.ContentProvider +import android.content.ContentResolver.EXTRA_SIZE +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Point import android.net.Uri +import android.os.Bundle +import android.os.Parcel +import android.os.UserHandle +import android.os.UserManager +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY +import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED +import android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED +import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK +import android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER +import android.provider.ICloudMediaSurfaceController +import android.provider.ICloudMediaSurfaceStateChangedCallback +import android.test.mock.MockContentResolver +import android.view.Surface +import androidx.core.os.bundleOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import com.android.photopicker.core.selection.Selection +import com.android.photopicker.core.user.UserMonitor import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaSource +import com.android.photopicker.test.utils.MockContentProviderWrapper +import com.android.photopicker.tests.utils.mockito.capture +import com.android.photopicker.tests.utils.mockito.mockSystemService +import com.android.photopicker.tests.utils.mockito.nonNullableEq +import com.android.photopicker.tests.utils.mockito.whenever import com.google.common.truth.Truth.assertWithMessage +import java.time.LocalDateTime +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.isNull +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class PreviewViewModelTest { - val mediaItem = + @Mock lateinit var mockContext: Context + @Mock lateinit var mockUserManager: UserManager + @Mock lateinit var mockPackageManager: PackageManager + @Mock lateinit var mockContentProvider: ContentProvider + @Mock lateinit var mockController: ICloudMediaSurfaceController.Stub + @Captor lateinit var controllerBundle: ArgumentCaptor<Bundle> + + private lateinit var mockContentResolver: MockContentResolver + + private val USER_HANDLE_PRIMARY: UserHandle + private val USER_ID_PRIMARY: Int = 0 + + init { + val parcel1 = Parcel.obtain() + parcel1.writeInt(USER_ID_PRIMARY) + parcel1.setDataPosition(0) + USER_HANDLE_PRIMARY = UserHandle(parcel1) + } + + val TEST_MEDIA_IMAGE = Media.Image( mediaId = "id", pickerId = 1000L, authority = "a", - mediaSource = MediaSource.LOCAL, - mediaUri = Uri.EMPTY.buildUpon() + mediaSource = MediaSource.LOCAL, + mediaUri = + Uri.EMPTY.buildUpon() .apply { scheme("content") authority("media") @@ -50,10 +125,11 @@ class PreviewViewModelTest { path("id") } .build(), - glideLoadableUri = Uri.EMPTY.buildUpon() + glideLoadableUri = + Uri.EMPTY.buildUpon() .apply { scheme("content") - authority("a") + authority(MockContentProviderWrapper.AUTHORITY) path("id") } .build(), @@ -63,6 +139,66 @@ class PreviewViewModelTest { standardMimeTypeExtension = 1, ) + val TEST_MEDIA_VIDEO = + Media.Video( + mediaId = "video_id", + pickerId = 987654321L, + authority = MockContentProviderWrapper.AUTHORITY, + mediaSource = MediaSource.LOCAL, + mediaUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("video_id") + } + .build(), + glideLoadableUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("video_id") + } + .build(), + dateTakenMillisLong = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000, + sizeInBytes = 1000L, + mimeType = "video/mp4", + standardMimeTypeExtension = 1, + duration = 10000, + ) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mockSystemService(mockContext, UserManager::class.java) { mockUserManager } + + // Stub for MockContentResolver constructor + whenever(mockContext.getApplicationInfo()) { + InstrumentationRegistry.getInstrumentation().getContext().getApplicationInfo() + } + mockContentResolver = MockContentResolver(mockContext) + val provider = MockContentProviderWrapper(mockContentProvider) + mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider) + + // Stubs for UserMonitor + whenever(mockContext.packageManager) { mockPackageManager } + whenever(mockContext.contentResolver) { mockContentResolver } + whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext } + + // Stubs for creating the RemoteSurfaceController + whenever( + mockContentProvider.call( + /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ capture(controllerBundle), + ) + ) { + bundleOf(EXTRA_SURFACE_CONTROLLER to mockController) + } + } + /** Ensures the view model can toggle items in the session selection. */ @Test fun testToggleInSelectionUpdatesSelection() { @@ -74,6 +210,12 @@ class PreviewViewModelTest { PreviewViewModel( this.backgroundScope, selection, + UserMonitor( + mockContext, + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ), ) assertWithMessage("Unexpected selection start size") @@ -81,23 +223,23 @@ class PreviewViewModelTest { .isEqualTo(0) // Toggle the item into the selection - viewModel.toggleInSelection(mediaItem) + viewModel.toggleInSelection(TEST_MEDIA_IMAGE) // Wait for selection update. advanceTimeBy(100) assertWithMessage("Selection did not contain expected item") .that(selection.snapshot()) - .contains(mediaItem) + .contains(TEST_MEDIA_IMAGE) // Toggle the item out of the selection - viewModel.toggleInSelection(mediaItem) + viewModel.toggleInSelection(TEST_MEDIA_IMAGE) advanceTimeBy(100) assertWithMessage("Selection contains unexpected item") .that(selection.snapshot()) - .doesNotContain(mediaItem) + .doesNotContain(TEST_MEDIA_IMAGE) } } @@ -106,12 +248,18 @@ class PreviewViewModelTest { fun testSnapshotSelection() { runTest { - val selection = Selection<Media>(scope = this.backgroundScope, setOf(mediaItem)) + val selection = Selection<Media>(scope = this.backgroundScope, setOf(TEST_MEDIA_IMAGE)) val viewModel = PreviewViewModel( this.backgroundScope, selection, + UserMonitor( + mockContext, + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ), ) var snapshot = viewModel.selectionSnapshot.first() @@ -129,7 +277,332 @@ class PreviewViewModelTest { assertWithMessage("Selection snapshot did not match expected") .that(snapshot) - .isEqualTo(setOf(mediaItem)) + .isEqualTo(setOf(TEST_MEDIA_IMAGE)) } } + + /** Ensures the creation parameters of remote surface controllers. */ + @Test + fun testRemotePreviewControllerCreation() { + + runTest { + val selection = Selection<Media>(scope = this.backgroundScope, setOf(TEST_MEDIA_IMAGE)) + val viewModel = + PreviewViewModel( + this.backgroundScope, + selection, + UserMonitor( + mockContext, + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ), + ) + + val controller = + viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) + + assertWithMessage("Returned controller was not expected to be null") + .that(controller) + .isNotNull() + + verify(mockContentProvider) + .call( + /*authority=*/ anyString(), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ any(Bundle::class.java), + ) + + val bundle = controllerBundle.getValue() + assertWithMessage("SurfaceStateChangedCallback was not provided") + .that(bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK)) + .isNotNull() + assertWithMessage("Surface controller was not looped by default") + // Default value from bundle is false so this fails if it wasn't set + .that(bundle.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false)) + .isTrue() + assertWithMessage("Surface controller was not muted by default") + // Default value from bundle is false so this fails if it wasn't set + .that(bundle.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, false)) + .isTrue() + } + } + + /** Ensures that remote preview controllers are cached for authorities. */ + @Test + fun testRemotePreviewControllersAreCached() { + + runTest { + val selection = Selection<Media>(scope = this.backgroundScope, setOf(TEST_MEDIA_IMAGE)) + val viewModel = + PreviewViewModel( + this.backgroundScope, + selection, + UserMonitor( + mockContext, + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ), + ) + + val controller = + viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) + val controllerTwo = + viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) + + assertWithMessage("Returned controller was not expected to be null") + .that(controller) + .isNotNull() + + assertWithMessage("Returned controller was not expected to be null") + .that(controllerTwo) + .isNotNull() + + assertWithMessage("Expected both controller instances to be the same") + .that(controller) + .isEqualTo(controllerTwo) + + verify(mockContentProvider, times(1)) + .call( + /*authority=*/ anyString(), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ any(Bundle::class.java), + ) + } + } + + /** Ensures that remote preview controllers are destroyed when the view model is cleared. */ + @Test + fun testRemotePreviewControllersAreDestroyed() { + + runTest { + // Setup a proxy to call the mocked controller, since IBinder uses onTransact under the + // hood and that is more complicated to verify. + val controllerProxy = + object : ICloudMediaSurfaceController.Stub() { + + override fun onSurfaceCreated( + surfaceId: Int, + surface: Surface, + mediaId: String + ) {} + + override fun onSurfaceChanged( + surfaceId: Int, + format: Int, + width: Int, + height: Int + ) {} + + override fun onSurfaceDestroyed(surfaceId: Int) {} + override fun onMediaPlay(surfaceId: Int) {} + override fun onMediaPause(surfaceId: Int) {} + override fun onMediaSeekTo(surfaceId: Int, timestampMillis: Long) {} + override fun onConfigChange(bundle: Bundle) {} + override fun onDestroy() { + mockController.onDestroy() + } + override fun onPlayerCreate() {} + override fun onPlayerRelease() {} + } + + whenever( + mockContentProvider.call( + /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY), + /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), + /*arg=*/ isNull(), + /*extras=*/ capture(controllerBundle), + ) + ) { + bundleOf(EXTRA_SURFACE_CONTROLLER to controllerProxy) + } + val selection = Selection<Media>(scope = this.backgroundScope, setOf(TEST_MEDIA_IMAGE)) + val viewModel = + PreviewViewModel( + this.backgroundScope, + selection, + UserMonitor( + mockContext, + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ), + ) + + viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) + + viewModel.callOnCleared() + verify(mockController).onDestroy() + } + } + + /** Ensures that surface playback updates are emitted. */ + @Test + fun testRemotePreviewSurfaceStateChangedCallbackEmitsUpdates() { + + runTest { + val selection = Selection<Media>(scope = this.backgroundScope, setOf(TEST_MEDIA_IMAGE)) + val viewModel = + PreviewViewModel( + this.backgroundScope, + selection, + UserMonitor( + mockContext, + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ), + ) + + viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) + + val bundle = controllerBundle.getValue() + val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) + val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) + + val emissions = mutableListOf<PlaybackInfo>() + backgroundScope.launch { + viewModel + .getPlaybackInfoForPlayer( + surfaceId = 1, + video = TEST_MEDIA_VIDEO, + ) + .toList(emissions) + } + + callback.setPlaybackState( + 1, + PLAYBACK_STATE_MEDIA_SIZE_CHANGED, + bundleOf(EXTRA_SIZE to Point(100, 200)) + ) + advanceTimeBy(100) + + val mediaSizeChangedInfo = emissions.removeFirst() + assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") + .that(mediaSizeChangedInfo.state) + .isEqualTo(PlaybackState.MEDIA_SIZE_CHANGED) + assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") + .that(mediaSizeChangedInfo.surfaceId) + .isEqualTo(1) + assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") + .that(mediaSizeChangedInfo.authority) + .isEqualTo(MockContentProviderWrapper.AUTHORITY) + assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") + .that( + mediaSizeChangedInfo.playbackStateInfo?.getParcelable( + EXTRA_SIZE, + Point::class.java + ) + ) + .isEqualTo(Point(100, 200)) + + callback.setPlaybackState(1, PLAYBACK_STATE_BUFFERING, null) + advanceTimeBy(100) + assertWithMessage("BUFFERING emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.BUFFERING, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + + callback.setPlaybackState(1, PLAYBACK_STATE_READY, null) + advanceTimeBy(100) + assertWithMessage("READY emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.READY, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + + callback.setPlaybackState(1, PLAYBACK_STATE_STARTED, null) + advanceTimeBy(100) + assertWithMessage("STARTED emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.STARTED, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + + callback.setPlaybackState(1, PLAYBACK_STATE_PAUSED, null) + advanceTimeBy(100) + assertWithMessage("PAUSED emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.PAUSED, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + + callback.setPlaybackState(1, PLAYBACK_STATE_COMPLETED, null) + advanceTimeBy(100) + assertWithMessage("COMPLETED emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.COMPLETED, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + + callback.setPlaybackState(1, PLAYBACK_STATE_ERROR_PERMANENT_FAILURE, null) + advanceTimeBy(100) + assertWithMessage("ERROR_PERMANENT_FAILURE emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.ERROR_PERMANENT_FAILURE, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + + callback.setPlaybackState(1, PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE, null) + advanceTimeBy(100) + assertWithMessage("ERROR_RETRIABLE_FAILURE emitted state was invalid") + .that(emissions.removeFirst()) + .isEqualTo( + PlaybackInfo( + state = PlaybackState.ERROR_RETRIABLE_FAILURE, + surfaceId = 1, + authority = MockContentProviderWrapper.AUTHORITY + ) + ) + } + } + + /** + * Extension function that will create new [ViewModelStore], add view model into it using + * [ViewModelProvider] and then call [ViewModelStore.clear], that will cause + * [ViewModel.onCleared] to be called + */ + private fun ViewModel.callOnCleared() { + val viewModelStore = ViewModelStore() + val viewModelProvider = + ViewModelProvider( + viewModelStore, + object : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T = + this@callOnCleared as T + } + ) + viewModelProvider.get(this@callOnCleared::class.java) + viewModelStore.clear() // To call clear() in ViewModel + } } diff --git a/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt index 6a4059624..bc2d08958 100644 --- a/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt @@ -16,7 +16,6 @@ package com.android.photopicker.features.selectionbar -import android.content.Context import android.content.Intent import android.net.Uri import android.provider.MediaStore @@ -101,8 +100,6 @@ class SelectionBarFeatureTest : PhotopickerFeatureBaseTest() { @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher - @BindValue val context: Context = getTestableContext() - @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var events: Events @@ -114,22 +111,24 @@ class SelectionBarFeatureTest : PhotopickerFeatureBaseTest() { pickerId = 1L, authority = "a", mediaSource = MediaSource.LOCAL, - mediaUri = Uri.EMPTY.buildUpon() - .apply { - scheme("content") - authority("media") - path("picker") - path("a") - path("1") - } - .build(), - glideLoadableUri = Uri.EMPTY.buildUpon() - .apply { - scheme("content") - authority("a") - path("1") - } - .build(), + mediaUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("media") + path("picker") + path("a") + path("1") + } + .build(), + glideLoadableUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("1") + } + .build(), dateTakenMillisLong = 123456789L, sizeInBytes = 1000L, mimeType = "image/png", diff --git a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt index a34dafd5a..3e385159a 100644 --- a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt +++ b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt @@ -37,6 +37,7 @@ import dagger.hilt.migration.DisableInstallInCheck import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import org.mockito.Mockito.mock /** * A basic Hilt test module that resolves common injected dependencies. Tests can install extend and @@ -58,6 +59,15 @@ abstract class PhotopickerTestModule { @Singleton @Provides + fun provideTestMockContext(): Context { + // Always provide a Mocked Context object to injected dependencies, to ensure that the + // device state never leaks into the test environment. Feature tests can obtain this mock + // by injecting a context object. + return mock(Context::class.java) + } + + @Singleton + @Provides fun createConfigurationManager( @Background scope: CoroutineScope, @Background dispatcher: CoroutineDispatcher, diff --git a/photopicker/tests/src/com/android/photopicker/utils/MockContentProviderWrapper.kt b/photopicker/tests/src/com/android/photopicker/utils/MockContentProviderWrapper.kt index f51ecba56..18dc372d7 100644 --- a/photopicker/tests/src/com/android/photopicker/utils/MockContentProviderWrapper.kt +++ b/photopicker/tests/src/com/android/photopicker/utils/MockContentProviderWrapper.kt @@ -34,7 +34,9 @@ import android.test.mock.MockContentProvider */ class MockContentProviderWrapper(val provider: ContentProvider) : MockContentProvider() { - val AUTHORITY = "MOCK_CONTENT_PROVIDER" + companion object { + val AUTHORITY = "MOCK_CONTENT_PROVIDER" + } /** Pass calls to the wrapped provider. */ override fun openTypedAssetFile( @@ -45,4 +47,9 @@ class MockContentProviderWrapper(val provider: ContentProvider) : MockContentPro ): AssetFileDescriptor? { return provider.openTypedAssetFile(uri, mimetype, opts, cancellationSignal) } + + /** Pass calls to the wrapped provider. */ + override fun call(authority: String, method: String, arg: String?, extras: Bundle?): Bundle? { + return provider.call(authority, method, arg, extras) + } } diff --git a/res/layout/activity_photo_picker.xml b/res/layout/activity_photo_picker.xml index 0d1cdf271..ce445dd42 100644 --- a/res/layout/activity_photo_picker.xml +++ b/res/layout/activity_photo_picker.xml @@ -117,45 +117,53 @@ </com.google.android.material.appbar.AppBarLayout> - <FrameLayout + <LinearLayout android:id="@+id/picker_bottom_bar" android:layout_width="match_parent" android:layout_height="@dimen/picker_bottom_bar_size" android:layout_gravity="bottom" android:background="@color/picker_background_color" android:elevation="@dimen/picker_bottom_bar_elevation" - android:visibility="gone"> + android:visibility="gone" + android:orientation="horizontal"> <com.google.android.material.button.MaterialButton android:id="@+id/button_view_selected" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_weight="2" android:layout_marginHorizontal="@dimen/picker_bottom_bar_horizontal_gap" - android:layout_gravity="start|center_vertical" + android:gravity="start|center_vertical" android:paddingVertical="@dimen/picker_bottom_bar_buttons_vertical_gap" android:text="@string/picker_view_selected" android:textAllCaps="false" android:textColor="?attr/pickerSelectedColor" + android:maxLines="1" + android:ellipsize="end" app:icon="@drawable/ic_collections" app:iconPadding="@dimen/picker_viewselected_icon_padding" app:iconSize="@dimen/picker_viewselected_icon_size" app:iconTint="?attr/pickerSelectedColor" + app:iconGravity="textStart" style="@style/MaterialBorderlessButtonStyle"/> <Button android:id="@+id/button_add" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_weight="1" android:layout_marginHorizontal="@dimen/picker_bottom_bar_horizontal_gap" - android:layout_gravity="end|center_vertical" + android:gravity="center|center_vertical" android:paddingVertical="@dimen/picker_bottom_bar_buttons_vertical_gap" android:text="@string/add" android:textAllCaps="false" android:textColor="?attr/pickerHighlightTextColor" + android:maxLines="1" + android:ellipsize="middle" android:backgroundTint="?attr/pickerHighlightColor" style="@style/MaterialButtonStyle"/> - </FrameLayout> + </LinearLayout> </FrameLayout> diff --git a/src/com/android/providers/media/ConfigStore.java b/src/com/android/providers/media/ConfigStore.java index 9094ccb92..cbf7670c0 100644 --- a/src/com/android/providers/media/ConfigStore.java +++ b/src/com/android/providers/media/ConfigStore.java @@ -258,6 +258,28 @@ public interface ConfigStore { writer.println(" transcodeCompatStale=" + getTranscodeCompatStale()); } + static ConfigStore getDefaultConfigStore() { + return new ConfigStore() { + @NonNull + @Override + public List<String> getTranscodeCompatManifest() { + return Collections.emptyList(); + } + + @NonNull + @Override + public List<String> getTranscodeCompatStale() { + return Collections.emptyList(); + } + + @Override + public void addOnChangeListener(@NonNull Executor executor, + @NonNull Runnable listener) { + // Do nothing + } + }; + } + /** * Implementation of the {@link ConfigStore} that reads "real" configs from * {@link android.provider.DeviceConfig}. Meant to be used by the "production" code. diff --git a/src/com/android/providers/media/MediaApplication.java b/src/com/android/providers/media/MediaApplication.java index 3cef162f5..2bab13026 100644 --- a/src/com/android/providers/media/MediaApplication.java +++ b/src/com/android/providers/media/MediaApplication.java @@ -104,7 +104,8 @@ public class MediaApplication extends Application { synchronized (MediaApplication.class) { sInstance = this; if (sConfigStore == null) { - sConfigStore = new ConfigStore.ConfigStoreImpl(getResources()); + sConfigStore = sIsTestProcess ? ConfigStore.getDefaultConfigStore() : + new ConfigStore.ConfigStoreImpl(getResources()); } configStore = sConfigStore; } @@ -141,7 +142,8 @@ public class MediaApplication extends Application { // Normally ConfigStore would be created in onCreate() above, but in some cases the // framework may create ContentProvider-s *before* the Application#onCreate() is called. // In this case we use the MediaProvider instance to create the ConfigStore. - sConfigStore = new ConfigStore.ConfigStoreImpl(getAppContext().getResources()); + sConfigStore = sIsTestProcess ? ConfigStore.getDefaultConfigStore() : + new ConfigStore.ConfigStoreImpl(getAppContext().getResources()); } return sConfigStore; } diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java index 63eb58e39..b1864dcc9 100644 --- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java +++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java @@ -278,13 +278,25 @@ public class PhotoPickerActivity extends AppCompatActivity { } /** - * Warning: This method is needed for tests, we are not customizing anything here. - * Allowing ourselves to control ViewModel creation helps us mock the ViewModel for test. + * Gets PickerViewModel instance populated with the current calling package's uid. + * + * This method is also needed for tests, allowing ourselves to control ViewModel creation + * helps us mock the ViewModel for test. */ @VisibleForTesting @NonNull protected PickerViewModel getOrCreateViewModel() { - return mViewModelProvider.get(PickerViewModel.class); + PickerViewModel viewModel = mViewModelProvider.get(PickerViewModel.class); + // populate calling package UID in PickerViewModel instance. + try { + if (getCallingPackage() != null) { + viewModel.setCallingPackageUid( + getPackageManager().getPackageUid(getCallingPackage(), 0)); + } + } catch (PackageManager.NameNotFoundException ignored) { + // no-op since the default value is -1. + } + return viewModel; } @Override diff --git a/src/com/android/providers/media/photopicker/data/Selection.java b/src/com/android/providers/media/photopicker/data/Selection.java index 1f928ecec..6fa9cb275 100644 --- a/src/com/android/providers/media/photopicker/data/Selection.java +++ b/src/com/android/providers/media/photopicker/data/Selection.java @@ -82,10 +82,10 @@ public class Selection { } /** - * @return A {@link Set} of selected {@link Item} ids. + * @return A {@link Set} of selected uris. */ - public Set<String> getSelectedItemsIds() { - return mSelectedItems.values().stream().map(Item::getId).collect( + public Set<Uri> getSelectedItemsUris() { + return mSelectedItems.values().stream().map(Item::getContentUri).collect( Collectors.toSet()); } @@ -176,13 +176,20 @@ public class Selection { * Add the selected {@code item} into {@link #mSelectedItems}. */ public void addSelectedItem(Item item) { - if (item.isPreGranted() && mItemGrantRevocationMap.containsKey(item.getContentUri())) { - mItemGrantRevocationMap.remove(item.getContentUri()); - } if (mIsSelectionOrdered) { mSelectedItemsOrder.put( item.getContentUri(), new MutableLiveData(getTotalItemsCount() + 1)); } + if (item.isPreGranted()) { + if (mPreGrantedUris == null) { + mPreGrantedUris = new HashSet<>(); + } + mPreGrantedUris.add(item.getContentUri()); + setTotalNumberOfPreGrantedItems(mPreGrantedUris.size()); + if (mItemGrantRevocationMap.containsKey(item.getContentUri())) { + mItemGrantRevocationMap.remove(item.getContentUri()); + } + } mSelectedItems.put(item.getContentUri(), item); mSelectedItemSize.postValue(getTotalItemsCount()); updateSelectionAllowed(); diff --git a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java index e127e0599..6e2158dca 100644 --- a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java +++ b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java @@ -32,6 +32,8 @@ public class PhotoPickerUiEventLogger { PHOTO_PICKER_OPEN_PERSONAL_PROFILE(942), @UiEvent(doc = "Photo picker opened in work profile") PHOTO_PICKER_OPEN_WORK_PROFILE(943), + @UiEvent(doc = "Photo picker opened in unknown profile") + PHOTO_PICKER_OPEN_UNKNOWN_PROFILE(1691), @UiEvent(doc = "Photo picker opened via GET_CONTENT intent") PHOTO_PICKER_OPEN_GET_CONTENT(1080), @UiEvent(doc = "Photo picker opened in half screen") @@ -54,10 +56,14 @@ public class PhotoPickerUiEventLogger { PHOTO_PICKER_CANCEL_WORK_PROFILE(1125), @UiEvent(doc = "Photo picker cancelled in personal profile") PHOTO_PICKER_CANCEL_PERSONAL_PROFILE(1126), + @UiEvent(doc = "Photo picker cancelled in unknown profile") + PHOTO_PICKER_CANCEL_UNKNOWN_PROFILE(1692), @UiEvent(doc = "Confirmed selection in Photo picker in work profile") PHOTO_PICKER_CONFIRM_WORK_PROFILE(1127), @UiEvent(doc = "Confirmed selection in Photo picker in personal profile") PHOTO_PICKER_CONFIRM_PERSONAL_PROFILE(1128), + @UiEvent(doc = "Confirmed selection in Photo picker in unknown profile") + PHOTO_PICKER_CONFIRM_UNKNOWN_PROFILE(1693), @UiEvent(doc = "Photo picker opened with an active cloud provider") PHOTO_PICKER_CLOUD_PROVIDER_ACTIVE(1198), @UiEvent(doc = "Clicked the mute / unmute button in a photo picker video preview") @@ -66,10 +72,15 @@ public class PhotoPickerUiEventLogger { PHOTO_PICKER_PREVIEW_ALL_SELECTED(1414), @UiEvent(doc = "Photo picker opened with the 'switch profile' button visible and enabled") PHOTO_PICKER_PROFILE_SWITCH_BUTTON_ENABLED(1415), + @UiEvent(doc = "Photo picker opened with the 'switch profile menu' button visible") + PHOTO_PICKER_PROFILE_SWITCH_MENU_BUTTON_VISIBLE(1694), @UiEvent(doc = "Photo picker opened with the 'switch profile' button visible but disabled") PHOTO_PICKER_PROFILE_SWITCH_BUTTON_DISABLED(1416), + @UiEvent(doc = "Clicked the 'switch profile' button in photo picker") PHOTO_PICKER_PROFILE_SWITCH_BUTTON_CLICK(1417), + @UiEvent(doc = "Clicked the 'switch profile menu' button in photo picker") + PHOTO_PICKER_PROFILE_SWITCH_MENU_BUTTON_CLICK(1695), @UiEvent(doc = "Exited photo picker by swiping down") PHOTO_PICKER_EXIT_SWIPE_DOWN(1420), @UiEvent(doc = "Back pressed in photo picker") @@ -172,6 +183,21 @@ public class PhotoPickerUiEventLogger { instanceId); } + /** + * Log metrics to notify that the picker has opened in unknown profile + * @param instanceId an identifier for the current picker session + * @param callingUid the uid of the app initiating the picker launch + * @param callingPackage the package name of the app initiating the picker launch + */ + public void logPickerOpenUnknown(InstanceId instanceId, int callingUid, + String callingPackage) { + logger.logWithInstanceId( + PhotoPickerEvent.PHOTO_PICKER_OPEN_UNKNOWN_PROFILE, + callingUid, + callingPackage, + instanceId); + } + public void logPickerOpenViaGetContent(InstanceId instanceId, int callingUid, String callingPackage) { logger.logWithInstanceId( @@ -327,6 +353,19 @@ public class PhotoPickerUiEventLogger { } /** + * Log metrics to notify that user has confirmed selection in unknown profile + */ + public void logPickerConfirmUnknown(InstanceId instanceId, int callingUid, + String callingPackage, int countOfItemsConfirmed) { + logger.logWithInstanceIdAndPosition( + PhotoPickerEvent.PHOTO_PICKER_CONFIRM_UNKNOWN_PROFILE, + callingUid, + callingPackage, + instanceId, + countOfItemsConfirmed); + } + + /** * Log metrics to notify that user has cancelled picker (without any selection) in personal * profile */ @@ -353,6 +392,19 @@ public class PhotoPickerUiEventLogger { } /** + * Log metrics to notify that user has cancelled picker (without any selection) in unknown + * profile + */ + public void logPickerCancelUnknown(InstanceId instanceId, int callingUid, + String callingPackage) { + logger.logWithInstanceId( + PhotoPickerEvent.PHOTO_PICKER_CANCEL_UNKNOWN_PROFILE, + callingUid, + callingPackage, + instanceId); + } + + /** * Log metrics to notify that the picker has opened with an active cloud provider * @param instanceId an identifier for the current picker session * @param cloudProviderUid the uid of the cloud provider app @@ -394,6 +446,15 @@ public class PhotoPickerUiEventLogger { } /** + * Log metrics to notify that the 'switch profile menu' button is visible + * @param instanceId an identifier for the current picker session + */ + public void logProfileSwitchMenuButtonVisible(InstanceId instanceId) { + logWithInstance( + PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_MENU_BUTTON_VISIBLE, instanceId); + } + + /** * Log metrics to notify that the 'switch profile' button is visible but disabled * @param instanceId an identifier for the current picker session */ @@ -410,6 +471,14 @@ public class PhotoPickerUiEventLogger { } /** + * Log metrics to notify that the user has clicked the 'switch profile menu' button + * @param instanceId an identifier for the current picker session + */ + public void logProfileSwitchMenuButtonClick(InstanceId instanceId) { + logWithInstance(PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_MENU_BUTTON_CLICK, instanceId); + } + + /** * Log metrics to notify that the user has cancelled the current session by swiping down * @param instanceId an identifier for the current picker session */ diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java index bbbefb297..4a51b26b4 100644 --- a/src/com/android/providers/media/photopicker/ui/TabFragment.java +++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java @@ -347,7 +347,20 @@ public abstract class TabFragment extends Fragment { if (crossProfileAllowed != null) { crossProfileAllowed.observe(this, crossProfileAllowedStatus -> { setUpProfileButtonAndProfileMenuButton(); - // Todo(b/318339948): need to put log metrics like present above; + if (mIsProfileButtonVisible) { + boolean isDisabled = true; + UserId userIdToSwitch = getUserToSwitchFromProfileButton(); + if (userIdToSwitch != null) { + isDisabled = !canSwitchToUser(userIdToSwitch); + } + if (isDisabled) { + mPickerViewModel.logProfileSwitchButtonDisabled(); + } else { + mPickerViewModel.logProfileSwitchButtonEnabled(); + } + } else if (mIsProfileMenuButtonVisible) { + mPickerViewModel.logProfileSwitchMenuButtonVisible(); + } }); } @@ -426,7 +439,7 @@ public abstract class TabFragment extends Fragment { if (mPickerViewModel.isManagedSelectionEnabled()) { animateAndShowBottomBar(context, selectedItemListSize); if (selectedItemListSize == 0) { - mViewSelectedButton.setVisibility(View.GONE); + mViewSelectedButton.setVisibility(View.INVISIBLE); // Update the add button to show "Allow none". mAddButton.setText(R.string.picker_add_button_allow_none_option); } @@ -508,6 +521,7 @@ public abstract class TabFragment extends Fragment { @RequiresApi(Build.VERSION_CODES.S) private void onClickProfileMenuButton(View view) { + mPickerViewModel.logProfileSwitchMenuButtonClick(); initialiseProfileMenuWindow(); View profileMenuView = LayoutInflater.from(requireContext()).inflate( R.layout.profile_menu_layout, null); @@ -659,7 +673,6 @@ public abstract class TabFragment extends Fragment { return; } - updateProfileButtonAndProfileMenuButtonContent(); updateProfileButtonAndProfileMenuButtonColor(); } @@ -718,8 +731,7 @@ public abstract class TabFragment extends Fragment { @RequiresApi(Build.VERSION_CODES.S) private void onClickProfileButtonGeneric() { - // todo add logs like above - + mPickerViewModel.logProfileSwitchButtonClick(); UserId userIdToSwitch = getUserToSwitchFromProfileButton(); if (userIdToSwitch != null) { if (canSwitchToUser(userIdToSwitch)) { diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java index 31ab8b8ad..e4bc122d5 100644 --- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java +++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java @@ -39,6 +39,7 @@ import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_ import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.Application; import android.content.ContentResolver; import android.content.Context; @@ -48,6 +49,7 @@ import android.content.pm.ProviderInfo; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; @@ -78,6 +80,7 @@ import com.android.providers.media.photopicker.PickerAccentColorParameters; import com.android.providers.media.photopicker.data.ItemsProvider; import com.android.providers.media.photopicker.data.MuteStatus; import com.android.providers.media.photopicker.data.PaginationParameters; +import com.android.providers.media.photopicker.data.PickerResult; import com.android.providers.media.photopicker.data.Selection; import com.android.providers.media.photopicker.data.UserIdManager; import com.android.providers.media.photopicker.data.UserManagerState; @@ -97,9 +100,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * PickerViewModel to store and handle data for PhotoPickerActivity. @@ -122,6 +127,8 @@ public class PickerViewModel extends AndroidViewModel { private final MuteStatus mMuteStatus; public boolean mEmptyPageDisplayed = false; + + private int mCallingPackageUid = -1; @MediaStore.PickImagesTab private int mPickerLaunchTab = MediaStore.PICK_IMAGES_TAB_IMAGES; @@ -172,6 +179,10 @@ public class PickerViewModel extends AndroidViewModel { // Note - Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates private boolean mIsUserSelectForApp; + private boolean mIsPickImagesAction; + + private boolean mIsPreSelectionInPickImagesEnabled; + private boolean mIsManagedSelectionEnabled; private boolean mIsLocalOnly; private boolean mIsAllCategoryItemsLoaded = false; @@ -242,6 +253,14 @@ public class PickerViewModel extends AndroidViewModel { } } + public void setCallingPackageUid(int callingPackageUid) { + mCallingPackageUid = callingPackageUid; + } + + private int getCallingPackageUid() { + return mCallingPackageUid; + } + public int getPickerLaunchTab() { return mPickerLaunchTab; } @@ -347,6 +366,14 @@ public class PickerViewModel extends AndroidViewModel { } /** + * @return {@code mIsPickImagesAction} if the picker is currently being used + * for the {@link MediaStore#ACTION_PICK_IMAGES} action. + */ + public boolean isPickImagesAction() { + return mIsPickImagesAction; + } + + /** * @return {@code mIsManagedSelectionEnabled} if the picker is currently being used * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action and flag * pickerChoiceManagedSelection is enabled.. @@ -356,6 +383,17 @@ public class PickerViewModel extends AndroidViewModel { } /** + * @return true if the picker is currently being used + * for the {@link MediaStore#ACTION_PICK_IMAGES} action and pre-selection is required or if the + * picker is being used in {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action and + * managed selection is enabled; + */ + public boolean isPreSelectionEnabled() { + return mIsPreSelectionInPickImagesEnabled || mIsManagedSelectionEnabled; + } + + + /** * @return a {@link LiveData} that holds the value (once it's fetched) of the * {@link android.content.ContentProvider#mAuthority authority} of the current * {@link android.provider.CloudMediaProvider}. @@ -492,6 +530,8 @@ public class PickerViewModel extends AndroidViewModel { selection.setPreGrantedItems(preGrantedUris); logPickerChoiceInitGrantsCount(preGrantedUris.size(), intentExtras); }, TOKEN, DELAY_MILLIS); + } else if (isPickImagesAction() && mSelection.canSelectMultiple()) { + initialisePreSelectionItems(intentExtras); } } @@ -621,17 +661,21 @@ public class PickerViewModel extends AndroidViewModel { Set<Uri> preGrantedUris = new HashSet<>(0); Set<Uri> deSelectedPreGrantedUris = new HashSet<>(0); - if (isManagedSelectionEnabled() && mSelection.getPreGrantedUris() != null) { + Set<Uri> currentSelection = mSelection.getSelectedItemsUris(); + if (isPreSelectionEnabled() && mSelection.getPreGrantedUris() != null) { preGrantedUris = mSelection.getPreGrantedUris(); deSelectedPreGrantedUris = mSelection.getDeselectedUrisToBeRevoked(); + Log.d(TAG, "pre granted items : " + preGrantedUris); } + while (cursor.moveToNext()) { - // TODO(b/188394433): Return userId in the cursor so that we do not need to pass it - // here again. final Item item = Item.fromCursor(cursor, userId); if (preGrantedUris.contains(item.getContentUri())) { item.setPreGranted(); - if (!deSelectedPreGrantedUris.contains(item.getContentUri())) { + if (!deSelectedPreGrantedUris.contains(item.getContentUri()) + && !currentSelection.contains(item.getContentUri())) { + // if the item has been de-selected or is already present in the current + // selection set, then it should not be added again. mSelection.addSelectedItem(item); } } @@ -688,6 +732,34 @@ public class PickerViewModel extends AndroidViewModel { } } + private void initialisePreSelectionItems(Bundle intentExtras) { + if (Boolean.TRUE.equals(mIsAllPreGrantedMediaLoaded.getValue())) { + return; + } + List<Uri> preSelectedUris; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // type safe getParcelableArrayList was introduced in Build.VERSION_CODES.TIRAMISU + preSelectedUris = intentExtras.getParcelableArrayList( + MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS, Uri.class); + } else { + preSelectedUris = intentExtras.getParcelableArrayList( + MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS); + } + if (preSelectedUris != null) { + // If more than 100 URIs are passed in as intent extras then this is not supported. + if (preSelectedUris.size() > mSelection.getMaxSelectionLimit()) { + throw new IllegalArgumentException( + "The number of URIs exceed the maximum allowed limit: " + + mSelection.getMaxSelectionLimit()); + } + getItemDataForUris(preSelectedUris, getCallingPackageUid(), + /* isFilterUrisForSelection */ true); + } else { + Log.d(TAG, "No pre-selection URIs to be loaded"); + mIsAllPreGrantedMediaLoaded.postValue(true); + } + } + private void getItemDataForUris(List<Uri> urisForItemsToBeFetched, int callingPackageUid, boolean shouldScreenSelectionUris) { if (!urisForItemsToBeFetched.isEmpty()) { @@ -697,8 +769,6 @@ public class PickerViewModel extends AndroidViewModel { urisForItemsToBeFetched, callingPackageUid, shouldScreenSelectionUris); // If new data has loaded then post value representing a successful operation. mIsAllPreGrantedMediaLoaded.postValue(true); - Log.d(TAG, "Fetched " + urisForItemsToBeFetched.size() - + " items for required preGranted ids"); }, TOKEN, 0); } } @@ -713,17 +783,44 @@ public class PickerViewModel extends AndroidViewModel { + ", either cursor is null or cursor count is zero"); return; } - - Set<String> selectedIdSet = new HashSet<>(mSelection.getSelectedItemsIds()); + Set<Uri> selectedUrisSet = mSelection.getSelectedItemsUris(); // Add all loaded items to selection after marking them as pre granted. + List<Item> preSelectedItems = new ArrayList<>(); while (cursor.moveToNext()) { final Item item = Item.fromCursor(cursor, userId); item.setPreGranted(); - if (!selectedIdSet.contains(item.getId())) { + if (!selectedUrisSet.contains(item.getContentUri())) { + preSelectedItems.add(item); + } + } + + if (isPickImagesAction()) { + // If the code has reached this point it implies that valid items are present for + // pre-selection. + mIsPreSelectionInPickImagesEnabled = true; + + List<Uri> preSelectedPickerUris = PickerResult.getPickerUrisForItems( + MediaStore.ACTION_PICK_IMAGES, preSelectedItems); + + Map<Uri, Item> preGrantedUriToItemMap = IntStream.range(0, + preSelectedPickerUris.size()) + .boxed() + .collect(Collectors.toMap(preSelectedPickerUris::get, + preSelectedItems::get)); + + // Now add loaded items to selection in the same order as they were received in the + // input list. This is done to maintain order in case + // MediaStore.EXTRA_PICK_IMAGES_IN_ORDER is also enabled. + for (Uri uri : selectionArg) { + if (preGrantedUriToItemMap.containsKey(uri)) { + mSelection.addSelectedItem(preGrantedUriToItemMap.get(uri)); + } + } + } else if (isManagedSelectionEnabled()) { + for (Item item : preSelectedItems) { mSelection.addSelectedItem(item); } } - Log.d(TAG, "Pre granted items have been loaded."); } } @@ -952,6 +1049,7 @@ public class PickerViewModel extends AndroidViewModel { * Parse values from {@code intent} and set corresponding fields */ public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException { + mIsPickImagesAction = MediaStore.ACTION_PICK_IMAGES.equals(intent.getAction()); final Bundle extras = intent.getExtras(); if (extras != null) { // Get the tab with which the picker needs to be launched @@ -1086,9 +1184,19 @@ public class PickerViewModel extends AndroidViewModel { */ public void logPickerOpened(int callingUid, String callingPackage, String intentAction) { if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { + UserManagerState userManagerState = getUserManagerState(); + if (userManagerState.getCurrentUserProfileId().getIdentifier() + == ActivityManager.getCurrentUser()) { + mLogger.logPickerOpenPersonal(mInstanceId, callingUid, callingPackage); + } else if (userManagerState.isManagedUserProfile( + userManagerState.getCurrentUserProfileId())) { + mLogger.logPickerOpenWork(mInstanceId, callingUid, callingPackage); + } else { + mLogger.logPickerOpenUnknown(mInstanceId, callingUid, callingPackage); + } return; } - //Todo(b/318614654): need to refactor + if (getUserIdManager().isManagedUserSelected()) { mLogger.logPickerOpenWork(mInstanceId, callingUid, callingPackage); } else { @@ -1183,9 +1291,21 @@ public class PickerViewModel extends AndroidViewModel { */ public void logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed) { if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { + UserManagerState userManagerState = getUserManagerState(); + if (userManagerState.getCurrentUserProfileId().getIdentifier() + == ActivityManager.getCurrentUser()) { + mLogger.logPickerConfirmPersonal(mInstanceId, callingUid, callingPackage, + countOfItemsConfirmed); + } else if (userManagerState.isManagedUserProfile( + userManagerState.getCurrentUserProfileId())) { + mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage, + countOfItemsConfirmed); + } else { + mLogger.logPickerConfirmUnknown( + mInstanceId, callingUid, callingPackage, countOfItemsConfirmed); + } return; } - //Todo(b/318614654): need to refactor if (getUserIdManager().isManagedUserSelected()) { mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage, countOfItemsConfirmed); @@ -1200,9 +1320,18 @@ public class PickerViewModel extends AndroidViewModel { */ public void logPickerCancel(int callingUid, String callingPackage) { if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { + UserManagerState userManagerState = getUserManagerState(); + if (userManagerState.getCurrentUserProfileId().getIdentifier() + == ActivityManager.getCurrentUser()) { + mLogger.logPickerCancelPersonal(mInstanceId, callingUid, callingPackage); + } else if (userManagerState.isManagedUserProfile( + userManagerState.getCurrentUserProfileId())) { + mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage); + } else { + mLogger.logPickerCancelUnknown(mInstanceId, callingUid, callingPackage); + } return; } - //Todo(b/318614654): need to refactor if (getUserIdManager().isManagedUserSelected()) { mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage); } else { @@ -1241,6 +1370,13 @@ public class PickerViewModel extends AndroidViewModel { } /** + * Log metrics to notify that the 'switch profile menu' button is visible + */ + public void logProfileSwitchMenuButtonVisible() { + mLogger.logProfileSwitchMenuButtonVisible(mInstanceId); + } + + /** * Log metrics to notify that the user has clicked the 'switch profile' button */ public void logProfileSwitchButtonClick() { @@ -1248,6 +1384,13 @@ public class PickerViewModel extends AndroidViewModel { } /** + * Log metrics to notify that the user has clicked the 'switch profile menu ' button + */ + public void logProfileSwitchMenuButtonClick() { + mLogger.logProfileSwitchMenuButtonClick(mInstanceId); + } + + /** * Log metrics to notify that the user has cancelled the current session by swiping down */ public void logSwipeDownExit() { diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java index a8191a17e..fb9be244d 100644 --- a/tests/src/com/android/providers/media/MediaProviderTest.java +++ b/tests/src/com/android/providers/media/MediaProviderTest.java @@ -81,6 +81,7 @@ import com.android.providers.media.util.SQLiteQueryBuilder; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Assume; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; @@ -113,7 +114,7 @@ public class MediaProviderTest { private static ContentResolver sIsolatedResolver; @BeforeClass - public static void setUp() { + public static void setUpBeforeClass() { InstrumentationRegistry.getInstrumentation().getUiAutomation() .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, Manifest.permission.READ_COMPAT_CHANGE_CONFIG, @@ -124,7 +125,10 @@ public class MediaProviderTest { // MANAGE_USERS permission for MediaProvider module. Manifest.permission.CREATE_USERS, Manifest.permission.INTERACT_ACROSS_USERS); + } + @Before + public void setUp() { resetIsolatedContext(); } diff --git a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java index 1bc29623c..a41ffec6a 100644 --- a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java +++ b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java @@ -478,6 +478,47 @@ public class SelectionTest { assertThat(mSelection.getNewlySelectedItems()).contains(item3); } + @Test + public void test_getSelectedItemsUris_correctValuesReturned() { + final String id1 = "1"; + final Item item1 = generateFakeImageItem(id1); + final String id2 = "2"; + final Item item2 = generateFakeImageItem(id2); + + mSelection.addSelectedItem(item1); + mSelection.addSelectedItem(item2); + + assertThat(mSelection.getSelectedItemsUris().size()).isEqualTo(2); + assertThat(mSelection.getSelectedItemsUris().contains(item1.getContentUri())).isTrue(); + assertThat(mSelection.getSelectedItemsUris().contains(item2.getContentUri())).isTrue(); + } + + @Test + public void test_addingPreGrantedItemToSelection_addsToPreGrantedSet() { + final String id1 = "1"; + final Item item1 = generateFakeImageItem(id1); + final String id2 = "2"; + final Item item2 = generateFakeImageItem(id2); + + item1.setPreGranted(); + mSelection.addSelectedItem(item1); + mSelection.addSelectedItem(item2); + + // assert that if a preGranted item is added to selection it also populates the preGranted + // set and remains in this set even when de-selected. + + assertThat(mSelection.getPreGrantedUris()).isNotNull(); + assertThat(mSelection.getPreGrantedUris().size()).isEqualTo(1); + assertThat(mSelection.getPreGrantedUris().contains(item1.getContentUri())).isTrue(); + + mSelection.removeSelectedItem(item1); + + assertThat(mSelection.getPreGrantedUris()).isNotNull(); + assertThat(mSelection.getPreGrantedUris().size()).isEqualTo(1); + assertThat(mSelection.getPreGrantedUris().contains(item1.getContentUri())).isTrue(); + } + + private static Item generateFakeImageItem(String id) { final long dateTakenMs = System.currentTimeMillis() + Long.parseLong(id) * DateUtils.DAY_IN_MILLIS; diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java index d6caf2d9f..629366cf0 100644 --- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java +++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java @@ -28,6 +28,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.android.providers.media.photopicker.data.ItemsProvider.getItemsUri; import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.atPositionOnItemViewType; import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.clickItem; import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_BANNER; @@ -41,8 +42,10 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.not; import android.app.Activity; +import android.content.ContentUris; import android.content.Intent; import android.net.Uri; +import android.os.UserHandle; import android.provider.MediaStore; import androidx.lifecycle.ViewModelProvider; @@ -54,7 +57,9 @@ import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; import com.android.providers.media.R; import com.android.providers.media.library.RunOnlyOnPostsubmit; import com.android.providers.media.photopicker.DataLoaderThread; +import com.android.providers.media.photopicker.PickerSyncController; import com.android.providers.media.photopicker.data.Selection; +import com.android.providers.media.photopicker.data.model.UserId; import com.android.providers.media.photopicker.viewmodel.PickerViewModel; import org.junit.After; @@ -242,8 +247,12 @@ public class PhotoPickerUserSelectActivityTest extends PhotoPickerBaseTest { launchValidActivityWithManagedSelectionEnabled(); onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed())); - final Uri uri = MediaStore.scanFile(getIsolatedContext().getContentResolver(), + final Uri mediaStoreUri = MediaStore.scanFile(getIsolatedContext().getContentResolver(), IMAGE_1_FILE); + // convert MediaStore uri to ItemsProvider Uri. + final Uri uri = getItemsUri(String.valueOf(ContentUris.parseId(mediaStoreUri)), + PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, + UserId.of(UserHandle.of(UserHandle.myUserId()))); MediaStore.waitForIdle(getIsolatedContext().getContentResolver()); mScenario.onActivity(activity -> { // Add an item id to the pre-granted set, so that when preview fragment gets opened up diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java index 7113295e1..607dca406 100644 --- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java +++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java @@ -64,6 +64,7 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Color; import android.net.Uri; +import android.os.Bundle; import android.os.CancellationSignal; import android.provider.MediaStore; import android.text.format.DateUtils; @@ -79,6 +80,7 @@ import com.android.providers.media.photopicker.DataLoaderThread; import com.android.providers.media.photopicker.PickerSyncController; import com.android.providers.media.photopicker.data.ItemsProvider; import com.android.providers.media.photopicker.data.PaginationParameters; +import com.android.providers.media.photopicker.data.PickerResult; import com.android.providers.media.photopicker.data.Selection; import com.android.providers.media.photopicker.data.UserIdManager; import com.android.providers.media.photopicker.data.UserManagerState; @@ -104,6 +106,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @RunWith(Parameterized.class) public class PickerViewModelTest { @@ -352,6 +355,96 @@ public class PickerViewModelTest { assertThat(itemUrisToBeRevoked.contains(expectedItems.get(0).getContentUri())).isTrue(); } + @Test + public void test_initialisePreSelectionItems_correctItemsLoaded() { + // Set the intent action as PICK_IMAGES + Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); + Bundle extras = new Bundle(); + extras.putInt(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit()); + intent.putExtras(extras); + + mPickerViewModel.parseValuesFromIntent(intent); + + // generate test items + final int numberOfTestItems = 4; + final List<Item> expectedItems = generateFakeImageItemList(numberOfTestItems); + + + // Mock the test items to return the required URI and id when used. + final List<Item> mockedExpectedItems = new ArrayList<>(); + for (int i = 0; i < expectedItems.size(); i++) { + Item item = mock(Item.class); + when(item.getContentUri()).thenReturn(ItemsProvider.getItemsUri( + expectedItems.get(i).getId(), + PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, + UserId.CURRENT_USER)); + when(item.getId()).thenReturn(expectedItems.get(i).getId()); + mockedExpectedItems.add(item); + } + mItemsProvider.setItems(mockedExpectedItems); + + // generate a list of input pre-selected picker URI and add them to test intent extras. + ArrayList<Uri> preGrantedPickerUris = new ArrayList<>(); + for (int i = 0; i < expectedItems.size(); i++) { + preGrantedPickerUris.add( + PickerResult.getPickerUrisForItems(MediaStore.ACTION_PICK_IMAGES, + List.of(mockedExpectedItems.get(i))).get(0)); + } + Bundle intentExtras = new Bundle(); + intentExtras.putParcelableArrayList(MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS, + preGrantedPickerUris); + + Selection selection = mPickerViewModel.getSelection(); + // Since no item has been selected and no pre-granted URIs have been loaded, thus the size + // of selection should be 0. + assertThat(selection.getSelectedItems().size()).isEqualTo(0); + + DataLoaderThread.waitForIdle(); + + // Initialise pre-granted items for selection. + mPickerViewModel.initialisePreGrantsIfNecessary(selection, intentExtras, + /* mimeTypeFilters */ null); + DataLoaderThread.waitForIdle(); + + // after initialization the items should have been added to selection. + assertThat(selection.getPreGrantedUris()).isNotNull(); + assertThat(selection.getPreGrantedUris().size()).isEqualTo(4); + assertThat(mPickerViewModel.getSelection().getSelectedItems().size()).isEqualTo(4); + } + + + @Test + public void test_preSelectionItemsExceedMaxLimit_initialisationOfItemsFails() { + // Generate a list of test uris, the size being 2 uris more than the max number of URIs + // accepted. + String testUriPrefix = "content://media/picker/0/com.test.package/media/"; + int numberOfPreselectedUris = MediaStore.getPickImagesMaxLimit() + 2; + ArrayList<Uri> testUrisAsString = new ArrayList<>(); + for (int i = 0; i < numberOfPreselectedUris; i++) { + testUrisAsString.add(Uri.parse(testUriPrefix + String.valueOf(i))); + } + + // set up the intent extras to contain the test uris. Also, parse a test PICK_IMAGES intent + // to ensure that PickerViewModel works in PICK_IMAGES action mode. + Bundle intentExtras = new Bundle(); + intentExtras.putInt(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit()); + intentExtras.putParcelableArrayList(MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS, + testUrisAsString); + final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); + intent.putExtras(intentExtras); + mPickerViewModel.parseValuesFromIntent(intent); + + try { + mPickerViewModel.initialisePreGrantsIfNecessary(null, intentExtras, null); + fail("The initialisation of items should have failed since the number of pre-selected" + + "items exceeds the max limit"); + } catch (IllegalArgumentException illegalArgumentException) { + assertThat(illegalArgumentException.getMessage()).isEqualTo( + "The number of URIs exceed the maximum allowed limit: " + + MediaStore.getPickImagesMaxLimit()); + } + } + private static Item generateFakeImageItem(String id) { final long dateTakenMs = System.currentTimeMillis() + Long.parseLong(id) * DateUtils.DAY_IN_MILLIS; @@ -521,7 +614,7 @@ public class PickerViewModelTest { }; final MatrixCursor c = new MatrixCursor(all_projection); List<String> preSelectedIds = preselectedUris.stream().map( - Uri::getLastPathSegment).toList(); + Uri::getLastPathSegment).collect(Collectors.toList()); int itr = 1; for (Item item : mItemList) { if (preSelectedIds.contains(item.getId())) { |