summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml3
-rw-r--r--apex/framework/api/current.txt1
-rw-r--r--apex/framework/java/android/provider/MediaStore.java27
-rw-r--r--photopicker/Android.bp5
-rw-r--r--photopicker/res/values-af/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-am/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ar/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-as/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-az/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-b+sr+Latn/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-be/core_strings.xml3
-rw-r--r--photopicker/res/values-be/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-bg/core_strings.xml3
-rw-r--r--photopicker/res/values-bg/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-bn/core_strings.xml3
-rw-r--r--photopicker/res/values-bn/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-bs/core_strings.xml3
-rw-r--r--photopicker/res/values-bs/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ca/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-cs/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-da/core_strings.xml3
-rw-r--r--photopicker/res/values-da/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-de/core_strings.xml3
-rw-r--r--photopicker/res/values-de/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-el/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-en-rAU/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-en-rCA/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-en-rGB/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-en-rIN/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-en-rXC/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-es-rUS/core_strings.xml3
-rw-r--r--photopicker/res/values-es-rUS/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-es/core_strings.xml3
-rw-r--r--photopicker/res/values-es/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-et/core_strings.xml3
-rw-r--r--photopicker/res/values-et/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-eu/core_strings.xml3
-rw-r--r--photopicker/res/values-eu/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-fa/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-fi/core_strings.xml3
-rw-r--r--photopicker/res/values-fi/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-fr-rCA/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-fr/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-gl/core_strings.xml3
-rw-r--r--photopicker/res/values-gl/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-gu/core_strings.xml3
-rw-r--r--photopicker/res/values-gu/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-hi/core_strings.xml3
-rw-r--r--photopicker/res/values-hi/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-hr/core_strings.xml3
-rw-r--r--photopicker/res/values-hr/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-hu/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-hy/core_strings.xml3
-rw-r--r--photopicker/res/values-hy/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-in/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-is/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-it/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-iw/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ja/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ka/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-kk/core_strings.xml3
-rw-r--r--photopicker/res/values-kk/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-km/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-kn/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ko/core_strings.xml3
-rw-r--r--photopicker/res/values-ko/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ky/core_strings.xml3
-rw-r--r--photopicker/res/values-ky/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-lo/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-lt/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-lv/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-mk/core_strings.xml3
-rw-r--r--photopicker/res/values-mk/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ml/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-mn/core_strings.xml3
-rw-r--r--photopicker/res/values-mn/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-mr/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ms/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-my/core_strings.xml3
-rw-r--r--photopicker/res/values-my/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-nb/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ne/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-nl/core_strings.xml3
-rw-r--r--photopicker/res/values-nl/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-or/core_strings.xml3
-rw-r--r--photopicker/res/values-or/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-pa/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-pl/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-pt-rBR/core_strings.xml3
-rw-r--r--photopicker/res/values-pt-rBR/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-pt-rPT/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-pt/core_strings.xml3
-rw-r--r--photopicker/res/values-pt/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ro/core_strings.xml3
-rw-r--r--photopicker/res/values-ro/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ru/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-si/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-sk/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-sl/core_strings.xml3
-rw-r--r--photopicker/res/values-sl/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-sq/core_strings.xml3
-rw-r--r--photopicker/res/values-sq/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-sr/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-sv/core_strings.xml3
-rw-r--r--photopicker/res/values-sv/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-sw/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ta/core_strings.xml3
-rw-r--r--photopicker/res/values-ta/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-te/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-th/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-tl/core_strings.xml3
-rw-r--r--photopicker/res/values-tl/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-tr/core_strings.xml3
-rw-r--r--photopicker/res/values-tr/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-uk/core_strings.xml3
-rw-r--r--photopicker/res/values-uk/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-ur/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-uz/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-vi/core_strings.xml3
-rw-r--r--photopicker/res/values-vi/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-zh-rCN/core_strings.xml3
-rw-r--r--photopicker/res/values-zh-rCN/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-zh-rHK/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-zh-rTW/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values-zu/feature_preview_strings.xml23
-rw-r--r--photopicker/res/values/feature_preview_strings.xml23
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/Preview.kt56
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt206
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/video/PlaybackInfo.kt34
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/video/PlaybackState.kt54
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/video/RemotePreviewControllerInfo.kt33
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/video/RemoteSurfaceController.kt113
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/video/RetriableErrorDialog.kt92
-rw-r--r--photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt690
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt2
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt3
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt689
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt495
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt37
-rw-r--r--photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt10
-rw-r--r--photopicker/tests/src/com/android/photopicker/utils/MockContentProviderWrapper.kt9
-rw-r--r--res/layout/activity_photo_picker.xml22
-rw-r--r--src/com/android/providers/media/ConfigStore.java22
-rw-r--r--src/com/android/providers/media/MediaApplication.java6
-rw-r--r--src/com/android/providers/media/photopicker/PhotoPickerActivity.java18
-rw-r--r--src/com/android/providers/media/photopicker/data/Selection.java19
-rw-r--r--src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java69
-rw-r--r--src/com/android/providers/media/photopicker/ui/TabFragment.java22
-rw-r--r--src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java169
-rw-r--r--tests/src/com/android/providers/media/MediaProviderTest.java6
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/SelectionTest.java41
-rw-r--r--tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java11
-rw-r--r--tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java95
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())) {