diff options
497 files changed, 11600 insertions, 1618 deletions
diff --git a/apex/framework/java/android/provider/EmbeddedPhotopickerFeatureInfo.java b/apex/framework/java/android/provider/EmbeddedPhotopickerFeatureInfo.java index 44d71d422..42dcb98fa 100644 --- a/apex/framework/java/android/provider/EmbeddedPhotopickerFeatureInfo.java +++ b/apex/framework/java/android/provider/EmbeddedPhotopickerFeatureInfo.java @@ -56,7 +56,7 @@ public final class EmbeddedPhotopickerFeatureInfo implements Parcelable { private final List<Uri> mPreSelectedUris; @NonNull - private static final List<String> DEFAULT_MIME_TYPES = Arrays.asList("*/*"); + private static final List<String> DEFAULT_MIME_TYPES = Arrays.asList("image/*", "video/*"); @ColorLong private static final long DEFAULT_ACCENT_COLOR = -1; private static final boolean DEFAULT_ORDERED_SELECTION = false; diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java index c0f1d84b3..77c2c9473 100644 --- a/apex/framework/java/android/provider/MediaStore.java +++ b/apex/framework/java/android/provider/MediaStore.java @@ -4655,6 +4655,11 @@ public final class MediaStore { final Bundle in = new Bundle(); in.putString(Intent.EXTRA_TEXT, volumeName); final Bundle out = resolver.call(AUTHORITY, GET_GENERATION_CALL, null, in); + if (out == null) { + throw new IllegalStateException("Failed to get generation for volume '" + + volumeName + "'. The ContentResolver call returned null."); + } + return out.getLong(Intent.EXTRA_INDEX); } diff --git a/mediaprovider_flags.aconfig b/mediaprovider_flags.aconfig index dc1ff99e2..826a67b95 100644 --- a/mediaprovider_flags.aconfig +++ b/mediaprovider_flags.aconfig @@ -63,6 +63,7 @@ flag { namespace: "mediaprovider" description: "This flag will enable the abstract service for media cognition processes" bug: "331771553" + is_fixed_read_only: true } flag { diff --git a/pdf/OWNERS b/pdf/OWNERS index 925f5fb9b..40f36b7fd 100644 --- a/pdf/OWNERS +++ b/pdf/OWNERS @@ -3,3 +3,4 @@ nishantpanwar@google.com bhavyajain@google.com anothermark@google.com +gulshansingh@google.com
\ No newline at end of file diff --git a/photopicker/Android.bp b/photopicker/Android.bp index 39d364384..dc0b64b2c 100644 --- a/photopicker/Android.bp +++ b/photopicker/Android.bp @@ -28,6 +28,7 @@ android_library { ], static_libs: [ "androidx.activity_activity-compose", + "androidx.appcompat_appcompat", "androidx.compose.foundation_foundation", "androidx.compose.material3_material3", "androidx.compose.material3_material3-window-size-class", diff --git a/photopicker/res/drawable/photopicker_selected_media.xml b/photopicker/res/drawable/photopicker_selected_media.xml new file mode 100644 index 000000000..49250c4d2 --- /dev/null +++ b/photopicker/res/drawable/photopicker_selected_media.xml @@ -0,0 +1,19 @@ +<!-- + 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. +--> +<!-- Circle Check --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> +<path android:fillColor="@android:color/white" android:pathData="M424,664L706,382L650,326L424,552L310,438L254,494L424,664ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/> +</vector> diff --git a/photopicker/res/values-af/core_strings.xml b/photopicker/res/values-af/core_strings.xml index 36119189f..ec127b90a 100644 --- a/photopicker/res/values-af/core_strings.xml +++ b/photopicker/res/values-af/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto’s en video’s"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Gekies"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Voeg <xliff:g id="COUNT">(%1$s)</xliff:g> by"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Klaar"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Ontkies almal"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Kies tot <xliff:g id="COUNT">%1$s</xliff:g> items"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto’s"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string> diff --git a/photopicker/res/values-af/feature_cloud_strings.xml b/photopicker/res/values-af/feature_cloud_strings.xml index 586dce823..c02152508 100644 --- a/photopicker/res/values-af/feature_cloud_strings.xml +++ b/photopicker/res/values-af/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> van <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> is gereed"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Sommige foto’s kan nie laai nie"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Probeer later weer. Jou foto’s sal beskikbaar wees sodra die kwessie opgelos is."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-af/feature_preview_strings.xml b/photopicker/res/values-af/feature_preview_strings.xml index 30dd03e24..2b16bf0b4 100644 --- a/photopicker/res/values-af/feature_preview_strings.xml +++ b/photopicker/res/values-af/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Kies alles <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Ontkies alles <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Voorbeskou"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Sukkel om video te speel"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Gaan jou internetverbinding na en probeer weer"</string> diff --git a/photopicker/res/values-am/core_strings.xml b/photopicker/res/values-am/core_strings.xml index fdcbb72cc..69a5b6492 100644 --- a/photopicker/res/values-am/core_strings.xml +++ b/photopicker/res/values-am/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ፎቶዎች እና ቪድዮዎች"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"ተከናውኗል"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ሁሉንም አትምረጥ"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"እስከ <xliff:g id="COUNT">%1$s</xliff:g> ንጥሎች ድረስ ይምረጡ"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ፎቶዎች"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"አልበሞች"</string> diff --git a/photopicker/res/values-am/feature_cloud_strings.xml b/photopicker/res/values-am/feature_cloud_strings.xml index ac66dd491..e792fc5e0 100644 --- a/photopicker/res/values-am/feature_cloud_strings.xml +++ b/photopicker/res/values-am/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ከ<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ዝግጁ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"አንዳንድ ፎቶዎችን መጫን አይቻለም"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ቆይተው እንደገና ይሞክሩ። የእርስዎ ፎቶዎች አንዴ ችግሩ ከተፈታ በኋላ ይገኛሉ።"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-am/feature_preview_strings.xml b/photopicker/res/values-am/feature_preview_strings.xml index 10d0a458a..e7fbc09b3 100644 --- a/photopicker/res/values-am/feature_preview_strings.xml +++ b/photopicker/res/values-am/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"ሁሉንም <xliff:g id="COUNT">(%1$s)</xliff:g> ምረጥ"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ሁሉንም <xliff:g id="COUNT">(%1$s)</xliff:g> አትምረጥ"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ቅድመ-ዕይታ"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ቪድዮን ማጫወት ላይ ችግር"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"የበይነመረብ ግንኙነትዎን ይፈትሹት እና እንደገና ይሞክሩ"</string> diff --git a/photopicker/res/values-ar/core_strings.xml b/photopicker/res/values-ar/core_strings.xml index c97f24d73..6547a039b 100644 --- a/photopicker/res/values-ar/core_strings.xml +++ b/photopicker/res/values-ar/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"الصور والفيديوهات"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"تم"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"إلغاء اختيار الكل"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"اختَر ما يصل إلى <xliff:g id="COUNT">%1$s</xliff:g> عنصر"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"الصور"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"الألبومات"</string> diff --git a/photopicker/res/values-ar/feature_cloud_strings.xml b/photopicker/res/values-ar/feature_cloud_strings.xml index 9fda12365..f590aaf84 100644 --- a/photopicker/res/values-ar/feature_cloud_strings.xml +++ b/photopicker/res/values-ar/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> صورة جاهزة من إجمالي <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"يتعذّر تحميل بعض الصور"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"يُرجى إعادة المحاولة لاحقًا. ستتوفّر صورك عند حل المشكلة."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ar/feature_preview_strings.xml b/photopicker/res/values-ar/feature_preview_strings.xml index 0affde1d6..a5aa40540 100644 --- a/photopicker/res/values-ar/feature_preview_strings.xml +++ b/photopicker/res/values-ar/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"اختيار الكل: <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"إلغاء اختيار الكل: <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"معاينة"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"مشكلة في تشغيل الفيديو"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"يُرجى التحقّق من الاتصال بالإنترنت، ثم إعادة المحاولة"</string> diff --git a/photopicker/res/values-as/core_strings.xml b/photopicker/res/values-as/core_strings.xml index 6c155d781..e3837e187 100644 --- a/photopicker/res/values-as/core_strings.xml +++ b/photopicker/res/values-as/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ফট’ আৰু ভিডিঅ’"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"হ’ল"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"বাছনিৰ পৰা আটাইবোৰ আঁতৰাওক"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> টা পৰ্যন্ত বস্তু বাছনি কৰক"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ফট’"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"এলবাম"</string> diff --git a/photopicker/res/values-as/feature_cloud_strings.xml b/photopicker/res/values-as/feature_cloud_strings.xml index 2f7a9d471..3f4aadf8a 100644 --- a/photopicker/res/values-as/feature_cloud_strings.xml +++ b/photopicker/res/values-as/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> টা বস্তুৰ ভিতৰত <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> টা সাজু"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"কিছুমান ফট’ ল’ড কৰিব নোৱাৰি"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"পাছত পুনৰ চেষ্টা কৰক। সমস্যাটো সমাধান হোৱাৰ পাছত আপোনাৰ ফট’সমূহ উপলব্ধ হ’ব।"</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"এতিয়া বেকআপ লোৱা ফট’ অন্তৰ্ভুক্ত কৰা হৈছে"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"আপুনি <xliff:g id="APP_NAME">%1$s</xliff:g>ৰ একাউণ্টৰ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>ৰ পৰা ফট’ বাছনি কৰিব পাৰে"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> একাউণ্ট বাছনি কৰক"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"ইয়াত <xliff:g id="APP_NAME">%1$s</xliff:g>ৰ পৰা ফট’ অন্তৰ্ভুক্ত কৰিবলৈ, এপ্টোত এটা একাউণ্ট বাছনি কৰক"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"একাউণ্ট বাছনি কৰক"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ক্লাউড মিডিয়া এপ্ বাছনি কৰক"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ইয়াত বেকআপ লোৱা ফট’ অন্তৰ্ভুক্ত কৰিবলৈ, ছেটিঙত এটা ক্লাউড মিডিয়া এপ্ বাছনি কৰক"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"এপ্ বাছনি কৰক"</string> </resources> diff --git a/photopicker/res/values-as/feature_preview_strings.xml b/photopicker/res/values-as/feature_preview_strings.xml index 989a9fc42..8fce9679b 100644 --- a/photopicker/res/values-as/feature_preview_strings.xml +++ b/photopicker/res/values-as/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> টাৰ আটাইবোৰ বাছনি কৰক"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> টাৰ আটাইবোৰ বাছনিৰ পৰা আঁতৰাওক"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"পূৰ্বদৰ্শন কৰক"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ভিডিঅ’ প্লে’ কৰাত সমস্যা হৈছে"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"আপোনাৰ ইণ্টাৰনেট সংযোগ পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক"</string> diff --git a/photopicker/res/values-az/core_strings.xml b/photopicker/res/values-az/core_strings.xml index 1977a9cba..54566b5dd 100644 --- a/photopicker/res/values-az/core_strings.xml +++ b/photopicker/res/values-az/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto və videolar"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Seçilib"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Əlavə edin: <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hazırdır"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Bütün seçimləri silin"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maksimum <xliff:g id="COUNT">%1$s</xliff:g> element seçin"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albomlar"</string> diff --git a/photopicker/res/values-az/feature_cloud_strings.xml b/photopicker/res/values-az/feature_cloud_strings.xml index 4ed5a8551..d3b3909d9 100644 --- a/photopicker/res/values-az/feature_cloud_strings.xml +++ b/photopicker/res/values-az/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>/<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> hazırdır"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Bəzi fotolar yüklənmir"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Sonra cəhd edin. Problem həll edildikdən sonra fotolar əlçatan olacaq."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-az/feature_preview_strings.xml b/photopicker/res/values-az/feature_preview_strings.xml index cf2bfca01..68d00eec6 100644 --- a/photopicker/res/values-az/feature_preview_strings.xml +++ b/photopicker/res/values-az/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Hamısını seçin <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Hamısının seçimini silin <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Önizləmə"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Videonu işə salarkən xəta oldu"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"İnternet bağlantısını yoxlayın və yenidən cəhd edin"</string> diff --git a/photopicker/res/values-b+sr+Latn/core_strings.xml b/photopicker/res/values-b+sr+Latn/core_strings.xml index 4f6eb3931..940db81f6 100644 --- a/photopicker/res/values-b+sr+Latn/core_strings.xml +++ b/photopicker/res/values-b+sr+Latn/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Slike i videi"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Mediji"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Izabrano"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotovo"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poništite sve"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Najveći broj stavki koje možete da izaberete je <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Slike"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string> diff --git a/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml b/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml index 92e3afe64..938525e94 100644 --- a/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml +++ b/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Spremno:<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Učitavanje nekih slika nije uspelo"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Probajte ponovo kasnije. Slike će biti dostupne kada se problem reši."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </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 index ba4b0f4a5..48d0bdca7 100644 --- a/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml +++ b/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Izaberi sve <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poništi izbor <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Došlo je do greške pri puštanju videa"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Proverite internet vezu i probajte ponovo"</string> diff --git a/photopicker/res/values-be/core_strings.xml b/photopicker/res/values-be/core_strings.xml index 8e4d6e4a6..b6fdb60e6 100644 --- a/photopicker/res/values-be/core_strings.xml +++ b/photopicker/res/values-be/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Фота і відэа"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Гатова"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Скасаваць выбар"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Выберыце элементы (не больш <xliff:g id="COUNT">%1$s</xliff:g>)"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фота"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбомы"</string> diff --git a/photopicker/res/values-be/feature_cloud_strings.xml b/photopicker/res/values-be/feature_cloud_strings.xml index 66dca165b..9d5add300 100644 --- a/photopicker/res/values-be/feature_cloud_strings.xml +++ b/photopicker/res/values-be/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Гатова: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> з <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Некаторыя фота не ўдалося загрузіць"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Паўтарыце спробу пазней. Калі праблема будзе вырашана, вашы фота стануць даступнымі."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-be/feature_preview_strings.xml b/photopicker/res/values-be/feature_preview_strings.xml index 94b9be341..95b6633db 100644 --- a/photopicker/res/values-be/feature_preview_strings.xml +++ b/photopicker/res/values-be/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Выбраць усе (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Скасаваць выбар для ўсіх (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Перадпрагляд"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Праблемы з прайграваннем відэа"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Праверце падключэнне да інтэрнэту і паўтарыце спробу"</string> diff --git a/photopicker/res/values-bg/core_strings.xml b/photopicker/res/values-bg/core_strings.xml index bacf2a503..6fd9f4514 100644 --- a/photopicker/res/values-bg/core_strings.xml +++ b/photopicker/res/values-bg/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Снимки и видеоклипове"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Премахване на избора от всички"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Изберете най-много <xliff:g id="COUNT">%1$s</xliff:g> елемента"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Снимки"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Албуми"</string> diff --git a/photopicker/res/values-bg/feature_cloud_strings.xml b/photopicker/res/values-bg/feature_cloud_strings.xml index 28dcdc365..38a2f4be0 100644 --- a/photopicker/res/values-bg/feature_cloud_strings.xml +++ b/photopicker/res/values-bg/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Готови: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> от <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Някои снимки не могат да се заредят"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Опитайте отново по-късно. Снимките ви ще бъдат налице, след като проблемът бъде разрешен."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-bg/feature_preview_strings.xml b/photopicker/res/values-bg/feature_preview_strings.xml index 98b31ca14..6641fc59c 100644 --- a/photopicker/res/values-bg/feature_preview_strings.xml +++ b/photopicker/res/values-bg/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Избиране на всички <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Отмяна на избора от всички <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Визуализация"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Проблем при възпроизвеждането на видеоклипа"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверете връзката си с интернет и опитайте отново"</string> diff --git a/photopicker/res/values-bn/core_strings.xml b/photopicker/res/values-bn/core_strings.xml index 8597ed9cc..4b2e6440d 100644 --- a/photopicker/res/values-bn/core_strings.xml +++ b/photopicker/res/values-bn/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ফটো & ভিডিও"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"হয়ে গেছে"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"সবকটি বাদ দিন"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"সর্বাধিক <xliff:g id="COUNT">%1$s</xliff:g>টি আইটেম বেছে নিন"</string> diff --git a/photopicker/res/values-bn/feature_cloud_strings.xml b/photopicker/res/values-bn/feature_cloud_strings.xml index 61f7cc85f..dae9d195f 100644 --- a/photopicker/res/values-bn/feature_cloud_strings.xml +++ b/photopicker/res/values-bn/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>টির মধ্যে <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> নম্বর রেডি"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"কিছু ফটো লোড করা যাচ্ছে না"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"পরে আবার চেষ্টা করুন। সমস্যার সমাধান হয়ে গেলে আপনার ফটো উপলভ্য হবে।"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-bn/feature_preview_strings.xml b/photopicker/res/values-bn/feature_preview_strings.xml index 6de7878dd..b137315b4 100644 --- a/photopicker/res/values-bn/feature_preview_strings.xml +++ b/photopicker/res/values-bn/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>টির সবকটি বেছে নিন"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>টির সবকটি বাদ দিন"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"প্রিভিউ দেখুন"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ভিডিও চালাতে সমস্যা হচ্ছে"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"আপনার ইন্টারনেট কানেকশন ঠিক আছে কিনা দেখে নিয়ে আবার চেষ্টা করুন"</string> diff --git a/photopicker/res/values-bs/core_strings.xml b/photopicker/res/values-bs/core_strings.xml index 04e4e0ba3..52943fad3 100644 --- a/photopicker/res/values-bs/core_strings.xml +++ b/photopicker/res/values-bs/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografije i videozapisi"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotovo"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poništavanje svih odabira"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> je maksimalni broj stavki koje možete odabrati"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string> diff --git a/photopicker/res/values-bs/feature_cloud_strings.xml b/photopicker/res/values-bs/feature_cloud_strings.xml index 32eed10b0..d06806e36 100644 --- a/photopicker/res/values-bs/feature_cloud_strings.xml +++ b/photopicker/res/values-bs/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Spremno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nije moguće učitati određene fotografije"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Pokušajte ponovo kasnije. Fotografije će biti dostupne čim se problem riješi."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-bs/feature_preview_strings.xml b/photopicker/res/values-bs/feature_preview_strings.xml index e02dec7d9..ac760afc7 100644 --- a/photopicker/res/values-bs/feature_preview_strings.xml +++ b/photopicker/res/values-bs/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Odaberi sve (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poništi odabir svega (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Poteškoće prilikom reprodukcije videozapisa"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Provjerite internetsku vezu i pokušajte ponovo"</string> diff --git a/photopicker/res/values-ca/core_strings.xml b/photopicker/res/values-ca/core_strings.xml index 299382c7a..8168b13d5 100644 --- a/photopicker/res/values-ca/core_strings.xml +++ b/photopicker/res/values-ca/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos i vídeos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Contingut multimèdia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Seleccionat"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Afegeix <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Fet"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desselecciona-ho tot"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecciona fins a <xliff:g id="COUNT">%1$s</xliff:g> elements"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Àlbums"</string> diff --git a/photopicker/res/values-ca/feature_cloud_strings.xml b/photopicker/res/values-ca/feature_cloud_strings.xml index 477bce8b3..977260d29 100644 --- a/photopicker/res/values-ca/feature_cloud_strings.xml +++ b/photopicker/res/values-ca/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> a punt"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"No es poden carregar algunes fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Torna-ho a provar més tard. Les teves fotos estaran disponibles un cop el problema s\'hagi resolt."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ca/feature_preview_strings.xml b/photopicker/res/values-ca/feature_preview_strings.xml index 522083783..397f71cd7 100644 --- a/photopicker/res/values-ca/feature_preview_strings.xml +++ b/photopicker/res/values-ca/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Selecciona-ho tot (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desselecciona-ho tot (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previsualitza"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Hi ha hagut un problema en reproduir el vídeo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Comprova la connexió a Internet i torna-ho a provar"</string> diff --git a/photopicker/res/values-cs/core_strings.xml b/photopicker/res/values-cs/core_strings.xml index faff0ebfd..ca946d429 100644 --- a/photopicker/res/values-cs/core_strings.xml +++ b/photopicker/res/values-cs/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotky a videa"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Média"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Vybráno"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Přidat <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hotovo"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Zrušit výběr všech"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Vyberte maximálně <xliff:g id="COUNT">%1$s</xliff:g> položek"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotky"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Alba"</string> diff --git a/photopicker/res/values-cs/feature_cloud_strings.xml b/photopicker/res/values-cs/feature_cloud_strings.xml index ebc59b21c..da6316fec 100644 --- a/photopicker/res/values-cs/feature_cloud_strings.xml +++ b/photopicker/res/values-cs/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Připraveno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Některé fotografie nelze načíst"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Zkuste to později. Fotky budou k dispozici po vyřešení tohoto problému."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-cs/feature_preview_strings.xml b/photopicker/res/values-cs/feature_preview_strings.xml index c027efb3a..f238c3e3c 100644 --- a/photopicker/res/values-cs/feature_preview_strings.xml +++ b/photopicker/res/values-cs/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Vybrat vše (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Zrušit výběr všech (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Zobrazit náhled"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Při přehrávání videa došlo k potížím"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Zkontrolujte připojení k internetu a zkuste to znovu"</string> diff --git a/photopicker/res/values-da/core_strings.xml b/photopicker/res/values-da/core_strings.xml index 794a32e1e..10e2ba85f 100644 --- a/photopicker/res/values-da/core_strings.xml +++ b/photopicker/res/values-da/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Billeder og videoer"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Udfør"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Fravælg alle"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Vælg højst <xliff:g id="COUNT">%1$s</xliff:g> elementer"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string> diff --git a/photopicker/res/values-da/feature_cloud_strings.xml b/photopicker/res/values-da/feature_cloud_strings.xml index 2d9acb1e2..eb21a6bef 100644 --- a/photopicker/res/values-da/feature_cloud_strings.xml +++ b/photopicker/res/values-da/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> af <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> er klar"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nogle billeder kan ikke indlæses"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Prøv igen senere. Dine billeder bliver tilgængelige, så snart problemet er løst."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-da/feature_preview_strings.xml b/photopicker/res/values-da/feature_preview_strings.xml index a51d7202b..6188f0f3e 100644 --- a/photopicker/res/values-da/feature_preview_strings.xml +++ b/photopicker/res/values-da/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Markér alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Fjern markeringen af alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Forhåndsvisning"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problemer med at afspille video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Tjek din internetforbindelse, og prøv igen"</string> diff --git a/photopicker/res/values-de/core_strings.xml b/photopicker/res/values-de/core_strings.xml index 2f5d5e35f..847b31698 100644 --- a/photopicker/res/values-de/core_strings.xml +++ b/photopicker/res/values-de/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos & Videos"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Fertig"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Auswahl für alle aufheben"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maximal <xliff:g id="COUNT">%1$s</xliff:g> Elemente auswählen"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Alben"</string> diff --git a/photopicker/res/values-de/feature_cloud_strings.xml b/photopicker/res/values-de/feature_cloud_strings.xml index c0e1187b7..6b8f81757 100644 --- a/photopicker/res/values-de/feature_cloud_strings.xml +++ b/photopicker/res/values-de/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> von <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> fertig"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Einige Fotos können nicht geladen werden"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Versuch es später noch einmal. Deine Fotos sind verfügbar, sobald das Problem gelöst wurde."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-de/feature_preview_strings.xml b/photopicker/res/values-de/feature_preview_strings.xml index bfc5c3544..b7fd06fa9 100644 --- a/photopicker/res/values-de/feature_preview_strings.xml +++ b/photopicker/res/values-de/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Alle <xliff:g id="COUNT">(%1$s)</xliff:g> auswählen"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Auswahl für alle <xliff:g id="COUNT">(%1$s)</xliff:g> aufheben"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vorschau anzeigen"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleme bei der Wiedergabe des Videos"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Prüfe deine Internetverbindung und versuch es noch einmal"</string> diff --git a/photopicker/res/values-el/core_strings.xml b/photopicker/res/values-el/core_strings.xml index 2924e659c..ccc46ef38 100644 --- a/photopicker/res/values-el/core_strings.xml +++ b/photopicker/res/values-el/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Φωτογραφίες και βίντεο"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Τέλος"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Αποεπιλογή όλων"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Επιλέξτε έως και <xliff:g id="COUNT">%1$s</xliff:g> στοιχεία"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Φωτογραφίες"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Λευκώματα"</string> diff --git a/photopicker/res/values-el/feature_cloud_strings.xml b/photopicker/res/values-el/feature_cloud_strings.xml index b06442350..ebc97134a 100644 --- a/photopicker/res/values-el/feature_cloud_strings.xml +++ b/photopicker/res/values-el/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Έτοιμα: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> από <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Δεν είναι δυνατή η φόρτωση ορισμένων φωτογραφιών"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Δοκιμάστε ξανά αργότερα. Οι φωτογραφίες σας θα καταστούν διαθέσιμες μόλις επιλυθεί το πρόβλημα."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-el/feature_preview_strings.xml b/photopicker/res/values-el/feature_preview_strings.xml index f117e32fa..2f7ed89d5 100644 --- a/photopicker/res/values-el/feature_preview_strings.xml +++ b/photopicker/res/values-el/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Επιλογή και των <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Αποεπιλογή και των <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Προεπισκόπηση"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Πρόβλημα με την αναπαραγωγή βίντεο"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά"</string> diff --git a/photopicker/res/values-en-rAU/core_strings.xml b/photopicker/res/values-en-rAU/core_strings.xml index 45d4fc70a..35b9beb33 100644 --- a/photopicker/res/values-en-rAU/core_strings.xml +++ b/photopicker/res/values-en-rAU/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos and videos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string> diff --git a/photopicker/res/values-en-rAU/feature_cloud_strings.xml b/photopicker/res/values-en-rAU/feature_cloud_strings.xml index 14f76f3ac..6fc9685e1 100644 --- a/photopicker/res/values-en-rAU/feature_cloud_strings.xml +++ b/photopicker/res/values-en-rAU/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Please try again later. Your photos will be available once the issue is resolved."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-en-rAU/feature_preview_strings.xml b/photopicker/res/values-en-rAU/feature_preview_strings.xml index 97f7b109c..3c44c6c3b 100644 --- a/photopicker/res/values-en-rAU/feature_preview_strings.xml +++ b/photopicker/res/values-en-rAU/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Please check your Internet connection and try again"</string> diff --git a/photopicker/res/values-en-rCA/core_strings.xml b/photopicker/res/values-en-rCA/core_strings.xml index b0e9fb221..ce2efb179 100644 --- a/photopicker/res/values-en-rCA/core_strings.xml +++ b/photopicker/res/values-en-rCA/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos & videos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string> diff --git a/photopicker/res/values-en-rCA/feature_cloud_strings.xml b/photopicker/res/values-en-rCA/feature_cloud_strings.xml index cb464a0d8..0edd4794b 100644 --- a/photopicker/res/values-en-rCA/feature_cloud_strings.xml +++ b/photopicker/res/values-en-rCA/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some Photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Try again later. Your photos will be available once the issue is resolved."</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string> </resources> diff --git a/photopicker/res/values-en-rCA/feature_preview_strings.xml b/photopicker/res/values-en-rCA/feature_preview_strings.xml index d4d7eb2a0..89462e22e 100644 --- a/photopicker/res/values-en-rCA/feature_preview_strings.xml +++ b/photopicker/res/values-en-rCA/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Check your internet connection and try again"</string> diff --git a/photopicker/res/values-en-rGB/core_strings.xml b/photopicker/res/values-en-rGB/core_strings.xml index 45d4fc70a..35b9beb33 100644 --- a/photopicker/res/values-en-rGB/core_strings.xml +++ b/photopicker/res/values-en-rGB/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos and videos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string> diff --git a/photopicker/res/values-en-rGB/feature_cloud_strings.xml b/photopicker/res/values-en-rGB/feature_cloud_strings.xml index 14f76f3ac..6fc9685e1 100644 --- a/photopicker/res/values-en-rGB/feature_cloud_strings.xml +++ b/photopicker/res/values-en-rGB/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Please try again later. Your photos will be available once the issue is resolved."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-en-rGB/feature_preview_strings.xml b/photopicker/res/values-en-rGB/feature_preview_strings.xml index 97f7b109c..3c44c6c3b 100644 --- a/photopicker/res/values-en-rGB/feature_preview_strings.xml +++ b/photopicker/res/values-en-rGB/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Please check your Internet connection and try again"</string> diff --git a/photopicker/res/values-en-rIN/core_strings.xml b/photopicker/res/values-en-rIN/core_strings.xml index 45d4fc70a..35b9beb33 100644 --- a/photopicker/res/values-en-rIN/core_strings.xml +++ b/photopicker/res/values-en-rIN/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos and videos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string> diff --git a/photopicker/res/values-en-rIN/feature_cloud_strings.xml b/photopicker/res/values-en-rIN/feature_cloud_strings.xml index 14f76f3ac..6fc9685e1 100644 --- a/photopicker/res/values-en-rIN/feature_cloud_strings.xml +++ b/photopicker/res/values-en-rIN/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Please try again later. Your photos will be available once the issue is resolved."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-en-rIN/feature_preview_strings.xml b/photopicker/res/values-en-rIN/feature_preview_strings.xml index 97f7b109c..3c44c6c3b 100644 --- a/photopicker/res/values-en-rIN/feature_preview_strings.xml +++ b/photopicker/res/values-en-rIN/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Please check your Internet connection and try again"</string> diff --git a/photopicker/res/values-en-rXC/core_strings.xml b/photopicker/res/values-en-rXC/core_strings.xml index e500fdafe..6e0c81a39 100644 --- a/photopicker/res/values-en-rXC/core_strings.xml +++ b/photopicker/res/values-en-rXC/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos & videos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string> diff --git a/photopicker/res/values-en-rXC/feature_cloud_strings.xml b/photopicker/res/values-en-rXC/feature_cloud_strings.xml index c1dbc9bf8..2131da35b 100644 --- a/photopicker/res/values-en-rXC/feature_cloud_strings.xml +++ b/photopicker/res/values-en-rXC/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some Photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Try again later. Your photos will be available once the issue is resolved."</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string> </resources> diff --git a/photopicker/res/values-en-rXC/feature_preview_strings.xml b/photopicker/res/values-en-rXC/feature_preview_strings.xml index 724474525..42273b73d 100644 --- a/photopicker/res/values-en-rXC/feature_preview_strings.xml +++ b/photopicker/res/values-en-rXC/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Check your internet connection and try again"</string> diff --git a/photopicker/res/values-es-rUS/core_strings.xml b/photopicker/res/values-es-rUS/core_strings.xml index c126698b2..8b02930a3 100644 --- a/photopicker/res/values-es-rUS/core_strings.xml +++ b/photopicker/res/values-es-rUS/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos y videos"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Listo"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Anular toda la selección"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Elige <xliff:g id="COUNT">%1$s</xliff:g> elementos como máximo"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbumes"</string> diff --git a/photopicker/res/values-es-rUS/feature_cloud_strings.xml b/photopicker/res/values-es-rUS/feature_cloud_strings.xml index d1e59ca80..5b670a0c9 100644 --- a/photopicker/res/values-es-rUS/feature_cloud_strings.xml +++ b/photopicker/res/values-es-rUS/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Elementos listos: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Se produjo un error al cargar algunas fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Vuelve a intentarlo más tarde. Tus fotos estarán disponibles una vez que se resuelva el problema."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-es-rUS/feature_preview_strings.xml b/photopicker/res/values-es-rUS/feature_preview_strings.xml index 161c4b5f3..5c1c8e590 100644 --- a/photopicker/res/values-es-rUS/feature_preview_strings.xml +++ b/photopicker/res/values-es-rUS/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Seleccionar todos los <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Anular la selección de todos los <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vista previa"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Se produjo un error al reproducir el video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Revisa la conexión a Internet y vuelve a intentarlo"</string> diff --git a/photopicker/res/values-es/core_strings.xml b/photopicker/res/values-es/core_strings.xml index dd41b186c..017648b08 100644 --- a/photopicker/res/values-es/core_strings.xml +++ b/photopicker/res/values-es/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos y vídeos"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hecho"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar todo"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecciona hasta <xliff:g id="COUNT">%1$s</xliff:g> elementos"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbumes"</string> diff --git a/photopicker/res/values-es/feature_cloud_strings.xml b/photopicker/res/values-es/feature_cloud_strings.xml index bfb4f4ef4..0872fe908 100644 --- a/photopicker/res/values-es/feature_cloud_strings.xml +++ b/photopicker/res/values-es/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> listos"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"No se pueden cargar algunas fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Inténtalo de nuevo más tarde. Tus fotos estarán disponibles cuando se resuelva el problema."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-es/feature_preview_strings.xml b/photopicker/res/values-es/feature_preview_strings.xml index 0b763d9fa..bb3377d03 100644 --- a/photopicker/res/values-es/feature_preview_strings.xml +++ b/photopicker/res/values-es/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Seleccionar todo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Deseleccionar todo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vista previa"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Hay problemas para reproducir el vídeo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Comprueba tu conexión a Internet y vuelve a intentarlo"</string> diff --git a/photopicker/res/values-et/core_strings.xml b/photopicker/res/values-et/core_strings.xml index 6aa415a54..df6ed079b 100644 --- a/photopicker/res/values-et/core_strings.xml +++ b/photopicker/res/values-et/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotod ja videod"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Valmis"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tühista kogu valik"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Valige kuni <xliff:g id="COUNT">%1$s</xliff:g> üksust"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotod"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumid"</string> diff --git a/photopicker/res/values-et/feature_cloud_strings.xml b/photopicker/res/values-et/feature_cloud_strings.xml index 84854a9c4..092a5f55c 100644 --- a/photopicker/res/values-et/feature_cloud_strings.xml +++ b/photopicker/res/values-et/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-st on valmis"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Mõnda fotot ei saa laadida"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Proovige hiljem uuesti. Teie fotod on saadaval pärast probleemi lahendamist."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-et/feature_preview_strings.xml b/photopicker/res/values-et/feature_preview_strings.xml index 40cff466d..0b85ea80b 100644 --- a/photopicker/res/values-et/feature_preview_strings.xml +++ b/photopicker/res/values-et/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Vali kõik <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tühista kõigi <xliff:g id="COUNT">(%1$s)</xliff:g> valik"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Eelvaade"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleem video esitamisel"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Kontrollige oma internetiühendust ja proovige uuesti"</string> diff --git a/photopicker/res/values-eu/core_strings.xml b/photopicker/res/values-eu/core_strings.xml index de15c3a37..a15bcc899 100644 --- a/photopicker/res/values-eu/core_strings.xml +++ b/photopicker/res/values-eu/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Argazkiak eta bideoak"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Eginda"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desautatu guztiak"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Hautatu <xliff:g id="COUNT">%1$s</xliff:g> elementu, gehienez"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Argazkiak"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumak"</string> diff --git a/photopicker/res/values-eu/feature_cloud_strings.xml b/photopicker/res/values-eu/feature_cloud_strings.xml index 7c94e494f..d29617af1 100644 --- a/photopicker/res/values-eu/feature_cloud_strings.xml +++ b/photopicker/res/values-eu/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> prest"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ezin dira kargatu argazki batzuk"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Saiatu berriro geroago. Arazoa konpondu ondoren egongo dira erabilgarri argazkiak."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-eu/feature_preview_strings.xml b/photopicker/res/values-eu/feature_preview_strings.xml index 79ea21331..1d494e011 100644 --- a/photopicker/res/values-eu/feature_preview_strings.xml +++ b/photopicker/res/values-eu/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Hautatu guztiak (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desautatu guztiak (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Aurreikusi"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Arazoren bat izan da bideoa erreproduzitzean"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Egiaztatu Internetera konektatuta zaudela eta saiatu berriro"</string> diff --git a/photopicker/res/values-fa/core_strings.xml b/photopicker/res/values-fa/core_strings.xml index 0e304b872..de5e6e371 100644 --- a/photopicker/res/values-fa/core_strings.xml +++ b/photopicker/res/values-fa/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"عکس و ویدیو"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"تمام"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"لغو انتخاب کردن همه"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"حداکثر <xliff:g id="COUNT">%1$s</xliff:g> مورد انتخاب کنید"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"آلبومها"</string> diff --git a/photopicker/res/values-fa/feature_cloud_strings.xml b/photopicker/res/values-fa/feature_cloud_strings.xml index 86d7c2a2b..e153beb4b 100644 --- a/photopicker/res/values-fa/feature_cloud_strings.xml +++ b/photopicker/res/values-fa/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> مورد از <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> مورد آماده است"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"نمیتوان برخیاز عکسها را بار کرد"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"بعداً دوباره امتحان کنید. عکسهایتان پساز رفع مشکل دردسترس خواهد بود."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-fa/feature_preview_strings.xml b/photopicker/res/values-fa/feature_preview_strings.xml index b4e6e4a07..ccbf9c13a 100644 --- a/photopicker/res/values-fa/feature_preview_strings.xml +++ b/photopicker/res/values-fa/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"انتخاب کل <xliff:g id="COUNT">(%1$s)</xliff:g> مورد"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"لغو انتخاب کل <xliff:g id="COUNT">(%1$s)</xliff:g> مورد"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"پیشنمایش"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"در پخش ویدیو مشکل رخ داد"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"اتصال اینترنت را بررسی کنید و دوباره امتحان کنید"</string> diff --git a/photopicker/res/values-fi/core_strings.xml b/photopicker/res/values-fi/core_strings.xml index 00a2043db..5d5706a25 100644 --- a/photopicker/res/values-fi/core_strings.xml +++ b/photopicker/res/values-fi/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Kuvat ja videot"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Valmis"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poista valinta kaikista"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Valitse enintään <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Kuvat"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumit"</string> diff --git a/photopicker/res/values-fi/feature_cloud_strings.xml b/photopicker/res/values-fi/feature_cloud_strings.xml index c850864cf..63cb36668 100644 --- a/photopicker/res/values-fi/feature_cloud_strings.xml +++ b/photopicker/res/values-fi/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> valmiina"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Joitain kuvia ei voi ladata"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Yritä myöhemmin uudelleen. Kuvat ovat saatavilla, kun ongelma on korjattu."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-fi/feature_preview_strings.xml b/photopicker/res/values-fi/feature_preview_strings.xml index faaf4ec6b..f69aef05e 100644 --- a/photopicker/res/values-fi/feature_preview_strings.xml +++ b/photopicker/res/values-fi/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Valitse kaikki <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poista kaikki <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Esikatsele"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ongelma videon toistamisessa"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Tarkista internetyhteys ja yritä uudelleen"</string> diff --git a/photopicker/res/values-fr-rCA/core_strings.xml b/photopicker/res/values-fr-rCA/core_strings.xml index e0293a664..f1230a9aa 100644 --- a/photopicker/res/values-fr-rCA/core_strings.xml +++ b/photopicker/res/values-fr-rCA/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos et vidéos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Contenu multimédia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Sélectionné"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Ajouter <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"OK"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tout désélectionner"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Sélectionnez jusqu\'à <xliff:g id="COUNT">%1$s</xliff:g> éléments"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string> diff --git a/photopicker/res/values-fr-rCA/feature_cloud_strings.xml b/photopicker/res/values-fr-rCA/feature_cloud_strings.xml index 8f106f5e6..d85eac017 100644 --- a/photopicker/res/values-fr-rCA/feature_cloud_strings.xml +++ b/photopicker/res/values-fr-rCA/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sur <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> prêt(s)"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Impossible de charger certaines photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Réessayez plus tard. Vos photos seront accessibles dès que le problème sera résolu."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-fr-rCA/feature_preview_strings.xml b/photopicker/res/values-fr-rCA/feature_preview_strings.xml index 73721a71a..1ac08e16b 100644 --- a/photopicker/res/values-fr-rCA/feature_preview_strings.xml +++ b/photopicker/res/values-fr-rCA/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Sélectionner les <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Désélectionner les <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Aperçu"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Difficulté à faire jouer la vidéo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Vérifiez votre connexion Internet et réessayez"</string> diff --git a/photopicker/res/values-fr/core_strings.xml b/photopicker/res/values-fr/core_strings.xml index 1e981ee9c..bf0ecc493 100644 --- a/photopicker/res/values-fr/core_strings.xml +++ b/photopicker/res/values-fr/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos et vidéos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Contenus multimédias"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Sélectionnée"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Ajouter <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"OK"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tout désélectionner"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Sélectionnez <xliff:g id="COUNT">%1$s</xliff:g> éléments maximum"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string> diff --git a/photopicker/res/values-fr/feature_cloud_strings.xml b/photopicker/res/values-fr/feature_cloud_strings.xml index b692fa919..17263fa71 100644 --- a/photopicker/res/values-fr/feature_cloud_strings.xml +++ b/photopicker/res/values-fr/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Prêt(s) : <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sur <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Impossible de charger certaines photos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Réessayez plus tard. Vos photos seront disponibles une fois le problème résolu."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-fr/feature_preview_strings.xml b/photopicker/res/values-fr/feature_preview_strings.xml index 88ee340ea..c22717fc1 100644 --- a/photopicker/res/values-fr/feature_preview_strings.xml +++ b/photopicker/res/values-fr/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Tout sélectionner (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tout désélectionner (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Prévisualiser"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problème de lecture vidéo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Vérifiez votre connexion Internet, puis réessayez"</string> diff --git a/photopicker/res/values-gl/core_strings.xml b/photopicker/res/values-gl/core_strings.xml index d47009396..05485636d 100644 --- a/photopicker/res/values-gl/core_strings.xml +++ b/photopicker/res/values-gl/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Feito"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Anular toda a selección"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecciona ata <xliff:g id="COUNT">%1$s</xliff:g> elementos"</string> diff --git a/photopicker/res/values-gl/feature_cloud_strings.xml b/photopicker/res/values-gl/feature_cloud_strings.xml index 56d5ef5ed..bb2b3e4e3 100644 --- a/photopicker/res/values-gl/feature_cloud_strings.xml +++ b/photopicker/res/values-gl/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Elementos listos: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Non se puideron cargar algunhas fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Téntao de novo máis tarde. As túas fotos estarán dispoñibles en canto se resolva o problema."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-gl/feature_preview_strings.xml b/photopicker/res/values-gl/feature_preview_strings.xml index 0e166464e..aa9c76052 100644 --- a/photopicker/res/values-gl/feature_preview_strings.xml +++ b/photopicker/res/values-gl/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Seleccionar todo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Anular selección (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previsualizar"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Produciuse un problema ao reproducir o vídeo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Comproba a túa conexión a Internet e téntao de novo"</string> diff --git a/photopicker/res/values-gu/core_strings.xml b/photopicker/res/values-gu/core_strings.xml index b803540f6..981a8dffc 100644 --- a/photopicker/res/values-gu/core_strings.xml +++ b/photopicker/res/values-gu/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ફોટા અને વીડિયો"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"થઈ ગયું"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"બધાને નાપસંદ કરો"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> જેટલી આઇટમ પસંદ કરો"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"આલ્બમ"</string> diff --git a/photopicker/res/values-gu/feature_cloud_strings.xml b/photopicker/res/values-gu/feature_cloud_strings.xml index a30450dbb..a379ea5ab 100644 --- a/photopicker/res/values-gu/feature_cloud_strings.xml +++ b/photopicker/res/values-gu/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>માંથી <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> તૈયાર"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"અમુક ફોટા લોડ કરી શકાતા નથી"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"થોડા સમય પછી ફરી પ્રયાસ કરો. એકવાર સમસ્યાનું નિરાકરણ થઈ જાય, તે પછી તમારા ફોટા ઉપલબ્ધ થશે."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-gu/feature_preview_strings.xml b/photopicker/res/values-gu/feature_preview_strings.xml index 9698c2449..3cd081417 100644 --- a/photopicker/res/values-gu/feature_preview_strings.xml +++ b/photopicker/res/values-gu/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"તમામ <xliff:g id="COUNT">(%1$s)</xliff:g> પસંદ કરો"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"તમામ <xliff:g id="COUNT">(%1$s)</xliff:g> નાપસંદ કરો"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"પ્રીવ્યૂ કરો"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"વીડિયો ચલાવવામાં સમસ્યા આવી"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"તમારું ઇન્ટરનેટ કનેક્શન ચેક કરો અને ફરી પ્રયાસ કરો"</string> diff --git a/photopicker/res/values-hi/core_strings.xml b/photopicker/res/values-hi/core_strings.xml index 1fe7f24f2..337e7391b 100644 --- a/photopicker/res/values-hi/core_strings.xml +++ b/photopicker/res/values-hi/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"फ़ोटो और वीडियो"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"हो गया"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"सभी से चुने हुए का निशान हटाएं"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ज़्यादा से ज़्यादा <xliff:g id="COUNT">%1$s</xliff:g> आइटम चुनें"</string> diff --git a/photopicker/res/values-hi/feature_cloud_strings.xml b/photopicker/res/values-hi/feature_cloud_strings.xml index 435c34fbe..8e2d3f211 100644 --- a/photopicker/res/values-hi/feature_cloud_strings.xml +++ b/photopicker/res/values-hi/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> में से <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> तैयार हैं"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"कुछ फ़ोटो लोड नहीं की जा सकीं"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"बाद में कोशिश करें. समस्या ठीक होने के बाद आपकी फ़ोटो उपलब्ध हो जाएंगी."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-hi/feature_preview_strings.xml b/photopicker/res/values-hi/feature_preview_strings.xml index e6ecaf4bb..04431b78a 100644 --- a/photopicker/res/values-hi/feature_preview_strings.xml +++ b/photopicker/res/values-hi/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"सभी चुनें <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"सभी से चुने हुए का निशान हटाएं <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"झलक"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"वीडियो चलाने में समस्या हो रही है"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"अपने इंटरनेट कनेक्शन की जांच करें और फिर से कोशिश करें"</string> diff --git a/photopicker/res/values-hr/core_strings.xml b/photopicker/res/values-hr/core_strings.xml index 504f04b5d..53387a8bf 100644 --- a/photopicker/res/values-hr/core_strings.xml +++ b/photopicker/res/values-hr/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografije i videozapisi"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotovo"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poništi odabir za sve"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Odaberite najviše ovoliko stavki: <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string> diff --git a/photopicker/res/values-hr/feature_cloud_strings.xml b/photopicker/res/values-hr/feature_cloud_strings.xml index ecfe6c303..0d5a12001 100644 --- a/photopicker/res/values-hr/feature_cloud_strings.xml +++ b/photopicker/res/values-hr/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Spremno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Neke fotografije ne mogu se učitati"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Pokušajte ponovno kasnije. Vaše fotografije bit će dostupne kad se problem riješi."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-hr/feature_preview_strings.xml b/photopicker/res/values-hr/feature_preview_strings.xml index 2dce4d5c7..d70aae754 100644 --- a/photopicker/res/values-hr/feature_preview_strings.xml +++ b/photopicker/res/values-hr/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Odaberi sve (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poništi odabir za sve (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Poteškoće s reprodukcijom videozapisa"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Provjerite internetsku vezu i pokušajte ponovo"</string> diff --git a/photopicker/res/values-hu/core_strings.xml b/photopicker/res/values-hu/core_strings.xml index e73a50c8e..80be78b4d 100644 --- a/photopicker/res/values-hu/core_strings.xml +++ b/photopicker/res/values-hu/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotók és videók"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Média"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Kijelölve"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> hozzáadása"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Kész"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Összes kijelölés megszüntetése"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Legfeljebb <xliff:g id="COUNT">%1$s</xliff:g> elemet jelölhet ki"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotók"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumok"</string> diff --git a/photopicker/res/values-hu/feature_cloud_strings.xml b/photopicker/res/values-hu/feature_cloud_strings.xml index c8b6714d7..7563bb074 100644 --- a/photopicker/res/values-hu/feature_cloud_strings.xml +++ b/photopicker/res/values-hu/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>/<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> kész"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Egyes fotók nem tölthetők be"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Próbálkozzon újra később. Fotói hozzáférhetők lesznek a probléma elhárítását követően."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-hu/feature_preview_strings.xml b/photopicker/res/values-hu/feature_preview_strings.xml index 033c2397b..82d4409af 100644 --- a/photopicker/res/values-hu/feature_preview_strings.xml +++ b/photopicker/res/values-hu/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Összes (<xliff:g id="COUNT">(%1$s)</xliff:g>) kijelölése"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Összes (<xliff:g id="COUNT">(%1$s)</xliff:g>) kijelölés megszüntetése"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Előnézet"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probléma merült fel a videó lejátszásakor"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Ellenőrizze az internetkapcsolatot, és próbálja újra"</string> diff --git a/photopicker/res/values-hy/core_strings.xml b/photopicker/res/values-hy/core_strings.xml index cebedb863..47df78421 100644 --- a/photopicker/res/values-hy/core_strings.xml +++ b/photopicker/res/values-hy/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Լուսանկարներ և տեսանյութեր"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Պատրաստ է"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Չեղարկել ընտրությունը"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Ընտրեք մինչև <xliff:g id="COUNT">%1$s</xliff:g> տարր"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Լուսանկարներ"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Ալբոմներ"</string> diff --git a/photopicker/res/values-hy/feature_cloud_strings.xml b/photopicker/res/values-hy/feature_cloud_strings.xml index 52360cae0..8d4b4d404 100644 --- a/photopicker/res/values-hy/feature_cloud_strings.xml +++ b/photopicker/res/values-hy/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> պատրաստ է"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Չհաջողվեց բեռնել որոշ լուսանկարներ"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Փորձեք ավելի ուշ։ Ձեր լուսանկարները հասանելի կլինեն, երբ խնդիրը լուծվի։"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-hy/feature_preview_strings.xml b/photopicker/res/values-hy/feature_preview_strings.xml index 49efb32fe..ced491778 100644 --- a/photopicker/res/values-hy/feature_preview_strings.xml +++ b/photopicker/res/values-hy/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Ընտրել բոլոր <xliff:g id="COUNT">(%1$s)</xliff:g>-ը"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Չեղարկել բոլոր <xliff:g id="COUNT">(%1$s)</xliff:g>-ի ընտրությունը"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Նախադիտել"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Տեսանյութի նվագարկման սխալ"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Ստուգեք ձեր ինտերնետ կապը և նորից փորձեք"</string> diff --git a/photopicker/res/values-in/core_strings.xml b/photopicker/res/values-in/core_strings.xml index 969eb474c..f4271bed6 100644 --- a/photopicker/res/values-in/core_strings.xml +++ b/photopicker/res/values-in/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto & video"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Dipilih"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Tambahkan <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Selesai"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Batalkan semua pilihan"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pilih hingga <xliff:g id="COUNT">%1$s</xliff:g> item"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string> diff --git a/photopicker/res/values-in/feature_cloud_strings.xml b/photopicker/res/values-in/feature_cloud_strings.xml index 1529c2331..470ec7c53 100644 --- a/photopicker/res/values-in/feature_cloud_strings.xml +++ b/photopicker/res/values-in/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> dari <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> siap"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Tidak dapat memuat beberapa Foto"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Coba lagi nanti. Foto Anda akan tersedia setelah masalah terselesaikan."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-in/feature_preview_strings.xml b/photopicker/res/values-in/feature_preview_strings.xml index 9c4c31181..a16824d47 100644 --- a/photopicker/res/values-in/feature_preview_strings.xml +++ b/photopicker/res/values-in/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Pilih semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Batalkan pilihan semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pratinjau"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Terjadi masalah saat memutar video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Periksa koneksi internet Anda, lalu coba lagi"</string> diff --git a/photopicker/res/values-is/core_strings.xml b/photopicker/res/values-is/core_strings.xml index 19b823610..e60ffed48 100644 --- a/photopicker/res/values-is/core_strings.xml +++ b/photopicker/res/values-is/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Myndir og vídeó"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Efni"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Valið"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Bæta <xliff:g id="COUNT">(%1$s)</xliff:g> við"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Lokið"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Afvelja allt"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Veldu allt að <xliff:g id="COUNT">%1$s</xliff:g> atriði"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Myndir"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Plötur"</string> diff --git a/photopicker/res/values-is/feature_cloud_strings.xml b/photopicker/res/values-is/feature_cloud_strings.xml index 1f3e25e4f..effad1f8a 100644 --- a/photopicker/res/values-is/feature_cloud_strings.xml +++ b/photopicker/res/values-is/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> af <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> til reiðu"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ekki tekst að hlaða sumum myndum"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Reyndu aftur síðar. Myndirnar þínar verða tiltækar um leið og vandamálið er leyst."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-is/feature_preview_strings.xml b/photopicker/res/values-is/feature_preview_strings.xml index 7207c2ee2..10c702a92 100644 --- a/photopicker/res/values-is/feature_preview_strings.xml +++ b/photopicker/res/values-is/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Velja allt (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Afturkalla val á öllu (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Forskoða"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Vandamál við spilun vídeós"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Athugaðu nettenginguna og reyndu aftur"</string> diff --git a/photopicker/res/values-it/core_strings.xml b/photopicker/res/values-it/core_strings.xml index ec7361025..377b2d6c1 100644 --- a/photopicker/res/values-it/core_strings.xml +++ b/photopicker/res/values-it/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto e video"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Contenuti multimediali"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Opzione selezionata"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Aggiungi <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Fine"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deseleziona tutto"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Seleziona massimo <xliff:g id="COUNT">%1$s</xliff:g> elementi"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string> diff --git a/photopicker/res/values-it/feature_cloud_strings.xml b/photopicker/res/values-it/feature_cloud_strings.xml index af0740f30..97ddc0555 100644 --- a/photopicker/res/values-it/feature_cloud_strings.xml +++ b/photopicker/res/values-it/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> su <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> pronti"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Impossibile caricare alcune foto"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Riprova più tardi. Le tue foto saranno disponibili dopo aver risolto il problema."</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Ora sono incluse le foto di cui hai eseguito il backup"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Puoi selezionare foto dall\'account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> nell\'app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Scegli un account <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Per includere qui le foto di <xliff:g id="APP_NAME">%1$s</xliff:g>, scegli un account nell\'app"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Scegli account"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Scegli un\'app multimediale con funzionalità cloud"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Per includere qui le foto di cui hai eseguito il backup, scegli un\'app multimediale con funzionalità cloud nelle Impostazioni"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Scegli app"</string> </resources> diff --git a/photopicker/res/values-it/feature_preview_strings.xml b/photopicker/res/values-it/feature_preview_strings.xml index 28bd2a93b..03834b554 100644 --- a/photopicker/res/values-it/feature_preview_strings.xml +++ b/photopicker/res/values-it/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Seleziona tutto (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Deseleziona tutto (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Anteprima"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Errore durante la riproduzione del video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Controlla la connessione a internet e riprova"</string> diff --git a/photopicker/res/values-iw/core_strings.xml b/photopicker/res/values-iw/core_strings.xml index 1ba3a8820..fb3d8770e 100644 --- a/photopicker/res/values-iw/core_strings.xml +++ b/photopicker/res/values-iw/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"תמונות וסרטונים"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"סיום"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ביטול הבחירה של הכול"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"אפשר לבחור <xliff:g id="COUNT">%1$s</xliff:g> פריטים לכל היותר"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"תמונות"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"אלבומים"</string> diff --git a/photopicker/res/values-iw/feature_cloud_strings.xml b/photopicker/res/values-iw/feature_cloud_strings.xml index 8a8de3ffc..63875f851 100644 --- a/photopicker/res/values-iw/feature_cloud_strings.xml +++ b/photopicker/res/values-iw/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> מתוך <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> מוכנים"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"לא ניתן לטעון חלק מהתמונות"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"כדאי לנסות שוב אחר כך. התמונות שלך יהיו זמינות כשהבעיה תיפתר."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-iw/feature_preview_strings.xml b/photopicker/res/values-iw/feature_preview_strings.xml index 17cbb35c7..5feaa3397 100644 --- a/photopicker/res/values-iw/feature_preview_strings.xml +++ b/photopicker/res/values-iw/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"בחירת הכול (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ביטול הבחירה של הכול (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"תצוגה מקדימה"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"בעיות בהפעלת הסרטון"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"כדאי לבדוק את החיבור לאינטרנט ולנסות שוב"</string> diff --git a/photopicker/res/values-ja/core_strings.xml b/photopicker/res/values-ja/core_strings.xml index 39e0f370e..a7c210652 100644 --- a/photopicker/res/values-ja/core_strings.xml +++ b/photopicker/res/values-ja/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"写真と動画"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"完了"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"選択をすべて解除"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> 件まで選択できます"</string> diff --git a/photopicker/res/values-ja/feature_cloud_strings.xml b/photopicker/res/values-ja/feature_cloud_strings.xml index 3a3adc6f3..9695b794e 100644 --- a/photopicker/res/values-ja/feature_cloud_strings.xml +++ b/photopicker/res/values-ja/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 件準備完了"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"読み込めなかった写真があります"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"しばらくしてからもう一度お試しください。問題が解決したら、写真をご利用いただけるようになります。"</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"バックアップした写真が追加されました"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> のアカウント <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> の写真を選択できます"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> のアカウントを選択してください"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> の写真をここに追加するには、アプリでアカウントを選択してください"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"アカウントの選択"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"クラウド メディアアプリを選択してください"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"バックアップした写真をここに追加するには、[設定] でクラウド メディアアプリを選択してください"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"アプリを選択"</string> </resources> diff --git a/photopicker/res/values-ja/feature_preview_strings.xml b/photopicker/res/values-ja/feature_preview_strings.xml index 4acf82c7f..5a451bbcd 100644 --- a/photopicker/res/values-ja/feature_preview_strings.xml +++ b/photopicker/res/values-ja/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"すべて選択(<xliff:g id="COUNT">(%1$s)</xliff:g> 件)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"すべての選択を解除(<xliff:g id="COUNT">(%1$s)</xliff:g> 件)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"プレビュー"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"動画を再生できません"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"インターネット接続を確認してもう一度お試しください"</string> diff --git a/photopicker/res/values-ka/core_strings.xml b/photopicker/res/values-ka/core_strings.xml index 15aad9a18..357089f11 100644 --- a/photopicker/res/values-ka/core_strings.xml +++ b/photopicker/res/values-ka/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ფოტოები და ვიდეოები"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"მზადაა"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ყველა არჩევის გაუქმება"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"აირჩიეთ <xliff:g id="COUNT">%1$s</xliff:g>-მდე ერთეული"</string> diff --git a/photopicker/res/values-ka/feature_cloud_strings.xml b/photopicker/res/values-ka/feature_cloud_strings.xml index 531f8c365..8a389a382 100644 --- a/photopicker/res/values-ka/feature_cloud_strings.xml +++ b/photopicker/res/values-ka/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> სულ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-დან მზადაა"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ზოგიერთი ფოტოს ჩატვირთვა ვერ ხერხდება"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ცადეთ მოგვიანებით. ხარვეზის აღმოფხვრის შემდეგ თქვენი ფოტოები ხელმისაწვდომი იქნება."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ka/feature_preview_strings.xml b/photopicker/res/values-ka/feature_preview_strings.xml index 31301adfa..1e14adadc 100644 --- a/photopicker/res/values-ka/feature_preview_strings.xml +++ b/photopicker/res/values-ka/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>-ვეს არჩევა"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>-ვეს არჩევის გაუქმება"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"გადახედვა"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ვიდეოს დაკვრისას წარმოიქმნა პრობლემა"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"შეამოწმეთ ინტერნეტ-კავშირი და ცადეთ ხელახლა"</string> diff --git a/photopicker/res/values-kk/core_strings.xml b/photopicker/res/values-kk/core_strings.xml index 5ed3b4fbf..4c49a8c32 100644 --- a/photopicker/res/values-kk/core_strings.xml +++ b/photopicker/res/values-kk/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Фотосуреттер мен бейнелер"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Дайын"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Барлық таңдаудан бас тарту"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> элементке дейін таңдаңыз."</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Aльбомдар"</string> diff --git a/photopicker/res/values-kk/feature_cloud_strings.xml b/photopicker/res/values-kk/feature_cloud_strings.xml index 787c59cc6..b82615615 100644 --- a/photopicker/res/values-kk/feature_cloud_strings.xml +++ b/photopicker/res/values-kk/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> дайын"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Кейбір фотосуреттерді жүктеу мүмкін емес"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Кейінірек қайталап көріңіз. Мәселе шешілген соң, фотосуреттеріңіз қолжетімді болады."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-kk/feature_preview_strings.xml b/photopicker/res/values-kk/feature_preview_strings.xml index 26dfc3f73..110afb00e 100644 --- a/photopicker/res/values-kk/feature_preview_strings.xml +++ b/photopicker/res/values-kk/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Барлығын (<xliff:g id="COUNT">(%1$s)</xliff:g>) таңдау"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Барлығын (<xliff:g id="COUNT">(%1$s)</xliff:g>) таңдаудан бас тарту"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Алдын ала көру"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Бейнені ойнату кезінде қиындық туындады"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Интернет байланысын тексеріп, әрекетті қайталап көріңіз."</string> diff --git a/photopicker/res/values-km/core_strings.xml b/photopicker/res/values-km/core_strings.xml index 39f59082e..c2b071d44 100644 --- a/photopicker/res/values-km/core_strings.xml +++ b/photopicker/res/values-km/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"រូបថត និងវីដេអូ"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"រួចរាល់"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ដកការជ្រើសរើសទាំងអស់"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ជ្រើសរើសធាតុរហូតដល់ <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"រូបថត"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"អាល់ប៊ុម"</string> diff --git a/photopicker/res/values-km/feature_cloud_strings.xml b/photopicker/res/values-km/feature_cloud_strings.xml index c82cd5f47..f8a393122 100644 --- a/photopicker/res/values-km/feature_cloud_strings.xml +++ b/photopicker/res/values-km/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> នៃ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> រួចរាល់ហើយ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"មិនអាចផ្ទុករូបថតមួយចំនួនបានទេ"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"សូមព្យាយាមម្ដងទៀតនៅពេលក្រោយ។ រូបថតរបស់អ្នកនឹងអាចប្រើបាន បន្ទាប់ពីដោះស្រាយបញ្ហា។"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-km/feature_preview_strings.xml b/photopicker/res/values-km/feature_preview_strings.xml index 76da0b8c0..4c808e957 100644 --- a/photopicker/res/values-km/feature_preview_strings.xml +++ b/photopicker/res/values-km/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"ជ្រើសរើសទាំង <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ដកការជ្រើសរើសទាំង <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"មើលសាកល្បង"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"មានបញ្ហាក្នុងការចាក់វីដេអូ"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ពិនិត្យការតភ្ជាប់អ៊ីនធឺណិតរបស់អ្នក រួចព្យាយាមម្ដងទៀត"</string> diff --git a/photopicker/res/values-kn/core_strings.xml b/photopicker/res/values-kn/core_strings.xml index c34506d3f..6c6653d64 100644 --- a/photopicker/res/values-kn/core_strings.xml +++ b/photopicker/res/values-kn/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳು"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"ಮುಗಿದಿದೆ"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ಎಲ್ಲಾ ಆಯ್ಕೆಯನ್ನು ರದ್ದುಮಾಡಿ"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ಐಟಂಗಳವರೆಗೆ ಆಯ್ಕೆಮಾಡಿ"</string> diff --git a/photopicker/res/values-kn/feature_cloud_strings.xml b/photopicker/res/values-kn/feature_cloud_strings.xml index 97c83d76f..3cf573ed0 100644 --- a/photopicker/res/values-kn/feature_cloud_strings.xml +++ b/photopicker/res/values-kn/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ರಲ್ಲಿ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ಸಿದ್ಧವಾಗಿವೆ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ಕೆಲವು ಫೋಟೋಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ನಂತರ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ. ಸಮಸ್ಯೆ ಬಗೆಹರಿದ ನಂತರ ನಿಮ್ಮ ಫೋಟೋಗಳು ಲಭ್ಯವಿರುತ್ತವೆ."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-kn/feature_preview_strings.xml b/photopicker/res/values-kn/feature_preview_strings.xml index c9afc75c9..dd4e374c3 100644 --- a/photopicker/res/values-kn/feature_preview_strings.xml +++ b/photopicker/res/values-kn/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> ಎಲ್ಲವನ್ನೂ ಆಯ್ಕೆಮಾಡಿ"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> ಎಲ್ಲವನ್ನೂ ಆಯ್ಕೆ ಮಾಡಬೇಡಿ"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ಪೂರ್ವವೀಕ್ಷಣೆ"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ವೀಡಿಯೊ ಪ್ಲೇ ಮಾಡಲು ಸಮಸ್ಯೆಯಾಗಿದೆ"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ನಿಮ್ಮ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ ಹಾಗೂ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"</string> diff --git a/photopicker/res/values-ko/core_strings.xml b/photopicker/res/values-ko/core_strings.xml index 736918781..d0c8ca032 100644 --- a/photopicker/res/values-ko/core_strings.xml +++ b/photopicker/res/values-ko/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"사진 및 동영상"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"완료"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"전체 선택 해제"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"최대 <xliff:g id="COUNT">%1$s</xliff:g>개 항목 선택"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"사진"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"앨범"</string> diff --git a/photopicker/res/values-ko/feature_cloud_strings.xml b/photopicker/res/values-ko/feature_cloud_strings.xml index 1653d863b..6d3b18763 100644 --- a/photopicker/res/values-ko/feature_cloud_strings.xml +++ b/photopicker/res/values-ko/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>개 중 <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>개가 준비됨"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"일부 사진을 로드할 수 없음"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"나중에 다시 시도해 주세요. 사진은 문제가 해결된 후에 사용할 수 있습니다."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ko/feature_preview_strings.xml b/photopicker/res/values-ko/feature_preview_strings.xml index 02471b058..097b5ed9a 100644 --- a/photopicker/res/values-ko/feature_preview_strings.xml +++ b/photopicker/res/values-ko/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>개 모두 선택"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>개 모두 선택 해제"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"미리보기"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"동영상 재생 중 문제 발생"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"인터넷 연결 상태를 확인하고 다시 시도해 주세요"</string> diff --git a/photopicker/res/values-ky/core_strings.xml b/photopicker/res/values-ky/core_strings.xml index 4ff54931a..596d21405 100644 --- a/photopicker/res/values-ky/core_strings.xml +++ b/photopicker/res/values-ky/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Сүрөттөр жана видеолор"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Бүттү"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Баарын тандоодон чыгаруу"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> объектке чейин тандаңыз"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Сүрөттөр"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбомдор"</string> diff --git a/photopicker/res/values-ky/feature_cloud_strings.xml b/photopicker/res/values-ky/feature_cloud_strings.xml index 6c49b2010..bc2153a97 100644 --- a/photopicker/res/values-ky/feature_cloud_strings.xml +++ b/photopicker/res/values-ky/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ичинен <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> даяр"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Айрым сүрөттөр жүктөлбөй жатат"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Кийинчерээк кайталап көрүңүз. Сүрөттөрүңүз маселе чечилгенден кийин жеткиликтүү болот."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ky/feature_preview_strings.xml b/photopicker/res/values-ky/feature_preview_strings.xml index 75174d736..3c1e55552 100644 --- a/photopicker/res/values-ky/feature_preview_strings.xml +++ b/photopicker/res/values-ky/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> тең тандоо"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> тең тандоодон чыгаруу"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Алдын ала көрүү"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Видеону ойнотууда маселе келип чыкты"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Интернет байланышыңызды текшерип, кайталап көрүңүз"</string> diff --git a/photopicker/res/values-lo/core_strings.xml b/photopicker/res/values-lo/core_strings.xml index 0cfcdc32b..eb20be86e 100644 --- a/photopicker/res/values-lo/core_strings.xml +++ b/photopicker/res/values-lo/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ຮູບພາບ ແລະ ວິດີໂອ"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"ແລ້ວໆ"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ເຊົາເລືອກທັງໝົດ"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ເລືອກໄດ້ສູງສຸດ <xliff:g id="COUNT">%1$s</xliff:g> ລາຍການ"</string> diff --git a/photopicker/res/values-lo/feature_cloud_strings.xml b/photopicker/res/values-lo/feature_cloud_strings.xml index d6509010d..59c7ad929 100644 --- a/photopicker/res/values-lo/feature_cloud_strings.xml +++ b/photopicker/res/values-lo/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"ພ້ອມແລ້ວ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ຈາກທັງໝົດ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ລາຍການ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ບໍ່ສາມາດໂຫຼດບາງຮູບພາບໄດ້"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ. ຮູບພາບຂອງທ່ານຈະພ້ອມນຳໃຊ້ເມື່ອບັນຫາໄດ້ຮັບການແກ້ໄຂແລ້ວ."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-lo/feature_preview_strings.xml b/photopicker/res/values-lo/feature_preview_strings.xml index 59a52a20d..4af627d33 100644 --- a/photopicker/res/values-lo/feature_preview_strings.xml +++ b/photopicker/res/values-lo/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"ເລືອກທັງໝົດ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ບໍ່ເລືອກທັງໝົດ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ຕົວຢ່າງ"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ເກີດບັນຫາໃນການຫຼິ້ນວິດີໂອ"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ກະລຸນາກວດສອບການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງທ່ານແລ້ວລອງໃໝ່"</string> diff --git a/photopicker/res/values-lt/core_strings.xml b/photopicker/res/values-lt/core_strings.xml index 1846fb1fc..d88197bb9 100644 --- a/photopicker/res/values-lt/core_strings.xml +++ b/photopicker/res/values-lt/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Nuotraukos ir vaizdo įrašai"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Medija"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Pasirinkta"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Pridėti <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Atlikta"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Panaikinti visus pasirinkimus"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pasirinkite iki <xliff:g id="COUNT">%1$s</xliff:g> elemen."</string> diff --git a/photopicker/res/values-lt/feature_cloud_strings.xml b/photopicker/res/values-lt/feature_cloud_strings.xml index 6fef8ab3d..f9b94ae3a 100644 --- a/photopicker/res/values-lt/feature_cloud_strings.xml +++ b/photopicker/res/values-lt/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Paruošta: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> iš <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nepavyko įkelti kai kurių nuotraukų"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Vėliau bandykite dar kartą. Nuotraukos bus pasiekiamos išsprendus problemą."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-lt/feature_preview_strings.xml b/photopicker/res/values-lt/feature_preview_strings.xml index b5faba4e2..4083517b9 100644 --- a/photopicker/res/values-lt/feature_preview_strings.xml +++ b/photopicker/res/values-lt/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Pasirinkti viską (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Atšaukti visų (<xliff:g id="COUNT">(%1$s)</xliff:g>) pasirinkimą"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Peržiūrėti"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Paleidžiant vaizdo įrašą kilo problema"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Patikrinkite interneto ryšį ir bandykite dar kartą"</string> diff --git a/photopicker/res/values-lv/core_strings.xml b/photopicker/res/values-lv/core_strings.xml index 02a67dfcb..5f9b1992c 100644 --- a/photopicker/res/values-lv/core_strings.xml +++ b/photopicker/res/values-lv/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotoattēli un videoklipi"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Multivides vienums"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Atlasīts"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Pievienot <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gatavs"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Noņemt visu atlasi"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Varat atlasīt ne vairāk kā <xliff:g id="COUNT">%1$s</xliff:g> vienumu(-us)."</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotoattēli"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string> diff --git a/photopicker/res/values-lv/feature_cloud_strings.xml b/photopicker/res/values-lv/feature_cloud_strings.xml index 85018a70c..3ddd281f2 100644 --- a/photopicker/res/values-lv/feature_cloud_strings.xml +++ b/photopicker/res/values-lv/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Gatavs: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> no <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nevar ielādēt dažus fotoattēlus"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Vēlāk mēģiniet vēlreiz. Fotoattēli būs pieejami, tiklīdz būs novērsta problēma."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-lv/feature_preview_strings.xml b/photopicker/res/values-lv/feature_preview_strings.xml index 5d8b9df38..75aa71225 100644 --- a/photopicker/res/values-lv/feature_preview_strings.xml +++ b/photopicker/res/values-lv/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Atlasīt visu (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Noņemt visu atlasi (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Priekšskatīt"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Atskaņojot videoklipu, radās kļūda"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Pārbaudiet interneta savienojumu un mēģiniet vēlreiz."</string> diff --git a/photopicker/res/values-mk/core_strings.xml b/photopicker/res/values-mk/core_strings.xml index 70e1ebb9a..30f5126da 100644 --- a/photopicker/res/values-mk/core_strings.xml +++ b/photopicker/res/values-mk/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Фотографии и видеа"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Поништи го изборот на сите"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Изберете до <xliff:g id="COUNT">%1$s</xliff:g> ставки"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фотографии"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Албуми"</string> diff --git a/photopicker/res/values-mk/feature_cloud_strings.xml b/photopicker/res/values-mk/feature_cloud_strings.xml index bf87905df..61e8a9a22 100644 --- a/photopicker/res/values-mk/feature_cloud_strings.xml +++ b/photopicker/res/values-mk/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Подготвени: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> од <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Некои фотографии не може да се вчитаат"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Обидете се повторно подоцна. Вашите фотографии ќе бидат достапни откако ќе се реши проблемот."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-mk/feature_preview_strings.xml b/photopicker/res/values-mk/feature_preview_strings.xml index 7239b35b1..b95554117 100644 --- a/photopicker/res/values-mk/feature_preview_strings.xml +++ b/photopicker/res/values-mk/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Избери ги сите <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Поништи го изборот на сите <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Прикажи"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Проблем со пуштањето на видеото"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверете ја интернет-врската и обидете се повторно"</string> diff --git a/photopicker/res/values-ml/core_strings.xml b/photopicker/res/values-ml/core_strings.xml index db05f3083..a83076510 100644 --- a/photopicker/res/values-ml/core_strings.xml +++ b/photopicker/res/values-ml/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ഫോട്ടോകളും വീഡിയോകളും"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"പൂർത്തിയായി"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"തിരഞ്ഞെടുത്തത് എല്ലാം മാറ്റുക"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ഇനങ്ങൾ വരെ തിരഞ്ഞെടുക്കുക"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ഫോട്ടോകൾ"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ആൽബങ്ങൾ"</string> diff --git a/photopicker/res/values-ml/feature_cloud_strings.xml b/photopicker/res/values-ml/feature_cloud_strings.xml index 613716057..72d73e059 100644 --- a/photopicker/res/values-ml/feature_cloud_strings.xml +++ b/photopicker/res/values-ml/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-ൽ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> എണ്ണം തയ്യാറാണ്"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ചില ഫോട്ടോകൾ ലോഡ് ചെയ്യാനാകുന്നില്ല"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"പിന്നീട് വീണ്ടും ശ്രമിക്കൂ. പ്രശ്നം പരിഹരിച്ച് കഴിഞ്ഞ് നിങ്ങളുടെ ഫോട്ടോകൾ ലഭ്യമാകും."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ml/feature_preview_strings.xml b/photopicker/res/values-ml/feature_preview_strings.xml index d26f94538..b530e7a1d 100644 --- a/photopicker/res/values-ml/feature_preview_strings.xml +++ b/photopicker/res/values-ml/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"എല്ലാം തിരഞ്ഞെടുക്കുക (<xliff:g id="COUNT">(%1$s)</xliff:g> എണ്ണം)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"തിരഞ്ഞെടുത്തത് മാറ്റുക (<xliff:g id="COUNT">(%1$s)</xliff:g> എണ്ണം)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"പ്രിവ്യൂ ചെയ്യുക"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"വീഡിയോ പ്ലേ ചെയ്യുന്നതിൽ പ്രശ്നം"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"നിങ്ങളുടെ ഇന്റർനെറ്റ് കണക്ഷൻ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക"</string> diff --git a/photopicker/res/values-mn/core_strings.xml b/photopicker/res/values-mn/core_strings.xml index d7dc5394e..e4a4ff4e8 100644 --- a/photopicker/res/values-mn/core_strings.xml +++ b/photopicker/res/values-mn/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Зураг болон видео"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Болсон"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Бүх сонголтыг цуцлах"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> хүртэлх зүйл сонгоно уу"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Зураг"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Цомог"</string> diff --git a/photopicker/res/values-mn/feature_cloud_strings.xml b/photopicker/res/values-mn/feature_cloud_strings.xml index f0d6e8208..09787c9ae 100644 --- a/photopicker/res/values-mn/feature_cloud_strings.xml +++ b/photopicker/res/values-mn/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-с <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> бэлэн байна"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Зарим зургийг ачаалах боломжгүй"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Дараа дахин оролдоно уу. Асуудлыг шийдвэрлэсний дараа таны зургууд боломжтой болно."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-mn/feature_preview_strings.xml b/photopicker/res/values-mn/feature_preview_strings.xml index 82f45d286..b14cdde92 100644 --- a/photopicker/res/values-mn/feature_preview_strings.xml +++ b/photopicker/res/values-mn/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Бүх <xliff:g id="COUNT">(%1$s)</xliff:g> сонголтыг сонгох"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Бүх <xliff:g id="COUNT">(%1$s)</xliff:g> сонголтыг болиулах"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Урьдчилан үзэх"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Видеог тоглуулахад асуудал гарлаа"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Интернэт холболтоо шалгаад, дахин оролдоно уу"</string> diff --git a/photopicker/res/values-mr/core_strings.xml b/photopicker/res/values-mr/core_strings.xml index 31a8eba0b..d5c2cd4a8 100644 --- a/photopicker/res/values-mr/core_strings.xml +++ b/photopicker/res/values-mr/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"फोटो आणि व्हिडिओ"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"पूर्ण झाले"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"सर्व निवडी रद्द करा"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"कमाल <xliff:g id="COUNT">%1$s</xliff:g> आयटम निवडा"</string> diff --git a/photopicker/res/values-mr/feature_cloud_strings.xml b/photopicker/res/values-mr/feature_cloud_strings.xml index 8dd1ddb51..332c8502e 100644 --- a/photopicker/res/values-mr/feature_cloud_strings.xml +++ b/photopicker/res/values-mr/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> पैकी <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> तयार"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"काही फोटो लोड करू शकत नाही"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"नंतर पुन्हा प्रयत्न करा. समस्येचे निराकरण झाल्यावर तुमचे फोटो उपलब्ध होतील."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-mr/feature_preview_strings.xml b/photopicker/res/values-mr/feature_preview_strings.xml index 471e2bf25..6086d79ff 100644 --- a/photopicker/res/values-mr/feature_preview_strings.xml +++ b/photopicker/res/values-mr/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"सर्व निवडा <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"सर्व निवडी रद्द करा <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"पूर्वावलोकन करा"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"व्हिडिओ प्ले करण्यात समस्या आली"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"तुमचे इंटरनेट कनेक्शन तपासा आणि पुन्हा प्रयत्न करा"</string> diff --git a/photopicker/res/values-ms/core_strings.xml b/photopicker/res/values-ms/core_strings.xml index 6e8b1571e..d02bf4b80 100644 --- a/photopicker/res/values-ms/core_strings.xml +++ b/photopicker/res/values-ms/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto & video"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Dipilih"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Tambahkan <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Selesai"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Nyahpilih semua"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pilih hingga <xliff:g id="COUNT">%1$s</xliff:g> item"</string> diff --git a/photopicker/res/values-ms/feature_cloud_strings.xml b/photopicker/res/values-ms/feature_cloud_strings.xml index 704fb4ee1..5fc30ca5d 100644 --- a/photopicker/res/values-ms/feature_cloud_strings.xml +++ b/photopicker/res/values-ms/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> daripada <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> sudah sedia"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Tidak dapat memuatkan beberapa Foto"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Cuba lagi nanti. Foto anda akan tersedia selepas masalah ini diselesaikan."</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Foto yang disandarkan kini disertakan"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Anda boleh memilih foto daripada akaun <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Pilih akaun <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Untuk menyertakan foto daripada <xliff:g id="APP_NAME">%1$s</xliff:g> di sini, pilih akaun dalam apl"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Pilih akaun"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Pilih apl media awan"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Untuk menyertakan foto yang disandarkan di sini, pilih apl media awan dalam Tetapan"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Pilih apl"</string> </resources> diff --git a/photopicker/res/values-ms/feature_preview_strings.xml b/photopicker/res/values-ms/feature_preview_strings.xml index 7e17ec862..272d0a33d 100644 --- a/photopicker/res/values-ms/feature_preview_strings.xml +++ b/photopicker/res/values-ms/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Pilih semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Nyahpilih semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pratonton"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Berlaku masalah semasa memainkan video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Semak sambungan Internet anda, kemudian cuba lagi"</string> diff --git a/photopicker/res/values-my/core_strings.xml b/photopicker/res/values-my/core_strings.xml index 5f1644a6c..c104a84a9 100644 --- a/photopicker/res/values-my/core_strings.xml +++ b/photopicker/res/values-my/core_strings.xml @@ -20,19 +20,16 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ဓာတ်ပုံနှင့် ဗီဒီယိုများ"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"ပြီးပြီ"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"အားလုံးကို ပြန်ဖြုတ်ရန်"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ဖိုင် <xliff:g id="COUNT">%1$s</xliff:g> ဖိုင်အထိ ရွေးနိုင်သည်"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ဓာတ်ပုံများ"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"အယ်လ်ဘမ်များ"</string> <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ဓာတ်ပုံ မရှိသေးပါ"</string> <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ဓာတ်ပုံနှင့် ဗီဒီယိုများကို စတင်ရိုက်ကူးနေသည်"</string> <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"အကြိုက်ဆုံး မရှိသေးပါ"</string> - <string name="photopicker_favorites_empty_state_body" msgid="3265404004750778533">"၎င်းသည် ကြယ်ပွင့်ပြထားသော (သို့) စိတ်ကြိုက်အဖြစ် မှတ်ထားသော ဖိုင်များကို စုစည်းသော အလိုအလျောက် အယ်လ်ဘမ်ဖြစ်သည်"</string> - <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"မည်သည့် ဗီဒီယိုမှ မရှိသေးပါ"</string> + <string name="photopicker_favorites_empty_state_body" msgid="3265404004750778533">"၎င်းသည် ကြယ်ပွင့်ပြထားသော (သို့) စိတ်ကြိုက်အဖြစ် မှတ်ထားသော ဖိုင်များကို စုစည်းသည့် အလိုအလျောက် အယ်လ်ဘမ်ဖြစ်သည်"</string> + <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"မည်သည့် ဗီဒီယိုမျှ မရှိသေးပါ"</string> <string name="photopicker_videos_empty_state_body" msgid="5149028843414577039">"၎င်းသည် သင့်ဗီဒီယိုအားလုံးကို စုစည်းသော အလိုအလျောက် အယ်လ်ဘမ်ဖြစ်သည်"</string> <string name="photopicker_back_option" msgid="986374743479020214">"နောက်သို့"</string> <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ပယ်ရန်"</string> diff --git a/photopicker/res/values-my/feature_cloud_strings.xml b/photopicker/res/values-my/feature_cloud_strings.xml index 34c93ce52..24ce838f8 100644 --- a/photopicker/res/values-my/feature_cloud_strings.xml +++ b/photopicker/res/values-my/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> အနက် <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> အသင့်ဖြစ်ပြီ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ဓာတ်ပုံအချို့ကို ဖွင့်၍မရပါ"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"နောက်မှထပ်စမ်းပါ။ ပြဿနာကို ဖြေရှင်းပြီးသည့်အခါ သင့်ဓာတ်ပုံများကို ရနိုင်မည်။"</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"အရန်သိမ်းထားသော ဓာတ်ပုံများ ယခုထည့်သွင်းထားပြီ"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> အကောင့် <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> မှ ဓာတ်ပုံများ ရွေးနိုင်သည်"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> အကောင့်ရွေးရန်"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> မှ ဓာတ်ပုံများကို ဤနေရာတွင်ထည့်သွင်းရန် အက်ပ်၌ အကောင့်ရွေးပါ"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"အကောင့်ရွေးရန်"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Cloud မီဒီယာအက်ပ် ရွေးရန်"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"အရန်သိမ်းထားသော ဓာတ်ပုံများ ဤနေရာတွင်ထည့်သွင်းရန် ဆက်တင်များ၌ cloud မီဒီယာအက်ပ် ရွေးပါ"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"အက်ပ်ရွေးရန်"</string> </resources> diff --git a/photopicker/res/values-my/feature_preview_strings.xml b/photopicker/res/values-my/feature_preview_strings.xml index 1631d1ff8..9688543c5 100644 --- a/photopicker/res/values-my/feature_preview_strings.xml +++ b/photopicker/res/values-my/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> ခုစလုံး ရွေးရန်"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> ခုစလုံး ပြန်ဖြုတ်ရန်"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"အစမ်းကြည့်ရှုရန်"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ဗီဒီယိုဖွင့်ရာတွင် ပြဿနာရှိသည်"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"အင်တာနက်ချိတ်ဆက်မှုကို စစ်ဆေးပြီး ထပ်စမ်းကြည့်ပါ"</string> diff --git a/photopicker/res/values-nb/core_strings.xml b/photopicker/res/values-nb/core_strings.xml index a90375cff..8a8d774da 100644 --- a/photopicker/res/values-nb/core_strings.xml +++ b/photopicker/res/values-nb/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Bilder og videoer"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Medieinnhold"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Valgt"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Legg til <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Ferdig"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Fjern alle valg"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Velg opptil <xliff:g id="COUNT">%1$s</xliff:g> elementer"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Bilder"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string> diff --git a/photopicker/res/values-nb/feature_cloud_strings.xml b/photopicker/res/values-nb/feature_cloud_strings.xml index 86abb75a5..564a38e31 100644 --- a/photopicker/res/values-nb/feature_cloud_strings.xml +++ b/photopicker/res/values-nb/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> av <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> er klare"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Noen bilder kan ikke lastes inn"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Prøv på nytt senere. Bildene dine blir tilgjengelige når problemet er løst."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-nb/feature_preview_strings.xml b/photopicker/res/values-nb/feature_preview_strings.xml index 7d253eeb7..33719f911 100644 --- a/photopicker/res/values-nb/feature_preview_strings.xml +++ b/photopicker/res/values-nb/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Merk av alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Fjern merkingen av alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Se forhåndsvisning"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problem med avspilling av videoen"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Sjekk internettilkoblingen og prøv på nytt"</string> diff --git a/photopicker/res/values-ne/core_strings.xml b/photopicker/res/values-ne/core_strings.xml index 0ef8f57c4..a05e7f958 100644 --- a/photopicker/res/values-ne/core_strings.xml +++ b/photopicker/res/values-ne/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"फोटो तथा भिडियोहरू"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"सम्पन्न भयो"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"सबैको चयन रद्द गर्नुहोस्"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"बढीमा <xliff:g id="COUNT">%1$s</xliff:g> वटा वस्तु चयन गर्नुहोस्"</string> diff --git a/photopicker/res/values-ne/feature_cloud_strings.xml b/photopicker/res/values-ne/feature_cloud_strings.xml index cf58d55ed..14e091827 100644 --- a/photopicker/res/values-ne/feature_cloud_strings.xml +++ b/photopicker/res/values-ne/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> मध्ये <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> वटा फोटो तयार छन्"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"केही फोटोहरू लोड गर्न सकिएन"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"पछि फेरि प्रयास गर्नुहोस्। समस्या समाधान हुनेबित्तिकै तपाईंका फोटो उपलब्ध हुने छन्।"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ne/feature_preview_strings.xml b/photopicker/res/values-ne/feature_preview_strings.xml index e336f92c2..e624a9a46 100644 --- a/photopicker/res/values-ne/feature_preview_strings.xml +++ b/photopicker/res/values-ne/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"सबै <xliff:g id="COUNT">(%1$s)</xliff:g> वटा चयन गर्नुहोस्"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"सबै <xliff:g id="COUNT">(%1$s)</xliff:g> वटाको चयन रद्द गर्नुहोस्"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"प्रिभ्यू गर्नुहोस्"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"भिडियो प्ले गर्दा समस्या भयो"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"इन्टरनेट कनेक्सन जाँच्नुहोस् र फेरि प्रयास गर्नुहोस्"</string> diff --git a/photopicker/res/values-nl/core_strings.xml b/photopicker/res/values-nl/core_strings.xml index 9ff040089..90def44ec 100644 --- a/photopicker/res/values-nl/core_strings.xml +++ b/photopicker/res/values-nl/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto\'s en video\'s"</string> <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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Klaar"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Alles deselecteren"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecteer maximaal <xliff:g id="COUNT">%1$s</xliff:g> items"</string> diff --git a/photopicker/res/values-nl/feature_cloud_strings.xml b/photopicker/res/values-nl/feature_cloud_strings.xml index 701fb6bb9..88a901fed 100644 --- a/photopicker/res/values-nl/feature_cloud_strings.xml +++ b/photopicker/res/values-nl/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> van <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> klaar"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Kan bepaalde foto\'s niet laden"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Probeer het later opnieuw. Je foto\'s komen beschikbaar nadat het probleem is opgelost."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-nl/feature_preview_strings.xml b/photopicker/res/values-nl/feature_preview_strings.xml index 39eb0937d..e4a735022 100644 --- a/photopicker/res/values-nl/feature_preview_strings.xml +++ b/photopicker/res/values-nl/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Alle <xliff:g id="COUNT">(%1$s)</xliff:g> selecteren"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Alle <xliff:g id="COUNT">(%1$s)</xliff:g> deselecteren"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Voorbeeld"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleem bij video afspelen"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Check de internetverbinding en probeer het opnieuw"</string> diff --git a/photopicker/res/values-or/core_strings.xml b/photopicker/res/values-or/core_strings.xml index f48fcf228..7a90f4889 100644 --- a/photopicker/res/values-or/core_strings.xml +++ b/photopicker/res/values-or/core_strings.xml @@ -20,18 +20,15 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ଫଟୋ ଓ ଭିଡିଓ"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"ହୋଇଗଲା"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ସମସ୍ତ ଅଚୟନ କରନ୍ତୁ"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g>ଟି ପର୍ଯ୍ୟନ୍ତ ଆଇଟମ ଚୟନ କରନ୍ତୁ"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ଆଲବମଗୁଡ଼ିକ"</string> <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଫଟୋ ନାହିଁ"</string> <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ଫଟୋ ଏବଂ ଭିଡିଓଗୁଡ଼ିକୁ କେପଚର କରିବା ଆରମ୍ଭ କରନ୍ତୁ"</string> <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ପସନ୍ଦର ଆଲବମ ନାହିଁ"</string> - <string name="photopicker_favorites_empty_state_body" msgid="3265404004750778533">"ଏହା ପସନ୍ଦର ଭାବେ ଷ୍ଟାରଯୁକ୍ତ କିମ୍ବା ଚିହିତ ଆଇଟମଗୁଡ଼ିକୁ ସଂଗ୍ରହ କରୁଥିବା ଏକ ସ୍ୱତଃ ଆଲବମ ଅଟେ"</string> + <string name="photopicker_favorites_empty_state_body" msgid="3265404004750778533">"ଏହା ପସନ୍ଦର ଭାବେ ଷ୍ଟାରଯୁକ୍ତ କିମ୍ବା ଚିହ୍ନିତ ଆଇଟମଗୁଡ଼ିକୁ ସଂଗ୍ରହ କରୁଥିବା ଏକ ସ୍ୱତଃ ଆଲବମ ଅଟେ"</string> <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଭିଡିଓ ନାହିଁ"</string> <string name="photopicker_videos_empty_state_body" msgid="5149028843414577039">"ଏହା ଆପଣଙ୍କର ସମସ୍ତ ଭିଡିଓକୁ ସଂଗ୍ରହ କରୁଥିବା ଏକ ସ୍ୱତଃ ଆଲବମ ଅଟେ"</string> <string name="photopicker_back_option" msgid="986374743479020214">"ପଛକୁ ଫେରନ୍ତୁ"</string> diff --git a/photopicker/res/values-or/feature_cloud_strings.xml b/photopicker/res/values-or/feature_cloud_strings.xml index a4a3607d2..edf357c0d 100644 --- a/photopicker/res/values-or/feature_cloud_strings.xml +++ b/photopicker/res/values-or/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>ରୁ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ପ୍ରସ୍ତୁତ ଅଛି"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"କିଛି ଫଟୋ ଲୋଡ କରାଯାଇପାରିବ ନାହିଁ"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ। ସମସ୍ୟାର ସମାଧାନ ହେବା ପରେ ଆପଣଙ୍କ ଫଟୋଗୁଡ଼ିକ ଉପଲବ୍ଧ ହେବ।"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-or/feature_preview_strings.xml b/photopicker/res/values-or/feature_preview_strings.xml index 9899e72b4..fe655f3bd 100644 --- a/photopicker/res/values-or/feature_preview_strings.xml +++ b/photopicker/res/values-or/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"ସମସ୍ତ <xliff:g id="COUNT">(%1$s)</xliff:g>କୁ ଚୟନ କରନ୍ତୁ"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ସମସ୍ତ <xliff:g id="COUNT">(%1$s)</xliff:g>କୁ ଅଚୟନ କରନ୍ତୁ"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ପ୍ରିଭ୍ୟୁ"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ଭିଡିଓ ପ୍ଲେ କରିବାରେ ସମସ୍ୟା"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ଆପଣଙ୍କ ଇଣ୍ଟରନେଟ କନେକ୍ସନ ଯାଞ୍ଚ କରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string> diff --git a/photopicker/res/values-pa/core_strings.xml b/photopicker/res/values-pa/core_strings.xml index 245f3d97c..61beef1e6 100644 --- a/photopicker/res/values-pa/core_strings.xml +++ b/photopicker/res/values-pa/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"ਹੋ ਗਿਆ"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ਸਭ ਅਣ-ਚੁਣਿਆ ਕਰੋ"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ਤੱਕ ਆਈਟਮਾਂ ਚੁਣੋ"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ਐਲਬਮਾਂ"</string> diff --git a/photopicker/res/values-pa/feature_cloud_strings.xml b/photopicker/res/values-pa/feature_cloud_strings.xml index e2752bdf2..90f8244ee 100644 --- a/photopicker/res/values-pa/feature_cloud_strings.xml +++ b/photopicker/res/values-pa/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ਵਿੱਚੋਂ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ਤਿਆਰ ਹੈ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ਕੁਝ ਫ਼ੋਟੋਆਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ਬਾਅਦ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ। ਸਮੱਸਿਆ ਹੱਲ ਹੋਣ ਤੋਂ ਬਾਅਦ, ਤੁਹਾਡੀਆਂ ਫ਼ੋਟੋਆਂ ਉਪਲਬਧ ਹੋ ਜਾਣਗੀਆਂ।"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-pa/feature_preview_strings.xml b/photopicker/res/values-pa/feature_preview_strings.xml index 2246d5fe7..3cf2f09b3 100644 --- a/photopicker/res/values-pa/feature_preview_strings.xml +++ b/photopicker/res/values-pa/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"ਸਭ ਚੁਣੋ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ਸਭ ਅਣਚੁਣਿਆ ਕਰੋ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ਪੂਰਵ-ਝਲਕ ਦੇਖੋ"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ਵੀਡੀਓ ਚਲਾਉਣ ਵਿੱਚ ਸਮੱਸਿਆ ਆ ਰਹੀ ਹੈ"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ਆਪਣੇ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰ ਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string> diff --git a/photopicker/res/values-pl/core_strings.xml b/photopicker/res/values-pl/core_strings.xml index 6e120b59c..8749df9d5 100644 --- a/photopicker/res/values-pl/core_strings.xml +++ b/photopicker/res/values-pl/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Zdjęcia i filmy"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Multimedia"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Wybrano"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotowe"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Odznacz wszystkie"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maksymalna liczba elementów, które można wybrać: <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Zdjęcia"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumy"</string> diff --git a/photopicker/res/values-pl/feature_cloud_strings.xml b/photopicker/res/values-pl/feature_cloud_strings.xml index 522101900..ca6e94c29 100644 --- a/photopicker/res/values-pl/feature_cloud_strings.xml +++ b/photopicker/res/values-pl/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Gotowe <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nie udało się wczytać niektórych zdjęć"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Spróbuj ponownie później. Zdjęcia będą dostępne po rozwiązaniu problemu."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-pl/feature_preview_strings.xml b/photopicker/res/values-pl/feature_preview_strings.xml index c7db685b7..8dd511a49 100644 --- a/photopicker/res/values-pl/feature_preview_strings.xml +++ b/photopicker/res/values-pl/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Zaznacz wszystkie <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Odznacz wszystkie <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Podgląd"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Wystąpiły problemy przy odtwarzaniu filmu"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Sprawdź połączenie z internetem i spróbuj ponownie"</string> diff --git a/photopicker/res/values-pt-rBR/core_strings.xml b/photopicker/res/values-pt-rBR/core_strings.xml index 6d0323f8f..3b77bd9ef 100644 --- a/photopicker/res/values-pt-rBR/core_strings.xml +++ b/photopicker/res/values-pt-rBR/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Concluir"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar tudo"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecione até <xliff:g id="COUNT">%1$s</xliff:g> itens"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbuns"</string> diff --git a/photopicker/res/values-pt-rBR/feature_cloud_strings.xml b/photopicker/res/values-pt-rBR/feature_cloud_strings.xml index c324be583..436af8c6b 100644 --- a/photopicker/res/values-pt-rBR/feature_cloud_strings.xml +++ b/photopicker/res/values-pt-rBR/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> itens prontos"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Não é possível carregar algumas fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-pt-rBR/feature_preview_strings.xml b/photopicker/res/values-pt-rBR/feature_preview_strings.xml index 4a37c35e3..fa21b16b0 100644 --- a/photopicker/res/values-pt-rBR/feature_preview_strings.xml +++ b/photopicker/res/values-pt-rBR/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Selecionar tudo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desmarcar tudo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Visualizar"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ocorreu um problema ao iniciar o vídeo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Confira sua conexão de Internet e tente de novo"</string> diff --git a/photopicker/res/values-pt-rPT/core_strings.xml b/photopicker/res/values-pt-rPT/core_strings.xml index c4798672d..9dd7d2de3 100644 --- a/photopicker/res/values-pt-rPT/core_strings.xml +++ b/photopicker/res/values-pt-rPT/core_strings.xml @@ -20,7 +20,6 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Multimé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> <string name="photopicker_done_button_label" msgid="2641444126618862287">"Concluir"</string> <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar tudo"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecione até <xliff:g id="COUNT">%1$s</xliff:g> itens"</string> diff --git a/photopicker/res/values-pt-rPT/feature_cloud_strings.xml b/photopicker/res/values-pt-rPT/feature_cloud_strings.xml index c9f1d65a7..ee54b876c 100644 --- a/photopicker/res/values-pt-rPT/feature_cloud_strings.xml +++ b/photopicker/res/values-pt-rPT/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> item(ns) pronto(s)"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Não é possível carregar algumas fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Tente mais tarde. As suas fotos vão estar disponíveis quando o problema estiver resolvido."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-pt-rPT/feature_preview_strings.xml b/photopicker/res/values-pt-rPT/feature_preview_strings.xml index 9e6d1983b..5c0f6cefb 100644 --- a/photopicker/res/values-pt-rPT/feature_preview_strings.xml +++ b/photopicker/res/values-pt-rPT/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Selecionar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desmarcar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pré-visualizar"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Dificuldades em reproduzir o vídeo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Verifique a ligação à Internet e tente novamente"</string> diff --git a/photopicker/res/values-pt/core_strings.xml b/photopicker/res/values-pt/core_strings.xml index 6d0323f8f..3b77bd9ef 100644 --- a/photopicker/res/values-pt/core_strings.xml +++ b/photopicker/res/values-pt/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Concluir"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar tudo"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecione até <xliff:g id="COUNT">%1$s</xliff:g> itens"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbuns"</string> diff --git a/photopicker/res/values-pt/feature_cloud_strings.xml b/photopicker/res/values-pt/feature_cloud_strings.xml index c324be583..436af8c6b 100644 --- a/photopicker/res/values-pt/feature_cloud_strings.xml +++ b/photopicker/res/values-pt/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> itens prontos"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Não é possível carregar algumas fotos"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-pt/feature_preview_strings.xml b/photopicker/res/values-pt/feature_preview_strings.xml index 4a37c35e3..fa21b16b0 100644 --- a/photopicker/res/values-pt/feature_preview_strings.xml +++ b/photopicker/res/values-pt/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Selecionar tudo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desmarcar tudo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Visualizar"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ocorreu um problema ao iniciar o vídeo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Confira sua conexão de Internet e tente de novo"</string> diff --git a/photopicker/res/values-ro/core_strings.xml b/photopicker/res/values-ro/core_strings.xml index 49f4b4ba5..cde7c6ff7 100644 --- a/photopicker/res/values-ro/core_strings.xml +++ b/photopicker/res/values-ro/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografii și videoclipuri"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gata"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselectează tot"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selectează maximum <xliff:g id="COUNT">%1$s</xliff:g> elemente"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografii"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albume"</string> diff --git a/photopicker/res/values-ro/feature_cloud_strings.xml b/photopicker/res/values-ro/feature_cloud_strings.xml index 4af50bcfb..5629f871e 100644 --- a/photopicker/res/values-ro/feature_cloud_strings.xml +++ b/photopicker/res/values-ro/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Finalizate: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> din <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Unele fotografii nu pot fi încărcate"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Încearcă din nou mai târziu. Fotografiile tale vor fi disponibile după ce se rezolvă problema."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ro/feature_preview_strings.xml b/photopicker/res/values-ro/feature_preview_strings.xml index 035ded54e..6372f66ef 100644 --- a/photopicker/res/values-ro/feature_preview_strings.xml +++ b/photopicker/res/values-ro/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Selectează-le pe toate cele <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Deselectează-le pe toate cele <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previzualizează"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleme la redarea videoclipului"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Verifică-ți conexiunea la internet și încearcă din nou"</string> diff --git a/photopicker/res/values-ru/core_strings.xml b/photopicker/res/values-ru/core_strings.xml index 84c8ff365..bf5719f96 100644 --- a/photopicker/res/values-ru/core_strings.xml +++ b/photopicker/res/values-ru/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Фото и видео"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Отменить выбор"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Выберите объекты (не более <xliff:g id="COUNT">%1$s</xliff:g>)."</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фото"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбомы"</string> diff --git a/photopicker/res/values-ru/feature_cloud_strings.xml b/photopicker/res/values-ru/feature_cloud_strings.xml index 7df0d760b..cc9fae509 100644 --- a/photopicker/res/values-ru/feature_cloud_strings.xml +++ b/photopicker/res/values-ru/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Предзагрузка: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> из <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>…"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Не удается загрузить некоторые фотографии"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Повторите попытку позже. Ваши фотографии станут доступны после устранения проблемы."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ru/feature_preview_strings.xml b/photopicker/res/values-ru/feature_preview_strings.xml index c30c12899..be21100d1 100644 --- a/photopicker/res/values-ru/feature_preview_strings.xml +++ b/photopicker/res/values-ru/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Выбрать все <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Отменить выбор всех <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Посмотреть"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Не удалось воспроизвести видео"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверьте подключение к интернету и повторите попытку."</string> diff --git a/photopicker/res/values-si/core_strings.xml b/photopicker/res/values-si/core_strings.xml index 60120313f..3f0460696 100644 --- a/photopicker/res/values-si/core_strings.xml +++ b/photopicker/res/values-si/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ඡායාරූප සහ වීඩියෝ"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"නිමයි"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"සියල්ල නොතෝරන්න"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"අයිතම <xliff:g id="COUNT">%1$s</xliff:g>ක් දක්වා තෝරන්න"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ඡායාරූප"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ඇල්බම"</string> diff --git a/photopicker/res/values-si/feature_cloud_strings.xml b/photopicker/res/values-si/feature_cloud_strings.xml index 2a3da29ce..a1fd882f7 100644 --- a/photopicker/res/values-si/feature_cloud_strings.xml +++ b/photopicker/res/values-si/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>කින් <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>ක් සූදානම්"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"සමහර ඡායාරූප පූරණය කළ නොහැක"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"පසුව නැවත උත්සාහ කරන්න. ගැටලුව විසඳූ පසු ඔබේ ඡායාරූප ලබා ගත හැකි වනු ඇත."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-si/feature_preview_strings.xml b/photopicker/res/values-si/feature_preview_strings.xml index 053b8e33e..3669e3ea0 100644 --- a/photopicker/res/values-si/feature_preview_strings.xml +++ b/photopicker/res/values-si/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"සියල්ල තෝරන්න <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"සියල්ල නොතෝරන්න <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"පෙරදසුන"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"වීඩියෝව වාදනය කිරීමේ ගැටලුවකි"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ඔබේ අන්තර්ජාල සම්බන්ධතාව පරීක්ෂා කර නැවත උත්සහ කරන්න"</string> diff --git a/photopicker/res/values-sk/core_strings.xml b/photopicker/res/values-sk/core_strings.xml index e8ad54272..992f118f7 100644 --- a/photopicker/res/values-sk/core_strings.xml +++ b/photopicker/res/values-sk/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotky a videá"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Médiá"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Vybrané"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Pridať <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hotovo"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Zrušiť výber všetkého"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Vyberte maximálne <xliff:g id="COUNT">%1$s</xliff:g> položiek"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotky"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumy"</string> diff --git a/photopicker/res/values-sk/feature_cloud_strings.xml b/photopicker/res/values-sk/feature_cloud_strings.xml index 4381ab081..2fab8ed6b 100644 --- a/photopicker/res/values-sk/feature_cloud_strings.xml +++ b/photopicker/res/values-sk/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Pripravené: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Niektoré fotky sa nedajú načítať"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Skúste to neskôr. Po vyriešení problému budú vaše fotky k dispozícii."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-sk/feature_preview_strings.xml b/photopicker/res/values-sk/feature_preview_strings.xml index 11e602d7b..6f31a677b 100644 --- a/photopicker/res/values-sk/feature_preview_strings.xml +++ b/photopicker/res/values-sk/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Vybrať všetky položky (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Zrušiť výber všetkých položiek (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Zobraziť ukážku"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ťažkosti s prehrávaním videa"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Skontrolujte internetové pripojenie a skúste to znova"</string> diff --git a/photopicker/res/values-sl/core_strings.xml b/photopicker/res/values-sl/core_strings.xml index e2a135f94..dc81f6ba9 100644 --- a/photopicker/res/values-sl/core_strings.xml +++ b/photopicker/res/values-sl/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografije in videoposnetki"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Končano"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Preklic celotnega izbora"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Izberite največ toliko elementov: <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string> diff --git a/photopicker/res/values-sl/feature_cloud_strings.xml b/photopicker/res/values-sl/feature_cloud_strings.xml index a878cc76e..b214e39fa 100644 --- a/photopicker/res/values-sl/feature_cloud_strings.xml +++ b/photopicker/res/values-sl/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Pripravljenih: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nekaterih fotografij ni mogoče naložiti"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Poskusite znova pozneje. Fotografije bodo na voljo, ko bo težava odpravljena."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-sl/feature_preview_strings.xml b/photopicker/res/values-sl/feature_preview_strings.xml index a4fcfd627..4e3a54388 100644 --- a/photopicker/res/values-sl/feature_preview_strings.xml +++ b/photopicker/res/values-sl/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Izberi vse (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Odznači vse (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Predogled"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Težave pri predvajanju videoposnetka"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Preverite internetno povezavo in poskusite znova"</string> diff --git a/photopicker/res/values-sq/core_strings.xml b/photopicker/res/values-sq/core_strings.xml index ae418f0ad..97f39a163 100644 --- a/photopicker/res/values-sq/core_strings.xml +++ b/photopicker/res/values-sq/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografitë dhe videot"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"U krye"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Anulo zgjedhjen për të gjitha"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Zgjidh deri në <xliff:g id="COUNT">%1$s</xliff:g> artikuj"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografitë"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumet"</string> diff --git a/photopicker/res/values-sq/feature_cloud_strings.xml b/photopicker/res/values-sq/feature_cloud_strings.xml index ba153def8..fcf3e0330 100644 --- a/photopicker/res/values-sq/feature_cloud_strings.xml +++ b/photopicker/res/values-sq/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> nga <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> gati"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Disa fotografi nuk mund të ngarkohen"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Provo përsëri më vonë. Fotografitë e tua do të ofrohen pasi të zgjidhet problemi."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-sq/feature_preview_strings.xml b/photopicker/res/values-sq/feature_preview_strings.xml index ab12ee1ae..d452f9dff 100644 --- a/photopicker/res/values-sq/feature_preview_strings.xml +++ b/photopicker/res/values-sq/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Zgjidh të gjitha (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Anulo zgjedhjen për të gjitha (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Shiko paraprakisht"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problem me luajtjen e videos"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Kontrollo lidhjen e internetit dhe provo përsëri"</string> diff --git a/photopicker/res/values-sr/core_strings.xml b/photopicker/res/values-sr/core_strings.xml index e2e30f0a6..bf88b436c 100644 --- a/photopicker/res/values-sr/core_strings.xml +++ b/photopicker/res/values-sr/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Слике и видеи"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Поништите све"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Највећи број ставки које можете да изаберете је <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Слике"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Албуми"</string> diff --git a/photopicker/res/values-sr/feature_cloud_strings.xml b/photopicker/res/values-sr/feature_cloud_strings.xml index 8acf3a161..4a25d1532 100644 --- a/photopicker/res/values-sr/feature_cloud_strings.xml +++ b/photopicker/res/values-sr/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Спремно:<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> од <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Учитавање неких слика није успело"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Пробајте поново касније. Слике ће бити доступне када се проблем реши."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-sr/feature_preview_strings.xml b/photopicker/res/values-sr/feature_preview_strings.xml index 4b32f2a43..178f9c484 100644 --- a/photopicker/res/values-sr/feature_preview_strings.xml +++ b/photopicker/res/values-sr/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Изабери све <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Поништи избор <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Преглед"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Дошло је до грешке при пуштању видеа"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверите интернет везу и пробајте поново"</string> diff --git a/photopicker/res/values-sv/core_strings.xml b/photopicker/res/values-sv/core_strings.xml index ca3155cac..b12f80bb6 100644 --- a/photopicker/res/values-sv/core_strings.xml +++ b/photopicker/res/values-sv/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Foton och videor"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Klar"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Avmarkera alla"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Välj upp till <xliff:g id="COUNT">%1$s</xliff:g> objekt"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string> diff --git a/photopicker/res/values-sv/feature_cloud_strings.xml b/photopicker/res/values-sv/feature_cloud_strings.xml index e232c7fa1..51ab10f45 100644 --- a/photopicker/res/values-sv/feature_cloud_strings.xml +++ b/photopicker/res/values-sv/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> av <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> är redo"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Det gick inte att läsa in vissa foton"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Försök igen senare. Dina foton blir tillgängliga när problemet har lösts."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-sv/feature_preview_strings.xml b/photopicker/res/values-sv/feature_preview_strings.xml index e9efd1145..ef502e520 100644 --- a/photopicker/res/values-sv/feature_preview_strings.xml +++ b/photopicker/res/values-sv/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Markera alla <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Avmarkera alla <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Förhandsgranska"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Det gick inte att spela upp videon"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Kontrollera internetanslutningen och försök igen"</string> diff --git a/photopicker/res/values-sw/core_strings.xml b/photopicker/res/values-sw/core_strings.xml index 129ff7e8a..e1c1b951d 100644 --- a/photopicker/res/values-sw/core_strings.xml +++ b/photopicker/res/values-sw/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Picha na video"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Maudhui"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Imechaguliwa"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Weka <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Nimemaliza"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Acha kuchagua zote"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Chagua hadi vipengee <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Picha"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albamu"</string> diff --git a/photopicker/res/values-sw/feature_cloud_strings.xml b/photopicker/res/values-sw/feature_cloud_strings.xml index 25c7bba58..c2f99bcfc 100644 --- a/photopicker/res/values-sw/feature_cloud_strings.xml +++ b/photopicker/res/values-sw/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> kati ya <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ziko tayari"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Imeshindwa kupakia baadhi ya Picha"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Jaribu tena baadaye. Picha zako zitapatikana tatizo hilo likitatuliwa."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-sw/feature_preview_strings.xml b/photopicker/res/values-sw/feature_preview_strings.xml index 7529edcfa..54cb31036 100644 --- a/photopicker/res/values-sw/feature_preview_strings.xml +++ b/photopicker/res/values-sw/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Chagua zote <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Acha kuchagua zote <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Kagua kwanza"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Tatizo limetokea wakati wa kucheza video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Angalia muunganisho wako wa intaneti kisha ujaribu tena"</string> diff --git a/photopicker/res/values-ta/core_strings.xml b/photopicker/res/values-ta/core_strings.xml index b1e7b8d05..d82993300 100644 --- a/photopicker/res/values-ta/core_strings.xml +++ b/photopicker/res/values-ta/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"படங்கள் & வீடியோக்கள்"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"முடிந்தது"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"அனைத்தையும் தேர்வு நீக்கும்"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ஆவணங்கள் வரை தேர்ந்தெடுங்கள்"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ஆல்பங்கள்"</string> diff --git a/photopicker/res/values-ta/feature_cloud_strings.xml b/photopicker/res/values-ta/feature_cloud_strings.xml index fe32c0af1..34293bdcd 100644 --- a/photopicker/res/values-ta/feature_cloud_strings.xml +++ b/photopicker/res/values-ta/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> / <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> தயாராக உள்ளது"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"சில படங்களை ஏற்ற முடியவில்லை"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"பிறகு மீண்டும் முயலவும். சிக்கல் சரிசெய்யப்பட்டதும் உங்கள் படங்கள் கிடைக்கும்."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ta/feature_preview_strings.xml b/photopicker/res/values-ta/feature_preview_strings.xml index 69527a4c7..2bb483f7c 100644 --- a/photopicker/res/values-ta/feature_preview_strings.xml +++ b/photopicker/res/values-ta/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>ஐயும் தேர்ந்தெடு"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>ஐயும் தேர்வுநீக்கு"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"மாதிரிக்காட்சி"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"வீடியோவைப் பிளே செய்வதில் சிக்கல்"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"இணைய இணைப்பைச் சரிபார்த்து மீண்டும் முயலவும்"</string> diff --git a/photopicker/res/values-te/core_strings.xml b/photopicker/res/values-te/core_strings.xml index 51e458c54..34fd3de00 100644 --- a/photopicker/res/values-te/core_strings.xml +++ b/photopicker/res/values-te/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"ఫోటోలు & వీడియోలు"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"పూర్తయింది"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"అన్నింటి ఎంపికను తొలగించండి"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"గరిష్ఠంగా <xliff:g id="COUNT">%1$s</xliff:g> ఐటెమ్లను ఎంచుకోండి"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ఫోటోలు"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ఆల్బమ్లు"</string> diff --git a/photopicker/res/values-te/feature_cloud_strings.xml b/photopicker/res/values-te/feature_cloud_strings.xml index ed2695662..665ee5861 100644 --- a/photopicker/res/values-te/feature_cloud_strings.xml +++ b/photopicker/res/values-te/feature_cloud_strings.xml @@ -21,4 +21,12 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>లో <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> సిద్ధంగా ఉన్నాయి"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"కొన్ని ఫోటోలను లోడ్ చేయడం సాధ్యపడదు"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"తర్వాత మళ్లీ ట్రై చేయండి. సమస్య పరిష్కరించబడిన తర్వాత మీ ఫోటోలు అందుబాటులో ఉంటాయి."</string> + <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"బ్యాకప్ చేసిన ఫోటోలు ఇప్పుడు జోడించబడ్డాయి"</string> + <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"మీరు <xliff:g id="APP_NAME">%1$s</xliff:g> ఖాతా <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> నుండి ఫోటోలను ఎంచుకోవచ్చు"</string> + <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ఖాతాను ఎంచుకోండి"</string> + <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> నుండి ఫోటోలను ఇక్కడ జోడించడానికి, యాప్లో ఖాతాను ఎంచుకోండి"</string> + <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ఖాతాను ఎంచుకోండి"</string> + <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"క్లౌడ్ మీడియా యాప్ను ఎంచుకోండి"</string> + <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"బ్యాకప్ చేసిన ఫోటోలను ఇక్కడజోడించడానికి, సెట్టింగ్లలో క్లౌడ్ మీడియా యాప్ను ఎంచుకోండి"</string> + <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"యాప్ను ఎంచుకోండి"</string> </resources> diff --git a/photopicker/res/values-te/feature_preview_strings.xml b/photopicker/res/values-te/feature_preview_strings.xml index 274aa56f5..12610c193 100644 --- a/photopicker/res/values-te/feature_preview_strings.xml +++ b/photopicker/res/values-te/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"మొత్తం <xliff:g id="COUNT">(%1$s)</xliff:g>ను ఎంచుకోండి"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"మొత్తం <xliff:g id="COUNT">(%1$s)</xliff:g> ఎంపిక రద్దు చేయండి"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ప్రివ్యూ చూడండి"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"వీడియోను ప్లే చేయడంలో సమస్య ఏర్పడింది"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"మీ ఇంటర్నెట్ కనెక్షన్ను చెక్ చేసి, మళ్లీ ట్రై చేయండి"</string> diff --git a/photopicker/res/values-th/core_strings.xml b/photopicker/res/values-th/core_strings.xml index 43b0c54ed..954db2fc5 100644 --- a/photopicker/res/values-th/core_strings.xml +++ b/photopicker/res/values-th/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"รูปภาพและวิดีโอ"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"เสร็จสิ้น"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ยกเลิกการเลือกทั้งหมด"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"เลือกได้สูงสุด <xliff:g id="COUNT">%1$s</xliff:g> รายการ"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"รูปภาพ"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"อัลบั้ม"</string> diff --git a/photopicker/res/values-th/feature_cloud_strings.xml b/photopicker/res/values-th/feature_cloud_strings.xml index 98a8c98d8..4e180d437 100644 --- a/photopicker/res/values-th/feature_cloud_strings.xml +++ b/photopicker/res/values-th/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"พร้อมแล้ว <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> จาก <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> รายการ"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"โหลดรูปภาพบางรูปไม่ได้"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"โปรดลองอีกครั้งในภายหลัง รูปภาพจะพร้อมใช้งานเมื่อปัญหาได้รับการแก้ไขแล้ว"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-th/feature_preview_strings.xml b/photopicker/res/values-th/feature_preview_strings.xml index 5ac9b2aec..f34bf4e27 100644 --- a/photopicker/res/values-th/feature_preview_strings.xml +++ b/photopicker/res/values-th/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"เลือกทั้งหมด <xliff:g id="COUNT">(%1$s)</xliff:g> รายการ"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ยกเลิกการเลือกทั้งหมด <xliff:g id="COUNT">(%1$s)</xliff:g> รายการ"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"ตัวอย่าง"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"เกิดปัญหาขณะเล่นวิดีโอ"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองอีกครั้ง"</string> diff --git a/photopicker/res/values-tl/core_strings.xml b/photopicker/res/values-tl/core_strings.xml index 7fddf2728..8331d5c72 100644 --- a/photopicker/res/values-tl/core_strings.xml +++ b/photopicker/res/values-tl/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Mga larawan at video"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Tapos na"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"I-deselect lahat"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pumili ng hanggang <xliff:g id="COUNT">%1$s</xliff:g> (na) item"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Mga Album"</string> diff --git a/photopicker/res/values-tl/feature_cloud_strings.xml b/photopicker/res/values-tl/feature_cloud_strings.xml index 1ac7661be..9126c7072 100644 --- a/photopicker/res/values-tl/feature_cloud_strings.xml +++ b/photopicker/res/values-tl/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Handa na ang <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sa <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Hindi ma-load ang ilang Larawan"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Subukan ulit sa ibang pagkakataon. Magiging available ang iyong mga larawan kapag nalutas na ang isyu."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-tl/feature_preview_strings.xml b/photopicker/res/values-tl/feature_preview_strings.xml index 7727d45d6..b742d610a 100644 --- a/photopicker/res/values-tl/feature_preview_strings.xml +++ b/photopicker/res/values-tl/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Piliin lahat <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"I-unselect lahat <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"I-preview"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Nagkaproblema sa pag-play ng video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Suriin ang iyong koneksyon sa internet at subukan ulit"</string> diff --git a/photopicker/res/values-tr/core_strings.xml b/photopicker/res/values-tr/core_strings.xml index aa8f17d9c..3fe96cb06 100644 --- a/photopicker/res/values-tr/core_strings.xml +++ b/photopicker/res/values-tr/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotoğraflar ve videolar"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Bitti"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tümünün seçimini kaldır"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"En fazla <xliff:g id="COUNT">%1$s</xliff:g> öğe seçin"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotoğraflar"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albümler"</string> diff --git a/photopicker/res/values-tr/feature_cloud_strings.xml b/photopicker/res/values-tr/feature_cloud_strings.xml index 9c2ea1487..7a42fb19e 100644 --- a/photopicker/res/values-tr/feature_cloud_strings.xml +++ b/photopicker/res/values-tr/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> adetten <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> adedi hazır"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Bazı fotoğraflar yüklenemiyor"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Daha sonra tekrar deneyin. Fotoğraflarınız, sorun çözüldükten sonra kullanılabilir."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-tr/feature_preview_strings.xml b/photopicker/res/values-tr/feature_preview_strings.xml index 5907c25af..24f094c55 100644 --- a/photopicker/res/values-tr/feature_preview_strings.xml +++ b/photopicker/res/values-tr/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Tümünü seç: <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tümünün seçimini kaldır: <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Önizle"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Video oynatılırken sorun oluştu"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"İnternet bağlantınızı kontrol edip tekrar deneyin"</string> diff --git a/photopicker/res/values-uk/core_strings.xml b/photopicker/res/values-uk/core_strings.xml index 784adf942..051142ce7 100644 --- a/photopicker/res/values-uk/core_strings.xml +++ b/photopicker/res/values-uk/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Фото й відео"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Не вибирати нічого"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Виберіть не більше стількох об’єктів: <xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фото"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбоми"</string> diff --git a/photopicker/res/values-uk/feature_cloud_strings.xml b/photopicker/res/values-uk/feature_cloud_strings.xml index 80b8c36e5..44db5caf0 100644 --- a/photopicker/res/values-uk/feature_cloud_strings.xml +++ b/photopicker/res/values-uk/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Готово: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> з <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Не вдається завантажити деякі фотографії"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Спробуйте пізніше. Ваші фотографії стануть доступними, коли проблему буде вирішено."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-uk/feature_preview_strings.xml b/photopicker/res/values-uk/feature_preview_strings.xml index 450d4dae7..5b607f9e7 100644 --- a/photopicker/res/values-uk/feature_preview_strings.xml +++ b/photopicker/res/values-uk/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Вибрати всі <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Скасувати вибір усіх <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Переглянути"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Проблема з відтворенням відео"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Перевірте інтернет-з’єднання й повторіть спробу"</string> diff --git a/photopicker/res/values-ur/core_strings.xml b/photopicker/res/values-ur/core_strings.xml index be14b82ba..00f84af70 100644 --- a/photopicker/res/values-ur/core_strings.xml +++ b/photopicker/res/values-ur/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"تصاویر اور ویڈیوز"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"ہو گیا"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"سبھی کو غیر منتخب کریں"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> آئٹمز تک منتخب کریں"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"تصاویر"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"البمز"</string> diff --git a/photopicker/res/values-ur/feature_cloud_strings.xml b/photopicker/res/values-ur/feature_cloud_strings.xml index 6bfe9ce03..a970d9860 100644 --- a/photopicker/res/values-ur/feature_cloud_strings.xml +++ b/photopicker/res/values-ur/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> میں سے <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> تیار ہیں"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"کچھ تصاویر لوڈ نہیں کی جا سکتیں"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"بعد میں دوبارہ کوشش کریں۔ مسئلہ حل ہو جانے کے بعد آپ کی تصاویر دستیاب ہوں گی۔"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-ur/feature_preview_strings.xml b/photopicker/res/values-ur/feature_preview_strings.xml index bd171e5d7..cb5cb5535 100644 --- a/photopicker/res/values-ur/feature_preview_strings.xml +++ b/photopicker/res/values-ur/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"سبھی <xliff:g id="COUNT">(%1$s)</xliff:g> کو منتخب کریں"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"سبھی <xliff:g id="COUNT">(%1$s)</xliff:g> کو غیر منتخب کریں"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"پیش منظر دیکھیں"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ویڈیو چلانے میں دشواری"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"اپنا انٹرنیٹ کنکشن چیک کریں اور دوبارہ کوشش کریں"</string> diff --git a/photopicker/res/values-uz/core_strings.xml b/photopicker/res/values-uz/core_strings.xml index f117db51f..39f4bdbc2 100644 --- a/photopicker/res/values-uz/core_strings.xml +++ b/photopicker/res/values-uz/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Suratlar va videolar"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Tanlangan"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ta qoʻshish"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Tayyor"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Hammasini bekor qilish"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> tagacha elementni tanlang"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albomlar"</string> diff --git a/photopicker/res/values-uz/feature_cloud_strings.xml b/photopicker/res/values-uz/feature_cloud_strings.xml index 95cadac6e..008b54df7 100644 --- a/photopicker/res/values-uz/feature_cloud_strings.xml +++ b/photopicker/res/values-uz/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> tayyor"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ayrim suratlar yuklanmadi"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Keyinroq qayta urining. Suratlaringiz muammo hal boʻlgandan keyin chiqadi."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-uz/feature_preview_strings.xml b/photopicker/res/values-uz/feature_preview_strings.xml index 36c0701ad..cd00be202 100644 --- a/photopicker/res/values-uz/feature_preview_strings.xml +++ b/photopicker/res/values-uz/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Hammasini tanlash (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Hammasini bekor qilish (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Razm solish"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Video ijrosida muammo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Internet aloqasini tekshiring va qayta urining"</string> diff --git a/photopicker/res/values-vi/core_strings.xml b/photopicker/res/values-vi/core_strings.xml index 4f6091aa9..f3268f183 100644 --- a/photopicker/res/values-vi/core_strings.xml +++ b/photopicker/res/values-vi/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Ảnh và video"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Xong"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Bỏ chọn tất cả"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Chọn tối đa <xliff:g id="COUNT">%1$s</xliff:g> mục"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Ảnh"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string> diff --git a/photopicker/res/values-vi/feature_cloud_strings.xml b/photopicker/res/values-vi/feature_cloud_strings.xml index 7b45f0818..a158512d2 100644 --- a/photopicker/res/values-vi/feature_cloud_strings.xml +++ b/photopicker/res/values-vi/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Đã sẵn sàng <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Không tải được một số ảnh"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Hãy thử lại sau. Ảnh của bạn sẽ xuất hiện sau khi vấn đề được giải quyết."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-vi/feature_preview_strings.xml b/photopicker/res/values-vi/feature_preview_strings.xml index 89ad8690c..0001cda4e 100644 --- a/photopicker/res/values-vi/feature_preview_strings.xml +++ b/photopicker/res/values-vi/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Chọn tất cả <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Bỏ chọn tất cả <xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Xem trước"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Sự cố khi phát video"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Hãy kiểm tra kết nối Internet rồi thử lại"</string> diff --git a/photopicker/res/values-zh-rCN/core_strings.xml b/photopicker/res/values-zh-rCN/core_strings.xml index a9a0dbe9e..00092d798 100644 --- a/photopicker/res/values-zh-rCN/core_strings.xml +++ b/photopicker/res/values-zh-rCN/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"照片和视频"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"完成"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"取消全选"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"最多选择 <xliff:g id="COUNT">%1$s</xliff:g> 项"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"照片"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"相册"</string> diff --git a/photopicker/res/values-zh-rCN/feature_cloud_strings.xml b/photopicker/res/values-zh-rCN/feature_cloud_strings.xml index 65f1de757..7e9d4ed9f 100644 --- a/photopicker/res/values-zh-rCN/feature_cloud_strings.xml +++ b/photopicker/res/values-zh-rCN/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 个已准备就绪,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 个"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"部分照片无法加载"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"请稍后再试。问题解决后,您的照片就可用了。"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-zh-rCN/feature_preview_strings.xml b/photopicker/res/values-zh-rCN/feature_preview_strings.xml index 1eb1dab07..1fe703281 100644 --- a/photopicker/res/values-zh-rCN/feature_preview_strings.xml +++ b/photopicker/res/values-zh-rCN/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"全选 (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"取消全选 (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"预览"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"播放视频时遇到问题"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"请检查互联网连接,然后重试"</string> diff --git a/photopicker/res/values-zh-rHK/core_strings.xml b/photopicker/res/values-zh-rHK/core_strings.xml index dd53edac9..283e170b9 100644 --- a/photopicker/res/values-zh-rHK/core_strings.xml +++ b/photopicker/res/values-zh-rHK/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"相片和影片"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"完成"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"全部取消揀"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"選取最多 <xliff:g id="COUNT">%1$s</xliff:g> 個項目"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"相片"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"相簿"</string> diff --git a/photopicker/res/values-zh-rHK/feature_cloud_strings.xml b/photopicker/res/values-zh-rHK/feature_cloud_strings.xml index 6c01305bc..98413f8eb 100644 --- a/photopicker/res/values-zh-rHK/feature_cloud_strings.xml +++ b/photopicker/res/values-zh-rHK/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 個項目已就緒,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 個"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"無法載入部分相片"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"請稍後再試。相片會在問題解決後顯示。"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-zh-rHK/feature_preview_strings.xml b/photopicker/res/values-zh-rHK/feature_preview_strings.xml index 25f364169..90d30335a 100644 --- a/photopicker/res/values-zh-rHK/feature_preview_strings.xml +++ b/photopicker/res/values-zh-rHK/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"選取全部共 <xliff:g id="COUNT">(%1$s)</xliff:g> 個"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"取消選取全部共 <xliff:g id="COUNT">(%1$s)</xliff:g> 個"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"預覽"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"播放影片時發生問題"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"請檢查你的互聯網連線,然後再試一次"</string> diff --git a/photopicker/res/values-zh-rTW/core_strings.xml b/photopicker/res/values-zh-rTW/core_strings.xml index 41863190f..66b6a1ed7 100644 --- a/photopicker/res/values-zh-rTW/core_strings.xml +++ b/photopicker/res/values-zh-rTW/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"相片和影片"</string> <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_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"完成"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"取消全選"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"最多可選取 <xliff:g id="COUNT">%1$s</xliff:g> 個項目"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"相片"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"相簿"</string> diff --git a/photopicker/res/values-zh-rTW/feature_cloud_strings.xml b/photopicker/res/values-zh-rTW/feature_cloud_strings.xml index dc411809b..2253df722 100644 --- a/photopicker/res/values-zh-rTW/feature_cloud_strings.xml +++ b/photopicker/res/values-zh-rTW/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"已備妥 <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 個項目,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 個項目"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"無法載入部分相片"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"請稍後再試。問題解決後即可存取相片。"</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-zh-rTW/feature_preview_strings.xml b/photopicker/res/values-zh-rTW/feature_preview_strings.xml index 98f089b1e..3b2894388 100644 --- a/photopicker/res/values-zh-rTW/feature_preview_strings.xml +++ b/photopicker/res/values-zh-rTW/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"選取全部 <xliff:g id="COUNT">(%1$s)</xliff:g> 個項目"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"取消選取全部 <xliff:g id="COUNT">(%1$s)</xliff:g> 個項目"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"預覽"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"播放影片時發生問題"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"請檢查網路連線,然後再試一次"</string> diff --git a/photopicker/res/values-zu/core_strings.xml b/photopicker/res/values-zu/core_strings.xml index ba7d74dde..2a3222028 100644 --- a/photopicker/res/values-zu/core_strings.xml +++ b/photopicker/res/values-zu/core_strings.xml @@ -20,11 +20,8 @@ <string name="photopicker_application_label" msgid="7272391964836190376">"Izithombe namavidiyo"</string> <string name="photopicker_media_item" msgid="3592234718212377636">"Imidiya"</string> <string name="photopicker_item_selected" msgid="3741045642641682375">"Okukhethiwe"</string> - <string name="photopicker_add_button_label" msgid="6805332693977632142">"Engeza i-<xliff:g id="COUNT">(%1$s)</xliff:g>"</string> - <!-- no translation found for photopicker_done_button_label (2641444126618862287) --> - <skip /> - <!-- no translation found for photopicker_clear_selection_button_description (8614016909754648208) --> - <skip /> + <string name="photopicker_done_button_label" msgid="2641444126618862287">"Kwenziwe"</string> + <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Ungakhethi konke"</string> <string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Khetha izinto ezifika kwezingu-<xliff:g id="COUNT">%1$s</xliff:g>"</string> <string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Izithombe"</string> <string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Ama-albhamu"</string> diff --git a/photopicker/res/values-zu/feature_cloud_strings.xml b/photopicker/res/values-zu/feature_cloud_strings.xml index fff028785..06043499c 100644 --- a/photopicker/res/values-zu/feature_cloud_strings.xml +++ b/photopicker/res/values-zu/feature_cloud_strings.xml @@ -21,4 +21,20 @@ <string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"U-<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> wokungu-<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ulungile"</string> <string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ayikwazi ukulayisha ezinye Izithombe"</string> <string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Zama futhi emuva kwesikhathi. Izithombe zakho zizotholakala uma inkinga isixazululiwe."</string> + <!-- no translation found for photopicker_banner_cloud_media_available_title (3630564281302679941) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_media_available_message (6224134038092008505) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_title (286679907089224434) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_message (5173210485511611652) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_account_button (3407910358624445230) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_title (992613053341538886) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_provider_message (1102889303996108506) --> + <skip /> + <!-- no translation found for photopicker_banner_cloud_choose_app_button (8228365266860220123) --> + <skip /> </resources> diff --git a/photopicker/res/values-zu/feature_preview_strings.xml b/photopicker/res/values-zu/feature_preview_strings.xml index 8eb8d9696..d60eb0647 100644 --- a/photopicker/res/values-zu/feature_preview_strings.xml +++ b/photopicker/res/values-zu/feature_preview_strings.xml @@ -17,8 +17,8 @@ <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_select_button_label" msgid="658102027531907034">"Khetha konke okungu-<xliff:g id="COUNT">(%1$s)</xliff:g>"</string> + <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Yekisa ukukhetha konke okungu-<xliff:g id="COUNT">(%1$s)</xliff:g>"</string> <string name="photopicker_preview_button_label" msgid="3567318300811305531">"Ukuhlola kuqala"</string> <string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Inkinga yokudlala ividiyo"</string> <string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Hlola ukuxhumeka kwe-inthanethi kwakho uphinde uzame futhi"</string> diff --git a/photopicker/res/values/core_strings.xml b/photopicker/res/values/core_strings.xml index c84777f1d..9fb3cfef5 100644 --- a/photopicker/res/values/core_strings.xml +++ b/photopicker/res/values/core_strings.xml @@ -46,6 +46,9 @@ <!-- Empty state message when the photo grid has no photos --> <string name="photopicker_photos_empty_state_body" translation_description="Message shown to the user when no photos are able to be shown underneath the primary message title">Start capturing photos and videos</string> + <!-- Empty state title when the camera album has no photos --> + <string name="photopicker_camera_empty_state_body" translation_description="Title of the message shown to the user when there are no photos in the camera album">Photos and videos captured by your camera app will appear here</string> + <!-- Empty state title when the favorites album has no photos --> <string name="photopicker_favorites_empty_state_title" translation_description="Title of the message shown to the user when there are no favorite photos to show">No favorites yet</string> diff --git a/photopicker/src/com/android/photopicker/MainActivity.kt b/photopicker/src/com/android/photopicker/MainActivity.kt index cf658c0c7..9e1b66113 100644 --- a/photopicker/src/com/android/photopicker/MainActivity.kt +++ b/photopicker/src/com/android/photopicker/MainActivity.kt @@ -28,6 +28,7 @@ import android.os.UserHandle import android.provider.MediaStore import android.util.Log import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -47,17 +48,27 @@ import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.IllegalIntentExtraException import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.Events import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.PhotopickerEventLogger +import com.android.photopicker.core.events.Telemetry import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager +import com.android.photopicker.core.navigation.PhotopickerDestinations import com.android.photopicker.core.selection.LocalSelection import com.android.photopicker.core.selection.Selection +import com.android.photopicker.core.theme.AccentColorHelper import com.android.photopicker.core.theme.PhotopickerTheme +import com.android.photopicker.core.user.UserMonitor +import com.android.photopicker.core.user.UserProfile import com.android.photopicker.data.DataService import com.android.photopicker.data.model.Media +import com.android.photopicker.data.model.MediaSource import com.android.photopicker.extensions.canHandleGetContentIntentMimeTypes +import com.android.photopicker.extensions.getUserProfilesVisibleToPhotopicker import com.android.photopicker.features.cloudmedia.CloudMediaFeature import dagger.Lazy import dagger.hilt.android.AndroidEntryPoint @@ -65,8 +76,11 @@ import dagger.hilt.android.scopes.ActivityRetainedScoped import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -88,6 +102,7 @@ class MainActivity : Hilt_MainActivity() { // on the ConfigurationManager. @Inject @ActivityRetainedScoped lateinit var featureManager: Lazy<FeatureManager> @Inject @Background lateinit var background: CoroutineDispatcher + @Inject lateinit var userMonitor: UserMonitor // Events requires the feature manager, so initialize this lazily until the action is set. @Inject lateinit var events: Lazy<Events> @@ -97,13 +112,27 @@ class MainActivity : Hilt_MainActivity() { } /** + * Keeps track of the result set for the calling activity that launched the photopicker for + * logging purposes + */ + private var activityResultSet = 0 + + /** + * Keeps track of whether or not the picker was closed by using the standard android back + * gesture instead of the picker bottom sheet swipe down + */ + private var isPickerClosedByBackGesture = false + + private lateinit var photopickerEventLogger: PhotopickerEventLogger + + /** * A flow used to trigger the preloader. When media is ready to be preloaded it should be * provided to the preloader by emitting into this flow. * * The main activity should create a new [_preloadDeferred] before emitting, and then monitor * that deferred to obtain the result of the preload operation that this flow will trigger. */ - val preloadMedia: MutableSharedFlow<Set<Media>> = MutableSharedFlow() + private val preloadMedia: MutableSharedFlow<Set<Media>> = MutableSharedFlow() /** * A deferred which tracks the current state of any preload operation requested by the main @@ -115,11 +144,24 @@ class MainActivity : Hilt_MainActivity() { * Public access to the deferred, behind a getter. (To ensure any access to this property always * obtains the latest value) */ - public val preloadDeferred: CompletableDeferred<Boolean> + val preloadDeferred: CompletableDeferred<Boolean> get() { return _preloadDeferred } + /** + * A top level flow that listens for disruptive data events from the [DataService]. This flow + * will emit when the DataService detects that its data is inaccurate or stale and will be used + * to force refresh the UI and navigate the user back to the start destination. + */ + private val disruptiveDataNotification: Flow<Int> by lazy { + dataService.get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold(initial = 0) { + prev, + _ -> + prev + 1 + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -145,6 +187,7 @@ class MainActivity : Hilt_MainActivity() { // configuration, then cancel the activity and close. Log.e(TAG, "Unable to start Photopicker with illegal configuration", exception) setResult(RESULT_CANCELED) + activityResultSet = RESULT_CANCELED finish() } @@ -154,6 +197,10 @@ class MainActivity : Hilt_MainActivity() { // Begin listening for events before starting the UI. listenForEvents() + // Picker event logger starts listening for events dispatched throughout the app + photopickerEventLogger = PhotopickerEventLogger(dataService) + photopickerEventLogger.start(lifecycleScope, background, events.get()) + /* * In single select sessions, the activity needs to end after a media object is selected, * so register a listener to the selection so the activity can handle calling @@ -178,7 +225,6 @@ class MainActivity : Hilt_MainActivity() { PhotopickerTheme(config = photopickerConfiguration) { PhotopickerAppWithBottomSheet( onDismissRequest = ::finish, - bannerManager = bannerManager.get(), onMediaSelectionConfirmed = { lifecycleScope.launch { // Move the work off the UI dispatcher. @@ -186,11 +232,24 @@ class MainActivity : Hilt_MainActivity() { } }, preloadMedia = preloadMedia, - obtainPreloaderDeferred = { preloadDeferred } + obtainPreloaderDeferred = { preloadDeferred }, + disruptiveDataNotification, ) } } } + // Check if the picker was closed by the back gesture instead of simply swiping it down + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + isPickerClosedByBackGesture = true + } + } + ) + + // Log the picker launch details + reportPhotopickerApiInfo() } override fun onResume() { @@ -209,6 +268,96 @@ class MainActivity : Hilt_MainActivity() { } } + /** Dispatches an event to log all details with which the photopicker launched */ + private fun reportPhotopickerApiInfo() { + val intent = getIntent() + val dispatcherToken = FeatureToken.CORE.token + val sessionId = configurationManager.configuration.value.sessionId + val intentAction = + when (intent.action) { + MediaStore.ACTION_PICK_IMAGES -> Telemetry.PickerIntentAction.ACTION_PICK_IMAGES + Intent.ACTION_GET_CONTENT -> Telemetry.PickerIntentAction.ACTION_GET_CONTENT + MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP -> + Telemetry.PickerIntentAction.ACTION_USER_SELECT + else -> Telemetry.PickerIntentAction.UNSET_PICKER_INTENT_ACTION + } + // We always launch the picker in collapsed state. We track change in the picker bottom + // sheet as UI event + val pickerSize = Telemetry.PickerSize.COLLAPSED + val mediaFilters = configurationManager.configuration.value.mimeTypes + val pickItemsMax = configurationManager.configuration.value.selectionLimit + val pickerConfig = configurationManager.configuration.value + val launchTab = configurationManager.configuration.value.startDestination + val selectedTab = + when (launchTab) { + PhotopickerDestinations.PHOTO_GRID -> Telemetry.SelectedTab.PHOTOS + PhotopickerDestinations.ALBUM_GRID -> Telemetry.SelectedTab.ALBUMS + else -> Telemetry.SelectedTab.UNSET_SELECTED_TAB + } + + val selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM + val isOrderedSelectionSet = pickerConfig.pickImagesInOrder + // TODO Creating a new instance of AccentColorHelper() to check color seems unnecessary. + // Fix later + val isAccentColorSet = AccentColorHelper.withIntent(intent).isValidAccentColorSet() + val isLaunchTabSet = pickerConfig.startDestination != PhotopickerDestinations.DEFAULT + // TODO Update when search is added + val isSearchEnabled = false + var mediaFilter = Telemetry.MediaType.UNSET_MEDIA_TYPE + if (mediaFilters.size > 1) { + for (filter in mediaFilters) { + if (filter.contains("image") && filter.contains("video")) { + mediaFilter = Telemetry.MediaType.PHOTO_VIDEO + } else if (filter.startsWith("image/")) { + mediaFilter = Telemetry.MediaType.PHOTO + } else if (filter.startsWith("video/")) { + mediaFilter = Telemetry.MediaType.VIDEO + } + lifecycleScope.launch { + events + .get() + .dispatch( + Event.ReportPhotopickerApiInfo( + dispatcherToken, + sessionId, + intentAction, + pickerSize, + mediaFilter, + pickItemsMax, + selectedTab, + selectedAlbum, + isOrderedSelectionSet, + isAccentColorSet, + isLaunchTabSet, + isSearchEnabled + ) + ) + } + } + } else { + lifecycleScope.launch { + events + .get() + .dispatch( + Event.ReportPhotopickerApiInfo( + dispatcherToken, + sessionId, + intentAction, + pickerSize, + mediaFilter, + pickItemsMax, + selectedTab, + selectedAlbum, + isOrderedSelectionSet, + isAccentColorSet, + isLaunchTabSet, + isSearchEnabled + ) + ) + } + } + } + /** * A collector that starts when Photopicker is running in single-select mode. This collector * will trigger [onMediaSelectionConfirmed] when the first (and only) item is selected. @@ -241,6 +390,90 @@ class MainActivity : Hilt_MainActivity() { } } + override fun finish() { + reportSessionInfo() + super.finish() + } + + /** Dispatches an event to log all the final state details of the picker */ + private fun reportSessionInfo() { + val configuration = configurationManager.configuration.value + val pickerSelection = + if (configuration.selectionLimit == 1) { + Telemetry.PickerSelection.SINGLE + } else { + Telemetry.PickerSelection.MULTIPLE + } + val cloudProviderUid = + dataService + .get() + .availableProviders + .value + .filter { provider -> provider.mediaSource == MediaSource.REMOTE } + .firstOrNull() + ?.uid ?: -1 + val userProfileType = userMonitor.userStatus.value.activeUserProfile.profileType + val currentActiveProfile = + when (userProfileType) { + UserProfile.ProfileType.PRIMARY -> Telemetry.UserProfile.PERSONAL + UserProfile.ProfileType.MANAGED -> Telemetry.UserProfile.WORK + else -> Telemetry.UserProfile.UNKNOWN + } + val pickedMediaItemsSet = selection.get().flow.value + val pickerStatus = + if (activityResultSet == RESULT_CANCELED) { + Telemetry.PickerStatus.CANCELED + } else { + Telemetry.PickerStatus.CONFIRMED + } + val pickedItemsCount = pickedMediaItemsSet.size + var pickedItemsSize = 0 + for (mediaItem in pickedMediaItemsSet) { + pickedItemsSize += mediaItem.sizeInBytes.toInt() + } + val pickerMode = + when { + configuration.action.equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) -> + Telemetry.PickerMode.PERMISSION_MODE_PICKER + configuration.runtimeEnv.equals(PhotopickerRuntimeEnv.ACTIVITY) -> + Telemetry.PickerMode.REGULAR_PICKER + configuration.runtimeEnv.equals(PhotopickerRuntimeEnv.EMBEDDED) -> + Telemetry.PickerMode.EMBEDDED_PICKER + else -> Telemetry.PickerMode.UNSET_PICKER_MODE + } + val pickerCloseMethod = + if (isPickerClosedByBackGesture) { + Telemetry.PickerCloseMethod.BACK_BUTTON + } else if (pickerStatus == Telemetry.PickerStatus.CONFIRMED) { + Telemetry.PickerCloseMethod.SELECTION_CONFIRMED + } else { + Telemetry.PickerCloseMethod.SWIPE_DOWN + } + + lifecycleScope.launch { + val profileSwitchButtonVisible = + userMonitor.userStatus.getUserProfilesVisibleToPhotopicker().first().size > 1 + events + .get() + .dispatch( + Event.ReportPhotopickerSessionInfo( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + pickerSelection, + cloudProviderUid, + currentActiveProfile, + pickerStatus, + pickedItemsCount, + pickedItemsSize, + profileSwitchButtonVisible, + pickerMode, + pickerCloseMethod + ) + ) + } + } + /** * Sets the caller related fields in [PhotopickerConfiguration] with the calling application's * information, if available. This should only be called once and will cause a configuration @@ -427,6 +660,7 @@ class MainActivity : Hilt_MainActivity() { } else { // The selection is empty, and there is no data to return to the caller. setResult(RESULT_CANCELED) + activityResultSet = RESULT_CANCELED return } @@ -434,6 +668,57 @@ class MainActivity : Hilt_MainActivity() { resultData.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) setResult(RESULT_OK, resultData) + activityResultSet = RESULT_OK + dispatchSelectedMediaItemsStatusEvent(selection) + } + + /** Dispatches an Event to log details of all the picked media items */ + private fun dispatchSelectedMediaItemsStatusEvent(selection: Set<Media>) { + val sessionId = configurationManager.configuration.value.sessionId + val mediaStatus = Telemetry.MediaStatus.SELECTED + + for (mediaItem in selection) { + // TODO Update the media item position here once the Media class holds the resultIndex + // property: b/342555096 + val itemPosition = 0 + val mimeType = mediaItem.mimeType + // TODO find live photo format + val mediaType = + if (mimeType.startsWith("image/")) { + if (mimeType.contains("gif")) { + Telemetry.MediaType.GIF + } else { + Telemetry.MediaType.PHOTO + } + } else if (mimeType.startsWith("video/")) { + Telemetry.MediaType.VIDEO + } else { + Telemetry.MediaType.OTHER + } + + val cloudOnly = mediaItem.mediaSource == MediaSource.REMOTE + // TODO Keeping for now while the field still exists in the actual atom to prevent the + // picker from crashing on selection with a null value + val pickerSize = Telemetry.PickerSize.EXPANDED + lifecycleScope.launch { + events + .get() + .dispatch( + Event.ReportPhotopickerMediaItemStatus( + FeatureToken.CORE.token, + sessionId, + mediaStatus, + mediaItem.selectionSource + ?: Telemetry.MediaLocation.UNSET_MEDIA_LOCATION, + itemPosition, + mediaItem.mediaItemAlbum, + mediaType, + cloudOnly, + pickerSize + ) + ) + } + } } /** @@ -469,6 +754,7 @@ class MainActivity : Hilt_MainActivity() { // No need to send any data back to the PermissionController, just send an OK signal // back to indicate the MediaGrants are available. setResult(RESULT_OK) + activityResultSet = RESULT_OK } /** diff --git a/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt b/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt index 5236df2ab..1b0120f14 100644 --- a/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt +++ b/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt @@ -40,7 +40,8 @@ class PhotopickerDeviceConfigReceiver : BroadcastReceiver() { companion object { const val TAG = "PhotopickerDeviceConfigReceiver" /** A list of activities that need to be enabled or disabled based on flag state. */ - val activities = listOf("MainActivity", "PhotopickerGetContentActivity") + val activities = listOf("MainActivity", "PhotopickerGetContentActivity", + "PhotopickerUserSelectActivity") } override fun onReceive(context: Context, intent: Intent) { diff --git a/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt b/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt index fab6a63a9..374fbacc0 100644 --- a/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt +++ b/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt @@ -16,7 +16,7 @@ package com.android.photopicker.core -import androidx.compose.animation.animateContentSize +import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -38,6 +38,7 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -50,15 +51,21 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController -import com.android.photopicker.core.banners.Banner -import com.android.photopicker.core.banners.BannerDefinitions -import com.android.photopicker.core.banners.BannerManager +import com.android.modules.utils.build.SdkLevel +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.embedded.LocalEmbeddedState +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.features.Location import com.android.photopicker.core.features.LocationParams import com.android.photopicker.core.navigation.LocalNavController import com.android.photopicker.core.navigation.PhotopickerNavGraph import com.android.photopicker.data.model.Media +import com.android.photopicker.extensions.transferTouchesToHostInEmbedded import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -69,7 +76,7 @@ private val MEASUREMENT_BANNER_PADDING = /* Spacing around the selection bar and the edges of the screen */ private val SELECTION_BAR_PADDING = - PaddingValues(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 64.dp) + PaddingValues(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 48.dp) /** * This is an entrypoint of the Photopicker Compose UI. This is called from the MainActivity and is @@ -77,19 +84,29 @@ private val SELECTION_BAR_PADDING = * an Activity's [setContent] block. * * @param onDismissRequest handler for when the BottomSheet is dismissed. + * @param onMediaSelectionConfirmed A callback to pass to the [Location.SELECTION_BAR] to indicate + * the user has indicated the media selection is final. + * @param preloadMedia A flow of Media that the [MEDIA_PRELOADER] should begin preloading. + * @param obtainPreloaderDeferred A callback to obtain a deferred for the currently requested media + * preload. + * @param disruptiveDataNotification The data disruption flow that emits when the underlying data + * the UI has been created with is invalid */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun PhotopickerAppWithBottomSheet( onDismissRequest: () -> Unit, - bannerManager: BannerManager, onMediaSelectionConfirmed: () -> Unit, preloadMedia: Flow<Set<Media>>, obtainPreloaderDeferred: () -> CompletableDeferred<Boolean>, + disruptiveDataNotification: Flow<Int>, ) { // Initialize and remember the NavController. This needs to be provided before the call to // the NavigationGraph, so this is done at the top. val navController = rememberNavController() + val scope = rememberCoroutineScope() + val events = LocalEvents.current + val configuration = LocalPhotopickerConfiguration.current val state = rememberBottomSheetScaffoldState( @@ -100,7 +117,29 @@ fun PhotopickerAppWithBottomSheet( when (sheetValue) { // When the sheet is hidden, trigger the onDismissRequest SheetValue.Hidden -> onDismissRequest() - else -> {} + // Log picker state change + SheetValue.Expanded -> + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.EXPAND_PICKER + ) + ) + } + SheetValue.PartiallyExpanded -> + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.COLLAPSE_PICKER + ) + ) + } } true // allow all value changes }, @@ -115,7 +154,9 @@ fun PhotopickerAppWithBottomSheet( val sheetPeekHeight = remember(localConfig) { (localConfig.screenHeightDp * .75).dp } // Provide the NavController to the rest of the Compose stack. - CompositionLocalProvider(LocalNavController provides navController) { + CompositionLocalProvider( + LocalNavController provides navController, + ) { Column( modifier = // Apply WindowInsets to this wrapping column to prevent the Bottom Sheet @@ -135,7 +176,7 @@ fun PhotopickerAppWithBottomSheet( modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.BottomCenter ) { - PhotopickerMain(bannerManager) + PhotopickerMain(disruptiveDataNotification) Column( modifier = // Some elements needs to be drawn over the UI inside of the @@ -185,26 +226,44 @@ fun PhotopickerAppWithBottomSheet( } /** - * This is an entrypoint of the Photopicker Compose UI. This is called from a hosting View and is + * This is an entry point of the Photopicker Compose UI. This is called from a hosting View and is * the top-most [@Composable] in the view based application. This should not be called by any * Activity code, and should only be called inside of the ComposeView [setContent] block. + * + * @param disruptiveDataNotification The data disruption flow that emits when the underlying data + * the UI has been created with is invalid */ -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun PhotopickerApp(bannerManager: BannerManager) { +fun PhotopickerApp(disruptiveDataNotification: Flow<Int>) { // Initialize and remember the NavController. This needs to be provided before the call to // the NavigationGraph, so this is done at the top. val navController = rememberNavController() // Provide the NavController to the rest of the Compose stack. CompositionLocalProvider(LocalNavController provides navController) { - PhotopickerMain(bannerManager) + Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.BottomCenter) { + PhotopickerMain(disruptiveDataNotification) + Column { + LocalFeatureManager.current.composeLocation( + Location.SNACK_BAR, + maxSlots = 1, + ) + hideWhenState(StateSelector.EmbeddedAndCollapsed) { + LocalFeatureManager.current.composeLocation( + Location.SELECTION_BAR, + maxSlots = 1, + modifier = Modifier.padding(SELECTION_BAR_PADDING), + params = LocationParams.None + ) + } + } + } } } /** - * This is the shared entrypoint for the Photopicker compose-UI. Composables above this function - * must provide the required dependencies to the compose UI before calling this entrypoint. + * This is the shared entry point for the Photopicker compose-UI. Composables above this function + * must provide the required dependencies to the compose UI before calling this entry point. * * It is presumed after this composable the compose UI can either be running inside of a wrapped * View or an Activity lifecycle. @@ -216,38 +275,41 @@ fun PhotopickerApp(bannerManager: BannerManager) { * - LocalPhotopickerConfiguration * - LocalSelection * - PhotopickerTheme + * + * @param disruptiveDataNotification The data disruption flow that emits when the underlying data + * the UI has been created with is invalid */ @Composable -fun PhotopickerMain(bannerManager: BannerManager) { - - val currentBanner by bannerManager.flow.collectAsStateWithLifecycle() - val scope = rememberCoroutineScope() +fun PhotopickerMain(disruptiveDataNotification: Flow<Int>) { + // Collect the data disrupt flow so that Photopicker will navigate on disruptive data changes. + // The data service can detect when the providers that are supplying grid data have changed + // in such a way that the grid should immediately be cleared as the new list of providers + // does not include the providers that have populated the currently displayed view. When + // this DisruptionSignal is sent, we collect the value here to force recomposition to rebuild + // the view immediately. + val disruptCounter by disruptiveDataNotification.collectAsStateWithLifecycle(initialValue = 0) + watchForDataDisruptions(disruptCounter) + val isEmbedded = + LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED + val host = LocalEmbeddedState.current?.host Box(modifier = Modifier.fillMaxSize()) { Column { // The navigation bar and banners are drawn above the navigation graph - LocalFeatureManager.current.composeLocation( - Location.NAVIGATION_BAR, - maxSlots = 1, - modifier = Modifier.fillMaxWidth() - ) - - Box(modifier = Modifier.animateContentSize()) { - currentBanner?.let { - Banner( - it, - modifier = Modifier.padding(MEASUREMENT_BANNER_PADDING), - onDismiss = { - scope.launch { - val declaration = it.declaration - if (declaration is BannerDefinitions) { - bannerManager.markBannerAsDismissed(declaration) - } - bannerManager.refreshBanners() - } + hideWhenState(selector = StateSelector.EmbeddedAndCollapsed) { + LocalFeatureManager.current.composeLocation( + Location.NAVIGATION_BAR, + maxSlots = 1, + modifier = + if (SdkLevel.isAtLeastU() && isEmbedded && host != null) { + Modifier.fillMaxWidth() + .transferTouchesToHostInEmbedded( + host = host, + ) + } else { + Modifier.fillMaxWidth() } - ) - } + ) } // Initialize the navigation graph. @@ -255,3 +317,55 @@ fun PhotopickerMain(bannerManager: BannerManager) { } } } + +/** + * Attaches a [LaunchedEffect] to the compose hierarchy that runs whenever the disruptionCounter is + * changed. This will attempt to navigate the session back to the navigation graph's starting route + * since a Data Disruption means that the current view is unstable and likely stale / obsolete. + * + * To prevent showing data that is irrelevant to the user in a route that may no longer exist (i.e + * inside of an Album in a provider that is no longer attached), the session is force-navigated to + * the main route. + * + * @param disruptionCounter the current disruptionCounter value from the data service. + */ +@Composable +private fun watchForDataDisruptions(disruptionCounter: Int) { + + val navController = LocalNavController.current + LaunchedEffect(disruptionCounter) { + if (disruptionCounter > 0) { + Log.d("Photopicker", "DisruptiveData notification received.") + + try { + val startDestination = + checkNotNull(navController.graph.startDestinationRoute) { + "startDestination was Null" + } + if (navController.currentBackStackEntry?.destination?.route != startDestination) { + + // Try to return to the start destination for the data reload, by attempting to + // move backwards via the backstack. + val navigated = + navController.popBackStack( + startDestination, + /* inclusive= */ false, + /* saveState = */ false, + ) + + // The start route is not on the backstack, so as a last resort, navigate + // directly. + if (!navigated) { + navController.navigate(startDestination, /* navOptions= */ null) + } + } + } catch (e: IllegalStateException) { + Log.e( + "Photopicker", + "disruptiveDataNotification was received, but unable to resolve the graph.", + e + ) + } + } + } +} diff --git a/photopicker/src/com/android/photopicker/core/banners/Banner.kt b/photopicker/src/com/android/photopicker/core/banners/Banner.kt index 2a59ee91e..9b3fbaefb 100644 --- a/photopicker/src/com/android/photopicker/core/banners/Banner.kt +++ b/photopicker/src/com/android/photopicker/core/banners/Banner.kt @@ -30,6 +30,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -37,6 +39,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.photopicker.R +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry.BannerType +import com.android.photopicker.core.events.Telemetry.UserBannerInteraction +import com.android.photopicker.core.features.FeatureToken.CORE +import kotlinx.coroutines.launch /** * Object interface to generate a banner element for the UI. This abstracts the appearance of the @@ -123,6 +132,9 @@ fun Banner( onDismiss: () -> Unit = {}, ) { + val config = LocalPhotopickerConfiguration.current + val events = LocalEvents.current + Card( // Consume the incoming modifier for positioning the banner. modifier = modifier, @@ -170,6 +182,8 @@ fun Banner( // The action Row, which sometimes may be empty if the banner is not dismissable and // does not provide its own Action if (banner.declaration.dismissable || banner.actionLabel() != null) { + val scope = rememberCoroutineScope() + Row( horizontalArrangement = Arrangement.spacedBy(MEASUREMENT_BANNER_BUTTON_ROW_SPACING), @@ -183,7 +197,23 @@ fun Banner( banner.actionLabel()?.let { val context = LocalContext.current TextButton( - onClick = { banner.onAction(context) }, + onClick = { + scope.launch { + events.dispatch( + Event.LogPhotopickerBannerInteraction( + dispatcherToken = CORE.token, + sessionId = config.sessionId, + bannerType = + BannerType.fromBannerDeclaration( + banner.declaration + ), + userInteraction = + UserBannerInteraction.CLICK_BANNER_ACTION_BUTTON + ) + ) + } + banner.onAction(context) + }, ) { Text(it) } @@ -194,7 +224,25 @@ fun Banner( // clicked is up to the caller. A core string is used here to ensure consistency // between banners. if (banner.declaration.dismissable) { - TextButton(onClick = onDismiss) { + TextButton( + onClick = { + scope.launch { + events.dispatch( + Event.LogPhotopickerBannerInteraction( + dispatcherToken = CORE.token, + sessionId = config.sessionId, + bannerType = + BannerType.fromBannerDeclaration( + banner.declaration + ), + userInteraction = + UserBannerInteraction.CLICK_BANNER_DISMISS_BUTTON + ) + ) + } + onDismiss() + } + ) { Text(stringResource(R.string.photopicker_dismiss_banner_button_label)) } } @@ -202,4 +250,17 @@ fun Banner( } } } + + // Add a log that the banner was shown. + LaunchedEffect(banner) { + events.dispatch( + Event.LogPhotopickerBannerInteraction( + dispatcherToken = CORE.token, + sessionId = config.sessionId, + bannerType = BannerType.fromBannerDeclaration(banner.declaration), + // TODO(b/357010907): Add banner shown interaction when the atom exists. + userInteraction = UserBannerInteraction.UNSET_BANNER_INTERACTION + ) + ) + } } diff --git a/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt b/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt index 912cf0fa7..60ccab1eb 100644 --- a/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt +++ b/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt @@ -50,7 +50,6 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Gif import androidx.compose.material.icons.filled.MotionPhotosOn import androidx.compose.material.icons.filled.PlayCircle @@ -64,6 +63,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -72,6 +72,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.onLongClick import androidx.compose.ui.semantics.semantics @@ -82,9 +83,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import com.android.modules.utils.build.SdkLevel import com.android.photopicker.R import com.android.photopicker.core.components.MediaGridItem.Companion.defaultBuildContentType import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.embedded.LocalEmbeddedState import com.android.photopicker.core.glide.Resolution import com.android.photopicker.core.glide.loadMedia import com.android.photopicker.core.theme.CustomAccentColorScheme @@ -94,6 +98,7 @@ import com.android.photopicker.extensions.circleBackground import com.android.photopicker.extensions.insertMonthSeparators import com.android.photopicker.extensions.toMediaGridItemFromAlbum import com.android.photopicker.extensions.toMediaGridItemFromMedia +import com.android.photopicker.extensions.transferGridTouchesToHostInEmbedded import java.text.NumberFormat /** The number of grid cells per row for Phone / narrow layouts */ @@ -148,7 +153,7 @@ val MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_PADDING = 16.dp val MEASUREMENT_DEFAULT_ALBUM_BOTTOM_PADDING = 16.dp /** Size of the spacer between the album icon and the album display label */ -val MEASUREMENT_DEFAULT_ALBUM_LABEL_SPACER_SIZE = 8.dp +val MEASUREMENT_DEFAULT_ALBUM_LABEL_SPACER_SIZE = 12.dp /** * Composable for creating a MediaItemGrid from a [PagingData] source of data that implements @@ -201,7 +206,7 @@ fun mediaGrid( item: MediaGridItem, isSelected: Boolean, onClick: ((item: MediaGridItem) -> Unit)?, - onLongPress: ((item: MediaGridItem) -> Unit)?, + onLongPress: ((item: MediaGridItem) -> Unit)? ) -> Unit = { item, isSelected, onClick, onLongPress, -> @@ -221,16 +226,46 @@ fun mediaGrid( contentSeparatorFactory: @Composable (item: MediaGridItem.SeparatorItem) -> Unit = { item -> defaultBuildSeparator(item) }, + bannerContent: (@Composable () -> Unit)? = null, ) { + // To know whether the request in coming from Embedded or PhotoPicker + val isEmbedded = + LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED + val host = LocalEmbeddedState.current?.host + /** + * Bottom sheet current state in runtime Embedded Photopicker. This assignment is necessary to + * get the regular updates of bottom sheet current state inside [LazyVerticalGrid] + */ + val isExpanded = rememberUpdatedState(LocalEmbeddedState.current?.isExpanded ?: false) LazyVerticalGrid( columns = columns, - modifier = modifier, + modifier = + if (SdkLevel.isAtLeastU() && isEmbedded && host != null) { + modifier.transferGridTouchesToHostInEmbedded(state, isExpanded, host) + } else { + modifier + }, state = state, contentPadding = contentPadding, userScrollEnabled = userScrollEnabled, horizontalArrangement = Arrangement.spacedBy(gridCellPadding), verticalArrangement = Arrangement.spacedBy(gridCellPadding), ) { + + // If banner content was passed add it to the grid as a full span item + // so that it appears inside the scroll container. + bannerContent?.let { + item( + span = { + if (isExpandedScreen) GridItemSpan(CELLS_PER_ROW_EXPANDED) + else GridItemSpan(CELLS_PER_ROW) + } + ) { + it() + } + } + + // Add the media items from the LazyPagingItems items( count = items.itemCount, key = { index -> MediaGridItem.keyFactory(items.peek(index), index) }, @@ -477,7 +512,7 @@ private fun SelectedIconOverlay(isSelected: Boolean, selectedIndex: Int) { } false -> Icon( - Icons.Filled.CheckCircle, + ImageVector.vectorResource(R.drawable.photopicker_selected_media), modifier = Modifier // Background is necessary because the icon has negative @@ -487,7 +522,7 @@ private fun SelectedIconOverlay(isSelected: Boolean, selectedIndex: Int) { // the image. .border( MEASUREMENT_SELECTED_ICON_BORDER, - MaterialTheme.colorScheme.surfaceVariant, + MaterialTheme.colorScheme.surfaceContainerHighest, CircleShape ), contentDescription = stringResource(R.string.photopicker_item_selected), @@ -580,7 +615,7 @@ private fun defaultBuildAlbumItem( @Composable private fun defaultBuildSeparator(item: MediaGridItem.SeparatorItem) { Box(Modifier.padding(MEASUREMENT_SEPARATOR_PADDING).semantics(mergeDescendants = true) {}) { - Text(item.label) + Text(item.label, style = MaterialTheme.typography.titleSmall) } } diff --git a/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt b/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt index e6c810b1f..4d86c22ad 100644 --- a/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt +++ b/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt @@ -17,8 +17,11 @@ package com.android.photopicker.core.configuration import android.content.Intent +import android.os.Build import android.provider.DeviceConfig +import android.provider.EmbeddedPhotopickerFeatureInfo import android.util.Log +import androidx.annotation.RequiresApi import androidx.compose.ui.graphics.isUnspecified import com.android.photopicker.core.navigation.PhotopickerDestinations import com.android.photopicker.core.theme.AccentColorHelper @@ -58,12 +61,14 @@ import kotlinx.coroutines.launch * in. * @property deviceConfigProxy This is provided to the ConfigurationManager to better support * testing various device flags, without relying on the device's actual flags at test time. + * @property sessionId A randomly generated integer to identify the current photopicker session */ class ConfigurationManager( private val runtimeEnv: PhotopickerRuntimeEnv, private val scope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val deviceConfigProxy: DeviceConfigProxy, + private val sessionId: Int, ) { companion object { @@ -123,6 +128,55 @@ class ConfigurationManager( } /** + * Updates the [PhotopickerConfiguration] with the [EmbeddedPhotopickerFeatureInfo] that the + * Embedded Photopicker is running with. + * + * Since [ConfigurationManager] is bound to the [EmbeddedServiceComponent], it does not have a + * reference to the currently running Session (if there is one). This allows the session to set + * the current FeatureInfo externally once the session is available. + * + * It's important that this method is called before the FeatureManager is started to prevent the + * feature manager from being re-initialized. + */ + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun setEmbeddedPhotopickerFeatureInfo(featureInfo: EmbeddedPhotopickerFeatureInfo) { + Log.d(TAG, "New featureInfo received: $featureInfo : Configuration will now update.") + + val selectionLimit = featureInfo.maxSelectionLimit + val mimeTypes = featureInfo.mimeTypes + val preSelectedUris = featureInfo.preSelectedUris + + /** + * Pick images in order is a combination of circumstances: + * - selectionLimit mode must be multiselect (more than 1) + * - The feature must be requested from the caller in the featureInfo + */ + val pickImagesInOrder = featureInfo.isOrderedSelection && (selectionLimit > 1) + + /** Check if the accent color was set and is valid. */ + val accentColor = + with(AccentColorHelper(featureInfo.accentColor)) { + if (getAccentColor().isUnspecified) { + null + } else { + inputColor + } + } + + // Use updateAndGet to ensure that the values are set before this method returns so that + // the new configuration is immediately available to the new subscribers. + _configuration.updateAndGet { + it.copy( + selectionLimit = selectionLimit, + accentColor = accentColor, + mimeTypes = mimeTypes.toCollection(ArrayList()), + preSelectedUris = preSelectedUris.toCollection(ArrayList()), + pickImagesInOrder = pickImagesInOrder, + ) + } + } + + /** * Sets the current intent & action Photopicker is running under. * * Since [ConfigurationManager] is bound to the [ActivityRetainedComponent] it does not have a @@ -210,6 +264,7 @@ class ConfigurationManager( runtimeEnv = runtimeEnv, action = "", flags = getFlagsFromDeviceConfig(), + sessionId = sessionId, ) Log.d(TAG, "Startup configuration: $config") diff --git a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt index a96dbe520..3682b3849 100644 --- a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt +++ b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt @@ -19,6 +19,7 @@ package com.android.photopicker.core.configuration import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ResolveInfo +import android.net.Uri import android.os.SystemProperties import com.android.photopicker.core.navigation.PhotopickerDestinations @@ -53,10 +54,13 @@ enum class PhotopickerRuntimeEnv { * set or set to too large a limit. * @property startDestination the start destination that should be consider the "home" view the user * is shown for the session. + * @property preSelectedUris an [ArrayList] of the [Uri]s of the items selected by the user in the + * previous photopicker sessions launched via the same calling app. * @property flags a snapshot of the relevant flags in [DeviceConfig]. These are not live values. * @property deviceIsDebuggable if the device is running a build which has [ro.debuggable == 1] * @property intent the [Intent] that Photopicker was launched with. This property is private to * restrict access outside of this class. + * @property sessionId identifies the current photopicker session */ data class PhotopickerConfiguration( val runtimeEnv: PhotopickerRuntimeEnv = PhotopickerRuntimeEnv.ACTIVITY, @@ -69,8 +73,10 @@ data class PhotopickerConfiguration( val pickImagesInOrder: Boolean = false, val selectionLimit: Int = DEFAULT_SELECTION_LIMIT, val startDestination: PhotopickerDestinations = PhotopickerDestinations.DEFAULT, + val preSelectedUris: ArrayList<Uri>? = null, val deviceIsDebuggable: Boolean = buildIsDebuggable, val flags: PhotopickerFlags = PhotopickerFlags(), + val sessionId: Int, private val intent: Intent? = null, ) { diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt index fa70c2153..26d4c4652 100644 --- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt +++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt @@ -17,6 +17,7 @@ package com.android.photopicker.core.embedded import android.app.Service import android.content.Intent +import android.net.Uri import android.os.Build import android.os.IBinder import android.provider.EmbeddedPhotopickerFeatureInfo @@ -25,7 +26,8 @@ import android.util.Log import androidx.annotation.RequiresApi import com.android.modules.utils.build.SdkLevel import com.android.photopicker.core.EmbeddedServiceComponentBuilder -import com.android.providers.media.flags.Flags.enableEmbeddedPhotopicker +import com.android.photopicker.core.configuration.DeviceConfigProxyImpl +import com.android.photopicker.core.configuration.NAMESPACE_MEDIAPROVIDER import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -56,13 +58,25 @@ class EmbeddedService : Hilt_EmbeddedService() { // EmbeddedService. private val allSessions: MutableList<Session> = mutableListOf() + private val FEATURE_MODERN_PICKER_ENABLED = Pair("enable_modern_picker", true) + + /** To check if modern photopicker is enabled on the device */ + private val isModernPickerEnabled = + DeviceConfigProxyImpl() + .getFlag( + NAMESPACE_MEDIAPROVIDER, + /* key= */ FEATURE_MODERN_PICKER_ENABLED.first, + /* defaultValue= */ FEATURE_MODERN_PICKER_ENABLED.second + ) + companion object { val TAG: String = "PhotopickerEmbeddedService" } // The binder object that is sent to all clients that bind this service. private val _binder: IBinder? = - if (SdkLevel.isAtLeastU() && enableEmbeddedPhotopicker()) { + if (SdkLevel.isAtLeastU() && isModernPickerEnabled) { + // TODO(b/357048672): Check embedded picker aconfig flag before the API release EmbeddedPhotopickerImpl(sessionFactory = ::buildSession) } else { // Embedded Photopicker is only available on U+ devices when the build flag is enabled. @@ -72,7 +86,6 @@ class EmbeddedService : Hilt_EmbeddedService() { } override fun onBind(intent: Intent?): IBinder? { - // If _binder is null, the device Sdk is too low, or a required flag was not enabled, and so // this session will be ignored. if (_binder == null) { @@ -136,8 +149,48 @@ class EmbeddedService : Hilt_EmbeddedService() { hostToken = hostToken, featureInfo = featureInfo, clientCallback = clientCallback, + grantUriPermission = ::grantUriToClient, + revokeUriPermission = ::revokeUriToClient, ) allSessions.add(newSession) return newSession } + + /** + * Grants [Intent.FLAG_GRANT_READ_URI_PERMISSION] to uri for given client. + * + * This happens during selection of new items recorded in [Session.listenForSelectionEvents] + */ + fun grantUriToClient(clientPackageName: String, uri: Uri): GrantResult { + try { + this.grantUriPermission(clientPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } catch (e: SecurityException) { + return GrantResult.FAILURE + } + + return GrantResult.SUCCESS + } + + /** + * Revokes [Intent.FLAG_GRANT_READ_URI_PERMISSION] to uri for given client. + * + * This happens during deselection of items recorded in [Session.listenForSelectionEvents] + */ + fun revokeUriToClient(clientPackageName: String, uri: Uri): GrantResult { + try { + this.revokeUriPermission(clientPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } catch (e: SecurityException) { + return GrantResult.FAILURE + } + return GrantResult.SUCCESS + } + + /** + * Enum that denotes if MediaProvider was able to successfully grant uri permission to a given + * package or not. + */ + enum class GrantResult { + SUCCESS, + FAILURE + } } diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt index 25dc2ff8d..a0131e9ef 100644 --- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt +++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt @@ -16,9 +16,20 @@ package com.android.photopicker.core.embedded +import android.view.SurfaceControlViewHost +import androidx.appcompat.app.AppCompatDelegate + /** - * Data object that represents the state of Photopicker in embedded runtime. + * Data object that represents the state of Photopicker and hold the instance of + * [SurfaceControlViewHost] in embedded runtime. * + * @param host the Instance of [SurfaceControlViewHost] * @property isExpanded true if photopicker is expanded/full-view, false if collapsed/half-view. */ -data class EmbeddedState(val isExpanded: Boolean = false) +data class EmbeddedState( + val isExpanded: Boolean = false, + val isDarkTheme: Boolean = + AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES, + val recomposeToggle: Boolean = false, + val host: SurfaceControlViewHost? = null, +) diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt index 9a206c283..077ce9312 100644 --- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt +++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt @@ -17,6 +17,7 @@ package com.android.photopicker.core.embedded import android.util.Log +import android.view.SurfaceControlViewHost import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -28,12 +29,16 @@ import kotlinx.coroutines.flow.update * See [EmbeddedState] for details about all the various pieces that make up the session state. * * Provides a long-living [StateFlow] that emits the currently known state. + * + * @param host the Instance of [SurfaceControlViewHost] for the current session */ -class EmbeddedStateManager { +class EmbeddedStateManager(host: SurfaceControlViewHost? = null) { companion object { const val TAG: String = "PhotopickerEmbeddedStateManager" } + private val _host = host + /* * Internal [EmbeddedState] flow. When the embedded state changes, this is what should * be updated to ensure all listeners are notified. @@ -46,16 +51,43 @@ class EmbeddedStateManager { */ val state: StateFlow<EmbeddedState> = _state + private var _recomposeToggle = state.value.recomposeToggle + /** Assembles an initial state upon embedded photopicker session launch. */ private fun generateInitialEmbeddedState(): EmbeddedState { - val initialEmbeddedState = EmbeddedState() + val initialEmbeddedState = EmbeddedState(host = _host) Log.d(TAG, "Initial embedded state: $initialEmbeddedState") return initialEmbeddedState } - /** Sets the current expanded or collapsed state of the embedded photopicker. */ + /** + * Updates the expanded state of the embedded photopicker. + * + * @param isExpanded true if the photopicker is expanded (full-screen view), false if it is + * collapsed (half-screen view). + */ fun setIsExpanded(isExpanded: Boolean) { Log.d(TAG, "Expanded state updated to $isExpanded") _state.update { it.copy(isExpanded = isExpanded) } } + + /** + * Sets the dark theme preference of the embedded photopicker + * + * @param isDarkTheme true to apply a dark theme, false for a light theme. + */ + fun setIsDarkTheme(isDarkTheme: Boolean) { + Log.d(TAG, "Dark theme state updated to $isDarkTheme") + _state.update { it.copy(isDarkTheme = isDarkTheme) } + } + + /** + * Updates the [_recomposeToggle] causing the photopicker to recompose its UI, to respond to + * change in config. + */ + fun triggerRecompose() { + _recomposeToggle = !_recomposeToggle + Log.d(TAG, "Recompose toggle updated to $_recomposeToggle") + _state.update { it.copy(recomposeToggle = _recomposeToggle) } + } } diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt index 14f28cb73..1f101a68f 100644 --- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt +++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.android.photopicker.core.Background +import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager @@ -60,9 +61,11 @@ import kotlinx.coroutines.CoroutineDispatcher * @property selection * @property userMonitor */ +@Suppress("UNCHECKED_CAST") class EmbeddedViewModelFactory( @Background val backgroundDispatcher: CoroutineDispatcher, val configurationManager: Lazy<ConfigurationManager>, + val bannerManager: Lazy<BannerManager>, val dataService: Lazy<DataService>, val events: Lazy<Events>, val featureManager: Lazy<FeatureManager>, @@ -73,28 +76,45 @@ class EmbeddedViewModelFactory( with(modelClass) { return when { isAssignableFrom(AlbumGridViewModel::class.java) -> - @Suppress("UNCHECKED_CAST") AlbumGridViewModel(null, selection.get(), dataService.get(), events.get()) as T isAssignableFrom(MediaPreloaderViewModel::class.java) -> - @Suppress("UNCHECKED_CAST") MediaPreloaderViewModel( null, backgroundDispatcher, selection.get(), userMonitor.get(), configurationManager.get(), + events.get(), ) as T isAssignableFrom(PhotoGridViewModel::class.java) -> - @Suppress("UNCHECKED_CAST") - PhotoGridViewModel(null, selection.get(), dataService.get(), events.get()) as T + PhotoGridViewModel( + null, + selection.get(), + dataService.get(), + events.get(), + bannerManager.get(), + ) + as T isAssignableFrom(PreviewViewModel::class.java) -> - @Suppress("UNCHECKED_CAST") - PreviewViewModel(null, selection.get(), userMonitor.get(), dataService.get()) + PreviewViewModel( + null, + selection.get(), + userMonitor.get(), + dataService.get(), + events.get(), + configurationManager.get(), + ) as T isAssignableFrom(ProfileSelectorViewModel::class.java) -> - @Suppress("UNCHECKED_CAST") - ProfileSelectorViewModel(null, selection.get(), userMonitor.get()) as T + ProfileSelectorViewModel( + null, + selection.get(), + userMonitor.get(), + events.get(), + configurationManager.get() + ) + as T else -> throw IllegalArgumentException( "Unknown ViewModel class: ${modelClass.simpleName}" diff --git a/photopicker/src/com/android/photopicker/core/embedded/Session.kt b/photopicker/src/com/android/photopicker/core/embedded/Session.kt index 618baa178..773757ce5 100644 --- a/photopicker/src/com/android/photopicker/core/embedded/Session.kt +++ b/photopicker/src/com/android/photopicker/core/embedded/Session.kt @@ -20,6 +20,7 @@ import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Configuration import android.hardware.display.DisplayManager +import android.net.Uri import android.os.Build import android.os.IBinder import android.provider.EmbeddedPhotopickerFeatureInfo @@ -33,8 +34,11 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import com.android.photopicker.core.Background import com.android.photopicker.core.EmbeddedServiceComponent import com.android.photopicker.core.Main import com.android.photopicker.core.PhotopickerApp @@ -58,6 +62,11 @@ import dagger.hilt.EntryPoints import dagger.hilt.InstallIn import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking /** Alias that describes a factory function that creates a Session. */ @@ -112,6 +121,8 @@ open class Session( private val height: Int, private val featureInfo: EmbeddedPhotopickerFeatureInfo, private val clientCallback: IEmbeddedPhotopickerClient, + private val grantUriPermission: (packageName: String, uri: Uri) -> EmbeddedService.GrantResult, + private val revokeUriPermission: (packageName: String, uri: Uri) -> EmbeddedService.GrantResult, // TODO(b/354929684): Replace AIDL implementations with wrapper classes. ) : IEmbeddedPhotopickerSession.Stub() { @@ -145,6 +156,8 @@ open class Session( @Main fun scope(): CoroutineScope + @Background fun backgroundScope(): CoroutineScope + fun selection(): Lazy<Selection<Media>> fun userMonitor(): Lazy<UserMonitor> @@ -155,11 +168,26 @@ open class Session( EntryPoints.get(component, EmbeddedEntryPoint::class.java) private val _embeddedViewLifecycle: EmbeddedLifecycle = _dependencies.lifecycle() - private val _scope: CoroutineScope = _dependencies.scope() private val _main: CoroutineDispatcher = _dependencies.mainDispatcher() + private val _backgroundScope: CoroutineScope = _dependencies.backgroundScope() + + // Wrap this in a lazy to prevent the [DataService] from getting initialized before the + // ComposeView is started. + // This flow is used to signal the UI when the DataService detects a provider update (or other + // data change which should disrupt the UI) + private val disruptiveDataNotification: Flow<Int> by lazy { + _dependencies.dataService().get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold( + initial = 0 + ) { prev, _ -> + prev + 1 + } + } private val _host: SurfaceControlViewHost private val _view: ComposeView + private val _stateManager: EmbeddedStateManager + + fun getView() = _view open val surfacePackage: SurfaceControlViewHost.SurfacePackage get() { @@ -208,8 +236,9 @@ open class Session( callingPackageLabel = clientPackageLabel ) - // TODO(b/350965066): set featureInfo in the ConfigurationManager and hand any errors back - // to the client + // Update the [PhotopickerConfiguration] associated with the session using the + // [EmbeddedPhotopickerFeatureInfo]. + _dependencies.configurationManager().get().setEmbeddedPhotopickerFeatureInfo(featureInfo) // Configuration is now stable, so the view can be created. // NOTE: Do not update the configuration after this line, it will cause the UI to @@ -217,7 +246,13 @@ open class Session( Log.d(TAG, "EmbeddedConfiguration is stable, UI will now start.") _view = createPhotopickerComposeView(context) _host = createSurfaceControlViewHost(context, displayId, hostToken) + // This initialization should happen only after receiving the [_host] + _stateManager = EmbeddedStateManager(_host) runBlocking(_main) { _host.setView(_view, width, height) } + + // Start listening to selection/deselection events for this Session so + // we can grant/revoke permission to selected/deselected uris immediately. + listenForSelectionEvents() } override fun close() { @@ -287,6 +322,8 @@ open class Session( .configuration .collectAsStateWithLifecycle() + val embeddedState by _stateManager.state.collectAsStateWithLifecycle() + // Provide values to the entire compose stack. CompositionLocalProvider( LocalFeatureManager provides _dependencies.featureManager().get(), @@ -298,9 +335,19 @@ open class Session( LocalEmbeddedLifecycle provides _embeddedViewLifecycle, LocalViewModelStoreOwner provides _embeddedViewLifecycle, LocalOnBackPressedDispatcherOwner provides _embeddedViewLifecycle, + LocalEmbeddedState provides embeddedState ) { - PhotopickerTheme(config = photopickerConfiguration) { - PhotopickerApp(_dependencies.bannerManager().get()) + val currentEmbeddedState = + checkNotNull(LocalEmbeddedState.current) { + "Embedded state cannot be null when runtime env is embedded." + } + PhotopickerTheme( + isDarkTheme = currentEmbeddedState.isDarkTheme, + config = photopickerConfiguration + ) { + PhotopickerApp( + disruptiveDataNotification, + ) } } } @@ -314,6 +361,65 @@ open class Session( } } + /** + * A collector that starts for a Session in embedded mode. This collector will grant/revoke uri + * permission when item is selected/deselected respectively. + * + * It emits both the previous and new selection of media items. + */ + fun listenForSelectionEvents() { + _backgroundScope.launch { + _dependencies + .selection() + .get() + .flow + .flowWithLifecycle(_embeddedViewLifecycle.lifecycle, Lifecycle.State.STARTED) + .runningFold(initial = emptySet<Media>()) { _prevSelection, _newSelection -> + // Get list of items removed/deselected by user so that we can revoke access to + // those uris. + var unselectedMedia: Set<Media> = _prevSelection.subtract(_newSelection) + Log.d(TAG, "Revoking uri permission to $unselectedMedia") + + // Get list of items added/selected by user so that we can grant access to + // those uris. + var newlySelectedMedia: Set<Media> = _newSelection.subtract(_prevSelection) + Log.d(TAG, "Granting uri permission to $newlySelectedMedia") + + // Grant uri to newly selected media and notify client + newlySelectedMedia.iterator().forEach { item -> + val result = grantUriPermission(clientPackageName, item.mediaUri) + if (result == EmbeddedService.GrantResult.SUCCESS) { + clientCallback.onItemSelected(item.mediaUri) + } else { + Log.w( + TAG, + "Error granting permission to uri ${item.mediaUri} " + + "for package $clientPackageName" + ) + } + } + + // Revoke uri to newly selected media and notify client + unselectedMedia.iterator().forEach { item -> + val result = revokeUriPermission(clientPackageName, item.mediaUri) + if (result == EmbeddedService.GrantResult.SUCCESS) { + clientCallback.onItemDeselected(item.mediaUri) + } else { + Log.w( + TAG, + "Error revoking permission to uri ${item.mediaUri} " + + "for package $clientPackageName" + ) + } + } + + // Update previous selection to current flow + _newSelection + } + .collect() + } + } + override fun notifyVisibilityChanged(isVisible: Boolean) { Log.d(TAG, "Session visibility has changed: $isVisible") when (isVisible) { @@ -323,14 +429,26 @@ open class Session( } override fun notifyResized(width: Int, height: Int) { - TODO("Not yet implemented") + _host.relayout(width, height) + _stateManager.triggerRecompose() } override fun notifyConfigurationChanged(configuration: Configuration?) { - TODO("Not yet implemented") + if (configuration == null) return + + // Check for dark theme + val isNewThemeDark = + (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES + + // Update embedded state manager + _stateManager.setIsDarkTheme(isNewThemeDark) + + // Pass the configuration change along to the view + _view.dispatchConfigurationChanged(configuration) } override fun notifyPhotopickerExpanded(isExpanded: Boolean) { - TODO("Not yet implemented") + _stateManager.setIsExpanded(isExpanded) } } diff --git a/photopicker/src/com/android/photopicker/core/events/Event.kt b/photopicker/src/com/android/photopicker/core/events/Event.kt index dcb3be568..3dfb854a6 100644 --- a/photopicker/src/com/android/photopicker/core/events/Event.kt +++ b/photopicker/src/com/android/photopicker/core/events/Event.kt @@ -16,6 +16,9 @@ package com.android.photopicker.core.events +import com.android.photopicker.core.banners.BannerDeclaration +import com.android.photopicker.core.banners.BannerDefinitions +import com.android.photopicker.data.model.Group import com.android.providers.media.MediaProviderStatsLog /* Convenience alias for classes that implement [Event] */ @@ -105,14 +108,21 @@ interface Event { val uiEvent: Telemetry.UiEvent ) : Event + data class LogPhotopickerAlbumOpenedUIEvent( + override val dispatcherToken: String, + val sessionId: Int, + val packageUid: Int, + val albumOpened: Group.Album + ) : Event + /** Details out the information of a picker media item */ data class ReportPhotopickerMediaItemStatus( override val dispatcherToken: String, val sessionId: Int, val mediaStatus: Telemetry.MediaStatus, - val mediaLocation: Telemetry.MediaLocation, + val selectionSource: Telemetry.MediaLocation, val itemPosition: Int, - val selectedAlbum: Telemetry.SelectedAlbum, + val selectedAlbum: Group.Album?, val mediaType: Telemetry.MediaType, val cloudOnly: Boolean, val pickerSize: Telemetry.PickerSize @@ -228,6 +238,7 @@ interface Telemetry { /* Number of items allowed to be picked */ + @Suppress("ktlint:standard:max-line-length") enum class PickerSelection(val selection: Int) { SINGLE( MediaProviderStatsLog @@ -236,6 +247,10 @@ interface Telemetry { MULTIPLE( MediaProviderStatsLog .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_PERMITTED_SELECTION__MULTIPLE + ), + UNSET_PICKER_SELECTION( + MediaProviderStatsLog + .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_PERMITTED_SELECTION__UNSET_PICKER_PERMITTED_SELECTION ) } @@ -248,7 +263,11 @@ interface Telemetry { PRIVATE_SPACE( MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__PRIVATE_SPACE ), - UNKNOWN(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__UNKNOWN) + UNKNOWN(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__UNKNOWN), + UNSET_USER_PROFILE( + MediaProviderStatsLog + .PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__UNSET_USER_PROFILE + ) } /* @@ -257,7 +276,13 @@ interface Telemetry { enum class PickerStatus(val status: Int) { OPENED(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__OPENED), CANCELED(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__CANCELED), - CONFIRMED(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__CONFIRMED) + CONFIRMED( + MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__CONFIRMED + ), + UNSET_PICKER_STATUS( + MediaProviderStatsLog + .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__UNSET_PICKER_STATUS + ) } /* @@ -274,6 +299,9 @@ interface Telemetry { PERMISSION_MODE_PICKER( MediaProviderStatsLog .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_MODE__PERMISSION_MODE_PICKER + ), + UNSET_PICKER_MODE( + MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_MODE__UNSET_PICKER_MODE ) } @@ -292,6 +320,14 @@ interface Telemetry { BACK_BUTTON( MediaProviderStatsLog .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__BACK_BUTTON + ), + SELECTION_CONFIRMED( + MediaProviderStatsLog + .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__PICKER_SELECTION_CONFIRMED + ), + UNSET_PICKER_CLOSE_METHOD( + MediaProviderStatsLog + .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__UNSET_PICKER_CLOSE_METHOD ) } @@ -300,7 +336,10 @@ interface Telemetry { */ enum class PickerSize(val size: Int) { COLLAPSED(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__COLLAPSED), - EXPANDED(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__EXPANDED) + EXPANDED(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__EXPANDED), + UNSET_PICKER_SIZE( + MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__UNSET_PICKER_SIZE + ) } /* @@ -314,6 +353,14 @@ interface Telemetry { ACTION_GET_CONTENT( MediaProviderStatsLog .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__ACTION_GET_CONTENT + ), + ACTION_USER_SELECT( + MediaProviderStatsLog + .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__ACTION_USER_SELECT + ), + UNSET_PICKER_INTENT_ACTION( + MediaProviderStatsLog + .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__UNSET_PICKER_INTENT_ACTION ) } @@ -323,11 +370,18 @@ interface Telemetry { enum class MediaType(val type: Int) { PHOTO(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__PHOTO), VIDEO(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__VIDEO), + PHOTO_VIDEO( + MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__PHOTO_VIDEO + ), GIF(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__GIF), LIVE_PHOTO( MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__LIVE_PHOTO ), - OTHER(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__OTHER) + OTHER(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__OTHER), + UNSET_MEDIA_TYPE( + MediaProviderStatsLog + .PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__UNSET_MEDIA_TYPE + ) } /* @@ -336,7 +390,10 @@ interface Telemetry { enum class SelectedTab(val tab: Int) { PHOTOS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__PHOTOS), ALBUMS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__ALBUMS), - COLLECTIONS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__COLLECTIONS) + COLLECTIONS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__COLLECTIONS), + UNSET_SELECTED_TAB( + MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__UNSET_SELECTED_TAB + ) } /* @@ -355,6 +412,10 @@ interface Telemetry { ), UNDEFINED_CLOUD( MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__UNDEFINED_CLOUD + ), + UNSET_SELECTED_ALBUM( + MediaProviderStatsLog + .PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__UNSET_SELECTED_ALBUM ) } @@ -462,7 +523,8 @@ interface Telemetry { ), SELECT_SEARCH_CATEGORY( MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__SELECT_SEARCH_CATEGORY - ) + ), + UNSET_UI_EVENT(MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__UNSET_UI_EVENT) } /* @@ -474,6 +536,10 @@ interface Telemetry { ), UNSELECTED( MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_STATUS__UNSELECTED + ), + UNSET_MEDIA_STATUS( + MediaProviderStatsLog + .PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_STATUS__UNSET_MEDIA_STATUS ) } @@ -485,7 +551,11 @@ interface Telemetry { MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__MAIN_GRID ), ALBUM(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__ALBUM), - GROUP(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__GROUP) + GROUP(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__GROUP), + UNSET_MEDIA_LOCATION( + MediaProviderStatsLog + .PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__UNSET_MEDIA_LOCATION + ) } /* @@ -497,16 +567,25 @@ interface Telemetry { ), LONG_PRESS( MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__PREVIEW_MODE_ENTRY__LONG_PRESS + ), + UNSET_PREVIEW_MODE_ENTRY( + MediaProviderStatsLog + .PHOTOPICKER_PREVIEW_INFO_LOGGED__PREVIEW_MODE_ENTRY__UNSET_PREVIEW_MODE_ENTRY ) } /* Defines different video playback user interactions */ + @Suppress("ktlint:standard:max-line-length") enum class VideoPlayBackInteractions(val interaction: Int) { PLAY(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__PLAY), PAUSE(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__PAUSE), - MUTE(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__MUTE) + MUTE(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__MUTE), + UNSET_VIDEO_PLAYBACK_INTERACTION( + MediaProviderStatsLog + .PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__UNSET_VIDEO_PLAYBACK_INTERACTION + ) } /* @@ -519,6 +598,10 @@ interface Telemetry { CLOUD_SETTINGS( MediaProviderStatsLog .PHOTOPICKER_MENU_INTERACTION_LOGGED__MENU_ITEM_SELECTED__CLOUD_SETTINGS + ), + UNSET_MENU_ITEM_SELECTED( + MediaProviderStatsLog + .PHOTOPICKER_MENU_INTERACTION_LOGGED__MENU_ITEM_SELECTED__UNSET_MENU_ITEM_SELECTED ) } @@ -539,7 +622,35 @@ interface Telemetry { ), CHOOSE_APP( MediaProviderStatsLog.PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__CHOOSE_APP - ) + ), + UNSET_BANNER_TYPE( + MediaProviderStatsLog + .PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__UNSET_BANNER_TYPE + ); + + companion object { + + /** + * Attempts to map a [BannerDeclaration] to the [BannerType] enum for logging banner + * related data. At worst, will return [UNSET_BANNER_TYPE] for a banner without a + * mapping. + * + * @param declaration The [BannerDeclaration] to convert to a [BannerType] + * @return The corresponding [BannerType] or [UNSET_BANNER_TYPE] if a mapping isn't + * found. + */ + fun fromBannerDeclaration(declaration: BannerDeclaration): BannerType { + return when (declaration.id) { + BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id -> BannerType.CHOOSE_ACCOUNT + BannerDefinitions.CLOUD_CHOOSE_PROVIDER.id -> BannerType.CHOOSE_APP + BannerDefinitions.CLOUD_MEDIA_AVAILABLE.id -> BannerType.CLOUD_MEDIA_AVAILABLE + BannerDefinitions.CLOUD_UPDATED_ACCOUNT.id -> BannerType.ACCOUNT_UPDATED + // TODO(b/357010907): add a BannerType enum for the PRIVACY_EXPLAINER + BannerDefinitions.PRIVACY_EXPLAINER.id -> BannerType.UNSET_BANNER_TYPE + else -> BannerType.UNSET_BANNER_TYPE + } + } + } } /* @@ -558,6 +669,10 @@ interface Telemetry { CLICK_BANNER( MediaProviderStatsLog .PHOTOPICKER_BANNER_INTERACTION_LOGGED__USER_BANNER_INTERACTION__CLICK_BANNER + ), + UNSET_BANNER_INTERACTION( + MediaProviderStatsLog + .PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__UNSET_BANNER_TYPE ) } @@ -574,6 +689,10 @@ interface Telemetry { SUGGESTED_SEARCHES( MediaProviderStatsLog .PHOTOPICKER_SEARCH_INFO_REPORTED__SEARCH_METHOD__SUGGESTED_SEARCHES + ), + UNSET_SEARCH_METHOD( + MediaProviderStatsLog + .PHOTOPICKER_SEARCH_INFO_REPORTED__SEARCH_METHOD__UNSET_SEARCH_METHOD ) } } diff --git a/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt b/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt index 9e44f5745..b547d9692 100644 --- a/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt +++ b/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt @@ -16,8 +16,18 @@ package com.android.photopicker.core.events +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS +import android.util.Log import com.android.photopicker.core.Background +import com.android.photopicker.data.DataService +import com.android.photopicker.data.model.Group +import com.android.photopicker.data.model.MediaSource import com.android.providers.media.MediaProviderStatsLog +import dagger.Lazy import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -26,7 +36,30 @@ import kotlinx.coroutines.launch * Photopicker telemetry class which intercepts the incoming events dispatched by various components * and maps them to their respective logging proto. All the logging occurs in background scope. */ -class PhotopickerEventLogger { +class PhotopickerEventLogger(val dataService: Lazy<DataService>) { + + private val TAG = "PhotopickerEventLogger" + + /** Maps album id to the corresponding selected album enum values */ + private val mapAlbumIdToSelectedAlbum = + hashMapOf( + ALBUM_ID_CAMERA to Telemetry.SelectedAlbum.CAMERA, + ALBUM_ID_SCREENSHOTS to Telemetry.SelectedAlbum.SCREENSHOTS, + ALBUM_ID_FAVORITES to Telemetry.SelectedAlbum.FAVOURITES, + ALBUM_ID_VIDEOS to Telemetry.SelectedAlbum.VIDEOS, + ALBUM_ID_DOWNLOADS to Telemetry.SelectedAlbum.DOWNLOADS + ) + + /** Maps album id to the corresponding selected album enum values */ + private val mapAlbumIdToAlbumOpened = + hashMapOf( + ALBUM_ID_CAMERA to Telemetry.UiEvent.ALBUM_CAMERA_OPEN, + ALBUM_ID_SCREENSHOTS to Telemetry.UiEvent.ALBUM_SCREENSHOTS_OPEN, + ALBUM_ID_FAVORITES to Telemetry.UiEvent.ALBUM_FAVOURITES_OPEN, + ALBUM_ID_VIDEOS to Telemetry.UiEvent.ALBUM_VIDEOS_OPEM, + ALBUM_ID_DOWNLOADS to Telemetry.UiEvent.ALBUM_DOWNLOADS_OPEN + ) + fun start( scope: CoroutineScope, @Background backgroundDispatcher: CoroutineDispatcher, @@ -69,20 +102,56 @@ class PhotopickerEventLogger { } is Event.LogPhotopickerUIEvent -> { MediaProviderStatsLog.write( - MediaProviderStatsLog.UI_EVENT_REPORTED, + MediaProviderStatsLog.PHOTOPICKER_UI_EVENT_LOGGED, event.sessionId, event.packageUid, event.uiEvent.event ) } + is Event.LogPhotopickerAlbumOpenedUIEvent -> { + val album = event.albumOpened + val albumOpened = + mapAlbumIdToAlbumOpened.getOrDefault( + album.id, + when (getAlbumDataSource(album)) { + MediaSource.REMOTE -> Telemetry.UiEvent.ALBUM_FROM_CLOUD_OPEN + // TODO replace with LOCAL value once added + MediaSource.LOCAL -> Telemetry.UiEvent.ALBUM_FROM_CLOUD_OPEN + } + ) + MediaProviderStatsLog.write( + MediaProviderStatsLog.PHOTOPICKER_UI_EVENT_LOGGED, + event.sessionId, + event.packageUid, + albumOpened.event + ) + } is Event.ReportPhotopickerMediaItemStatus -> { + val mediaAlbum = event.selectedAlbum + val selectedAlbum: Telemetry.SelectedAlbum = + if ( + event.selectionSource == Telemetry.MediaLocation.ALBUM && + mediaAlbum != null + ) { + mapAlbumIdToSelectedAlbum.getOrDefault( + mediaAlbum.id, + when (getAlbumDataSource(mediaAlbum)) { + MediaSource.REMOTE -> + Telemetry.SelectedAlbum.UNDEFINED_CLOUD + MediaSource.LOCAL -> Telemetry.SelectedAlbum.UNDEFINED_LOCAL + } + ) + } else { + Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM + } + MediaProviderStatsLog.write( MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED, event.sessionId, event.mediaStatus.status, - event.mediaLocation.location, + event.selectionSource.location, event.itemPosition, - event.selectedAlbum.album, + selectedAlbum.album, event.mediaType.type, event.cloudOnly, event.pickerSize.size @@ -189,4 +258,26 @@ class PhotopickerEventLogger { } } } + + /** + * Fetch the data source of the album by matching it against the authority of the provider so + * that we do not have to depend on glide's internal implementation(by using + * album.getDataSource()) to fetch the album's data source + */ + private fun getAlbumDataSource(album: Group.Album): MediaSource { + for (provider in dataService.get().availableProviders.value) { + if (provider.authority == album.authority) { + return provider.mediaSource + } + } + Log.w( + TAG, + "Unable to find an authority match with any provider for album " + + album.displayName + + " with authority " + + album.authority + + " while fetching the album data source" + ) + return MediaSource.LOCAL + } } diff --git a/photopicker/src/com/android/photopicker/core/events/SessionId.kt b/photopicker/src/com/android/photopicker/core/events/SessionId.kt new file mode 100644 index 000000000..2c612edb5 --- /dev/null +++ b/photopicker/src/com/android/photopicker/core/events/SessionId.kt @@ -0,0 +1,35 @@ +/* + * 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.core.events + +import java.security.SecureRandom + +// The sessionId can contain at most 20 bits which gives ~1M possibilities for the same, so ~0.5% +// collision probability in 100 values +const val MAX_SESSION_ID: Int = 1 shl 20 + +/** + * Generates a random integer between 1 and [MAX_SESSION_ID] to identify a particular photopicker + * session. The id gets attached to all the picker atoms so that it is easy to identify logs that + * are session specific. + * + * @return photopicker sessionId + */ +fun generatePickerSessionId(): Int { + val getRandom = SecureRandom() + return 1 + getRandom.nextInt(MAX_SESSION_ID) +} diff --git a/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt b/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt index 42d5186eb..74c8168c8 100644 --- a/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt +++ b/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt @@ -36,7 +36,6 @@ import com.android.photopicker.features.snackbar.SnackbarFeature import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** @@ -95,6 +94,7 @@ class FeatureManager( Event.ReportPhotopickerSessionInfo::class.java, Event.ReportPhotopickerApiInfo::class.java, Event.LogPhotopickerUIEvent::class.java, + Event.LogPhotopickerAlbumOpenedUIEvent::class.java, Event.ReportPhotopickerMediaItemStatus::class.java, Event.LogPhotopickerPreviewInfo::class.java, Event.LogPhotopickerMenuInteraction::class.java, diff --git a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt index e0bb21286..3fa4a421f 100644 --- a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt +++ b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt @@ -16,6 +16,7 @@ package com.android.photopicker.core.selection +import android.util.Log import androidx.annotation.GuardedBy import com.android.photopicker.core.configuration.PhotopickerConfiguration import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED @@ -26,8 +27,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -55,17 +58,20 @@ import kotlinx.coroutines.sync.withLock * @property scope A [CoroutineScope] that the flow is shared and updated in. * @property initialSelection A collection to include initial selection value. * @property configuration a collectable [StateFlow] of configuration changes - * @property preGrantedItemsCount represents the total number of grants help by the current package. + * @property preGrantedItemsCount represents the flow for total number of grants help by the current + * package. */ class GrantsAwareSelectionImpl<T : Grantable>( val scope: CoroutineScope, val initialSelection: Collection<T>? = null, private val configuration: StateFlow<PhotopickerConfiguration>, - val preGrantedItemsCount: Int = 0, + private val preGrantedItemsCount: StateFlow<Int?>, ) : Selection<T> { + private val TAG = "GrantsAwareSelection" // An internal mutex is used to enforce thread-safe access of the selection set. private val mutex = Mutex() + private val _deSelection: LinkedHashSet<T> = LinkedHashSet() private val _selection: LinkedHashSet<T> = LinkedHashSet() @@ -73,10 +79,24 @@ class GrantsAwareSelectionImpl<T : Grantable>( override val flow: StateFlow<GrantsAwareSet<T>> init { + scope.launch { + // Observe the refresh of the stateFlow that holds the count of pre-granted media. + // Note that this will always be null in case the intent action is anything other than + // [MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP]. + preGrantedItemsCount + .filter { it != null } + .collect { + Log.i(TAG, "Received notification for preGranted media count. ") + updateFlow() + } + } if (initialSelection != null) { _selection.addAll(initialSelection) } - _flow = MutableStateFlow(GrantsAwareSet(_selection, _deSelection, preGrantedItemsCount)) + _flow = + MutableStateFlow( + GrantsAwareSet(_selection, _deSelection, preGrantedItemsCount.value ?: 0) + ) flow = _flow.stateIn( scope, @@ -134,8 +154,8 @@ class GrantsAwareSelectionImpl<T : Grantable>( val itemsWithPregrants = LinkedHashSet<T>() val itemsToAdd = LinkedHashSet<T>() - for (item in items){ - if (item.isPreGranted){ + for (item in items) { + if (item.isPreGranted) { itemsWithPregrants.add(item) } else { itemsToAdd.add(item) @@ -153,7 +173,8 @@ class GrantsAwareSelectionImpl<T : Grantable>( } } - /** Empties the current selection of objects, returning the selection to an empty state. + /** + * Empties the current selection of objects, returning the selection to an empty state. * * Also, any pre-granted item that was de-selected will now reset i.e. no grants will be * revoked. @@ -171,8 +192,7 @@ class GrantsAwareSelectionImpl<T : Grantable>( @GuardedBy("mutex") override suspend fun contains(item: T): Boolean { return mutex.withLock { - _selection.contains(item) || - (item.isPreGranted && !_deSelection.contains(item)) + _selection.contains(item) || (item.isPreGranted && !_deSelection.contains(item)) } } @@ -203,6 +223,7 @@ class GrantsAwareSelectionImpl<T : Grantable>( /** * Removes the requested item from the selection. If the item is not in the selection, this has * no effect. Afterwards, will emit the new selection into the exposed flow. + * * @return [SelectionModifiedResult] of the outcome of the removal. */ @GuardedBy("mutex") @@ -224,6 +245,7 @@ class GrantsAwareSelectionImpl<T : Grantable>( * * If one or more items are not present in the selection, this has no effect. Afterwards, will * emit the new selection into the exposed flow. + * * @return [SelectionModifiedResult] of the outcome of the removal. */ @GuardedBy("mutex") @@ -231,8 +253,7 @@ class GrantsAwareSelectionImpl<T : Grantable>( return mutex.withLock { _selection.removeAll(items) for (item in items) { - if (item.isPreGranted) - _deSelection.add(item) + if (item.isPreGranted) _deSelection.add(item) } updateFlow() SUCCESS @@ -249,7 +270,11 @@ class GrantsAwareSelectionImpl<T : Grantable>( override suspend fun snapshot(): Set<T> { return mutex.withLock { // Create a new [grantsSet] to emit updated values. - GrantsAwareSet(_selection.toSet(), _deSelection.toSet(), preGrantedItemsCount) + GrantsAwareSet( + _selection.toSet(), + _deSelection.toSet(), + preGrantedItemsCount.value ?: 0 + ) } } @@ -260,8 +285,8 @@ class GrantsAwareSelectionImpl<T : Grantable>( * such an item is toggled, if it is not part of _deSelection then it is added to _deselection * otherwise removed from it. * - * For non preGranted items: if the item is already in the selection, it is removed. - * If the item is not in the selection, it is added. + * For non preGranted items: if the item is already in the selection, it is removed. If the item + * is not in the selection, it is added. * * Afterwards, will emit the new selection into the exposed flow. * @@ -354,14 +379,12 @@ class GrantsAwareSelectionImpl<T : Grantable>( */ @GuardedBy("mutex") override suspend fun getDeselection(): Collection<T> { - return mutex.withLock { - _deSelection.toSet() - } + return mutex.withLock { _deSelection.toSet() } } /** Internal method that snapshots the current selection and emits it to the exposed flow. */ private suspend fun updateFlow() { - _flow.update { GrantsAwareSet(_selection, _deSelection, preGrantedItemsCount) } + _flow.update { GrantsAwareSet(_selection, _deSelection, preGrantedItemsCount.value ?: 0) } } /** diff --git a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt index 69080a1ac..dafa93c4e 100644 --- a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt +++ b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt @@ -21,9 +21,8 @@ import com.android.photopicker.data.model.Grantable /** * A specialized set implementation that is aware of both user selections and pre-granted elements. * - * This class extends the behavior of a standard set by incorporating pre-granted elements - * into its logic. An element is considered to be part of the set if either: - * + * This class extends the behavior of a standard set by incorporating pre-granted elements into its + * logic. An element is considered to be part of the set if either: * 1. It has been explicitly selected by the user. * 2. It is pre-granted and hasn't been explicitly de-selected by the user. * @@ -33,32 +32,28 @@ import com.android.photopicker.data.model.Grantable * * @property selection The set of elements explicitly selected by the user. * @property deSelection The set of pre-granted elements that have been explicitly de-selected. - * @property preGrantedelementsCount The number of pre-granted elements (not including those in `deSelection`). + * @property preGrantedElementsCount The number of pre-granted elements (not including those in + * `deSelection`). */ class GrantsAwareSet<T : Grantable>( val selection: Set<T>, val deSelection: Set<T>, - val preGrantedelementsCount: Int = 0, + private val preGrantedElementsCount: Int = 0, ) : Set<T> { - /** - * Size of the set based on current selection and preGranted elements. - */ - override val size: Int = selection.size - deSelection.size + preGrantedelementsCount + /** Size of the set based on current selection and preGranted elements. */ + override val size: Int = selection.size - deSelection.size + preGrantedElementsCount /** * Checks if the set contains a specific element. * * This implementation considers two scenarios: - * * 1. **Direct Presence in the Selection:** - * - Returns `true` if the `element` is directly present in the current user selection. - * + * - Returns `true` if the `element` is directly present in the current user selection. * 2. **Pre-Granted Media:** - * - If the `element` is a `Media` object: - * - Returns `true` if the `Media` is pre-granted (via `isPreGranted()`) AND - * it is not present in the deSelection set (i.e., the user has not explicitly - * de-selected it). + * - If the `element` is a `Media` object: + * - Returns `true` if the `Media` is pre-granted (via `isPreGranted()`) AND it is not + * present in the deSelection set (i.e., the user has not explicitly de-selected it). * * @param element The element to check for. * @return `true` if the element is considered to be in the set, `false` otherwise. @@ -97,9 +92,7 @@ class GrantsAwareSet<T : Grantable>( return selection.iterator() } - /** - * Checks if all elements provided in the input are present in the set. - */ + /** Checks if all elements provided in the input are present in the set. */ override fun containsAll(elements: Collection<T>): Boolean { for (element in elements) { if (!contains(element)) { @@ -108,4 +101,4 @@ class GrantsAwareSet<T : Grantable>( } return true } -}
\ No newline at end of file +} diff --git a/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt b/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt index d71a59752..30fa7c535 100644 --- a/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt +++ b/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt @@ -135,4 +135,9 @@ class AccentColorHelper(val inputColor: Long) { fun getTextColorForAccentComponents(): Color { return textColorForAccentComponents } + + /** Indicates that a valid accent color is used for the photopicker theme */ + fun isValidAccentColorSet(): Boolean { + return accentColor != Color.Unspecified + } } diff --git a/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt b/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt index 798a615a4..cbcd0ec1c 100644 --- a/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt +++ b/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt @@ -23,9 +23,7 @@ import androidx.compose.ui.graphics.isUnspecified /** CompositionLocal used to pass [AccentColorScheme] down the tree. */ val CustomAccentColorScheme = staticCompositionLocalOf<AccentColorScheme> { - throw IllegalStateException( - "No CustomAccentColorScheme configured." - ) + throw IllegalStateException("No CustomAccentColorScheme configured.") } /** @@ -51,6 +49,13 @@ class AccentColorScheme(accentColorHelper: AccentColorHelper) { } /** + * Returns if an accentColor is defined for this color scheme. + * + * @return true if [accentColor] is a defined color. + */ + fun isAccentColorDefined() = !accentColor.isUnspecified + + /** * Returns the appropriate text color for components using the accent color as the background * which has been passed as an input in the picker intent. * diff --git a/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt b/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt index 466cdc3b1..44c3ebd25 100644 --- a/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt +++ b/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt @@ -76,8 +76,28 @@ fun PhotopickerTheme( val colorScheme = remember(isDarkTheme) { when (isDarkTheme) { - true -> darkTheme - false -> lightTheme + true -> + if (accentColorHelper.getAccentColor().isUnspecified) { + darkTheme + } else { + // When an accent color has been specified, set primary and onPrimary + // in the theme to use the accent color. + darkTheme.copy( + primary = accentColorHelper.getAccentColor(), + onPrimary = accentColorHelper.getTextColorForAccentComponents() + ) + } + false -> + if (accentColorHelper.getAccentColor().isUnspecified) { + lightTheme + } else { + lightTheme.copy( + // When an accent color has been specified, set primary and onPrimary + // in the theme to use the accent color. + primary = accentColorHelper.getAccentColor(), + onPrimary = accentColorHelper.getTextColorForAccentComponents() + ) + } } } val fixedAccentColors = diff --git a/photopicker/src/com/android/photopicker/data/DataService.kt b/photopicker/src/com/android/photopicker/data/DataService.kt index 457e0e969..70bccb354 100644 --- a/photopicker/src/com/android/photopicker/data/DataService.kt +++ b/photopicker/src/com/android/photopicker/data/DataService.kt @@ -41,6 +41,9 @@ interface DataService { /** A [StateFlow] with a list of available [Provider]-s. */ val availableProviders: StateFlow<List<Provider>> + /** Count of all preGranted media for the current package and userID. */ + val preGrantedMediaCount: StateFlow<Int?> + /** * A [Channel] that emits a [Unit] when a disruptive data change is observed in the backend. The * UI can treat this emission as a signal to reset the UI. @@ -105,4 +108,7 @@ interface DataService { * @return The [CollectionInfo] of the given [Provider]. */ suspend fun getCollectionInfo(provider: Provider): CollectionInfo + + /** Refreshes the [preGrantedMediaCount] with the latest value in the data source. */ + fun refreshPreGrantedItemsCount() } diff --git a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt index e6e9f0257..ccc95b8b2 100644 --- a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt +++ b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt @@ -23,9 +23,11 @@ import android.content.pm.ResolveInfo import android.database.ContentObserver import android.net.Uri import android.provider.CloudMediaProviderContract +import android.provider.MediaStore import android.util.Log import androidx.paging.PagingSource import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.user.UserStatus import com.android.photopicker.data.model.CloudMediaProviderDetails @@ -83,13 +85,18 @@ class DataServiceImpl( private val mediaProviderClient: MediaProviderClient, private val config: StateFlow<PhotopickerConfiguration>, private val featureManager: FeatureManager, - private val appContext: Context + private val appContext: Context, + private val events: Events, ) : DataService { private val _activeContentResolver = MutableStateFlow<ContentResolver>(userStatus.value.activeContentResolver) - // Keep track of the photo grid media and album grid paging source so that we can invalidate - // them in case the underlying data changes. + // Here default value being null signifies that the look up for the grants has not happened yet. + // Use [refreshPreGrantedItemsCount] to populate this with the latest value. + private var _preGrantedMediaCount: MutableStateFlow<Int?> = MutableStateFlow(null) + + // Keep track of the photo grid media, album grid and preview media paging sources so that we + // can invalidate them in case the underlying data changes. private val mediaPagingSources: MutableList<MediaPagingSource> = mutableListOf() private val albumPagingSources: MutableList<AlbumPagingSource> = mutableListOf() @@ -181,6 +188,12 @@ class DataServiceImpl( override val disruptiveDataUpdateChannel = Channel<Unit>(CONFLATED) + /** + * Same as [_preGrantedMediaCount] but as an immutable StateFlow. The count contains the latest + * value set during the most recent [refreshPreGrantedItemsCount] call. + */ + override val preGrantedMediaCount: StateFlow<Int?> = _preGrantedMediaCount + companion object { const val FLOW_TIMEOUT_MILLI_SECONDS: Long = 5000 } @@ -394,6 +407,7 @@ class DataServiceImpl( mediaProviderClient, dispatcher, config.value, + events, ) Log.v( @@ -420,6 +434,7 @@ class DataServiceImpl( mediaProviderClient, dispatcher, config.value, + events, ) Log.v( @@ -448,6 +463,7 @@ class DataServiceImpl( mediaProviderClient, dispatcher, config.value, + events, ) Log.v(DataService.TAG, "Created a media paging source that queries $availableProviders") @@ -460,8 +476,35 @@ class DataServiceImpl( override fun previewMediaPagingSource( currentSelection: Set<Media>, currentDeselection: Set<Media> - ): PagingSource<MediaPageKey, Media> = - throw NotImplementedError("This method is not implemented yet.") + ): PagingSource<MediaPageKey, Media> = runBlocking { + mediaPagingSourceMutex.withLock { + val availableProviders: List<Provider> = availableProviders.value + val contentResolver: ContentResolver = _activeContentResolver.value + val mediaPagingSource = + MediaPagingSource( + contentResolver, + availableProviders, + mediaProviderClient, + dispatcher, + config.value, + events, + /* is_preview_request */ true, + currentSelection.mapNotNull { it.mediaId }.toCollection(ArrayList()), + currentDeselection + .mapNotNull { it.mediaId } + .toCollection( + ArrayList(), + ), + ) + + Log.v( + DataService.TAG, + "Created a media paging source that queries database for" + "preview items." + ) + mediaPagingSources.add(mediaPagingSource) + mediaPagingSource + } + } override suspend fun refreshMedia() { val availableProviders: List<Provider> = availableProviders.value @@ -568,6 +611,9 @@ class DataServiceImpl( // successful sync enables cloud queries, which then updates the UI. refreshMedia(providers) + // refresh count for preGranted media. + refreshPreGrantedItemsCount() + val previouslyAvailableProviders = _availableProviders.value _availableProviders.update { providers } @@ -585,6 +631,24 @@ class DataServiceImpl( collectionInfoState.clear() } + override fun refreshPreGrantedItemsCount() { + // value for _preGrantedMediaCount being null signifies that the count has not been fetched + // yet for this photopicker session. + // This should only be used in ACTION_USER_SELECT_IMAGES_FOR_APP mode since grants only + // exist for this mode. + if ( + _preGrantedMediaCount.value == null && + MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(config.value.action) + ) { + _preGrantedMediaCount.update { + mediaProviderClient.fetchMediaGrantsCount( + _activeContentResolver.value, + config.value.callingPackageUid ?: -1 + ) + } + } + } + /** * Sends a refresh media notification to the data source. This signal tells the data source to * refresh its cache. diff --git a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt index df14d98f7..a47feef28 100644 --- a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt +++ b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt @@ -47,6 +47,7 @@ open class MediaProviderClient { private const val EXTRA_LOCAL_ONLY = "is_local_only" private const val EXTRA_ALBUM_ID = "album_id" private const val EXTRA_ALBUM_AUTHORITY = "album_authority" + private const val COLUMN_GRANTS_COUNT = "grants_count" } /** Contains all optional and mandatory keys required to make a Media query */ @@ -94,6 +95,7 @@ open class MediaProviderClient { MIME_TYPE("mime_type"), STANDARD_MIME_TYPE_EXT("standard_mime_type_extension"), DURATION("duration_millis"), + IS_PRE_GRANTED("is_pre_granted"), } /** Contains all optional and mandatory keys for data in the Media query response extras. */ @@ -115,6 +117,13 @@ open class MediaProviderClient { COVER_MEDIA_SOURCE("media_source") } + /** Contains all optional and mandatory keys for the Preview Media Query. */ + enum class PreviewMediaQuery(val key: String) { + CURRENT_SELECTION("current_selection"), + CURRENT_DE_SELECTION("current_de_selection"), + IS_FIRST_PAGE("is_first_page") + } + /** Fetch available [Provider]-s from the Media Provider process. */ fun fetchAvailableProviders( contentResolver: ContentResolver, @@ -158,7 +167,7 @@ open class MediaProviderClient { pageSize: Int, contentResolver: ContentResolver, availableProviders: List<Provider>, - config: PhotopickerConfiguration + config: PhotopickerConfiguration, ): LoadResult<MediaPageKey, Media> { val input: Bundle = bundleOf( @@ -170,7 +179,8 @@ open class MediaProviderClient { availableProviders.forEach { provider -> add(provider.authority) } }, EXTRA_MIME_TYPES to config.mimeTypes, - EXTRA_INTENT_ACTION to config.action + EXTRA_INTENT_ACTION to config.action, + Intent.EXTRA_UID to config.callingPackageUid, ) try { @@ -198,6 +208,59 @@ open class MediaProviderClient { } } + /** Fetch a list of [Media] from MediaProvider for the given page key. */ + fun fetchPreviewMedia( + pageKey: MediaPageKey, + pageSize: Int, + contentResolver: ContentResolver, + availableProviders: List<Provider>, + config: PhotopickerConfiguration, + currentSelection: List<String> = emptyList(), + currentDeSelection: List<String> = emptyList(), + isFirstPage: Boolean = false, + ): LoadResult<MediaPageKey, Media> { + val input: Bundle = + bundleOf( + MediaQuery.PICKER_ID.key to pageKey.pickerId, + MediaQuery.DATE_TAKEN.key to pageKey.dateTakenMillis, + MediaQuery.PAGE_SIZE.key to pageSize, + MediaQuery.PROVIDERS.key to + ArrayList<String>().apply { + availableProviders.forEach { provider -> add(provider.authority) } + }, + EXTRA_MIME_TYPES to config.mimeTypes, + EXTRA_INTENT_ACTION to config.action, + Intent.EXTRA_UID to config.callingPackageUid, + PreviewMediaQuery.CURRENT_SELECTION.key to currentSelection, + PreviewMediaQuery.CURRENT_DE_SELECTION.key to currentDeSelection, + PreviewMediaQuery.IS_FIRST_PAGE.key to isFirstPage, + ) + + try { + return contentResolver + .query( + MEDIA_PREVIEW_URI, + /* projection */ null, + input, + /* cancellationSignal */ null // TODO + ) + .use { cursor -> + cursor?.let { + LoadResult.Page( + data = cursor.getListOfMedia(), + prevKey = cursor.getPrevPageKey(), + nextKey = cursor.getNextPageKey() + ) + } + ?: throw IllegalStateException( + "Received a null response from Content Provider" + ) + } + } catch (e: RuntimeException) { + throw RuntimeException("Could not fetch preview media", e) + } + } + /** Fetch a list of [Group.Album] from MediaProvider for the given page key. */ fun fetchAlbums( pageKey: MediaPageKey, @@ -216,9 +279,9 @@ open class MediaProviderClient { availableProviders.forEach { provider -> add(provider.authority) } }, EXTRA_MIME_TYPES to config.mimeTypes, - EXTRA_INTENT_ACTION to config.action + EXTRA_INTENT_ACTION to config.action, + Intent.EXTRA_UID to config.callingPackageUid, ) - try { return contentResolver .query( @@ -265,7 +328,8 @@ open class MediaProviderClient { availableProviders.forEach { provider -> add(provider.authority) } }, EXTRA_MIME_TYPES to config.mimeTypes, - EXTRA_INTENT_ACTION to config.action + EXTRA_INTENT_ACTION to config.action, + Intent.EXTRA_UID to config.callingPackageUid, ) try { @@ -318,6 +382,45 @@ open class MediaProviderClient { } /** + * Fetches the count of pre-granted media for a given package from the MediaProvider. + * + * This function is designed to be used within the MediaProvider client-side context. It queries + * the `MEDIA_GRANTS_URI` using a Bundle containing the calling package's UID to retrieve the + * count of media grants. + * + * @param contentResolver The ContentResolver used to interact with the MediaProvider. + * @param callingPackageUid The UID of the calling package (app) for which to fetch the count. + * @return The count of media grants for the calling package. + * @throws RuntimeException if an error occurs during the query or fetching of the grants count. + */ + fun fetchMediaGrantsCount(contentResolver: ContentResolver, callingPackageUid: Int): Int { + if (callingPackageUid < 0) { + // return with 0 value since the input callingUid is invalid. + Log.e(TAG, "invalid calling package UID.") + throw IllegalArgumentException("Invalid input for uid.") + } + // Create a Bundle containing the calling package's UID. This is used as a selection + // argument for the query. + val input: Bundle = bundleOf(Intent.EXTRA_UID to callingPackageUid) + + try { + contentResolver.query(MEDIA_GRANTS_COUNT_URI, /* projection */ null, input, null).use { + cursor -> + if (cursor != null && cursor.moveToFirst()) { + // Move the cursor to the first row and extract the count. + + return cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_GRANTS_COUNT)) + } else { + // return 0 if cursor is empty. + return 0 + } + } + } catch (e: Exception) { + throw RuntimeException("Could not fetch media grants count. ", e) + } + } + + /** * Send a refresh media request to MediaProvider. This is a signal for MediaProvider to refresh * its cache, if required. */ @@ -337,6 +440,7 @@ open class MediaProviderClient { extras.putBoolean(EXTRA_LOCAL_ONLY, initLocalOnlyMedia) extras.putStringArrayList(EXTRA_MIME_TYPES, config.mimeTypes) extras.putString(EXTRA_INTENT_ACTION, config.action) + extras.putInt(Intent.EXTRA_UID, config.callingPackageUid ?: -1) refreshMedia(extras, resolver) } @@ -469,7 +573,8 @@ open class MediaProviderClient { val mimeType: String = getString(getColumnIndexOrThrow(MediaResponse.MIME_TYPE.key)) val standardMimeTypeExtension: Int = getInt(getColumnIndexOrThrow(MediaResponse.STANDARD_MIME_TYPE_EXT.key)) - + val isPregranted: Int = + getInt(getColumnIndexOrThrow(MediaResponse.IS_PRE_GRANTED.key)) if (mimeType.startsWith("image/")) { result.add( Media.Image( @@ -483,6 +588,7 @@ open class MediaProviderClient { sizeInBytes = sizeInBytes, mimeType = mimeType, standardMimeTypeExtension = standardMimeTypeExtension, + isPreGranted = (isPregranted == 1) // here 1 denotes true else false ) ) } else if (mimeType.startsWith("video/")) { @@ -499,6 +605,7 @@ open class MediaProviderClient { mimeType = mimeType, standardMimeTypeExtension = standardMimeTypeExtension, duration = getInt(getColumnIndexOrThrow(MediaResponse.DURATION.key)), + isPreGranted = (isPregranted == 1) // here 1 denotes true else false ) ) } else { diff --git a/photopicker/src/com/android/photopicker/data/UriHelper.kt b/photopicker/src/com/android/photopicker/data/UriHelper.kt index 1723865f7..b2368e6a6 100644 --- a/photopicker/src/com/android/photopicker/data/UriHelper.kt +++ b/photopicker/src/com/android/photopicker/data/UriHelper.kt @@ -27,6 +27,8 @@ private const val AVAILABLE_PROVIDERS_PATH_SEGMENT = "available_providers" private const val COLLECTION_INFO_SEGMENT = "collection_info" private const val MEDIA_PATH_SEGMENT = "media" private const val ALBUM_PATH_SEGMENT = "album" +private const val MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count" +private const val PREVIEW_PATH_SEGMENT = "preview" private val pickerUri: Uri = Uri.Builder() @@ -59,6 +61,20 @@ val AVAILABLE_PROVIDERS_CHANGE_NOTIFICATION_URI: Uri = /** URI for media metadata. */ val MEDIA_URI: Uri = pickerUri.buildUpon().apply { appendPath(MEDIA_PATH_SEGMENT) }.build() +/** URI for media_grants table. */ +val MEDIA_GRANTS_COUNT_URI: Uri = + pickerUri.buildUpon().apply { appendPath(MEDIA_GRANTS_COUNT_PATH_SEGMENT) }.build() + +/** URI for media_grants table. */ +val MEDIA_PREVIEW_URI: Uri = + pickerUri + .buildUpon() + .apply { + appendPath(MEDIA_PATH_SEGMENT) + appendPath(PREVIEW_PATH_SEGMENT) + } + .build() + /** URI that receives [ContentProvider] change notifications for media updates. */ val MEDIA_CHANGE_NOTIFICATION_URI: Uri = MEDIA_URI.buildUpon().apply { appendPath(UPDATE_PATH_SEGMENT) }.build() diff --git a/photopicker/src/com/android/photopicker/data/model/Media.kt b/photopicker/src/com/android/photopicker/data/model/Media.kt index e0e31b256..43e59dbef 100644 --- a/photopicker/src/com/android/photopicker/data/model/Media.kt +++ b/photopicker/src/com/android/photopicker/data/model/Media.kt @@ -19,6 +19,8 @@ package com.android.photopicker.data.model import android.net.Uri import android.os.Parcel import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import com.android.photopicker.core.events.Telemetry import com.android.photopicker.core.glide.GlideLoadable import com.android.photopicker.core.glide.Resolution import com.android.photopicker.util.hashCodeOf @@ -26,7 +28,7 @@ import com.bumptech.glide.load.DataSource import com.bumptech.glide.signature.ObjectKey /** Holds metadata for a type of media item like [Image] or [Video]. */ -sealed interface Media : GlideLoadable, Grantable, Parcelable { +sealed interface Media : GlideLoadable, Grantable, Parcelable, Selectable { /** This is the ID that provider has shared with Picker */ val mediaId: String @@ -40,8 +42,23 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { val sizeInBytes: Long val mimeType: String val standardMimeTypeExtension: Int + override val selectionSource: Telemetry.MediaLocation? + override val mediaItemAlbum: Group.Album? override val isPreGranted: Boolean + companion object { + fun withSelectable( + item: Media, + selectionSource: Telemetry.MediaLocation, + album: Group.Album? + ): Media { + return when (item) { + is Image -> item.copy(selectionSource = selectionSource, mediaItemAlbum = album) + is Video -> item.copy(selectionSource = selectionSource, mediaItemAlbum = album) + } + } + } + override fun getSignature(resolution: Resolution): ObjectKey { return ObjectKey("${mediaUri}_$resolution") } @@ -80,8 +97,10 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { out.writeInt(standardMimeTypeExtension) } + // TODO Make selectable values hold UNSET values instead of null /** Holds metadata for an image item. */ - data class Image( + data class Image + constructor( override val mediaId: String, override val pickerId: Long, override val authority: String, @@ -93,6 +112,8 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { override val mimeType: String, override val standardMimeTypeExtension: Int, override val isPreGranted: Boolean = false, + override val selectionSource: Telemetry.MediaLocation? = null, + override val mediaItemAlbum: Group.Album? = null ) : Media { override fun writeToParcel(out: Parcel, flags: Int) { @@ -124,6 +145,7 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { companion object CREATOR : Parcelable.Creator<Image> { + @OptIn(ExperimentalMaterial3Api::class) override fun createFromParcel(parcel: Parcel): Image { val image = Image( @@ -148,8 +170,10 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { } } + // TODO Make selectable values hold UNSET values instead of null /** Holds metadata for a video item. */ - data class Video( + data class Video + constructor( override val mediaId: String, override val pickerId: Long, override val authority: String, @@ -162,6 +186,8 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { override val standardMimeTypeExtension: Int, val duration: Int, override val isPreGranted: Boolean = false, + override val selectionSource: Telemetry.MediaLocation? = null, + override val mediaItemAlbum: Group.Album? = null ) : Media { override fun writeToParcel(out: Parcel, flags: Int) { @@ -194,6 +220,7 @@ sealed interface Media : GlideLoadable, Grantable, Parcelable { companion object CREATOR : Parcelable.Creator<Video> { + @OptIn(ExperimentalMaterial3Api::class) override fun createFromParcel(parcel: Parcel): Video { val video = Video( diff --git a/photopicker/src/com/android/photopicker/data/model/Selectable.kt b/photopicker/src/com/android/photopicker/data/model/Selectable.kt new file mode 100644 index 000000000..aa68c45ee --- /dev/null +++ b/photopicker/src/com/android/photopicker/data/model/Selectable.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 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.data.model + +import com.android.photopicker.core.events.Telemetry + +/** + * The base interface to hold additional properties for any type of media object like [Image] or + * [Video] + */ +interface Selectable { + /** Holds whether the media items is present in the main grid or the albums grid */ + val selectionSource: Telemetry.MediaLocation? + /** Holds the album the media item is part of in case it is present in the albums grid */ + val mediaItemAlbum: Group.Album? +} diff --git a/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt index d1cb099f1..ab63e0567 100644 --- a/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt +++ b/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt @@ -21,6 +21,9 @@ import android.util.Log import androidx.paging.PagingSource import androidx.paging.PagingState import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaPageKey @@ -41,38 +44,54 @@ class AlbumMediaPagingSource( private val availableProviders: List<Provider>, private val mediaProviderClient: MediaProviderClient, private val dispatcher: CoroutineDispatcher, - private val config: PhotopickerConfiguration, + private val configuration: PhotopickerConfiguration, + private val events: Events, ) : PagingSource<MediaPageKey, Media>() { companion object { val TAG: String = "PickerAlbumMediaPagingSource" } override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Media> { + val pageKey = params.key ?: MediaPageKey() + val pageSize = params.loadSize + // Switch to the background thread from the main thread using [withContext]. - return withContext(dispatcher) { - val pageKey = params.key ?: MediaPageKey() - val pageSize = params.loadSize + val albumMediaFetchResult = + withContext(dispatcher) { + try { - try { + if (availableProviders.isEmpty()) { + throw IllegalArgumentException("No available providers found.") + } - if (availableProviders.isEmpty()) { - throw IllegalArgumentException("No available providers found.") + mediaProviderClient.fetchAlbumMedia( + albumId, + albumAuthority, + pageKey, + pageSize, + contentResolver, + availableProviders, + configuration + ) + } catch (e: Exception) { + Log.e(TAG, "Could not fetch page from MediaProvider for album $albumId", e) + LoadResult.Error(e) } - - mediaProviderClient.fetchAlbumMedia( - albumId, - albumAuthority, - pageKey, - pageSize, - contentResolver, - availableProviders, - config - ) - } catch (e: Exception) { - Log.e(TAG, "Could not fetch page from MediaProvider for album $albumId", e) - LoadResult.Error(e) } + if (albumMediaFetchResult is LoadResult.Page) { + // Dispatch a pageInfo event to log paging details for fetching album media item + // Keeping page number as 0 for all dispatched events for now for simplicity + events.dispatch( + Event.LogPhotopickerPageInfo( + FeatureToken.CORE.token, + configuration.sessionId, + /* pageNumber */ 0, + pageSize + ) + ) } + + return albumMediaFetchResult } override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = null diff --git a/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt index a14608519..60c5dc34d 100644 --- a/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt +++ b/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt @@ -21,6 +21,9 @@ import android.util.Log import androidx.paging.PagingSource import androidx.paging.PagingState import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.model.Group.Album import com.android.photopicker.data.model.MediaPageKey @@ -39,35 +42,50 @@ class AlbumPagingSource( private val availableProviders: List<Provider>, private val mediaProviderClient: MediaProviderClient, private val dispatcher: CoroutineDispatcher, - private val config: PhotopickerConfiguration, + private val configuration: PhotopickerConfiguration, + private val events: Events, ) : PagingSource<MediaPageKey, Album>() { companion object { val TAG: String = "PickerAlbumPagingSource" } override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Album> { + val pageKey = params.key ?: MediaPageKey() + val pageSize = params.loadSize // Switch to the background thread from the main thread using [withContext]. - return withContext(dispatcher) { - val pageKey = params.key ?: MediaPageKey() - val pageSize = params.loadSize + val albumFetchResult = + withContext(dispatcher) { + try { + if (availableProviders.isEmpty()) { + throw IllegalArgumentException("No available providers found.") + } - try { - if (availableProviders.isEmpty()) { - throw IllegalArgumentException("No available providers found.") + mediaProviderClient.fetchAlbums( + pageKey, + pageSize, + contentResolver, + availableProviders, + configuration + ) + } catch (e: Exception) { + Log.e(TAG, "Could not fetch page from Media provider", e) + LoadResult.Error(e) } + } - mediaProviderClient.fetchAlbums( - pageKey, - pageSize, - contentResolver, - availableProviders, - config + if (albumFetchResult is LoadResult.Page) { + // Dispatch a pageInfo event to log paging details for fetching albums + // Keeping page number as 0 for all dispatched events for now for simplicity + events.dispatch( + Event.LogPhotopickerPageInfo( + FeatureToken.CORE.token, + configuration.sessionId, + /* pageNumber */ 0, + pageSize ) - } catch (e: Exception) { - Log.e(TAG, "Could not fetch page from Media provider", e) - LoadResult.Error(e) - } + ) } + return albumFetchResult } override fun getRefreshKey(state: PagingState<MediaPageKey, Album>): MediaPageKey? = null diff --git a/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt index f3bd14848..d8bf7bcde 100644 --- a/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt +++ b/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt @@ -21,6 +21,9 @@ import android.util.Log import androidx.paging.PagingSource import androidx.paging.PagingState import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaPageKey @@ -39,35 +42,66 @@ class MediaPagingSource( private val availableProviders: List<Provider>, private val mediaProviderClient: MediaProviderClient, private val dispatcher: CoroutineDispatcher, - private val config: PhotopickerConfiguration, + private val configuration: PhotopickerConfiguration, + private val events: Events, + private val isPreviewSession: Boolean = false, + private val currentSelection: List<String> = emptyList(), + private val currentDeSelection: List<String> = emptyList(), ) : PagingSource<MediaPageKey, Media>() { companion object { val TAG: String = "PickerMediaPagingSource" } override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Media> { + val pageKey = params.key ?: MediaPageKey() + val pageSize = params.loadSize // Switch to the background thread from the main thread using [withContext]. - return withContext(dispatcher) { - val pageKey = params.key ?: MediaPageKey() - val pageSize = params.loadSize - - try { - if (availableProviders.isEmpty()) { - throw IllegalArgumentException("No available providers found.") + val mediaFetchResult = + withContext(dispatcher) { + try { + if (availableProviders.isEmpty()) { + throw IllegalArgumentException("No available providers found.") + } + if (isPreviewSession) { + mediaProviderClient.fetchPreviewMedia( + pageKey, + pageSize, + contentResolver, + availableProviders, + configuration, + currentSelection, + currentDeSelection, + // only true for first page or refreshes. + /* isFirstPage */ (params.key == null) + ) + } else { + mediaProviderClient.fetchMedia( + pageKey, + pageSize, + contentResolver, + availableProviders, + configuration + ) + } + } catch (e: Exception) { + Log.e(TAG, "Could not fetch page from Media provider", e) + LoadResult.Error(e) } + } - mediaProviderClient.fetchMedia( - pageKey, - pageSize, - contentResolver, - availableProviders, - config + if (mediaFetchResult is LoadResult.Page) { + // Dispatch a pageInfo event to log paging details for fetching media items + // Keeping page number as 0 for all dispatched events for now for simplicity + events.dispatch( + Event.LogPhotopickerPageInfo( + FeatureToken.CORE.token, + configuration.sessionId, + /* pageNumber */ 0, + pageSize ) - } catch (e: Exception) { - Log.e(TAG, "Could not fetch page from Media provider", e) - LoadResult.Error(e) - } + ) } + return mediaFetchResult } override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = null diff --git a/photopicker/src/com/android/photopicker/extensions/Flow.kt b/photopicker/src/com/android/photopicker/extensions/Flow.kt index 50f60aea4..8fa05bf35 100644 --- a/photopicker/src/com/android/photopicker/extensions/Flow.kt +++ b/photopicker/src/com/android/photopicker/extensions/Flow.kt @@ -20,6 +20,8 @@ import androidx.paging.PagingData import androidx.paging.insertSeparators import androidx.paging.map import com.android.photopicker.core.components.MediaGridItem +import com.android.photopicker.core.user.UserProfile +import com.android.photopicker.core.user.UserStatus import com.android.photopicker.data.model.Group import com.android.photopicker.data.model.Media import java.time.LocalDateTime @@ -55,11 +57,11 @@ fun Flow<PagingData<Group.Album>>.toMediaGridItemFromAlbum(): Flow<PagingData<Me * [Media] grid representation wrappers) and processes them inserting month separators in between * items that have different month. * - * TODO(b/323830434): Update logic for separators after 4th row when UX finalizes. - * Note: This does not include a separator for the first month of data. - * * @return A [PagingData<MediaGridItem] that can be processed further, or provided to the * [MediaGrid]. + * + * TODO(b/323830434): Update logic for separators after 4th row when UX finalizes. Note: This does + * not include a separator for the first month of data. */ fun Flow<PagingData<MediaGridItem.MediaItem>>.insertMonthSeparators(): Flow<PagingData<MediaGridItem>> { @@ -99,3 +101,17 @@ fun Flow<PagingData<MediaGridItem.MediaItem>>.insertMonthSeparators(): } } } + +/** + * An extension function which filters all the available user profiles based on whether a profile is + * hidden or not. + * + * @return A list of all the user profiles available to the photopicker + */ +fun Flow<UserStatus>.getUserProfilesVisibleToPhotopicker(): Flow<List<UserProfile>> { + return this.map { + it.allProfiles.filterNot { + it.disabledReasons.contains(UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW) + } + } +} diff --git a/photopicker/src/com/android/photopicker/extensions/Intent.kt b/photopicker/src/com/android/photopicker/extensions/Intent.kt index 5531c8302..2b874e80c 100644 --- a/photopicker/src/com/android/photopicker/extensions/Intent.kt +++ b/photopicker/src/com/android/photopicker/extensions/Intent.kt @@ -55,6 +55,8 @@ fun Intent.getPhotopickerSelectionLimitOrDefault(default: Int): Int { getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) ) { MediaStore.getPickImagesMaxLimit() + } else if (getAction() == MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) { + MediaStore.getPickImagesMaxLimit() } else { // No EXTRA_PICK_IMAGES_MAX was set, return the provided default default diff --git a/photopicker/src/com/android/photopicker/extensions/Modifier.kt b/photopicker/src/com/android/photopicker/extensions/Modifier.kt index bd04906c8..4e75f4f99 100644 --- a/photopicker/src/com/android/photopicker/extensions/Modifier.kt +++ b/photopicker/src/com/android/photopicker/extensions/Modifier.kt @@ -16,16 +16,20 @@ package com.android.photopicker.extensions -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +import android.os.Build +import android.view.SurfaceControlViewHost +import androidx.annotation.RequiresApi +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.isUnspecified +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -84,3 +88,132 @@ fun Modifier.circleBackground( return this then backgroundModifier then layoutModifier } + +/** + * Transfer necessary touch events occurred on Photos/Albums grid to host at runtime in Embedded + * Photopicker + * + * @param state the state of Photos/albums grid. If state is null means Photos/Albums grid has not + * requested the custom modifier + * @param isExpanded the updates on current status of embedded photopicker + * @param host the instance of [SurfaceControlViewHost] + * @return a [Modifier] to transfer the touch gestures at runtime in Embedded photopicker + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +fun Modifier.transferGridTouchesToHostInEmbedded( + state: LazyGridState, + isExpanded: State<Boolean>, + host: SurfaceControlViewHost +): Modifier { + return this then + transferTouchesToSurfaceControlViewHost( + state = state, + isExpanded = isExpanded, + host = host, + ) +} + +/** + * Transfer necessary touch events occurred outside of Photos/Albums grid to host on runtime in + * Embedded Photopicker + * + * @param host the instance of [SurfaceControlViewHost] + * @return a [Modifier] to transfer the touch gestures at runtime in Embedded photopicker + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +fun Modifier.transferTouchesToHostInEmbedded(host: SurfaceControlViewHost): Modifier { + return this then + transferTouchesToSurfaceControlViewHost(state = null, isExpanded = null, host = host) +} + +/** + * Transfer necessary touch events to host on runtime in Embedded Photopicker + * + * @param state the state of Photos/albums grid. If state is null means Photos/Albums grid has not + * requested the custom modifier + * @param isExpanded the updates on current status of embedded photopicker + * @param host the instance of [SurfaceControlViewHost] + * @return a [Modifier] to transfer the touch gestures at runtime in Embedded photopicker + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +private fun Modifier.transferTouchesToSurfaceControlViewHost( + state: LazyGridState?, + isExpanded: State<Boolean>?, + host: SurfaceControlViewHost +): Modifier { + + /** + * Initial y position when user touches the screen or when [PointerEventType.Press] is received + */ + var initialY = 0F + + /** + * Difference in Y position with respect to initialY as user starts scrolling on the screen, to + * know the direction of the movement + */ + var dy = 0F + + val pointerInputModifier = + pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + // Suspend until next pointer event + val event: PointerEvent = awaitPointerEvent() + event.changes.forEach { change -> + if (state != null) { + when (event.type) { + PointerEventType.Press -> { + // Set initial Y position when user touches the screen + initialY = change.position.y + } + PointerEventType.Move -> { + // Position difference with respect to initial position + dy = change.position.y - initialY + } + PointerEventType.Release -> { + // Resetting the position change for next touch event + dy = 0F + } + } + } + } + + // Todo(b/356790658) : Avoid recalculate these every time, just do it when + // argument changes + val isGridCollapsed = state != null && isExpanded != null && !isExpanded.value + val isGridExpanded = state != null && isExpanded != null && isExpanded.value + + // Event is done being processed, make a decision about if this event should + // be transferred + val shouldTransferToHost = + when { + + // Never transfer if the event type isn't move + event.type != PointerEventType.Move -> false + + // Case for Not Grid attached modifiers + state == null -> true + + // Case for grid attached when embedded is collapsed + isGridCollapsed && dy != 0F -> true + + // Case for grid attached when embedded is expanded, and + // the lazy grid is at the top of its scroll container + isGridExpanded && + (state.firstVisibleItemIndex == 0 && + state.firstVisibleItemScrollOffset == 0 && + dy > 0) -> true + + // Otherwise don't transfer + else -> false + } + + if (shouldTransferToHost) { + // TODO(b/356671436): Use V API when available + @Suppress("DEPRECATION") host.transferTouchGestureToHost() + } + } + } + } + return this then pointerInputModifier +} diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt index ed810d719..de26312da 100644 --- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt +++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt @@ -25,6 +25,8 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource @@ -33,6 +35,11 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.android.photopicker.R import com.android.photopicker.core.components.MediaGridItem import com.android.photopicker.core.components.mediaGrid +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.navigation.LocalNavController import com.android.photopicker.core.navigation.PhotopickerDestinations @@ -43,6 +50,7 @@ import com.android.photopicker.extensions.navigateToAlbumMediaGrid import com.android.photopicker.extensions.navigateToPhotoGrid import com.android.photopicker.features.navigationbar.NavigationBarButton import com.android.photopicker.features.photogrid.PhotoGridFeature +import kotlinx.coroutines.launch /** The number of grid cells per row for Phone / narrow layouts */ private val CELLS_PER_ROW_FOR_ALBUM_GRID = 2 @@ -65,6 +73,9 @@ fun AlbumGrid(viewModel: AlbumGridViewModel = obtainViewModel()) { val state = rememberLazyGridState() val navController = LocalNavController.current val featureManager = LocalFeatureManager.current + val configuration = LocalPhotopickerConfiguration.current + val events = LocalEvents.current + val scope = rememberCoroutineScope() // Use the expanded layout any time the Width is Medium or larger. val isExpandedScreen: Boolean = @@ -84,8 +95,20 @@ fun AlbumGrid(viewModel: AlbumGridViewModel = obtainViewModel()) { // pretty well as is. if (dragAmount > 0) { // Positive is a right swipe - if (featureManager.isFeatureEnabled(PhotoGridFeature::class.java)) + if (featureManager.isFeatureEnabled(PhotoGridFeature::class.java)) { navController.navigateToPhotoGrid() + // Dispatch UI event to indicate switching to photos tab + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.ALBUM_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.SWITCH_PICKER_TAB + ) + ) + } + } } } ) @@ -96,8 +119,28 @@ fun AlbumGrid(viewModel: AlbumGridViewModel = obtainViewModel()) { mediaGrid( items = items, onItemClick = { item -> - if (item is MediaGridItem.AlbumItem) + if (item is MediaGridItem.AlbumItem) { + // Dispatch events to log album related details + scope.launch { + events.dispatch( + Event.LogPhotopickerAlbumOpenedUIEvent( + FeatureToken.ALBUM_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + item.album + ) + ) + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.ALBUM_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_ALBUMS_INTERACTION + ) + ) + } navController.navigateToAlbumMediaGrid(album = item.album) + } }, isExpandedScreen = isExpandedScreen, columns = @@ -110,6 +153,19 @@ fun AlbumGrid(viewModel: AlbumGridViewModel = obtainViewModel()) { contentPadding = PaddingValues(MEASUREMENT_HORIZONTAL_CELL_SPACING_ALBUM_GRID), state = state, ) + LaunchedEffect(Unit) { + // Dispatch UI event to denote loading of media albums + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PHOTO_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.UI_LOADED_ALBUMS + ) + ) + } + } } } @@ -120,9 +176,26 @@ fun AlbumGrid(viewModel: AlbumGridViewModel = obtainViewModel()) { @Composable fun AlbumGridNavButton(modifier: Modifier) { val navController = LocalNavController.current + val scope = rememberCoroutineScope() + val events = LocalEvents.current + val sessionId = LocalPhotopickerConfiguration.current.sessionId + val packageUid = LocalPhotopickerConfiguration.current.callingPackageUid ?: -1 NavigationBarButton( - onClick = navController::navigateToAlbumGrid, + onClick = { + // Dispatch UI event to denote switching to albums tab + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.ALBUM_GRID.token, + sessionId, + packageUid, + Telemetry.UiEvent.SWITCH_PICKER_TAB + ) + ) + } + navController.navigateToAlbumGrid() + }, modifier = modifier, isCurrentRoute = { route -> route == PhotopickerDestinations.ALBUM_GRID.route }, ) { diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt index f52e009e2..f763737e6 100644 --- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt +++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt @@ -64,7 +64,12 @@ class AlbumGridFeature : PhotopickerUiFeature { override val eventsConsumed = emptySet<RegisteredEventClass>() /** Events produced by the Album grid */ - override val eventsProduced = setOf(Event.ShowSnackbarMessage::class.java) + override val eventsProduced = + setOf( + Event.ShowSnackbarMessage::class.java, + Event.LogPhotopickerUIEvent::class.java, + Event.LogPhotopickerAlbumOpenedUIEvent::class.java + ) override fun registerLocations(): List<Pair<Location, Int>> { return listOf( diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt index ba473e05e..2ef86432e 100644 --- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt +++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt @@ -25,6 +25,7 @@ import androidx.paging.cachedIn import com.android.photopicker.core.components.MediaGridItem import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.Telemetry import com.android.photopicker.core.features.FeatureToken.ALBUM_GRID import com.android.photopicker.core.selection.Selection import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED @@ -129,9 +130,16 @@ constructor( * in the viewModelScope to ensure they aren't cancelled if the user navigates away from the * AlbumMediaGrid composable. */ - fun handleAlbumMediaGridItemSelection(item: Media, selectionLimitExceededMessage: String) { + fun handleAlbumMediaGridItemSelection( + item: Media, + selectionLimitExceededMessage: String, + album: Group.Album + ) { + // Update the selectable values in the received media item. + val updatedMediaItem = + Media.withSelectable(item, /* selectionSource */ Telemetry.MediaLocation.ALBUM, album) scope.launch { - val result = selection.toggle(item) + val result = selection.toggle(updatedMediaItem) if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) { events.dispatch( Event.ShowSnackbarMessage(ALBUM_GRID.token, selectionLimitExceededMessage) diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt index e944812b4..0f45e81b1 100644 --- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt +++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt @@ -16,6 +16,7 @@ package com.android.photopicker.features.albumgrid +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS import androidx.compose.foundation.layout.Column @@ -25,12 +26,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Image +import androidx.compose.material.icons.outlined.PhotoCamera import androidx.compose.material.icons.outlined.StarOutline import androidx.compose.material.icons.outlined.Videocam import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration @@ -45,6 +49,10 @@ import com.android.photopicker.core.components.EmptyState import com.android.photopicker.core.components.MediaGridItem import com.android.photopicker.core.components.mediaGrid import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.navigation.LocalNavController import com.android.photopicker.core.navigation.PhotopickerDestinations @@ -56,6 +64,7 @@ import com.android.photopicker.extensions.navigateToPreviewMedia import com.android.photopicker.features.preview.PreviewFeature import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch /** * Primary composable for drawing the Album content grid on @@ -101,6 +110,9 @@ private fun AlbumMediaGrid( val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit val selectionLimitExceededMessage = stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit) + val scope = rememberCoroutineScope() + val events = LocalEvents.current + val configuration = LocalPhotopickerConfiguration.current // Use the expanded layout any time the Width is Medium or larger. val isExpandedScreen: Boolean = @@ -144,18 +156,52 @@ private fun AlbumMediaGrid( if (item is MediaGridItem.MediaItem) { viewModel.handleAlbumMediaGridItemSelection( item.media, - selectionLimitExceededMessage + selectionLimitExceededMessage, + album ) } }, onItemLongPress = { item -> + // Dispatch UI event to log long pressing the media item + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_LONG_SELECT_MEDIA_ITEM + ) + ) + } // If the [PreviewFeature] is enabled, launch the preview route. if (isPreviewEnabled && item is MediaGridItem.MediaItem) { + // Dispatch UI event to log entry into preview mode + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE + ) + ) + } navController.navigateToPreviewMedia(item.media) } }, state = state, ) + LaunchedEffect(Unit) { + // Dispatch UI event to log loading of album contents + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PHOTO_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.UI_LOADED_ALBUM_CONTENTS + ) + ) + } } } } @@ -182,6 +228,12 @@ private fun getEmptyStateContentForAlbum(album: Group.Album): Triple<String, Str stringResource(R.string.photopicker_videos_empty_state_body), Icons.Outlined.Videocam, ) + ALBUM_ID_CAMERA -> + Triple( + stringResource(R.string.photopicker_photos_empty_state_title), + stringResource(R.string.photopicker_camera_empty_state_body), + Icons.Outlined.PhotoCamera, + ) // Use the empty state messages of the main photo grid in all other cases. else -> Triple( diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt index a5ea043e1..bad19e6a6 100644 --- a/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt +++ b/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt @@ -19,6 +19,7 @@ package com.android.photopicker.features.cloudmedia import android.content.Intent import android.provider.MediaStore import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -26,8 +27,12 @@ import com.android.photopicker.R import com.android.photopicker.core.banners.Banner import com.android.photopicker.core.banners.BannerDefinitions import com.android.photopicker.core.banners.BannerState +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents import com.android.photopicker.core.events.RegisteredEventClass +import com.android.photopicker.core.events.Telemetry import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.core.features.FeatureToken @@ -41,6 +46,7 @@ import com.android.photopicker.data.model.CollectionInfo import com.android.photopicker.data.model.MediaSource import com.android.photopicker.data.model.Provider import com.android.photopicker.features.overflowmenu.OverflowMenuItem +import kotlinx.coroutines.launch /** * Feature class for the Photopicker's cloud media implementation. @@ -169,7 +175,11 @@ class CloudMediaFeature : PhotopickerUiFeature { override val eventsConsumed = setOf<RegisteredEventClass>() /** Events produced by the Cloud Media */ - override val eventsProduced = setOf<RegisteredEventClass>() + override val eventsProduced = + setOf<RegisteredEventClass>( + Event.LogPhotopickerMenuInteraction::class.java, + Event.LogPhotopickerUIEvent::class.java + ) override fun registerLocations(): List<Pair<Location, Int>> { return listOf( @@ -190,6 +200,9 @@ class CloudMediaFeature : PhotopickerUiFeature { modifier: Modifier, params: LocationParams, ) { + val events = LocalEvents.current + val scope = rememberCoroutineScope() + val configuration = LocalPhotopickerConfiguration.current when (location) { Location.MEDIA_PRELOADER -> MediaPreloader(modifier, params) Location.OVERFLOW_MENU_ITEMS -> { @@ -200,6 +213,18 @@ class CloudMediaFeature : PhotopickerUiFeature { onClick = { clickAction?.onClick() context.startActivity(Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS)) + // Dispatch event to log user's interactiuon with the cloud settings menu + // item in the photopicker + scope.launch { + events.dispatch( + Event.LogPhotopickerMenuInteraction( + token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.MenuItemSelected.CLOUD_SETTINGS + ) + ) + } } ) } diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt index 963ae5809..ffa864de5 100644 --- a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt +++ b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt @@ -36,6 +36,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,8 +44,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.photopicker.R +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocationParams import com.android.photopicker.core.obtainViewModel +import kotlinx.coroutines.launch /* Size of the spacer between dialog elements. */ private val MEASUREMENT_DIALOG_SPACER_SIZE = 24.dp @@ -80,10 +87,27 @@ fun MediaPreloader( // These must be set by the parent composable for the preloader to have any effect. val preloaderParameters = params as? LocationParams.WithMediaPreloader + val configuration = LocalPhotopickerConfiguration.current + val scope = rememberCoroutineScope() + val events = LocalEvents.current + preloaderParameters?.let { LaunchedEffect(params) { // Listen for emissions of media to preload, and begin the preload when requested. - it.preloadMedia.collect { media -> viewModel.startPreload(media, it.obtainDeferred()) } + it.preloadMedia.collect { media -> + // Dispatch UI event to log the beginning of media items preloading + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_PRELOADING_START + ) + ) + } + viewModel.startPreload(media, it.obtainDeferred()) + } } } // If no preloaderParameters were passed to this location, there is no way to trigger diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt index 634724751..7a3292eea 100644 --- a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt +++ b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt @@ -22,6 +22,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.photopicker.core.Background import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.selection.Selection import com.android.photopicker.core.user.UserMonitor import com.android.photopicker.data.model.Media @@ -98,6 +102,7 @@ constructor( private val selection: Selection<Media>, private val userMonitor: UserMonitor, private val configurationManager: ConfigurationManager, + private val events: Events, ) : ViewModel() { companion object { @@ -151,6 +156,8 @@ constructor( initialValue = _dialogData.value ) + val configuration = configurationManager.configuration.value + init { // If the active user's resolver changes, cancel any pending preload work. @@ -302,6 +309,17 @@ constructor( CloudMediaFeature.TAG, "Failure detected, cancelling the rest of the preload operation." ) + // Log failure of media items preloading + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_PRELOADING_FAILED + ) + ) + } // Mark the item as failed in the result status. mutex.withLock { remoteItems.set(item, LoadResult.FAILED) } // Emit a new heartbeat so the monitor will react to this failure. @@ -372,6 +390,17 @@ constructor( // application to send the selected Media to the caller. Log.d(CloudMediaFeature.TAG, "Preload operation was successful.") deferred.complete(true) + // Dispatch UI event to mark the end of preloading of media items + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_PRELOADING_FINISHED + ) + ) + } } } @@ -399,6 +428,17 @@ constructor( job?.let { it.cancel() Log.i(CloudMediaFeature.TAG, "Preload operation was cancelled.") + // Dispatch an event to log cancellation of media items preloading + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.CORE.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_PRELOADING_CANCELLED + ) + ) + } } // In the event of single selection mode, the selection needs to be cleared. diff --git a/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt b/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt index 4abae8d1a..50614bfdc 100644 --- a/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt +++ b/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt @@ -147,16 +147,17 @@ fun NavigationBar(modifier: Modifier = Modifier) { ) NavigationBarButtons(Modifier.weight(1f)) - - LocalFeatureManager.current.composeLocation( - Location.OVERFLOW_MENU, - // Weight should match the profile switcher slot so they are the same size. - modifier = - Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH) - .padding(end = MEASUREMENT_ICON_BUTTON_OUTSIDE_PADDING) - ) } } + + // Always show the overflow menu, it will hide itself if it has no content. + LocalFeatureManager.current.composeLocation( + Location.OVERFLOW_MENU, + // Weight should match the profile switcher slot so they are the same size. + modifier = + Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH) + .padding(end = MEASUREMENT_ICON_BUTTON_OUTSIDE_PADDING) + ) } } diff --git a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt index 83f4da820..b0ee47f07 100644 --- a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt +++ b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt @@ -31,15 +31,22 @@ import androidx.compose.runtime.Composable 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.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.photopicker.R import com.android.photopicker.core.components.ElevationTokens +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.features.Location import com.android.photopicker.core.features.LocationParams +import kotlinx.coroutines.launch /** * Top of the OverflowMenu feature. @@ -63,10 +70,30 @@ fun OverflowMenu(modifier: Modifier = Modifier) { // Only show the overflow menu anchor if there will actually be items to select. if (LocalFeatureManager.current.getSizeOfLocationInRegistry(Location.OVERFLOW_MENU_ITEMS) > 0) { var expanded by remember { mutableStateOf(false) } + val events = LocalEvents.current + val scope = rememberCoroutineScope() + val configuration = LocalPhotopickerConfiguration.current // Wrapped in a box to consume anything in the incoming modifier. Box(modifier = modifier) { - IconButton(onClick = { expanded = !expanded }) { + IconButton( + onClick = { + expanded = !expanded + // Dispatch UI event to log interaction with picker menu + if (expanded) { + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.OVERFLOW_MENU.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_MENU_CLICK + ) + ) + } + } + } + ) { Icon( Icons.Filled.MoreVert, contentDescription = diff --git a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt index 6008d427c..de7f0c135 100644 --- a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt +++ b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt @@ -19,6 +19,8 @@ package com.android.photopicker.features.overflowmenu import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.RegisteredEventClass import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration @@ -34,7 +36,8 @@ class OverflowMenuFeature : PhotopickerUiFeature { companion object Registration : FeatureRegistration { override val TAG: String = "PhotopickerOverflowMenuFeature" - override fun isEnabled(config: PhotopickerConfiguration) = true + override fun isEnabled(config: PhotopickerConfiguration) = + config.runtimeEnv != PhotopickerRuntimeEnv.EMBEDDED override fun build(featureManager: FeatureManager) = OverflowMenuFeature() } @@ -49,7 +52,8 @@ class OverflowMenuFeature : PhotopickerUiFeature { override val eventsConsumed = setOf<RegisteredEventClass>() /** Events produced by the OverflowMenu */ - override val eventsProduced = setOf<RegisteredEventClass>() + override val eventsProduced = + setOf<RegisteredEventClass>(Event.LogPhotopickerUIEvent::class.java) @Composable override fun compose(location: Location, modifier: Modifier, params: LocationParams) { diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt index 3f9e5bbf0..f84b1ac91 100644 --- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt +++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt @@ -16,8 +16,11 @@ package com.android.photopicker.features.photogrid +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -27,8 +30,10 @@ import androidx.compose.material.icons.outlined.Image import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalConfiguration @@ -38,11 +43,20 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import com.android.photopicker.R +import com.android.photopicker.core.banners.Banner +import com.android.photopicker.core.banners.BannerDefinitions import com.android.photopicker.core.components.EmptyState import com.android.photopicker.core.components.MediaGridItem import com.android.photopicker.core.components.mediaGrid import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.embedded.LocalEmbeddedState +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager +import com.android.photopicker.core.features.Location import com.android.photopicker.core.navigation.LocalNavController import com.android.photopicker.core.navigation.PhotopickerDestinations import com.android.photopicker.core.navigation.PhotopickerDestinations.PHOTO_GRID @@ -55,6 +69,10 @@ import com.android.photopicker.extensions.navigateToPreviewMedia import com.android.photopicker.features.albumgrid.AlbumGridFeature import com.android.photopicker.features.navigationbar.NavigationBarButton import com.android.photopicker.features.preview.PreviewFeature +import kotlinx.coroutines.launch + +private val MEASUREMENT_BANNER_PADDING = + PaddingValues(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 24.dp) /** * Primary composable for drawing the main PhotoGrid on [PhotopickerDestinations.PHOTO_GRID] @@ -84,22 +102,51 @@ fun PhotoGrid(viewModel: PhotoGridViewModel = obtainViewModel()) { val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit val selectionLimitExceededMessage = stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit) + val events = LocalEvents.current + val scope = rememberCoroutineScope() + val configuration = LocalPhotopickerConfiguration.current - Column( - modifier = - Modifier.fillMaxSize().pointerInput(Unit) { - detectHorizontalDragGestures( - onHorizontalDrag = { _, dragAmount -> - // This may need some additional fine tuning by looking at a certain - // distance in dragAmount, but initial testing suggested this worked - // pretty well as is. - if (dragAmount < 0) { - // Negative is a left swipe - if (featureManager.isFeatureEnabled(AlbumGridFeature::class.java)) - navController.navigateToAlbumGrid() + // Modifier applied when photo grid to album grid navigation is disabled + val baseModifier = Modifier.fillMaxSize() + // Modifier applied when photo grid to album grid navigation is enabled + val modifierWithNavigation = + Modifier.fillMaxSize().pointerInput(Unit) { + detectHorizontalDragGestures( + onHorizontalDrag = { _, dragAmount -> + // This may need some additional fine tuning by looking at a certain + // distance in dragAmount, but initial testing suggested this worked + // pretty well as is. + if (dragAmount < 0) { + // Negative is a left swipe + if (featureManager.isFeatureEnabled(AlbumGridFeature::class.java)) { + // Dispatch UI event to indicate switching to albums tab + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.ALBUM_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.SWITCH_PICKER_TAB + ) + ) + } + navController.navigateToAlbumGrid() } } - ) + } + ) + } + + val isEmbedded = + LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED + val isExpanded = LocalEmbeddedState.current?.isExpanded ?: false + val isEmbeddedAndCollapsed = isEmbedded && !isExpanded + + Column( + modifier = + when (isEmbeddedAndCollapsed) { + true -> baseModifier + false -> modifierWithNavigation } ) { val isEmptyAndNoMorePages = @@ -121,42 +168,140 @@ fun PhotoGrid(viewModel: PhotoGridViewModel = obtainViewModel()) { ) } else -> { + + // When the PhotoGrid is ready to show, also collect the latest banner + // data from [BannerManager] so it can be placed inside of the mediaGrid's + // scroll container. + val currentBanner by viewModel.banners.collectAsStateWithLifecycle() + mediaGrid( items = items, isExpandedScreen = isExpandedScreen, selection = selection, + bannerContent = { AnimatedBannerWrapper(currentBanner) }, onItemClick = { item -> if (item is MediaGridItem.MediaItem) { viewModel.handleGridItemSelection( item = item.media, selectionLimitExceededMessage = selectionLimitExceededMessage ) + // Log user's interaction with picker's main grid(photo grid) + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PHOTO_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_MAIN_GRID_INTERACTION + ) + ) + } } }, onItemLongPress = { item -> + // Log long pressing a media item in the photo grid + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_LONG_SELECT_MEDIA_ITEM + ) + ) + } // If the [PreviewFeature] is enabled, launch the preview route. if (isPreviewEnabled) { - if (item is MediaGridItem.MediaItem) + if (item is MediaGridItem.MediaItem) { + // Log entry into the photopicker preview mode + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE + ) + ) + } navController.navigateToPreviewMedia(item.media) + } } }, state = state, ) + LaunchedEffect(Unit) { + // Log loading of photos in the photo grid + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PHOTO_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.UI_LOADED_PHOTOS + ) + ) + } } } } } /** + * A container that animates its size to show the banner if one is defined. It also handles the + * banner's onDismiss action by sending the dismissal to the [PhotoGridViewModel]. + * + * @param currentBanner The current banner that [BannerManager] is exposing. + */ +@Composable +private fun AnimatedBannerWrapper( + currentBanner: Banner?, + viewModel: PhotoGridViewModel = obtainViewModel(), +) { + Box(modifier = Modifier.animateContentSize()) { + currentBanner?.let { + Banner( + it, + modifier = Modifier.padding(MEASUREMENT_BANNER_PADDING), + onDismiss = { + val declaration = it.declaration + + // Coerce the type back to [BannerDefinitions] + // so that it can be dismissed. + if (declaration is BannerDefinitions) { + viewModel.markBannerAsDismissed(declaration) + } + } + ) + } + } +} + +/** * The navigation button for the main photo grid. Composable for * [Location.NAVIGATION_BAR_NAV_BUTTON] */ @Composable fun PhotoGridNavButton(modifier: Modifier) { val navController = LocalNavController.current + val scope = rememberCoroutineScope() + val events = LocalEvents.current + val configuration = LocalPhotopickerConfiguration.current NavigationBarButton( - onClick = navController::navigateToPhotoGrid, + onClick = { + // Log switching tab to the photos tab + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PHOTO_GRID.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.SWITCH_PICKER_TAB + ) + ) + } + navController.navigateToPhotoGrid() + }, modifier = modifier, isCurrentRoute = { route -> route == PHOTO_GRID.route }, ) { diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt index 215a67763..7e0b7ea9b 100644 --- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt +++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt @@ -59,7 +59,8 @@ class PhotoGridFeature : PhotopickerUiFeature { override val eventsConsumed = emptySet<RegisteredEventClass>() /** Events produced by the Photo grid */ - override val eventsProduced = setOf(Event.ShowSnackbarMessage::class.java) + override val eventsProduced = + setOf(Event.ShowSnackbarMessage::class.java, Event.LogPhotopickerUIEvent::class.java) override fun registerLocations(): List<Pair<Location, Int>> { return listOf( diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt index 5c316d36f..2d74275c1 100644 --- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt +++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt @@ -21,8 +21,11 @@ import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import com.android.photopicker.core.banners.BannerDefinitions +import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.Telemetry import com.android.photopicker.core.features.FeatureToken.PHOTO_GRID import com.android.photopicker.core.selection.Selection import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED @@ -50,6 +53,7 @@ constructor( private val selection: Selection<Media>, private val dataService: DataService, private val events: Events, + private val bannerManager: BannerManager, ) : ViewModel() { // Check if a scope override was injected before using the default [viewModelScope] @@ -98,14 +102,40 @@ constructor( // when navigating back to the PhotoGrid route. .cachedIn(scope) + /** Export the [Banner] flow from BannerManager to the UI */ + val banners = bannerManager.flow + + /** + * Dismissal handler from the UI to mark a particular banner as dismissed by the user. This call + * is handed off to the bannerManager to persist any relevant dismissal state. + * + * Afterwards, refreshBanners is called to check for any new Banners from [BannerManager]. + */ + fun markBannerAsDismissed(banner: BannerDefinitions) { + scope.launch { + bannerManager.markBannerAsDismissed(banner) + bannerManager.refreshBanners() + } + } + /** * Click handler that is called when items in the grid are clicked. Selection updates are made * in the viewModelScope to ensure they aren't canceled if the user navigates away from the * PhotoGrid composable. */ - fun handleGridItemSelection(item: Media, selectionLimitExceededMessage: String) { + fun handleGridItemSelection( + item: Media, + selectionLimitExceededMessage: String, + ) { + // Update the selectable values in the received media object. + val updatedMediaItem = + Media.withSelectable( + item, /* selectionSource */ + Telemetry.MediaLocation.MAIN_GRID, /* album */ + null + ) scope.launch { - val result = selection.toggle(item) + val result = selection.toggle(updatedMediaItem) if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) { scope.launch { events.dispatch( diff --git a/photopicker/src/com/android/photopicker/features/preview/Preview.kt b/photopicker/src/com/android/photopicker/features/preview/Preview.kt index 23954cc4c..8d566d0b0 100644 --- a/photopicker/src/com/android/photopicker/features/preview/Preview.kt +++ b/photopicker/src/com/android/photopicker/features/preview/Preview.kt @@ -38,7 +38,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.outlined.Circle import androidx.compose.material3.ButtonDefaults @@ -56,6 +55,7 @@ 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 import androidx.compose.ui.Modifier @@ -69,6 +69,12 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.android.photopicker.R import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.Location import com.android.photopicker.core.glide.RESOLUTION_REQUESTED import com.android.photopicker.core.glide.Resolution @@ -82,6 +88,7 @@ import com.android.photopicker.core.theme.LocalFixedAccentColors import com.android.photopicker.data.model.Media import com.android.photopicker.extensions.navigateToPreviewSelection import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch /** * Entry point for the [PhotopickerDestinations.PREVIEW_SELECTION] and @@ -126,7 +133,8 @@ fun PreviewSelection( viewModel .getPreviewMediaIncludingPreGrantedItems( selectionSnapshot, - LocalPhotopickerConfiguration.current + LocalPhotopickerConfiguration.current, + /* isSingleItemPreview */ false ) .collectAsLazyPagingItems() } @@ -148,7 +156,7 @@ fun PreviewSelection( ) { Row( modifier = - Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 4.dp, start = 4.dp), + Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 4.dp, start = 8.dp), ) { // back button IconButton(onClick = { navController.popBackStack() }) { @@ -174,9 +182,9 @@ fun PreviewSelection( Modifier.align(Alignment.Center), selection, state, - snackbarHostState + snackbarHostState, + /* singleItemPreview */ previewSingleItem ) - IconButton( modifier = Modifier.align(Alignment.TopStart), onClick = { @@ -186,7 +194,9 @@ fun PreviewSelection( ) { if (currentSelection.contains(selection.get(state.currentPage))) { Icon( - Icons.Filled.CheckCircle, + ImageVector.vectorResource( + R.drawable.photopicker_selected_media + ), modifier = Modifier // Background is necessary because the icon has negative @@ -227,7 +237,7 @@ fun PreviewSelection( Row( modifier = Modifier.fillMaxWidth() - .padding(bottom = 68.dp, start = 4.dp, end = 16.dp, top = 12.dp), + .padding(bottom = 48.dp, start = 4.dp, end = 16.dp, top = 12.dp), horizontalArrangement = Arrangement.SpaceBetween ) { if (previewSingleItem) { @@ -277,7 +287,12 @@ private fun SelectionButton( }, colors = ButtonDefaults.textButtonColors( - contentColor = LocalFixedAccentColors.current.primaryFixedDim + contentColor = + // The background color for Preview is always fixed to Black, so when the + // custom accent color is defined, switch to a White color for this button + // so it doesn't clash with the custom color. + if (CustomAccentColorScheme.current.isAccentColorDefined()) Color.White + else LocalFixedAccentColors.current.primaryFixedDim ) ) { if (currentSelection.size > 0) { @@ -310,7 +325,8 @@ private fun PreviewPager( modifier: Modifier, selection: LazyPagingItems<Media>, state: PagerState, - snackbarHostState: SnackbarHostState + snackbarHostState: SnackbarHostState, + singleItemPreview: Boolean, ) { // Preview session state to keep track if the video player's audio is muted. var audioIsMuted by remember { mutableStateOf(true) } @@ -322,9 +338,15 @@ private fun PreviewPager( val media = selection.get(page) if (media != null) { when (media) { - is Media.Image -> ImageUi(media) + is Media.Image -> ImageUi(media, singleItemPreview) is Media.Video -> - VideoUi(media, audioIsMuted, { audioIsMuted = it }, snackbarHostState) + VideoUi( + media, + audioIsMuted, + { audioIsMuted = it }, + snackbarHostState, + singleItemPreview + ) } } } @@ -336,7 +358,32 @@ private fun PreviewPager( * @param image */ @Composable -private fun ImageUi(image: Media.Image) { +private fun ImageUi(image: Media.Image, singleItemPreview: Boolean) { + if (singleItemPreview) { + val events = LocalEvents.current + val scope = rememberCoroutineScope() + val configuration = LocalPhotopickerConfiguration.current + + scope.launch { + val mediaType = + if (image.mimeType.contains("gif")) { + Telemetry.MediaType.GIF + } else { + Telemetry.MediaType.PHOTO + } + // Mark entry into preview mode by long pressing on the media item + events.dispatch( + Event.LogPhotopickerPreviewInfo( + FeatureToken.PREVIEW.token, + configuration.sessionId, + Telemetry.PreviewModeEntry.LONG_PRESS, + previewItemCount = 1, + mediaType, + Telemetry.VideoPlayBackInteractions.UNSET_VIDEO_PLAYBACK_INTERACTION + ) + ) + } + } loadMedia( media = image, resolution = Resolution.FULL, @@ -355,9 +402,20 @@ private fun ImageUi(image: Media.Image) { @Composable fun PreviewSelectionButton(modifier: Modifier) { val navController = LocalNavController.current + val events = LocalEvents.current + val scope = rememberCoroutineScope() + // TODO(b/353659535): Use Selection.size api when available + val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle() + val previewItemCount = currentSelection.size + val configuration = LocalPhotopickerConfiguration.current TextButton( - onClick = navController::navigateToPreviewSelection, + onClick = { + scope.launch { + logPreviewSelectionButtonClicked(configuration, previewItemCount, events) + } + navController.navigateToPreviewSelection() + }, modifier = modifier, ) { Text( @@ -369,3 +427,44 @@ fun PreviewSelectionButton(modifier: Modifier) { ) } } + +/** + * Dispatches all the relevant logging events for the picker's preview mode when the Preview button + * is clicked + */ +private suspend fun logPreviewSelectionButtonClicked( + configuration: PhotopickerConfiguration, + previewItemCount: Int, + events: Events, +) { + // Log preview item details + events.dispatch( + Event.LogPhotopickerPreviewInfo( + FeatureToken.PREVIEW.token, + configuration.sessionId, + Telemetry.PreviewModeEntry.VIEW_SELECTED, + previewItemCount, + Telemetry.MediaType.UNSET_MEDIA_TYPE, + Telemetry.VideoPlayBackInteractions.UNSET_VIDEO_PLAYBACK_INTERACTION + ) + ) + + // Log preview related UI events including clicking the 'preview' button + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE + ) + ) + + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_CLICK_VIEW_SELECTED + ) + ) +} diff --git a/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt b/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt index a74102163..7a9dde03d 100644 --- a/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt +++ b/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt @@ -23,6 +23,8 @@ import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.RegisteredEventClass import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration @@ -46,7 +48,8 @@ class PreviewFeature : PhotopickerUiFeature { companion object Registration : FeatureRegistration { override val TAG: String = "PhotopickerPreviewFeature" - override fun isEnabled(config: PhotopickerConfiguration) = true + override fun isEnabled(config: PhotopickerConfiguration) = + config.runtimeEnv != PhotopickerRuntimeEnv.EMBEDDED override fun build(featureManager: FeatureManager) = PreviewFeature() @@ -59,7 +62,11 @@ class PreviewFeature : PhotopickerUiFeature { override val eventsConsumed = emptySet<RegisteredEventClass>() /** Events produced by the Preview page */ - override val eventsProduced = emptySet<RegisteredEventClass>() + override val eventsProduced = + setOf<RegisteredEventClass>( + Event.LogPhotopickerUIEvent::class.java, + Event.LogPhotopickerPreviewInfo::class.java + ) override fun registerLocations(): List<Pair<Location, Int>> { return listOf( diff --git a/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt b/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt index cdc4d1780..c7ea5a166 100644 --- a/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt +++ b/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt @@ -36,7 +36,12 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.cachedIn +import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.selection.Selection import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED import com.android.photopicker.core.selection.SelectionStrategy @@ -72,6 +77,8 @@ constructor( private val selection: Selection<Media>, private val userMonitor: UserMonitor, private val dataService: DataService, + private val events: Events, + private val configManager: ConfigurationManager, ) : ViewModel() { companion object { @@ -289,6 +296,19 @@ constructor( val binder = controllerBundle.getBinder(EXTRA_SURFACE_CONTROLLER) + val configuration = configManager.configuration.value + // UI event to mark the start of surface controller creation + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.CREATE_SURFACE_CONTROLLER_START + ) + ) + } + // Produce the [RemotePreviewControllerInfo] and save it for future re-use. val controllerInfo = RemotePreviewControllerInfo( @@ -314,6 +334,18 @@ constructor( try { controllerInfo.controller.onDestroy() + val configuration = configManager.configuration.value + // UI event to mark the end of surface controller creation + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PREVIEW.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.CREATE_SURFACE_CONTROLLER_END + ) + ) + } } catch (e: RemoteException) { Log.d(TAG, "Failed to destroy surface controller.", e) } diff --git a/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt b/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt index 63f1d1df4..547808dd8 100644 --- a/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt +++ b/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt @@ -60,6 +60,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -70,12 +71,18 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.os.bundleOf import com.android.photopicker.R +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.obtainViewModel 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 +import kotlinx.coroutines.launch /** [AudioAttributes] to use with all VideoUi instances. */ private val AUDIO_ATTRIBUTES = @@ -91,7 +98,7 @@ 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 = 16.dp +private val MEASUREMENT_PLAYER_CONTROLS_PADDING_VERTICAL = 12.dp /** Delay in milliseconds before the player controls are faded. */ private val TIME_MS_PLAYER_CONTROLS_FADE_DELAY = 3000L @@ -115,6 +122,7 @@ fun VideoUi( audioIsMuted: Boolean, onRequestAudioMuteChange: (Boolean) -> Unit, snackbarHostState: SnackbarHostState, + singleItemPreview: Boolean, viewModel: PreviewViewModel = obtainViewModel(), ) { @@ -145,6 +153,25 @@ fun VideoUi( val aspectRatio by produceAspectRatio(surfaceId, video) val context = LocalContext.current + val scope = rememberCoroutineScope() + val events = LocalEvents.current + val configuration = LocalPhotopickerConfiguration.current + + // Log that the video audio is muted + if (singleItemPreview && audioIsMuted) { + LaunchedEffect(video) { + events.dispatch( + Event.LogPhotopickerPreviewInfo( + FeatureToken.PREVIEW.token, + configuration.sessionId, + Telemetry.PreviewModeEntry.LONG_PRESS, + previewItemCount = 1, + Telemetry.MediaType.VIDEO, + Telemetry.VideoPlayBackInteractions.MUTE + ) + ) + } + } /** Run these effects when a new PlaybackInfo is received */ LaunchedEffect(playbackInfo) { @@ -200,8 +227,41 @@ fun VideoUi( areControlsVisible = areControlsVisible, onPlayPause = { when (playbackInfo.state) { - PlaybackState.STARTED -> controller.onMediaPause(surfaceId) - PlaybackState.PAUSED -> controller.onMediaPlay(surfaceId) + PlaybackState.STARTED -> { + if (singleItemPreview) { + // Log video playback interactions + scope.launch { + events.dispatch( + Event.LogPhotopickerPreviewInfo( + FeatureToken.PREVIEW.token, + configuration.sessionId, + Telemetry.PreviewModeEntry.LONG_PRESS, + previewItemCount = 1, + Telemetry.MediaType.VIDEO, + Telemetry.VideoPlayBackInteractions.PLAY + ) + ) + } + } + controller.onMediaPause(surfaceId) + } + PlaybackState.PAUSED -> { + if (singleItemPreview) { + scope.launch { + events.dispatch( + Event.LogPhotopickerPreviewInfo( + FeatureToken.PREVIEW.token, + configuration.sessionId, + Telemetry.PreviewModeEntry.LONG_PRESS, + previewItemCount = 1, + Telemetry.MediaType.VIDEO, + Telemetry.VideoPlayBackInteractions.PAUSE + ) + ) + } + } + controller.onMediaPlay(surfaceId) + } else -> {} } }, diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt index 3909d9919..fdf685cec 100644 --- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt +++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt @@ -45,7 +45,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.photopicker.R +import com.android.photopicker.core.StateSelector import com.android.photopicker.core.components.ElevationTokens +import com.android.photopicker.core.hideWhenState import com.android.photopicker.core.obtainViewModel import com.android.photopicker.core.user.UserProfile @@ -178,7 +180,9 @@ fun ProfileSelector( } } else { // Return a spacer which consumes the modifier so the space is still occupied, but is empty. - Spacer(modifier) + // Hide the spacer when the runtime is embedded, so that the navigation tabs stay in the + // center as the overflow menu is disabled in embedded runtime. + hideWhenState(StateSelector.Embedded) { Spacer(modifier) } } } diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt index 5c4dd0193..6843593d9 100644 --- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt +++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt @@ -19,6 +19,7 @@ package com.android.photopicker.features.profileselector import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.RegisteredEventClass import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration @@ -49,7 +50,8 @@ class ProfileSelectorFeature : PhotopickerUiFeature { override val eventsConsumed = setOf<RegisteredEventClass>() /** Events produced by the ProfileSelector */ - override val eventsProduced = setOf<RegisteredEventClass>() + override val eventsProduced = + setOf<RegisteredEventClass>(Event.LogPhotopickerUIEvent::class.java) @Composable override fun compose( diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt index 86afba71a..02d13094f 100644 --- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt +++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt @@ -21,12 +21,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.selection.Selection import com.android.photopicker.core.user.SwitchUserProfileResult import com.android.photopicker.core.user.UserMonitor import com.android.photopicker.core.user.UserProfile import com.android.photopicker.core.user.UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW import com.android.photopicker.data.model.Media +import com.android.photopicker.extensions.getUserProfilesVisibleToPhotopicker import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -47,6 +53,8 @@ constructor( private val scopeOverride: CoroutineScope?, private val selection: Selection<Media>, private val userMonitor: UserMonitor, + private val events: Events, + private val configurationManager: ConfigurationManager, ) : ViewModel() { companion object { @@ -64,12 +72,7 @@ constructor( /** All of the profiles that are available to Photopicker */ val allProfiles: StateFlow<List<UserProfile>> = userMonitor.userStatus - // For any profiles that contain a [QUIET_MODE_DO_NOT_SHOW] reason, this profile - // should be skipped as it is currently in quiet mode and the profile specs - // state it should be hidden from sharing surfaces when it is in quiet mode. - .map { - it.allProfiles.filterNot { it.disabledReasons.contains(QUIET_MODE_DO_NOT_SHOW) } - } + .getUserProfilesVisibleToPhotopicker() .stateIn( scope, SharingStarted.WhileSubscribed(), @@ -103,6 +106,16 @@ constructor( // If the profile is actually changed, ensure the selection is cleared since // content cannot be chosen from multiple profiles simultaneously. selection.clear() + val configuration = configurationManager.configuration.value + // Log switching user profile in the picker + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.PROFILE_SELECTOR.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.SWITCH_USER_PROFILE + ) + ) } } } diff --git a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt index 79212d1f8..877f4296e 100644 --- a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt +++ b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt @@ -49,6 +49,11 @@ import com.android.photopicker.R import com.android.photopicker.core.animations.emphasizedAccelerate import com.android.photopicker.core.animations.emphasizedDecelerate import com.android.photopicker.core.components.ElevationTokens +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.features.FeatureToken import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.features.Location import com.android.photopicker.core.features.LocationParams @@ -78,6 +83,8 @@ fun SelectionBar(modifier: Modifier = Modifier, params: LocationParams) { val selection = LocalSelection.current val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle() val visible = currentSelection.isNotEmpty() + val configuration = LocalPhotopickerConfiguration.current + val events = LocalEvents.current val scope = rememberCoroutineScope() // The entire selection bar is hidden if the selection is empty, and @@ -139,6 +146,17 @@ fun SelectionBar(modifier: Modifier = Modifier, params: LocationParams) { Spacer(Modifier.size(MEASUREMENT_BUTTONS_SPACER_SIZE)) FilledTonalButton( onClick = { + // Log clicking the picker Add media button + scope.launch { + events.dispatch( + Event.LogPhotopickerUIEvent( + FeatureToken.SELECTION_BAR.token, + configuration.sessionId, + configuration.callingPackageUid ?: -1, + Telemetry.UiEvent.PICKER_CLICK_ADD_BUTTON + ) + ) + } // The selection bar should receive a click handler from its parent // to handle the primary button click. val clickAction = params as? LocationParams.WithClickAction diff --git a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt index 8f721a907..9434b8e3a 100644 --- a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt +++ b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt @@ -19,6 +19,8 @@ package com.android.photopicker.features.selectionbar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.RegisteredEventClass import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration @@ -38,7 +40,14 @@ class SelectionBarFeature : PhotopickerUiFeature { // The selection bar is only shown when in multi-select mode. For single select, // the activity ends as soon as the first Media is selected, so this feature is // disabled to prevent it's animation for playing when the selection changes. - override fun isEnabled(config: PhotopickerConfiguration) = config.selectionLimit > 1 + override fun isEnabled(config: PhotopickerConfiguration): Boolean { + if (config.runtimeEnv == PhotopickerRuntimeEnv.ACTIVITY) { + return config.selectionLimit > 1 + } + // This is static enablement of feature. It will be hidden in collapsed + // mode for embedded at runtime. + return config.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED + } override fun build(featureManager: FeatureManager) = SelectionBarFeature() } @@ -57,7 +66,8 @@ class SelectionBarFeature : PhotopickerUiFeature { override val eventsConsumed = setOf<RegisteredEventClass>() /** Events produced by the selection bar */ - override val eventsProduced = setOf<RegisteredEventClass>() + override val eventsProduced = + setOf<RegisteredEventClass>(Event.LogPhotopickerUIEvent::class.java) @Composable override fun compose(location: Location, modifier: Modifier, params: LocationParams) { diff --git a/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt b/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt index 10c60e3bd..6f4bc0749 100644 --- a/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt +++ b/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt @@ -20,14 +20,11 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.LocalEvents -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @Composable @@ -47,7 +44,8 @@ fun Snackbar(modifier: Modifier) { snackbarEvents.collect { when (it) { is Event.ShowSnackbarMessage -> { - // Only enqueue a new snackbar if its message does not match the current snackbar + // Only enqueue a new snackbar if its message does not match the current + // snackbar // to ensure that duplicate events are suppressed. if (snackbarHostState.currentSnackbarData?.visuals?.message != it.message) { scope.launch { snackbarHostState.showSnackbar(it.message) } diff --git a/photopicker/src/com/android/photopicker/inject/ActivityModule.kt b/photopicker/src/com/android/photopicker/inject/ActivityModule.kt index 1af82e0bd..9c2dd8965 100644 --- a/photopicker/src/com/android/photopicker/inject/ActivityModule.kt +++ b/photopicker/src/com/android/photopicker/inject/ActivityModule.kt @@ -28,6 +28,7 @@ import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv import com.android.photopicker.core.database.DatabaseManager import com.android.photopicker.core.database.DatabaseManagerImpl import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.selection.GrantsAwareSelectionImpl import com.android.photopicker.core.selection.Selection @@ -160,6 +161,7 @@ class ActivityModule { /* scope= */ scope, /* dispatcher= */ dispatcher, /* deviceConfigProxy= */ deviceConfigProxy, + /* sessionId */ generatePickerSessionId(), ) return configurationManager } @@ -193,6 +195,7 @@ class ActivityModule { @ActivityRetainedScoped configurationManager: ConfigurationManager, @ActivityRetainedScoped featureManager: FeatureManager, @ApplicationContext appContext: Context, + events: Events, ): DataService { if (!::dataService.isInitialized) { Log.d( @@ -209,6 +212,7 @@ class ActivityModule { configurationManager.configuration, featureManager, appContext, + events, ) } return dataService @@ -300,6 +304,7 @@ class ActivityModule { fun provideSelection( @ActivityRetainedScoped @Background scope: CoroutineScope, configurationManager: ConfigurationManager, + dataService: DataService ): Selection<Media> { if (::selection.isInitialized) { @@ -312,6 +317,7 @@ class ActivityModule { GrantsAwareSelectionImpl( scope = scope, configuration = configurationManager.configuration, + preGrantedItemsCount = dataService.preGrantedMediaCount ) SelectionStrategy.DEFAULT -> SelectionImpl( diff --git a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt index bde2b0af1..034dc9cc4 100644 --- a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt +++ b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt @@ -31,6 +31,7 @@ import com.android.photopicker.core.database.DatabaseManagerImpl import com.android.photopicker.core.embedded.EmbeddedLifecycle import com.android.photopicker.core.embedded.EmbeddedViewModelFactory import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.selection.GrantsAwareSelectionImpl import com.android.photopicker.core.selection.Selection @@ -108,6 +109,7 @@ class EmbeddedServiceModule { @Background backgroundDispatcher: CoroutineDispatcher, featureManager: Lazy<FeatureManager>, configurationManager: Lazy<ConfigurationManager>, + bannerManager: Lazy<BannerManager>, selection: Lazy<Selection<Media>>, userMonitor: Lazy<UserMonitor>, dataService: Lazy<DataService>, @@ -121,6 +123,7 @@ class EmbeddedServiceModule { EmbeddedViewModelFactory( backgroundDispatcher, configurationManager, + bannerManager, dataService, events, featureManager, @@ -218,6 +221,7 @@ class EmbeddedServiceModule { /* scope= */ scope, /* dispatcher= */ dispatcher, /* deviceConfigProxy= */ deviceConfigProxy, + /* sessionId */ generatePickerSessionId(), ) return configurationManager } @@ -235,7 +239,8 @@ class EmbeddedServiceModule { notificationService: NotificationService, configurationManager: ConfigurationManager, featureManager: FeatureManager, - @ApplicationContext appContext: Context + @ApplicationContext appContext: Context, + events: Events, ): DataService { if (!::dataService.isInitialized) { @@ -252,7 +257,8 @@ class EmbeddedServiceModule { MediaProviderClient(), configurationManager.configuration, featureManager, - appContext + appContext, + events, ) } return dataService @@ -373,6 +379,7 @@ class EmbeddedServiceModule { GrantsAwareSelectionImpl( scope = scope, configuration = configurationManager.configuration, + preGrantedItemsCount = dataService.preGrantedMediaCount, ) SelectionStrategy.DEFAULT -> SelectionImpl( diff --git a/photopicker/tests/Android.bp b/photopicker/tests/Android.bp index 53a71b199..14489fe07 100644 --- a/photopicker/tests/Android.bp +++ b/photopicker/tests/Android.bp @@ -31,6 +31,7 @@ android_test { "androidx.test.core", "androidx.test.rules", "flag-junit", + "glide-mocks", "hilt_android", "hilt_android_testing", "mockito-target", diff --git a/photopicker/tests/AndroidTest.xml b/photopicker/tests/AndroidTest.xml index fd9cebea6..6c4eff269 100644 --- a/photopicker/tests/AndroidTest.xml +++ b/photopicker/tests/AndroidTest.xml @@ -15,7 +15,7 @@ --> <configuration description="Config for Android Photopicker test cases"> - <!-- Ensure test APK is enstalled and cleaned up after the run --> + <!-- Ensure test APK is installed and cleaned up after the run --> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true"/> <option name="test-file-name" value="PhotopickerTests.apk"/> diff --git a/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt b/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt index 8356a4112..964ebb8df 100644 --- a/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt +++ b/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt @@ -33,7 +33,6 @@ import androidx.test.platform.app.InstrumentationRegistry import com.android.modules.utils.build.SdkLevel import com.android.photopicker.R import com.android.photopicker.core.PhotopickerMain -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration import com.android.photopicker.core.events.Events @@ -47,6 +46,8 @@ import com.android.photopicker.core.theme.PhotopickerTheme import com.android.photopicker.data.model.Media import com.android.photopicker.tests.utils.mockito.mockSystemService import com.android.photopicker.tests.utils.mockito.whenever +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString @@ -137,8 +138,8 @@ abstract class PhotopickerFeatureBaseTest { featureManager: FeatureManager, selection: Selection<Media>, events: Events, - bannerManager: BannerManager, navController: TestNavHostController = createNavController(), + disruptiveDataFlow: Flow<Int> = flow { emit(0) } ) { val photopickerConfiguration by configurationManager.configuration.collectAsStateWithLifecycle() @@ -148,10 +149,10 @@ abstract class PhotopickerFeatureBaseTest { LocalSelection provides selection, LocalPhotopickerConfiguration provides photopickerConfiguration, LocalNavController provides navController, - LocalEvents provides events, + LocalEvents provides events ) { PhotopickerTheme(config = photopickerConfiguration) { - PhotopickerMain(bannerManager = bannerManager) + PhotopickerMain(disruptiveDataNotification = disruptiveDataFlow) } } } diff --git a/photopicker/tests/src/com/android/photopicker/core/PhotopickerAppTest.kt b/photopicker/tests/src/com/android/photopicker/core/PhotopickerAppTest.kt new file mode 100644 index 000000000..37047d48f --- /dev/null +++ b/photopicker/tests/src/com/android/photopicker/core/PhotopickerAppTest.kt @@ -0,0 +1,172 @@ +/* + * 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.core + +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.os.UserManager +import android.test.mock.MockContentResolver +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.android.photopicker.R +import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule +import com.android.photopicker.core.navigation.PhotopickerDestinations +import com.android.photopicker.core.selection.Selection +import com.android.photopicker.data.DataService +import com.android.photopicker.data.TestDataServiceImpl +import com.android.photopicker.data.model.Media +import com.android.photopicker.extensions.navigateToAlbumGrid +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.whenever +import com.google.common.truth.Truth.assertWithMessage +import dagger.Lazy +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.MockitoAnnotations + +/** This test class will run Photopicker's actual MainActivity. */ +@UninstallModules( + ApplicationModule::class, + ActivityModule::class, + EmbeddedServiceModule::class, +) +@HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class) +class PhotopickerAppTest : PhotopickerFeatureBaseTest() { + /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */ + @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() + + val testDispatcher = StandardTestDispatcher() + + /** Overrides for ActivityModule */ + val testScope: TestScope = TestScope(testDispatcher) + @BindValue @Main val mainScope: CoroutineScope = testScope + @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope + + /** Setup dependencies for the UninstallModules for the test class. */ + @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() + + @Inject override lateinit var configurationManager: ConfigurationManager + @Inject lateinit var mockContext: Context + @Inject lateinit var featureManager: Lazy<FeatureManager> + @Inject lateinit var selection: Lazy<Selection<Media>> + @Inject lateinit var events: Lazy<Events> + @Inject lateinit var dataService: Lazy<DataService> + @Mock lateinit var mockUserManager: UserManager + @Mock lateinit var mockPackageManager: PackageManager + @Mock lateinit var mockContentProvider: ContentProvider + + @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver + private lateinit var provider: MockContentProviderWrapper + + @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) + 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) + } + setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager) + } + + @Test + fun testDataDisruptionResetsTheUi() { + testScope.runTest { + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager.get(), + selection = selection.get(), + events = events.get(), + disruptiveDataFlow = + dataService.get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold( + initial = 0 + ) { prev, _ -> + prev + 1 + } + ) + } + + val startDestination = navController.currentBackStackEntry?.destination?.route + assertWithMessage("Expected the starting destination to not be album grid") + .that(startDestination) + .isNotEqualTo(PhotopickerDestinations.ALBUM_GRID.route) + + composeTestRule.runOnUiThread { navController.navigateToAlbumGrid() } + composeTestRule.waitForIdle() + + val albumRoute = navController.currentBackStackEntry?.destination?.route + assertWithMessage("Expected current route to be AlbumGrid") + .that(albumRoute) + .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route) + + val testDataService = + checkNotNull(dataService.get() as? TestDataServiceImpl) { + "Expected a TestDataServiceImpl" + } + + testDataService.sendDisruptiveDataUpdateNotification() + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + val endRoute = navController.currentBackStackEntry?.destination?.route + assertWithMessage("Expected to return to start destination") + .that(endRoute) + .isEqualTo(startDestination) + } + } +} diff --git a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt index c088ab011..ba1ec95b3 100644 --- a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt @@ -37,6 +37,7 @@ import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testActionPickImagesConfiguration import com.android.photopicker.core.database.DatabaseManagerTestImpl +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.core.user.UserMonitor @@ -117,6 +118,8 @@ class BannerManagerImplTest { ) } + val sessionId = generatePickerSessionId() + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -180,6 +183,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -229,6 +233,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -282,6 +287,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -360,6 +366,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -428,6 +435,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -484,6 +492,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -548,6 +557,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -607,6 +617,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( @@ -680,6 +691,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = @@ -755,6 +767,7 @@ class BannerManagerImplTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId ) val featureManager = FeatureManager( diff --git a/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt b/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt index 9ffb297c3..763e5cf35 100644 --- a/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt @@ -20,6 +20,8 @@ import android.content.Context import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.VerifiedUser import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed @@ -27,16 +29,35 @@ import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.photopicker.R +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.configuration.testPhotopickerConfiguration +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Event.LogPhotopickerBannerInteraction +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.Telemetry.BannerType +import com.android.photopicker.core.events.Telemetry.UserBannerInteraction +import com.android.photopicker.core.features.FeatureManager +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) class BannerTest { @get:Rule val composeTestRule = createComposeRule() @@ -90,44 +111,190 @@ class BannerTest { @Composable override fun getIcon() = Icons.Filled.VerifiedUser } + @Composable + private fun showBanner(banner: Banner, config: PhotopickerConfiguration, events: Events) { + + CompositionLocalProvider( + LocalPhotopickerConfiguration provides config, + LocalEvents provides events, + ) { + Banner(banner) + } + } + @Test - fun testBannerDisplaysTitleAndMessage() { - composeTestRule.setContent { Banner(banner = TEST_BANNER_1) } + fun testBannerDisplaysTitleAndMessage() = runTest { + val featureManager = + FeatureManager( + configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + scope = this.backgroundScope, + ) + + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager = featureManager, + ) + + val emissions = mutableListOf<Event>() + backgroundScope.launch { events.flow.toList(emissions) } + + composeTestRule.setContent { + showBanner(banner = TEST_BANNER_1, testPhotopickerConfiguration, events) + } + + advanceTimeBy(100) + composeTestRule.waitForIdle() composeTestRule.onNodeWithText(TEST_BANNER_1_TITLE).assertIsDisplayed() composeTestRule.onNodeWithText(TEST_BANNER_1_MESSAGE).assertIsDisplayed() + + val event: LogPhotopickerBannerInteraction = + checkNotNull(emissions.first() as? LogPhotopickerBannerInteraction) { + "Emitted event was not LogPhotopickerBannerInteraction." + } + + assertWithMessage("Expected a banner type in event.") + .that(event.bannerType) + .isEqualTo(BannerType.UNSET_BANNER_TYPE) + assertWithMessage("Expected a banner displayed interaction") + .that(event.userInteraction) + .isEqualTo(UserBannerInteraction.UNSET_BANNER_INTERACTION) } @Test - fun testBannerDisplaysActionButton() { - composeTestRule.setContent { Banner(banner = TEST_BANNER_1) } + fun testBannerDisplaysActionButton() = runTest { + val featureManager = + FeatureManager( + configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + scope = this.backgroundScope, + ) + + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager = featureManager, + ) + + val emissions = mutableListOf<Event>() + backgroundScope.launch { events.flow.toList(emissions) } + + composeTestRule.setContent { + showBanner(banner = TEST_BANNER_1, testPhotopickerConfiguration, events) + } composeTestRule .onNodeWithText(TEST_BANNER_1_ACTION_LABEL) .assertIsDisplayed() .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + val event: LogPhotopickerBannerInteraction = + checkNotNull(emissions.last() as? LogPhotopickerBannerInteraction) { + "Emitted event was not LogPhotopickerBannerInteraction." + } + + assertWithMessage("Expected a banner type in event.") + .that(event.bannerType) + .isEqualTo(BannerType.UNSET_BANNER_TYPE) + assertWithMessage("Expected a banner action button clicked interaction") + .that(event.userInteraction) + .isEqualTo(UserBannerInteraction.CLICK_BANNER_ACTION_BUTTON) } @Test - fun testBannerDisplaysIcon() { - composeTestRule.setContent { Banner(banner = TEST_BANNER_1) } + fun testBannerDisplaysIcon() = runTest { + val featureManager = + FeatureManager( + configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + scope = this.backgroundScope, + ) + + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager = featureManager, + ) + + composeTestRule.setContent { + showBanner(banner = TEST_BANNER_1, testPhotopickerConfiguration, events) + } composeTestRule .onNode(hasContentDescription(TEST_BANNER_1_ICON_DESCRIPTION)) .assertIsDisplayed() } @Test - fun testBannerDisplaysDismissButtonForDismissable() { + fun testBannerDisplaysDismissButtonForDismissable() = runTest { + val featureManager = + FeatureManager( + configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + scope = this.backgroundScope, + ) + + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager = featureManager, + ) + + val emissions = mutableListOf<Event>() + backgroundScope.launch { events.flow.toList(emissions) } + val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources() val dismissString = resources.getString(R.string.photopicker_dismiss_banner_button_label) - composeTestRule.setContent { Banner(TEST_BANNER_1) } - composeTestRule.onNodeWithText(dismissString).assertIsDisplayed() + composeTestRule.setContent { + showBanner(TEST_BANNER_1, testPhotopickerConfiguration, events) + } + + composeTestRule + .onNodeWithText(dismissString) + .assertIsDisplayed() + .assert(hasClickAction()) + .performClick() + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + val event: LogPhotopickerBannerInteraction = + checkNotNull(emissions.last() as? LogPhotopickerBannerInteraction) { + "Emitted event was not LogPhotopickerBannerInteraction." + } + + assertWithMessage("Expected a banner type in event.") + .that(event.bannerType) + .isEqualTo(BannerType.UNSET_BANNER_TYPE) + assertWithMessage("Expected a banner dismiss button clicked interaction") + .that(event.userInteraction) + .isEqualTo(UserBannerInteraction.CLICK_BANNER_DISMISS_BUTTON) } @Test - fun testBannerHidesDismissButton() { + fun testBannerHidesDismissButton() = runTest { + val featureManager = + FeatureManager( + configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + scope = this.backgroundScope, + ) + + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager = featureManager, + ) + val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources() val dismissString = resources.getString(R.string.photopicker_dismiss_banner_button_label) - composeTestRule.setContent { Banner(TEST_BANNER_2) } + composeTestRule.setContent { + showBanner(TEST_BANNER_2, testPhotopickerConfiguration, events) + } composeTestRule.onNodeWithText(dismissString).assertIsNotDisplayed() } } diff --git a/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt b/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt index b672e4d51..d59a58503 100644 --- a/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt @@ -19,6 +19,8 @@ package com.android.photopicker.core.components import android.content.ContentProvider import android.content.ContentResolver import android.net.Uri +import android.os.Build +import android.view.SurfaceControlViewHost import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.material3.Text @@ -32,6 +34,7 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertAll import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.click import androidx.compose.ui.test.filter import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag @@ -42,13 +45,16 @@ import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeUp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems +import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry +import com.android.modules.utils.build.SdkLevel import com.android.photopicker.R import com.android.photopicker.core.ActivityModule import com.android.photopicker.core.ApplicationModule @@ -63,7 +69,11 @@ import com.android.photopicker.core.configuration.PhotopickerConfiguration import com.android.photopicker.core.configuration.SINGLE_SELECT_CONFIG import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testActionPickImagesConfiguration +import com.android.photopicker.core.configuration.testEmbeddedPhotopickerConfiguration import com.android.photopicker.core.configuration.testPhotopickerConfiguration +import com.android.photopicker.core.embedded.EmbeddedState +import com.android.photopicker.core.embedded.LocalEmbeddedState +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.SelectionImpl import com.android.photopicker.core.theme.PhotopickerTheme import com.android.photopicker.data.model.Group @@ -78,7 +88,6 @@ import com.android.photopicker.extensions.toMediaGridItemFromMedia import com.android.photopicker.inject.PhotopickerTestModule import com.android.photopicker.test.utils.MockContentProviderWrapper import com.android.photopicker.tests.utils.mockito.whenever -import com.bumptech.glide.Glide import com.google.common.truth.Truth.assertWithMessage import dagger.Module import dagger.hilt.InstallIn @@ -100,12 +109,14 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.atLeast +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations /** @@ -130,6 +141,7 @@ class MediaGridTest { /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */ @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createComposeRule() + @get:Rule(order = 2) val glideRule = GlideTestRule() /** * MediaGrid uses Glide for loading images, so we have to mock out the dependencies for Glide @@ -154,10 +166,25 @@ class MediaGridTest { @Mock lateinit var mockContentProvider: ContentProvider + @Mock lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost + + /** + * A [EmbeddedState] having a mocked [SurfaceControlViewHost] instance that can be used for + * testing in collapsed mode + */ + private lateinit var testEmbeddedStateWithHostInCollapsedState: EmbeddedState + + /** + * A [EmbeddedState] having a mocked [SurfaceControlViewHost] instance that can be used for + * testing in Expanded state + */ + private lateinit var testEmbeddedStateWithHostInExpandedState: EmbeddedState + lateinit var pager: Pager<MediaPageKey, Media> lateinit var flow: Flow<PagingData<MediaGridItem>> private val MEDIA_GRID_TEST_TAG = "media_grid" + private val BANNER_CONTENT_TEST_TAG = "banner_content" private val CUSTOM_ITEM_TEST_TAG = "custom_item" private val CUSTOM_ITEM_SEPARATOR_TAG = "custom_separator" private val CUSTOM_ITEM_FACTORY_TEXT = "custom item factory" @@ -227,6 +254,8 @@ class MediaGridTest { .openRawResourceFd(R.drawable.android) } + initEmbeddedStates() + // Normally this would be created in the view model that owns the paged data. pager = Pager(PagingConfig(pageSize = 50, maxSize = 500)) { FakeInMemoryMediaPagingSource() } @@ -236,11 +265,17 @@ class MediaGridTest { flow = pager.flow.toMediaGridItemFromMedia().insertMonthSeparators() } - @After() - fun teardown() { - // It is important to tearDown glide after every test to ensure it picks up the updated - // mocks from Hilt and mocks aren't leaked between tests. - Glide.tearDown() + /** Initialize [EmbeddedState] instances */ + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private fun initEmbeddedStates() { + if (SdkLevel.isAtLeastU()) { + @Suppress("DEPRECATION") + whenever(mockSurfaceControlViewHost.transferTouchGestureToHost()) { true } + testEmbeddedStateWithHostInCollapsedState = + EmbeddedState(isExpanded = false, host = mockSurfaceControlViewHost) + testEmbeddedStateWithHostInExpandedState = + EmbeddedState(isExpanded = true, host = mockSurfaceControlViewHost) + } } /** @@ -252,6 +287,7 @@ class MediaGridTest { selection: SelectionImpl<Media>, onItemClick: (MediaGridItem) -> Unit, onItemLongPress: (MediaGridItem) -> Unit = {}, + bannerContent: (@Composable () -> Unit)? = null, ) { val items = flow.collectAsLazyPagingItems() val selected by selection.flow.collectAsStateWithLifecycle() @@ -261,6 +297,7 @@ class MediaGridTest { selection = selected, onItemClick = onItemClick, onItemLongPress = onItemLongPress, + bannerContent = bannerContent, modifier = Modifier.testTag(MEDIA_GRID_TEST_TAG) ) } @@ -307,18 +344,56 @@ class MediaGridTest { scope = backgroundScope, configuration = provideTestConfigurationFlow(scope = backgroundScope) ) - composeTestRule.setContent { - grid( - /* selection= */ selection, - /* onItemClick= */ {}, - ) + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + PhotopickerTheme(isDarkTheme = false, config = testPhotopickerConfiguration) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } } val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) mediaGrid.assertIsDisplayed() } + /** Ensures the MediaGrid shows any banner content that is provided. */ + @Test + fun testMediaGridDisplaysBannerContent() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + PhotopickerTheme(isDarkTheme = false, config = testPhotopickerConfiguration) { + grid( + selection = selection, + onItemClick = {}, + onItemLongPress = {}, + bannerContent = { + Text( + text = "bannerContent", + modifier = Modifier.testTag(BANNER_CONTENT_TEST_TAG) + ) + } + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(BANNER_CONTENT_TEST_TAG)) + mediaGrid.assertIsDisplayed() + } + /** Ensures the AlbumGrid loads media with the correct semantic information */ @Test fun testAlbumGridDisplaysMedia() = runTest { @@ -339,10 +414,16 @@ class MediaGridTest { flow = pagerForAlbums.flow.toMediaGridItemFromAlbum() composeTestRule.setContent { - grid( - /* selection= */ selection, - /* onItemClick= */ {}, - ) + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + PhotopickerTheme(isDarkTheme = false, config = testPhotopickerConfiguration) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } } val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) @@ -362,10 +443,16 @@ class MediaGridTest { ) composeTestRule.setContent { - grid( - /* selection= */ selection, - /* onItemClick= */ {}, - ) + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + PhotopickerTheme(isDarkTheme = false, config = testPhotopickerConfiguration) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } } val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) @@ -567,12 +654,10 @@ class MediaGridTest { ) composeTestRule.setContent { - val photopickerConfiguration: PhotopickerConfiguration = - testPhotopickerConfiguration CompositionLocalProvider( - LocalPhotopickerConfiguration provides photopickerConfiguration, + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, ) { - PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) { + PhotopickerTheme(isDarkTheme = false, config = testPhotopickerConfiguration) { grid( /* selection= */ selection, /* onItemClick= */ {}, @@ -624,14 +709,19 @@ class MediaGridTest { ) composeTestRule.setContent { - val items = dataFlow.collectAsLazyPagingItems() - val selected by selection.flow.collectAsStateWithLifecycle() - - mediaGrid( - items = items, - selection = selected, - onItemClick = {}, - ) + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + val items = dataFlow.collectAsLazyPagingItems() + val selected by selection.flow.collectAsStateWithLifecycle() + PhotopickerTheme(isDarkTheme = false, config = testPhotopickerConfiguration) { + mediaGrid( + items = items, + selection = selected, + onItemClick = {}, + ) + } + } } composeTestRule.onAllNodes(hasContentDescription(mediaItemString)).assertCountEquals(3) @@ -651,17 +741,21 @@ class MediaGridTest { ) composeTestRule.setContent { - val items = flow.collectAsLazyPagingItems() - val selected by selection.flow.collectAsStateWithLifecycle() - mediaGrid( - items = items, - selection = selected, - onItemClick = {}, - onItemLongPress = {}, - contentItemFactory = { item, _, onClick, _ -> - customContentItemFactory(item, onClick) - }, - ) + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + val items = flow.collectAsLazyPagingItems() + val selected by selection.flow.collectAsStateWithLifecycle() + mediaGrid( + items = items, + selection = selected, + onItemClick = {}, + onItemLongPress = {}, + contentItemFactory = { item, _, onClick, _ -> + customContentItemFactory(item, onClick) + }, + ) + } } composeTestRule @@ -686,14 +780,18 @@ class MediaGridTest { ) composeTestRule.setContent { - val items = dataFlow.collectAsLazyPagingItems() - val selected by selection.flow.collectAsStateWithLifecycle() - mediaGrid( - items = items, - selection = selected, - onItemClick = {}, - contentSeparatorFactory = { _ -> customContentSeparatorFactory() } - ) + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + ) { + val items = dataFlow.collectAsLazyPagingItems() + val selected by selection.flow.collectAsStateWithLifecycle() + mediaGrid( + items = items, + selection = selected, + onItemClick = {}, + contentSeparatorFactory = { _ -> customContentSeparatorFactory() } + ) + } } composeTestRule @@ -701,4 +799,226 @@ class MediaGridTest { .assertAll(hasText(CUSTOM_ITEM_SEPARATOR_TEXT)) } } + + /** Ensures that touches are transferring for embedded when swipe up in collapsed mode */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testTouchesAreTransferringToHostInEmbedded_CollapsedMode_SwipeUp() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateWithHostInCollapsedState + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) + + mediaGrid.performTouchInput { swipeUp() } + composeTestRule.waitForIdle() + mediaGrid.assertIsDisplayed() + // Verify whether the method to transfer touch events is invoked during testing + @Suppress("DEPRECATION") + verify(mockSurfaceControlViewHost, atLeast(1)).transferTouchGestureToHost() + } + + /** Ensures that touches are transferring for embedded when swipe down in collapsed mode */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testTouchesAreTransferringToHostInEmbedded_CollapsedMode_SwipeDown() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateWithHostInCollapsedState + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) + + mediaGrid.performTouchInput { swipeDown() } + composeTestRule.waitForIdle() + mediaGrid.assertIsDisplayed() + // Verify whether the method to transfer touch events is invoked during testing + @Suppress("DEPRECATION") + verify(mockSurfaceControlViewHost, atLeast(1)).transferTouchGestureToHost() + } + + /** Ensures that clicks are not transferring for embedded in collapsed mode */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testTouchesAreNotTransferringToHostInEmbedded_CollapsedMode_Click() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateWithHostInCollapsedState + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) + + mediaGrid.performTouchInput { click() } + composeTestRule.waitForIdle() + mediaGrid.assertIsDisplayed() + // Verify whether the method to transfer touch events is not invoked during testing + @Suppress("DEPRECATION") + verify(mockSurfaceControlViewHost, never()).transferTouchGestureToHost() + } + + /** Ensures that touches are not transferring for embedded when swipe up in Expanded mode */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testTouchesAreNotTransferringToHostInEmbedded_ExpandedMode_SwipeUP() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateWithHostInExpandedState + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) + + mediaGrid.performTouchInput { swipeUp() } + composeTestRule.waitForIdle() + mediaGrid.assertIsDisplayed() + // Verify whether the method to transfer touch events is not invoked during testing + @Suppress("DEPRECATION") + verify(mockSurfaceControlViewHost, never()).transferTouchGestureToHost() + } + + /** Ensures that touches are transferring for embedded when swipe down in Expanded mode */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testTouchesAreTransferringToHostInEmbedded_ExpandedMode_SwipeDown() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateWithHostInExpandedState + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) + + mediaGrid.performTouchInput { swipeDown() } + composeTestRule.waitForIdle() + mediaGrid.assertIsDisplayed() + // Verify whether the method to transfer touch events is invoked during testing + @Suppress("DEPRECATION") + verify(mockSurfaceControlViewHost, atLeast(1)).transferTouchGestureToHost() + } + + /** Ensures that clicks are not transferring for embedded in Expanded mode */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testTouchesAreNotTransferringToHostInEmbedded_ExpandedMode_Click() = runTest { + val selection = + SelectionImpl<Media>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow(scope = backgroundScope) + ) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateWithHostInExpandedState + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + grid( + /* selection= */ selection, + /* onItemClick= */ {}, + ) + } + } + } + + val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG)) + + mediaGrid.performTouchInput { click() } + composeTestRule.waitForIdle() + mediaGrid.assertIsDisplayed() + // Verify whether the method to transfer touch events is not invoked during testing + @Suppress("DEPRECATION") + verify(mockSurfaceControlViewHost, never()).transferTouchGestureToHost() + } } diff --git a/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt b/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt index 420163763..defc38d8d 100644 --- a/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt @@ -17,10 +17,15 @@ package com.android.photopicker.core.configuration import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.EmbeddedPhotopickerFeatureInfo import android.provider.MediaStore import androidx.core.os.bundleOf import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import androidx.test.filters.SmallTest +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.navigation.PhotopickerDestinations import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,6 +50,7 @@ class ConfigurationManagerTest { // Isolate the test device by providing a test wrapper around device config so that the // tests can control the flag values that are returned. val deviceConfigProxy = TestDeviceConfigProxyImpl() + val sessionId = generatePickerSessionId() @Before fun setup() { @@ -63,10 +69,11 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration with an action matching the test action. - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) backgroundScope.launch { val reportedConfiguration = configurationManager.configuration.first() @@ -81,7 +88,6 @@ class ConfigurationManagerTest { */ @Test fun testConfigurationEmitsFlagChanges() { - runTest { val configurationManager = ConfigurationManager( @@ -89,9 +95,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration with an action matching the test action. - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -133,9 +140,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration with an action matching the test action. - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -191,9 +199,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration with an action matching the test action. - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -238,9 +247,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -257,6 +267,7 @@ class ConfigurationManagerTest { @Test fun testSetCallerUpdatesConfiguration() { + runTest { val configurationManager = ConfigurationManager( @@ -264,9 +275,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -311,9 +323,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -346,9 +359,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -382,9 +396,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -422,9 +437,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -458,6 +474,7 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) assertThrows(IllegalIntentExtraException::class.java) { configurationManager.setIntent(intent) @@ -485,9 +502,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -527,9 +545,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -568,9 +587,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -610,9 +630,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -647,9 +668,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -684,9 +706,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -724,9 +747,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -768,9 +792,10 @@ class ConfigurationManagerTest { scope = this.backgroundScope, dispatcher = StandardTestDispatcher(this.testScheduler), deviceConfigProxy, + sessionId = sessionId ) // Expect the default configuration - val expectedConfiguration = PhotopickerConfiguration(action = "") + val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId) val emissions = mutableListOf<PhotopickerConfiguration>() backgroundScope.launch { configurationManager.configuration.toList(emissions) } @@ -790,4 +815,167 @@ class ConfigurationManagerTest { assertThat(emissions.first()).isEqualTo(expectedConfiguration) } } + + /** + * Ensures that [ConfigurationManager.configuration] will emit an updated + * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.selectionLimit]. + */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testSetEmbeddedPhotopickerFeatureInfoSetsSelectionLimit() { + val featureInfo = EmbeddedPhotopickerFeatureInfo.Builder().build() + + runTest { + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + sessionId = sessionId + ) + // Expect the default configuration + val expectedConfiguration = + PhotopickerConfiguration( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + action = "", + sessionId = sessionId + ) + + val emissions = mutableListOf<PhotopickerConfiguration>() + backgroundScope.launch { configurationManager.configuration.toList(emissions) } + + advanceTimeBy(100) + configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo) + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedConfiguration) + assertThat(emissions.last().selectionLimit) + .isEqualTo(MediaStore.getPickImagesMaxLimit()) + } + } + + /** + * Ensures that [ConfigurationManager.configuration] will emit an updated + * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.mimeTypes]. + */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testSetEmbeddedPhotopickerFeatureInfoSetsMimeTypes() { + val featureInfo = + EmbeddedPhotopickerFeatureInfo.Builder() + .setMimeTypes(arrayListOf("image/png", "video/mp4")) + .build() + + runTest { + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + sessionId = sessionId + ) + // Expect the default configuration + val expectedConfiguration = + PhotopickerConfiguration( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + action = "", + sessionId = sessionId + ) + + val emissions = mutableListOf<PhotopickerConfiguration>() + backgroundScope.launch { configurationManager.configuration.toList(emissions) } + + advanceTimeBy(100) + configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo) + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedConfiguration) + assertThat(emissions.last().mimeTypes).isEqualTo(arrayListOf("image/png", "video/mp4")) + } + } + + /** + * Ensures that [ConfigurationManager.configuration] will emit an updated + * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.pickImagesInOrder]. + */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testSetEmbeddedPhotopickerFeatureInfoSetsPickImagesInOrder() { + val featureInfo = EmbeddedPhotopickerFeatureInfo.Builder().setOrderedSelection(true).build() + + runTest { + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + sessionId = sessionId + ) + // Expect the default configuration + val expectedConfiguration = + PhotopickerConfiguration( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + action = "", + sessionId = sessionId + ) + + val emissions = mutableListOf<PhotopickerConfiguration>() + backgroundScope.launch { configurationManager.configuration.toList(emissions) } + + advanceTimeBy(100) + configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo) + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedConfiguration) + assertThat(emissions.last().pickImagesInOrder).isTrue() + } + } + + /** + * Ensures that [ConfigurationManager.configuration] will emit an updated + * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.preSelectedUris]. + */ + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testSetEmbeddedPhotopickerFeatureInfoSetsPreSelectedUris() { + val featureInfo = + EmbeddedPhotopickerFeatureInfo.Builder() + .setPreSelectedUris(arrayListOf(Uri.EMPTY)) + .build() + + runTest { + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + sessionId = sessionId + ) + // Expect the default configuration + val expectedConfiguration = + PhotopickerConfiguration( + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + action = "", + sessionId = sessionId + ) + + val emissions = mutableListOf<PhotopickerConfiguration>() + backgroundScope.launch { configurationManager.configuration.toList(emissions) } + + advanceTimeBy(100) + configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo) + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedConfiguration) + assertThat(emissions.last().preSelectedUris).isEqualTo(arrayListOf(Uri.EMPTY)) + } + } } diff --git a/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt b/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt index f65cadc64..64ffb443b 100644 --- a/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt +++ b/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt @@ -18,17 +18,31 @@ package com.android.photopicker.core.configuration import android.content.Intent import android.provider.MediaStore +import com.android.photopicker.core.events.generatePickerSessionId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn +val testSessionId = generatePickerSessionId() + /** A [PhotopickerConfiguration] that allows selection of only a single item. */ -val SINGLE_SELECT_CONFIG = PhotopickerConfiguration(action = "", selectionLimit = 1) +val SINGLE_SELECT_CONFIG = + PhotopickerConfiguration(action = "", selectionLimit = 1, sessionId = testSessionId) /** A [PhotopickerConfiguration] that allows selection of multiple (50 in this case) items. */ -val MULTI_SELECT_CONFIG = PhotopickerConfiguration(action = "", selectionLimit = 50) +val MULTI_SELECT_CONFIG = + PhotopickerConfiguration(action = "", selectionLimit = 50, sessionId = testSessionId) + +/** A test package name used in test photopicker configurations. */ +val TEST_CALLING_PACKAGE = "com.example.test" + +/** A test calling uid used in test photopicker configurations. */ +val TEST_CALLING_UID = 1234 + +/** A test package label used in test photopicker configurations. */ +val TEST_CALLING_PACKAGE_LABEL = "test_app" /** * A [PhotopickerConfiguration] that can be used with most tests, that comes with sensible default @@ -38,6 +52,7 @@ val testPhotopickerConfiguration: PhotopickerConfiguration = PhotopickerConfiguration( action = "TEST_ACTION", intent = Intent("TEST_ACTION"), + sessionId = testSessionId ) /** @@ -48,6 +63,7 @@ val testActionPickImagesConfiguration: PhotopickerConfiguration = PhotopickerConfiguration( action = MediaStore.ACTION_PICK_IMAGES, intent = Intent(MediaStore.ACTION_PICK_IMAGES), + sessionId = testSessionId ) /** @@ -58,6 +74,7 @@ val testGetContentConfiguration: PhotopickerConfiguration = PhotopickerConfiguration( action = Intent.ACTION_GET_CONTENT, intent = Intent(Intent.ACTION_GET_CONTENT), + sessionId = testSessionId ) /** @@ -68,6 +85,10 @@ val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration = PhotopickerConfiguration( action = MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, intent = Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP), + callingPackage = TEST_CALLING_PACKAGE, + callingPackageUid = TEST_CALLING_UID, + callingPackageLabel = TEST_CALLING_PACKAGE_LABEL, + sessionId = testSessionId ) /** @@ -75,7 +96,11 @@ val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration = * [PhotopickerRuntimeEnv.EMBEDDED] */ val testEmbeddedPhotopickerConfiguration: PhotopickerConfiguration = - PhotopickerConfiguration(action = "", runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED) + PhotopickerConfiguration( + action = "", + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + sessionId = testSessionId + ) /** * Helper function to generate a [StateFlow] that mimics the flow emitted by the diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedFeaturesTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedFeaturesTest.kt new file mode 100644 index 000000000..480233e33 --- /dev/null +++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedFeaturesTest.kt @@ -0,0 +1,509 @@ +/* + * Copyright (C) 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.core.embedded + +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Parcel +import android.os.UserHandle +import android.os.UserManager +import android.test.mock.MockContentResolver +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.runtime.CompositionLocalProvider +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.hasAnyChild +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.onAllNodesWithContentDescription +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.test.filters.SdkSuppress +import com.android.photopicker.R +import com.android.photopicker.core.ActivityModule +import com.android.photopicker.core.ApplicationModule +import com.android.photopicker.core.ApplicationOwned +import com.android.photopicker.core.Background +import com.android.photopicker.core.EmbeddedServiceModule +import com.android.photopicker.core.Main +import com.android.photopicker.core.PhotopickerApp +import com.android.photopicker.core.ViewModelModule +import com.android.photopicker.core.banners.BannerManager +import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration +import com.android.photopicker.core.configuration.testEmbeddedPhotopickerConfiguration +import com.android.photopicker.core.database.DatabaseManager +import com.android.photopicker.core.events.Event +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureToken +import com.android.photopicker.core.features.LocalFeatureManager +import com.android.photopicker.core.glide.GlideTestRule +import com.android.photopicker.core.navigation.PhotopickerDestinations +import com.android.photopicker.core.selection.LocalSelection +import com.android.photopicker.core.selection.Selection +import com.android.photopicker.core.theme.PhotopickerTheme +import com.android.photopicker.data.model.Media +import com.android.photopicker.data.model.MediaSource +import com.android.photopicker.features.overflowmenu.OverflowMenuFeature +import com.android.photopicker.features.preview.PreviewFeature +import com.android.photopicker.features.snackbar.SnackbarFeature +import com.android.photopicker.inject.EmbeddedTestModule +import com.android.photopicker.test.utils.MockContentProviderWrapper +import com.android.photopicker.tests.HiltTestActivity +import com.android.photopicker.tests.utils.mockito.whenever +import com.google.common.truth.Truth.assertWithMessage +import dagger.Lazy +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.MockitoAnnotations + +@UninstallModules( + ActivityModule::class, + ApplicationModule::class, + EmbeddedServiceModule::class, + ViewModelModule::class, +) +@HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +class EmbeddedFeaturesTest : EmbeddedPhotopickerFeatureBaseTest() { + /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */ + @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + + @get:Rule(order = 2) val glideRule = GlideTestRule() + + /** Setup dependencies for the UninstallModules for the test class. */ + @Module @InstallIn(SingletonComponent::class) class TestModule : EmbeddedTestModule() + + val testDispatcher = StandardTestDispatcher() + + /* Overrides for EmbeddedServiceModule */ + val testScope: TestScope = TestScope(testDispatcher) + + @BindValue @Main val mainScope: CoroutineScope = testScope + + @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope + + @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher + + /* Overrides for ViewModelModule */ + @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope + + /** + * 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 + + @Inject lateinit var events: Events + @Inject lateinit var selection: Selection<Media> + @Inject lateinit var featureManager: FeatureManager + @Inject lateinit var userHandle: UserHandle + @Inject lateinit var bannerManager: Lazy<BannerManager> + @Inject lateinit var embeddedLifecycle: EmbeddedLifecycle + @Inject lateinit var databaseManager: DatabaseManager + @Inject override lateinit var configurationManager: ConfigurationManager + + // Needed for UserMonitor + @Inject lateinit var mockContext: Context + @Mock lateinit var mockUserManager: UserManager + @Mock lateinit var mockPackageManager: PackageManager + + private val USER_HANDLE_MANAGED: UserHandle + private val USER_ID_MANAGED: Int = 10 + + init { + + // Create a UserHandle for a managed profile. + val parcel = Parcel.obtain() + parcel.writeInt(USER_ID_MANAGED) + parcel.setDataPosition(0) + USER_HANDLE_MANAGED = UserHandle(parcel) + parcel.recycle() + } + + private val TEST_TAG_SELECTION_BAR = "selection_bar" + private val MEDIA_ITEM = + Media.Image( + mediaId = "1", + 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(), + dateTakenMillisLong = 123456789L, + sizeInBytes = 1000L, + mimeType = "image/png", + standardMimeTypeExtension = 1, + ) + + @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) + 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) + } + setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager) + } + + @Test + fun testNavigationBarIsNotDisplayedInEmbeddedWhenCollapsed() = + testScope.runTest { + val resources = getTestableContext().getResources() + val photosGridNavButtonLabel = + resources.getString(R.string.photopicker_photos_nav_button_label) + val albumsGridNavButtonLabel = + resources.getString(R.string.photopicker_albums_nav_button_label) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateCollapsed, + ) { + callEmbeddedPhotopickerMain( + embeddedLifecycle = embeddedLifecycle, + featureManager = featureManager, + selection = selection, + events = events, + ) + } + } + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onNode( + hasAnyChild(hasText(photosGridNavButtonLabel)) and + hasAnyChild(hasText(albumsGridNavButtonLabel)) + ) + .assertIsNotDisplayed() + } + + @Test + fun testNavigationBarIsDisplayedInEmbeddedWhenExpanded() = + testScope.runTest { + val resources = getTestableContext().getResources() + val photosGridNavButtonLabel = + resources.getString(R.string.photopicker_photos_nav_button_label) + val albumsGridNavButtonLabel = + resources.getString(R.string.photopicker_albums_nav_button_label) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateExpanded, + ) { + callEmbeddedPhotopickerMain( + embeddedLifecycle = embeddedLifecycle, + featureManager = featureManager, + selection = selection, + events = events, + ) + } + } + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Photos Grid Nav Button and Albums Grid Nav Button + composeTestRule + .onNode(hasText(photosGridNavButtonLabel)) + .assertIsDisplayed() + .assert(hasClickAction()) + + composeTestRule + .onNode(hasText(albumsGridNavButtonLabel)) + .assertIsDisplayed() + .assert(hasClickAction()) + } + + @Test + fun testSwipeLeftToNavigateDisabledInEmbeddedWhenCollapsed() = + testScope.runTest { + val resources = getTestableContext().getResources() + val mediaItemString = resources.getString(R.string.photopicker_media_item) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateCollapsed, + ) { + callEmbeddedPhotopickerMain( + embeddedLifecycle = embeddedLifecycle, + featureManager = featureManager, + selection = selection, + events = events, + ) + } + } + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onAllNodesWithContentDescription(mediaItemString) + .onFirst() + .performTouchInput { swipeLeft() } + composeTestRule.waitForIdle() + val route = navController.currentBackStackEntry?.destination?.route + assertWithMessage("Expected swipe to be disabled") + .that(route) + .isEqualTo(PhotopickerDestinations.PHOTO_GRID.route) + } + + @Test + fun testSwipeLeftToAlbumWorksInEmbeddedWhenExpanded() = + testScope.runTest { + val resources = getTestableContext().getResources() + val mediaItemString = resources.getString(R.string.photopicker_media_item) + + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateExpanded, + ) { + callEmbeddedPhotopickerMain( + embeddedLifecycle = embeddedLifecycle, + featureManager = featureManager, + selection = selection, + events = events, + ) + } + } + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onAllNodesWithContentDescription(mediaItemString) + .onFirst() + .performTouchInput { swipeLeft() } + composeTestRule.waitForIdle() + val route = navController.currentBackStackEntry?.destination?.route + assertWithMessage("Expected swipe to navigate to AlbumGrid") + .that(route) + .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route) + } + + @Test + fun testProfileSelectorIsNotDisplayedInEmbeddedWhenCollapsed() = + testScope.runTest { + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateCollapsed, + ) { + callEmbeddedPhotopickerMain( + embeddedLifecycle = embeddedLifecycle, + featureManager = featureManager, + selection = selection, + events = events, + ) + } + } + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onNode( + hasContentDescription( + getTestableContext() + .getResources() + .getString(R.string.photopicker_profile_switch_button_description) + ) + ) + .assertIsNotDisplayed() + } + + @Test + fun testProfileSelectorIsDisplayedInEmbeddedWhenExpanded() = + testScope.runTest { + + // Initial setup state: Two profiles (Personal/Work), both enabled + whenever(mockUserManager.userProfiles) { listOf(userHandle, USER_HANDLE_MANAGED) } + whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true } + whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false } + whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { userHandle } + + withContext(Dispatchers.Main) { + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateExpanded, + ) { + callEmbeddedPhotopickerMain( + embeddedLifecycle = embeddedLifecycle, + featureManager = featureManager, + selection = selection, + events = events, + ) + } + } + } + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onNode( + hasContentDescription( + getTestableContext() + .getResources() + .getString(R.string.photopicker_profile_switch_button_description) + ) + ) + .assertIsDisplayed() + } + + @Test + fun testSnackbarIsAlwaysEnabledInEmbedded() { + + assertWithMessage("SnackbarFeature is not always enabled for action pick image") + .that(SnackbarFeature.Registration.isEnabled(testEmbeddedPhotopickerConfiguration)) + .isEqualTo(true) + } + + @Test + fun testSnackbarDisplaysOnEvent() = + testScope.runTest { + composeTestRule.setContent { + CompositionLocalProvider( + LocalPhotopickerConfiguration provides testEmbeddedPhotopickerConfiguration, + LocalEmbeddedState provides testEmbeddedStateCollapsed, + LocalFeatureManager provides featureManager, + LocalSelection provides selection, + LocalEvents provides events, + LocalEmbeddedLifecycle provides embeddedLifecycle, + LocalViewModelStoreOwner provides embeddedLifecycle, + LocalOnBackPressedDispatcherOwner provides embeddedLifecycle, + ) { + PhotopickerTheme( + isDarkTheme = false, + config = testEmbeddedPhotopickerConfiguration + ) { + PhotopickerApp(disruptiveDataNotification = flow { emit(0) }) + } + } + } + + // Advance the UI clock manually to control for the fade animations on the snackbar. + composeTestRule.mainClock.autoAdvance = false + + val TEST_MESSAGE = "This is a test message" + events.dispatch(Event.ShowSnackbarMessage(FeatureToken.CORE.token, TEST_MESSAGE)) + advanceTimeBy(500) + + // Advance ui clock to allow fade in + composeTestRule.mainClock.advanceTimeBy(2000L) + composeTestRule.onNode(hasText(TEST_MESSAGE)).assertIsDisplayed() + + // Advance ui clock to allow fade out + composeTestRule.mainClock.advanceTimeBy(10_000L) + composeTestRule.onNode(hasText(TEST_MESSAGE)).assertIsNotDisplayed() + } + + @Test + fun testOverflowMenuDisabledInEmbedded() { + + assertWithMessage("Expected OverflowMenuFeature to be disabled in embedded runtime") + .that(OverflowMenuFeature.Registration.isEnabled(testEmbeddedPhotopickerConfiguration)) + .isEqualTo(false) + } + + @Test + fun testPreviewDisabledInEmbedded() { + + assertWithMessage("Expected PreviewFeature to be disabled in embedded runtime") + .that(PreviewFeature.Registration.isEnabled(testEmbeddedPhotopickerConfiguration)) + .isEqualTo(false) + } +} diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt index b472aae71..1bbaa409d 100644 --- a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.android.photopicker.core.PhotopickerMain -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.selection.Selection @@ -47,14 +46,13 @@ abstract class EmbeddedPhotopickerFeatureBaseTest : PhotopickerFeatureBaseTest() featureManager: FeatureManager, selection: Selection<Media>, events: Events, - bannerManager: BannerManager, ) { CompositionLocalProvider( LocalEmbeddedLifecycle provides embeddedLifecycle, LocalViewModelStoreOwner provides embeddedLifecycle, LocalOnBackPressedDispatcherOwner provides embeddedLifecycle, ) { - callPhotopickerMain(featureManager, selection, events, bannerManager) + callPhotopickerMain(featureManager, selection, events) } } } diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt index 33741f162..f011dd73f 100644 --- a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mockito.MockitoAnnotations @@ -111,6 +112,7 @@ class EmbeddedServiceTest { @DisableFlags(Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER) @Test + @Ignore("b/357048672") fun testEmbeddedServiceOnBindIsNullWhenEmbeddedDisabled() { assertThat(embeddedService.onBind(Intent())).isNull() } diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt index e662befc3..d5a2d15f3 100644 --- a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt @@ -18,9 +18,13 @@ package com.android.photopicker.core.embedded import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -28,21 +32,80 @@ import org.junit.runner.RunWith /** Unit tests for the [EmbeddedStateManager] */ @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) class EmbeddedStateManagerTest { @Test - fun testEmitsEmbeddedState() { - runTest { - val embeddedStateManager = EmbeddedStateManager() - - val expectedEmbeddedState = EmbeddedState() - - backgroundScope.launch { - val reportedEmbeddedState = embeddedStateManager.state.first() - assertWithMessage("Reported embedded state is not correct") - .that(reportedEmbeddedState) - .isEqualTo(expectedEmbeddedState) - } + fun testEmitsEmbeddedState() = runTest { + val embeddedStateManager = EmbeddedStateManager() + + val expectedEmbeddedState = EmbeddedState() + + backgroundScope.launch { + val reportedEmbeddedState = embeddedStateManager.state.first() + assertWithMessage("Reported embedded state is not correct") + .that(reportedEmbeddedState) + .isEqualTo(expectedEmbeddedState) } } + + @Test + fun testEmitsExpandedStateChanged() = runTest { + val embeddedStateManager = EmbeddedStateManager() + + val expectedEmbeddedState = EmbeddedState(isExpanded = false) + + val emissions = mutableListOf<EmbeddedState>() + backgroundScope.launch { embeddedStateManager.state.toList(emissions) } + + advanceTimeBy(100) + + embeddedStateManager.setIsExpanded(isExpanded = true) + + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedEmbeddedState) + assertThat(emissions.last()).isEqualTo(expectedEmbeddedState.copy(isExpanded = true)) + } + + @Test + fun testEmitsDarkThemeStateChanged() = runTest { + val embeddedStateManager = EmbeddedStateManager() + + val expectedEmbeddedState = EmbeddedState(isDarkTheme = false) + + val emissions = mutableListOf<EmbeddedState>() + backgroundScope.launch { embeddedStateManager.state.toList(emissions) } + + advanceTimeBy(100) + + embeddedStateManager.setIsDarkTheme(isDarkTheme = true) + + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedEmbeddedState) + assertThat(emissions.last()).isEqualTo(expectedEmbeddedState.copy(isDarkTheme = true)) + } + + @Test + fun testTriggerRecomposeFlipsRecomposeToggle() = runTest { + val embeddedStateManager = EmbeddedStateManager() + + val expectedEmbeddedState = EmbeddedState(recomposeToggle = false) + + val emissions = mutableListOf<EmbeddedState>() + backgroundScope.launch { embeddedStateManager.state.toList(emissions) } + + advanceTimeBy(100) + + embeddedStateManager.triggerRecompose() + + advanceTimeBy(100) + + assertThat(emissions.size).isEqualTo(2) + assertThat(emissions.first()).isEqualTo(expectedEmbeddedState) + assertThat(emissions.last()).isEqualTo(expectedEmbeddedState.copy(recomposeToggle = true)) + } } diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt index 760da82b7..9827a0689 100644 --- a/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt @@ -18,6 +18,7 @@ package com.android.photopicker.core.embedded import android.content.ContentProvider import android.content.ContentResolver import android.content.Context +import android.content.ContextWrapper import android.content.pm.PackageManager import android.hardware.display.DisplayManager import android.net.Uri @@ -26,12 +27,12 @@ import android.os.Build import android.os.Process import android.os.UserManager import android.provider.EmbeddedPhotopickerFeatureInfo -import android.provider.EmbeddedPhotopickerSessionResponse import android.provider.IEmbeddedPhotopickerClient import android.test.mock.MockContentResolver import android.view.SurfaceView import android.view.WindowManager import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteractionCollection import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasClickAction @@ -53,14 +54,24 @@ import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.ViewModelModule import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection +import com.android.photopicker.data.DataService +import com.android.photopicker.data.TestDataServiceImpl +import com.android.photopicker.data.model.CollectionInfo import com.android.photopicker.data.model.Media +import com.android.photopicker.data.model.MediaSource +import com.android.photopicker.data.model.Provider import com.android.photopicker.extensions.requireSystemService import com.android.photopicker.inject.EmbeddedTestModule import com.android.photopicker.test.utils.MockContentProviderWrapper import com.android.photopicker.tests.HiltTestActivity +import com.android.photopicker.tests.utils.StubProvider +import com.android.photopicker.tests.utils.mockito.capture import com.android.photopicker.tests.utils.mockito.whenever -import com.bumptech.glide.Glide +import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.Module import dagger.hilt.EntryPoints @@ -80,13 +91,20 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.After +import org.junit.Assert.fail import org.junit.Assume.assumeTrue 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.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.spy import org.mockito.Mockito.times +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @UninstallModules( @@ -103,6 +121,7 @@ class SessionTest : EmbeddedPhotopickerFeatureBaseTest() { @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /** Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : EmbeddedTestModule() @@ -126,28 +145,30 @@ class SessionTest : EmbeddedPhotopickerFeatureBaseTest() { @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver private lateinit var provider: MockContentProviderWrapper @Mock lateinit var mockContentProvider: ContentProvider + // Needed for UserMonitor @Mock lateinit var mockUserManager: UserManager @Mock lateinit var mockPackageManager: PackageManager @Inject lateinit var mockContext: Context @Inject lateinit var embeddedServiceComponentBuilder: EmbeddedServiceComponentBuilder @Inject lateinit var selection: Selection<Media> + @Inject lateinit var featureManager: FeatureManager + @Inject lateinit var events: Events @Inject override lateinit var configurationManager: ConfigurationManager + @Inject lateinit var dataService: DataService + @Inject lateinit var embeddedLifecycle: EmbeddedLifecycle - val featureInfo = EmbeddedPhotopickerFeatureInfo.Builder().build() + @Captor lateinit var uriCaptor: ArgumentCaptor<Uri> - val client = - // TODO(b/354929684): Replace AIDL implementation with wrapper class. - object : IEmbeddedPhotopickerClient.Stub() { + @Captor lateinit var uriCaptor2: ArgumentCaptor<Uri> - override fun onSessionOpened(response: EmbeddedPhotopickerSessionResponse) {} + @Captor lateinit var uriCaptor3: ArgumentCaptor<Uri> - override fun onSessionError(errorMsg: String) {} + @Mock lateinit var mockClient: IEmbeddedPhotopickerClient.Stub - override fun onItemSelected(uri: Uri) {} + private lateinit var mockTextContextWrapper: FakeTestContextWrapper - override fun onItemDeselected(uri: Uri) {} - } + private val featureInfo = EmbeddedPhotopickerFeatureInfo.Builder().build() // Session has a surfacePackage which outlives the test if not closed, so it always needs to be // closed at the end of each test to prevent any existing UI activity from leaking into the next @@ -174,7 +195,9 @@ class SessionTest : EmbeddedPhotopickerFeatureBaseTest() { displayId = displayId, hostToken = Binder(), featureInfo = featureInfo, - clientCallback = client, + clientCallback = mockClient, + grantUriPermission = { _, uri -> mockTextContextWrapper.grantUriPermission(uri) }, + revokeUriPermission = { _, uri -> mockTextContextWrapper.revokeUriPermission(uri) }, ) session = newSession return newSession @@ -185,6 +208,8 @@ class SessionTest : EmbeddedPhotopickerFeatureBaseTest() { MockitoAnnotations.initMocks(this) hiltRule.inject() + mockTextContextWrapper = spy(FakeTestContextWrapper()) + whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() } val mockContentResolver = MockContentResolver(mockContext) @@ -205,7 +230,6 @@ class SessionTest : EmbeddedPhotopickerFeatureBaseTest() { // mocks from Hilt and mocks aren't leaked between tests. session?.close() session = null - Glide.tearDown() } /** @@ -379,4 +403,361 @@ class SessionTest : EmbeddedPhotopickerFeatureBaseTest() { .that(configuration.callingPackageLabel) .isNotNull() } + + @Test + fun testSessionSetsEmbeddedPhotopickerFeatureInfoInConfiguration() = + testScope.runTest { + val component = embeddedServiceComponentBuilder.build() + val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java) + + // Create a session with the component and let it initialize. + getSessionUnderTest(component) + advanceTimeBy(100) + + val configuration = entryPoint.configurationManager().get().configuration.value + assertWithMessage( + "Expected configuration to contain the featureInfo max selection limit" + ) + .that(configuration.selectionLimit) + .isEqualTo(featureInfo.maxSelectionLimit) + assertWithMessage("Expected configuration to contain the featureInfo mime types") + .that(configuration.mimeTypes) + .isEqualTo(featureInfo.mimeTypes) + assertWithMessage( + "Expected configuration to contain the featureInfo ordered selection flag" + ) + .that(configuration.pickImagesInOrder) + .isEqualTo(featureInfo.isOrderedSelection) + assertWithMessage("Expected configuration to contain the featureInfo pre-selected URIs") + .that(configuration.preSelectedUris) + .isEqualTo(featureInfo.preSelectedUris) + } + + @Test + fun testSelectionUpdateGrantsAndRevokesPermissionSuccess() = + testScope.runTest { + val component = embeddedServiceComponentBuilder.build() + val session = getSessionUnderTest(component) + + val itemCount = 20 + setUpTestDataWithStubProvider(itemCount) + + advanceTimeBy(100) + + // Now the view is in the test's compose tree, so do a simple check to make sure + // the view actually initialized and the test can locate the photo grid / modify the + // selection. + composeTestRule.setContent { + // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to + // the test. + AndroidView( + factory = { + SurfaceView(getTestableContext()).apply { + setChildSurfacePackage(session.surfacePackage) + } + } + ) + } + + composeTestRule.waitForIdle() + + clearInvocations(mockTextContextWrapper, mockClient) + + val resources = getTestableContext().getResources() + + // This is the accessibility label for a Photo in the grid. + val mediaItemString = resources.getString(R.string.photopicker_media_item) + + // Get all image nodes + val allImageNodes = composeTestRule.onAllNodesWithContentDescription(mediaItemString) + + // Make list of indices to select + var indicesToSelect = setOf(2, 0, 4) // Select images at indices 2, 0, and 4 + var expectedUris: List<Uri> = constructUrisForIndices(indicesToSelect) + + // Filter image nodes based on the indices to select and performClick + performClickForIndices(allImageNodes, indicesToSelect) + + // Wait for PhotoGridViewModel to modify Selection + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Ensure the click handler correctly ran by checking the selection snapshot. + assertWithMessage("Expected selection to contain an item, but it did not.") + .that(selection.snapshot().size) + .isEqualTo(3) + + // Verify that grantUriPermission is invoked for all newly selected media. + verify(mockTextContextWrapper, times(3)).grantUriPermission(capture(uriCaptor)) + var capturedUris = uriCaptor.allValues + + assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris) + + // Verify that client callback is invoked for all uris that were successfully + // granted permission + for (uri in expectedUris) { + verify(mockClient, times(1)).onItemSelected(uri) + } + + clearInvocations(mockTextContextWrapper, mockClient) + + // Make list of indices to deselect. + val indicesToDeselect = setOf(2, 0) // Deselect images at indices 2, 0 + // Get difference of two list which is the final selected uris and get expectedUri list. + expectedUris = constructUrisForIndices(indicesToDeselect) + + // Filter image nodes based on the indices to select and performClick + performClickForIndices(allImageNodes, indicesToDeselect) + + // Wait for PhotoGridViewModel to modify Selection + advanceTimeBy(100) + composeTestRule.waitForIdle() + + assertWithMessage("Expected selection to contain an item, but it did not.") + .that(selection.snapshot().size) + .isEqualTo(1) + + // Verify that revokeUriPermission is invoked for all newly deselected media. + verify(mockTextContextWrapper, times(2)).revokeUriPermission(capture(uriCaptor2)) + capturedUris = uriCaptor2.allValues + + assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris) + + // Verify that client callback is invoked for all uris that were successfully + // revoked permission + for (uri in expectedUris) { + verify(mockClient, times(1)).onItemDeselected(uri) + } + + clearInvocations(mockTextContextWrapper, mockClient) + + // Make list of indices to select again + indicesToSelect = setOf(7, 8) // Select images at indices 7,8 + expectedUris = constructUrisForIndices(indicesToSelect) + + // Filter image nodes based on the indices to select and performClick + performClickForIndices(allImageNodes, indicesToSelect) + + // Wait for PhotoGridViewModel to modify Selection + advanceTimeBy(100) + composeTestRule.waitForIdle() + + assertWithMessage("Expected selection to contain an item, but it did not.") + .that(selection.snapshot().size) + .isEqualTo(3) + + // Verify that grantUriPermission is invoked for all newly selected media. + verify(mockTextContextWrapper, times(2)).grantUriPermission(capture(uriCaptor3)) + capturedUris = uriCaptor3.allValues + + assertThat(capturedUris).containsExactlyElementsIn(expectedUris) + + // Verify that client callback is invoked for all uris that were successfully + // granted permission + for (uri in expectedUris) { + verify(mockClient, times(1)).onItemSelected(uri) + } + } + + @Test + fun testSelectionGrantOrRevokePermissionFailed() = + testScope.runTest { + setUpTestDataWithStubProvider(20) + + // Mark image at node 0 as media item we aren't able to grant permission. + val grantFailureUri = constructUrisForIndices(setOf(0))[0] + whenever(mockTextContextWrapper.grantUriPermission(grantFailureUri)) { + EmbeddedService.GrantResult.FAILURE + } + + val component = embeddedServiceComponentBuilder.build() + val session = getSessionUnderTest(component) + advanceTimeBy(100) + + // Now the view is in the test's compose tree, so do a simple check to make sure + // the view actually initialized and the test can locate the photo grid / modify the + // selection. + composeTestRule.setContent { + // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to + // the test. + AndroidView( + factory = { + SurfaceView(getTestableContext()).apply { + setChildSurfacePackage(session.surfacePackage) + } + } + ) + } + + composeTestRule.waitForIdle() + + val resources = getTestableContext().getResources() + // This is the accessibility label for a Photo in the grid. + val mediaItemString = resources.getString(R.string.photopicker_media_item) + + // Get all image nodes + val allImageNodes = composeTestRule.onAllNodesWithContentDescription(mediaItemString) + + // Make list of indices to select + var indicesToSelect = setOf(2, 0, 4) // Select images at indices 2, 0, and 4 + var expectedUris: List<Uri> = constructUrisForIndices(indicesToSelect) + + // Filter image nodes based on the indices to select and performClick + performClickForIndices(allImageNodes, indicesToSelect) + + // Wait for PhotoGridViewModel to modify Selection + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Ensure the click handler correctly ran by checking the selection snapshot. + assertWithMessage("Expected selection to contain an item, but it did not.") + .that(selection.snapshot().size) + .isEqualTo(3) + + // Verify that client callback is invoked for all uris that were successfully + // granted permission and never for the uri that we failed granting permission + verify(mockTextContextWrapper, times(3)).grantUriPermission(capture(uriCaptor)) + var capturedUris = uriCaptor.allValues + + assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris) + + for (uri in expectedUris) { + if (uri == grantFailureUri) continue + verify(mockClient, times(1)).onItemSelected(uri) + } + verify(mockClient, never()).onItemSelected(grantFailureUri) + + clearInvocations(mockTextContextWrapper, mockClient) + + // Mark image at node 2 as media item we aren't able to revoke permission. + val revokeFailureUri = constructUrisForIndices(setOf(2))[0] + whenever(mockTextContextWrapper.revokeUriPermission(revokeFailureUri)) { + EmbeddedService.GrantResult.FAILURE + } + + // Make list of indices to select + var indicesToDeselect = setOf(2) // Deselect image at indices 2 + expectedUris = constructUrisForIndices(indicesToDeselect) + + // Filter image nodes based on the indices to select and performClick + performClickForIndices(allImageNodes, indicesToDeselect) + + // Wait for PhotoGridViewModel to modify Selection + advanceTimeBy(100) + composeTestRule.waitForIdle() + + // Ensure the click handler correctly ran by checking the selection snapshot. + assertWithMessage("Expected selection to contain an item, but it did not.") + .that(selection.snapshot().size) + .isEqualTo(2) // images at indices 0, 4 are still selected + + // Verify client callback is never invoked if we failed to revoke permission to uri + verify(mockTextContextWrapper, times(1)).revokeUriPermission(capture(uriCaptor2)) + capturedUris = uriCaptor2.allValues + + assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris) + + verify(mockClient, never()).onItemDeselected(revokeFailureUri) + } + + /** Gets the correct nodes of media item for given indices and performs click. */ + private fun performClickForIndices( + allImageNodes: SemanticsNodeInteractionCollection, + indicesToSelect: Set<Int> + ) { + var imageNodesToSelect = + indicesToSelect.mapNotNull { index -> + try { + allImageNodes.get(index) + } catch (e: AssertionError) { + // Fail the test if no node found at given position + fail("Unexpected AssertionError: Index out of bounds") // Fail the test + null + } + } + + for (node in imageNodesToSelect) { + node.assert(hasClickAction()).assertIsDisplayed().performClick() + } + } + + /** Using [StubProvider] as a backing provider, set custom number of media */ + private fun setUpTestDataWithStubProvider(mediaCount: Int): List<Media> { + val stubProvider = + Provider( + authority = StubProvider.AUTHORITY, + mediaSource = MediaSource.LOCAL, + uid = 1, + displayName = "Stub Provider" + ) + + val testDataService = dataService as? TestDataServiceImpl + checkNotNull(testDataService) { "Expected a TestDataServiceImpl" } + testDataService.setAvailableProviders(listOf(stubProvider)) + testDataService.collectionInfo.put( + stubProvider, + CollectionInfo( + authority = stubProvider.authority, + collectionId = null, + accountName = null, + ) + ) + + val testImages = StubProvider.getTestMediaFromStubProvider(mediaCount) + testDataService.mediaList = testImages + return testImages + } + + /** + * Fake [ContextWrapper] class to mock [ContextWrapper#grantUriPermission] and + * [ContextWrapper#revokeUriPermission] in tests. + * + * These methods by default return Success. Tests can manipulate the behaviour for specific uri + * in their tests as we are spying this class instead of mocking. + */ + open class FakeTestContextWrapper { + open fun grantUriPermission(uri: Uri): EmbeddedService.GrantResult { + return EmbeddedService.GrantResult.SUCCESS + } + + open fun revokeUriPermission(uri: Uri): EmbeddedService.GrantResult { + return EmbeddedService.GrantResult.SUCCESS + } + } + + /** + * Constructs URI for given indices for test. + * + * Follows format "content://stubprovider/$id" + */ + private fun constructUrisForIndices(uriIndices: Set<Int>): List<Uri> { + val newUris = uriIndices.map { index -> Uri.parse("content://stubprovider/${index + 1}") } + return newUris + } + + @Test + fun testSessionNotifyResizedChangesViewSize() = + testScope.runTest { + val component = embeddedServiceComponentBuilder.build() + + val session = getSessionUnderTest(component) + advanceTimeBy(100) + + val initialWidth = session.getView().width + val initialHeight = session.getView().height + + val newWidth = 2 * initialWidth + val newHeight = 2 * initialHeight + + async { session.notifyResized(newWidth, newHeight) }.await() + advanceTimeBy(100) + + assertWithMessage("Expected view's width to be resized") + .that(session.getView().width) + .isEqualTo(newWidth) + + assertWithMessage("Expected view's height to be resized") + .that(session.getView().height) + .isEqualTo(newHeight) + } } diff --git a/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt b/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt index 81f7c079d..22a56d820 100644 --- a/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt @@ -57,6 +57,7 @@ class EventsTest { val token = "MockedFeatureToken" } private val testRegistrations = setOf(mockRegistration) + private val sessionId = generatePickerSessionId() private data class TestEvent(override val dispatcherToken: String) : Event @@ -213,7 +214,11 @@ class EventsTest { scope = backgroundScope, provideTestConfigurationFlow( scope = backgroundScope, - PhotopickerConfiguration(action = "TEST", deviceIsDebuggable = true) + PhotopickerConfiguration( + action = "TEST", + deviceIsDebuggable = true, + sessionId = sessionId + ) ), buildFeatureManagerWithFeatures(testRegistrations, backgroundScope) ) @@ -241,7 +246,11 @@ class EventsTest { scope = backgroundScope, provideTestConfigurationFlow( scope = backgroundScope, - PhotopickerConfiguration(action = "TEST", deviceIsDebuggable = false) + PhotopickerConfiguration( + action = "TEST", + deviceIsDebuggable = false, + sessionId = sessionId + ) ), buildFeatureManagerWithFeatures(testRegistrations, backgroundScope) ) diff --git a/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt b/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt index 09ec82e3a..5f2dbff08 100644 --- a/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt @@ -36,6 +36,7 @@ import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testPhotopickerConfiguration import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.RegisteredEventClass +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.features.alwaysdisabledfeature.AlwaysDisabledFeature import com.android.photopicker.features.highpriorityuifeature.HighPriorityUiFeature import com.android.photopicker.features.simpleuifeature.SimpleUiFeature @@ -77,6 +78,8 @@ class FeatureManagerTest { AlwaysDisabledFeature.Registration, ) + val sessionId = generatePickerSessionId() + @Composable private fun featureManagerTestUiComposeTop( featureManager: FeatureManager, @@ -263,6 +266,7 @@ class FeatureManagerTest { PhotopickerConfiguration( action = "TEST", deviceIsDebuggable = true, + sessionId = sessionId ) ) @@ -300,6 +304,7 @@ class FeatureManagerTest { PhotopickerConfiguration( action = "TEST", deviceIsDebuggable = false, + sessionId = sessionId ) ) diff --git a/photopicker/tests/src/com/android/photopicker/core/glide/GlideTestRule.kt b/photopicker/tests/src/com/android/photopicker/core/glide/GlideTestRule.kt new file mode 100644 index 000000000..1f5995fe1 --- /dev/null +++ b/photopicker/tests/src/com/android/photopicker/core/glide/GlideTestRule.kt @@ -0,0 +1,51 @@ +/* + * 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.core.glide + +import androidx.test.core.app.ApplicationProvider +import com.bumptech.glide.Glide +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.load.engine.executor.GlideExecutor +import com.bumptech.glide.load.engine.executor.MockGlideExecutor +import org.junit.rules.ExternalResource + +/** + * A JUnit rule that configures Glide for use in tests. + * + * ``` + * @get:Rule val glideRule = GlideTestRule() + * ``` + */ +class GlideTestRule : ExternalResource() { + + override fun before() { + + // For tests, force Glide onto the main thread, rather than it's private executor pool. + val glideExecutor: GlideExecutor = MockGlideExecutor.newMainThreadExecutor() + Glide.init( + ApplicationProvider.getApplicationContext(), + GlideBuilder() + .setDiskCacheExecutor(glideExecutor) + .setAnimationExecutor(glideExecutor) + .setSourceExecutor(glideExecutor), + ) + } + + override fun after() { + Glide.tearDown() + } +} 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 db0e7996e..d9b83f6ea 100644 --- a/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt @@ -41,7 +41,6 @@ import com.android.photopicker.test.utils.GlideLoadableIdlingResource import com.android.photopicker.test.utils.MockContentProviderWrapper import com.android.photopicker.tests.utils.mockito.capture import com.android.photopicker.tests.utils.mockito.whenever -import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -98,8 +97,9 @@ import org.mockito.MockitoAnnotations class LoadMediaTest { /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */ - @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this) + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createComposeRule() + @get:Rule(order = 2) val glideRule = GlideTestRule() private val glideIdlingResource: GlideLoadableIdlingResource = GlideLoadableIdlingResource() private lateinit var provider: MockContentProviderWrapper @@ -168,10 +168,6 @@ class LoadMediaTest { fun teardown() { composeTestRule.unregisterIdlingResource(glideIdlingResource) glideIdlingResource.reset() - - // It is important to tearDown glide after every test to ensure it picks up the updated - // mocks from Hilt and mocks aren't leaked between tests. - Glide.tearDown() } /** Ensures that a [GlideLoadable] can be loaded via the [loadMedia] composable using Glide. */ diff --git a/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt b/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt index a244ba344..91258ac40 100644 --- a/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt @@ -32,6 +32,7 @@ import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration import com.android.photopicker.core.configuration.PhotopickerConfiguration import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.events.RegisteredEventClass +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.core.features.LocalFeatureManager @@ -56,6 +57,7 @@ class PhotopickerNavGraphTest { lateinit var navController: TestNavHostController lateinit var featureManager: FeatureManager + private val sessionId = generatePickerSessionId() val testRegistrations = setOf( @@ -86,7 +88,8 @@ class PhotopickerNavGraphTest { @Composable private fun testNavGraph( featureManager: FeatureManager, - configuration: PhotopickerConfiguration = PhotopickerConfiguration(action = "") + configuration: PhotopickerConfiguration = + PhotopickerConfiguration(action = "", sessionId = sessionId) ) { navController = TestNavHostController(LocalContext.current) navController.navigatorProvider.addNavigator(ComposeNavigator()) @@ -150,7 +153,8 @@ class PhotopickerNavGraphTest { val config = PhotopickerConfiguration( action = "", - startDestination = PhotopickerDestinations.ALBUM_GRID + startDestination = PhotopickerDestinations.ALBUM_GRID, + sessionId = sessionId ) composeTestRule.setContent { testNavGraph(featureManager, config) } @@ -165,7 +169,8 @@ class PhotopickerNavGraphTest { val config = PhotopickerConfiguration( action = "", - startDestination = PhotopickerDestinations.PHOTO_GRID + startDestination = PhotopickerDestinations.PHOTO_GRID, + sessionId = sessionId ) composeTestRule.setContent { testNavGraph(featureManager, config) } diff --git a/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt b/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt index 1302f6022..d6c68e3fc 100644 --- a/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt @@ -1,18 +1,18 @@ /* -* 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. -*/ + * 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.core.selection @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.photopicker.core.configuration.MULTI_SELECT_CONFIG import com.android.photopicker.core.configuration.SINGLE_SELECT_CONFIG import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.data.TestDataServiceImpl import com.android.photopicker.data.model.Grantable import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -57,10 +58,11 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = SINGLE_SELECT_CONFIG - ) + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = SINGLE_SELECT_CONFIG, + ), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val snapshot = selection.snapshot() @@ -77,11 +79,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = INITIAL_SELECTION + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = INITIAL_SELECTION, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val snapshot = selection.snapshot() @@ -99,17 +102,38 @@ class GrantsAwareSelectionTest { } @Test - fun testSelectionReturnsSuccess() = runTest { + fun testPreGrantsCountIsReflectedInSize() = runTest { + var countOfGrants = 120 + val dataService = TestDataServiceImpl() + dataService.setInitPreGrantsCount(countOfGrants) val selection: Selection<SelectionData> = GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + preGrantedItemsCount = dataService.preGrantedMediaCount ) + assertWithMessage("Unexpected size of selection") + .that(selection.snapshot().size) + .isEqualTo(countOfGrants) + } + + @Test + fun testSelectionReturnsSuccess() = runTest { + val selection: Selection<SelectionData> = + GrantsAwareSelectionImpl( + scope = backgroundScope, + configuration = + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, + ) assertWithMessage("Selection addition was expected to be successful: item 1") .that(selection.add(SelectionData(1))) @@ -128,14 +152,14 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = SINGLE_SELECT_CONFIG - ), - initialSelection = setOf(SelectionData(1)) + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = SINGLE_SELECT_CONFIG + ), + initialSelection = setOf(SelectionData(1)), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) - assertWithMessage("Snapshot was expected to contain the initial selection") .that(selection.add(SelectionData(2))) .isEqualTo(SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED) @@ -148,10 +172,11 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ) + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -181,10 +206,11 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ) + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -221,11 +247,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = INITIAL_SELECTION + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = INITIAL_SELECTION, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -260,11 +287,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = setOf(testItem, anotherTestItem) + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = setOf(testItem, anotherTestItem), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -311,11 +339,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = values + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = values, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -350,11 +379,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = INITIAL_SELECTION + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = INITIAL_SELECTION, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -391,11 +421,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = INITIAL_SELECTION + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = INITIAL_SELECTION, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val emissions = mutableListOf<Set<SelectionData>>() backgroundScope.launch { selection.flow.toList(emissions) } @@ -442,11 +473,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = values + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = values, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) assertWithMessage("Received unexpected position for item.") @@ -461,11 +493,12 @@ class GrantsAwareSelectionTest { GrantsAwareSelectionImpl( scope = backgroundScope, configuration = - provideTestConfigurationFlow( - scope = backgroundScope, - defaultConfiguration = MULTI_SELECT_CONFIG - ), - initialSelection = INITIAL_SELECTION + provideTestConfigurationFlow( + scope = backgroundScope, + defaultConfiguration = MULTI_SELECT_CONFIG + ), + initialSelection = INITIAL_SELECTION, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount, ) val missingElement = SelectionData(id = 999) @@ -477,69 +510,70 @@ class GrantsAwareSelectionTest { /** Ensures a single preGranted item can be removed and added again. */ @Test - fun testSelectionCanRemoveSinglePreGrantedItem() = - runTest { - // mock a test item to return isPreGranted as true. - val testItem = SelectionData(id = 999, isPreGrantedParam = true) - - val selection = - GrantsAwareSelectionImpl<SelectionData>( - scope = backgroundScope, - configuration = + fun testSelectionCanRemoveSinglePreGrantedItem() = runTest { + // mock a test item to return isPreGranted as true. + val testItem = SelectionData(id = 999, isPreGrantedParam = true) + + val dataService = TestDataServiceImpl() + dataService.setInitPreGrantsCount(1) + val selection = + GrantsAwareSelectionImpl<SelectionData>( + scope = backgroundScope, + configuration = provideTestConfigurationFlow( scope = backgroundScope, defaultConfiguration = MULTI_SELECT_CONFIG, ), - preGrantedItemsCount = 1, // corresponding to testItem - ) - - val emissions = mutableListOf<Set<SelectionData>>() - backgroundScope.launch { selection.flow.toList(emissions) } - - val initialSnapshot = selection.snapshot() - // There is only one preGranted item - assertWithMessage("Initial Snapshot has an unexpected size") - .that(initialSnapshot) - .hasSize(1) - - // remove preGranted item - selection.remove(testItem) - - val snapshot = selection.snapshot() - advanceTimeBy(100) - val flow = emissions.last() - - assertWithMessage("Deselection should contain test item").that( - selection.getDeselection() - ).contains(testItem) - - assertWithMessage("Snapshot contains the removed item.") - .that(snapshot).doesNotContain(testItem) - - assertWithMessage("Emitted flow value contains the removed item.") - .that(flow) - .doesNotContain(testItem) - assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0) - - // Now add the preGranted item again and verify that it was removed from deselection. - selection.add(testItem) - - val snapshot2 = selection.snapshot() - advanceTimeBy(100) - val flow2 = emissions.last() - assertWithMessage("Deselection should not contain test item").that( - selection - .getDeselection(), + preGrantedItemsCount = dataService.preGrantedMediaCount ) - .doesNotContain(testItem) - assertWithMessage("Snapshot contains the added item.") - .that(snapshot2).contains(testItem) - assertWithMessage("Snapshot has an unexpected size").that(snapshot2).hasSize(1) + val emissions = mutableListOf<Set<SelectionData>>() + backgroundScope.launch { selection.flow.toList(emissions) } - assertWithMessage("Emitted flow value contains the removed item.") - .that(flow2) - .contains(testItem) - assertWithMessage("Emitted flow has an unexpected size").that(flow2).hasSize(1) - } -}
\ No newline at end of file + val initialSnapshot = selection.snapshot() + // There is only one preGranted item + assertWithMessage("Initial Snapshot has an unexpected size") + .that(initialSnapshot) + .hasSize(1) + + // remove preGranted item + selection.remove(testItem) + + val snapshot = selection.snapshot() + advanceTimeBy(100) + val flow = emissions.last() + + assertWithMessage("Deselection should contain test item") + .that(selection.getDeselection()) + .contains(testItem) + + assertWithMessage("Snapshot contains the removed item.") + .that(snapshot) + .doesNotContain(testItem) + + assertWithMessage("Emitted flow value contains the removed item.") + .that(flow) + .doesNotContain(testItem) + assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0) + + // Now add the preGranted item again and verify that it was removed from deselection. + selection.add(testItem) + + val snapshot2 = selection.snapshot() + advanceTimeBy(100) + val flow2 = emissions.last() + assertWithMessage("Deselection should not contain test item") + .that( + selection.getDeselection(), + ) + .doesNotContain(testItem) + + assertWithMessage("Snapshot contains the added item.").that(snapshot2).contains(testItem) + assertWithMessage("Snapshot has an unexpected size").that(snapshot2).hasSize(1) + + assertWithMessage("Emitted flow value contains the removed item.") + .that(flow2) + .contains(testItem) + assertWithMessage("Emitted flow has an unexpected size").that(flow2).hasSize(1) + } +} diff --git a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt index 3e1da0f75..ccee584e4 100644 --- a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt @@ -33,7 +33,9 @@ import com.android.photopicker.core.configuration.PhotopickerConfiguration import com.android.photopicker.core.configuration.PhotopickerFlags import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testPhotopickerConfiguration +import com.android.photopicker.core.events.Events import com.android.photopicker.core.events.RegisteredEventClass +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.user.UserProfile import com.android.photopicker.core.user.UserStatus @@ -96,6 +98,8 @@ class DataServiceImplTest { ) } + private val sessionId = generatePickerSessionId() + private lateinit var testFeatureManager: FeatureManager private lateinit var testContentProvider: TestMediaProvider private lateinit var testContentResolver: ContentResolver @@ -104,6 +108,7 @@ class DataServiceImplTest { private lateinit var userStatus: UserStatus private lateinit var mockContext: Context private lateinit var mockPackageManager: PackageManager + private lateinit var events: Events @Before fun setup() { @@ -133,6 +138,12 @@ class DataServiceImplTest { @Test fun testInitialAllowedProvider() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -143,7 +154,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val emissions = mutableListOf<List<Provider>>() @@ -159,6 +171,12 @@ class DataServiceImplTest { @Test fun testUpdateAvailableProviders() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -169,7 +187,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val emissions = mutableListOf<List<Provider>>() @@ -232,6 +251,20 @@ class DataServiceImplTest { ) val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) val scope = TestScope() + val featureManager = + FeatureManager( + provideTestConfigurationFlow(scope = scope.backgroundScope), + scope, + setOf(), // Don't register CloudMediaFeature + setOf<RegisteredEventClass>(), + setOf<RegisteredEventClass>(), + ) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + featureManager + ) val dataService: DataService = DataServiceImpl( userStatus = userStatusFlow, @@ -240,15 +273,9 @@ class DataServiceImplTest { mediaProviderClient = mediaProviderClient, dispatcher = StandardTestDispatcher(this.testScheduler), config = MutableStateFlow(testPhotopickerConfiguration), - featureManager = - FeatureManager( - provideTestConfigurationFlow(scope = scope.backgroundScope), - scope, - setOf(), // Don't register CloudMediaFeature - setOf<RegisteredEventClass>(), - setOf<RegisteredEventClass>(), - ), - appContext = mockContext + appContext = mockContext, + featureManager = featureManager, + events = events ) val emissions = mutableListOf<List<Provider>>() @@ -267,6 +294,12 @@ class DataServiceImplTest { @Test fun testAvailableProvidersWhenUserChanges() = runTest { val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -277,7 +310,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val emissions = mutableListOf<List<Provider>>() @@ -349,6 +383,12 @@ class DataServiceImplTest { fun testContentObserverRegistrationWhenUserChanges() = runTest { val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus) val mockNotificationService = mock(NotificationService::class.java) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -359,7 +399,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val emissions = mutableListOf<List<Provider>>() @@ -460,6 +501,12 @@ class DataServiceImplTest { @Test fun testMediaPagingSourceInvalidation() = runTest { val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -470,7 +517,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val emissions = mutableListOf<List<Provider>>() @@ -522,6 +570,12 @@ class DataServiceImplTest { @Test fun testAlbumPagingSourceInvalidation() = runTest { val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -532,7 +586,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) // Check initial available provider emissions @@ -584,6 +639,12 @@ class DataServiceImplTest { @Test fun testAlbumMediaPagingSourceCacheUpdates() = runTest { testContentProvider.lastRefreshMediaRequest = null + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus) val dataService: DataService = @@ -595,7 +656,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) advanceTimeBy(100) @@ -656,6 +718,12 @@ class DataServiceImplTest { @Test fun testAlbumMediaPagingSourceInvalidation() = runTest { val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -666,7 +734,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) // Check initial available provider emissions @@ -744,6 +813,12 @@ class DataServiceImplTest { @Test fun testOnUpdateMediaNotification() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -754,7 +829,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) advanceTimeBy(100) @@ -787,6 +863,12 @@ class DataServiceImplTest { @Test fun testOnUpdateAlbumMediaNotification() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -797,7 +879,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) advanceTimeBy(100) @@ -853,6 +936,12 @@ class DataServiceImplTest { @Test fun testDisruptiveDataUpdate() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) testContentProvider.providers = mutableListOf( @@ -873,7 +962,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val availableProviderEmissions = mutableListOf<List<Provider>>() @@ -942,6 +1032,12 @@ class DataServiceImplTest { @Test fun testCollectionInfoUpdate() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val dataService: DataService = DataServiceImpl( @@ -952,7 +1048,8 @@ class DataServiceImplTest { dispatcher = StandardTestDispatcher(this.testScheduler), config = provideTestConfigurationFlow(this.backgroundScope), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val availableProviderEmissions = mutableListOf<List<Provider>>() @@ -986,6 +1083,12 @@ class DataServiceImplTest { @Test fun testGetAllAllowedProviders() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val cloudProvider1 = Provider( "cloud_primary", @@ -1029,11 +1132,13 @@ class DataServiceImplTest { cloudProvider2.authority ), CLOUD_ENFORCE_PROVIDER_ALLOWLIST = true - ) + ), + sessionId = sessionId ) ), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val actualAllAllowedProviders = dataService.getAllAllowedProviders() @@ -1045,6 +1150,12 @@ class DataServiceImplTest { @Test fun testGetAllAllowedProvidersWhenAllowlistIsEnforced() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val cloudProvider1 = Provider( "cloud_primary", @@ -1084,11 +1195,13 @@ class DataServiceImplTest { PhotopickerFlags( CLOUD_ALLOWED_PROVIDERS = arrayOf(cloudProvider1.authority), CLOUD_ENFORCE_PROVIDER_ALLOWLIST = true - ) + ), + sessionId = sessionId ) ), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val actualAllAllowedProviders = dataService.getAllAllowedProviders() @@ -1099,6 +1212,12 @@ class DataServiceImplTest { @Test fun testGetAllAllowedProvidersWhenDeviceHasLimitedProviders() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val cloudProvider1 = Provider( "cloud_primary", @@ -1141,11 +1260,13 @@ class DataServiceImplTest { cloudProvider2.authority ), CLOUD_ENFORCE_PROVIDER_ALLOWLIST = true - ) + ), + sessionId = sessionId ) ), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val actualAllAllowedProviders = dataService.getAllAllowedProviders() @@ -1156,6 +1277,12 @@ class DataServiceImplTest { @Test fun testGetAllAllowedProvidersWhenAllowlistIsNotEnforced() = runTest { val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus) + events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope), + testFeatureManager + ) val cloudProvider1 = Provider( "cloud_primary", @@ -1195,11 +1322,13 @@ class DataServiceImplTest { PhotopickerFlags( CLOUD_ALLOWED_PROVIDERS = arrayOf(), CLOUD_ENFORCE_PROVIDER_ALLOWLIST = false - ) + ), + sessionId = sessionId ) ), featureManager = testFeatureManager, - appContext = mockContext + appContext = mockContext, + events = events ) val actualAllAllowedProviders = dataService.getAllAllowedProviders() diff --git a/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt b/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt index 836171a94..dbb7ca51b 100644 --- a/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt +++ b/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt @@ -59,10 +59,18 @@ class TestDataServiceImpl() : DataService { val collectionInfo: HashMap<Provider, CollectionInfo> = HashMap() + private var _preGrantsCount = MutableStateFlow(/* default value */ 0) + fun setAvailableProviders(newProviders: List<Provider>) { _availableProviders.update { newProviders } } + override val preGrantedMediaCount: StateFlow<Int> = _preGrantsCount + + fun setInitPreGrantsCount(count: Int) { + _preGrantsCount.update { count } + } + override fun albumMediaPagingSource(album: Album): PagingSource<MediaPageKey, Media> { return albumMediaList?.let { FakeInMemoryMediaPagingSource(it) } ?: FakeInMemoryMediaPagingSource(albumMediaSetSize) @@ -97,10 +105,18 @@ class TestDataServiceImpl() : DataService { override val disruptiveDataUpdateChannel = Channel<Unit>(CONFLATED) + suspend fun sendDisruptiveDataUpdateNotification() { + disruptiveDataUpdateChannel.send(Unit) + } + override suspend fun getCollectionInfo(provider: Provider): CollectionInfo = collectionInfo.getOrElse(provider, { CollectionInfo(provider.authority) }) override suspend fun ensureProviders() {} override fun getAllAllowedProviders(): List<Provider> = allowedProviders + + override fun refreshPreGrantedItemsCount() { + // no_op + } } diff --git a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt index b3628e557..40a095988 100644 --- a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt +++ b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt @@ -111,6 +111,7 @@ class TestMediaProvider( var albumMedia: Map<String, List<Media>> = DEFAULT_ALBUM_MEDIA ) : MockContentProvider() { var lastRefreshMediaRequest: Bundle? = null + var TEST_GRANTS_COUNT = 2 override fun query( uri: Uri, @@ -123,6 +124,7 @@ class TestMediaProvider( "collection_info" -> getCollectionInfo() "media" -> getMedia() "album" -> getAlbums() + "media_grants_count" -> fetchMediaGrantsCount() else -> { val pathSegments: MutableList<String> = uri.getPathSegments() if (pathSegments.size == 4 && pathSegments[2].equals("album")) { @@ -209,6 +211,7 @@ class TestMediaProvider( MediaProviderClient.MediaResponse.MIME_TYPE.key, MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key, MediaProviderClient.MediaResponse.DURATION.key, + MediaProviderClient.MediaResponse.IS_PRE_GRANTED.key, ) ) mediaItems.forEach { mediaItem -> @@ -224,7 +227,8 @@ class TestMediaProvider( mediaItem.sizeInBytes.toString(), mediaItem.mimeType, mediaItem.standardMimeTypeExtension.toString(), - if (mediaItem is Media.Video) mediaItem.duration else "0" + if (mediaItem is Media.Video) mediaItem.duration else "0", + if (mediaItem.isPreGranted) 1 else 0, ) ) } @@ -260,6 +264,12 @@ class TestMediaProvider( return cursor } + private fun fetchMediaGrantsCount(): Cursor { + val cursor = MatrixCursor(arrayOf("grants_count")) + cursor.addRow(arrayOf(TEST_GRANTS_COUNT)) + return cursor + } + private fun getAlbumMedia(albumId: String): Cursor? { return getMedia(albumMedia.getOrDefault(albumId, emptyList())) } diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt index 1cb67d586..553f23234 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt @@ -17,11 +17,17 @@ package com.android.photopicker.features.data.paging import android.content.ContentResolver +import android.content.Intent import android.provider.MediaStore import androidx.paging.PagingSource.LoadParams import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.configuration.testSessionId +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.TestMediaProvider import com.android.photopicker.data.model.MediaPageKey @@ -49,6 +55,12 @@ class AlbumMediaPagingSourceTest { private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider) private val availableProviders: List<Provider> = listOf(Provider("auth", MediaSource.LOCAL, 0, "")) + private val testPhotopickerConfiguration: PhotopickerConfiguration = + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + intent = Intent(MediaStore.ACTION_PICK_IMAGES), + sessionId = testSessionId + ) @Mock private lateinit var mockMediaProviderClient: MediaProviderClient @@ -61,7 +73,19 @@ class AlbumMediaPagingSourceTest { fun testLoad() = runTest { val albumId = "test-album-id" val albumAuthority = availableProviders[0].authority - val config = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES) + val featureManager = + FeatureManager( + provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration), + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration), + featureManager + ) + val albumMediaPagingSource = AlbumMediaPagingSource( albumId = albumId, @@ -70,7 +94,8 @@ class AlbumMediaPagingSourceTest { availableProviders = availableProviders, mediaProviderClient = mockMediaProviderClient, dispatcher = StandardTestDispatcher(this.testScheduler), - config = config + testPhotopickerConfiguration, + events ) val pageKey = MediaPageKey() @@ -93,7 +118,7 @@ class AlbumMediaPagingSourceTest { pageSize, contentResolver, availableProviders, - config + testPhotopickerConfiguration ) } } diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt index 19b95836d..361725c00 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt @@ -17,11 +17,17 @@ package com.android.photopicker.features.data.paging import android.content.ContentResolver +import android.content.Intent import android.provider.MediaStore import androidx.paging.PagingSource.LoadParams import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.configuration.testSessionId +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.TestMediaProvider import com.android.photopicker.data.model.MediaPageKey @@ -47,6 +53,12 @@ class AlbumPagingSourceTest { private val testContentProvider: TestMediaProvider = TestMediaProvider() private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider) private val availableProviders: List<Provider> = emptyList() + private val testPhotopickerConfiguration: PhotopickerConfiguration = + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + intent = Intent(MediaStore.ACTION_PICK_IMAGES), + sessionId = testSessionId + ) @Mock private lateinit var mockMediaProviderClient: MediaProviderClient @@ -57,14 +69,27 @@ class AlbumPagingSourceTest { @Test fun testLoad() = runTest { - val config = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES) + val featureManager = + FeatureManager( + provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration), + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration), + featureManager + ) + val albumPagingSource = AlbumPagingSource( contentResolver = contentResolver, availableProviders = availableProviders, mediaProviderClient = mockMediaProviderClient, dispatcher = StandardTestDispatcher(this.testScheduler), - config = config + testPhotopickerConfiguration, + events ) val pageKey = MediaPageKey() @@ -80,6 +105,12 @@ class AlbumPagingSourceTest { advanceTimeBy(100) verify(mockMediaProviderClient, times(1)) - .fetchAlbums(pageKey, pageSize, contentResolver, emptyList(), config) + .fetchAlbums( + pageKey, + pageSize, + contentResolver, + emptyList(), + testPhotopickerConfiguration + ) } } diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt index 50c63f4bb..eb277d75c 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt @@ -17,11 +17,17 @@ package com.android.photopicker.features.data.paging import android.content.ContentResolver +import android.content.Intent import android.provider.MediaStore import androidx.paging.PagingSource.LoadParams import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.configuration.testSessionId +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.TestMediaProvider import com.android.photopicker.data.model.MediaPageKey @@ -47,6 +53,12 @@ class MediaPagingSourceTest { private val testContentProvider: TestMediaProvider = TestMediaProvider() private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider) private val availableProviders: List<Provider> = emptyList() + private val testPhotopickerConfiguration: PhotopickerConfiguration = + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + intent = Intent(MediaStore.ACTION_PICK_IMAGES), + sessionId = testSessionId + ) @Mock private lateinit var mockMediaProviderClient: MediaProviderClient @@ -57,14 +69,27 @@ class MediaPagingSourceTest { @Test fun testLoad() = runTest { - val config = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES) + val featureManager = + FeatureManager( + provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration), + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration), + featureManager + ) + val mediaPagingSource = MediaPagingSource( contentResolver = contentResolver, availableProviders = availableProviders, mediaProviderClient = mockMediaProviderClient, dispatcher = StandardTestDispatcher(this.testScheduler), - config = config + testPhotopickerConfiguration, + events ) val pageKey: MediaPageKey = MediaPageKey() @@ -80,6 +105,12 @@ class MediaPagingSourceTest { advanceTimeBy(100) verify(mockMediaProviderClient, times(1)) - .fetchMedia(pageKey, pageSize, contentResolver, emptyList(), config) + .fetchMedia( + pageKey, + pageSize, + contentResolver, + emptyList(), + testPhotopickerConfiguration + ) } } diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt index ae29a22a3..90bdccda1 100644 --- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt +++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt @@ -17,11 +17,14 @@ package com.android.photopicker.features.data.paging import android.content.ContentResolver +import android.content.Intent import android.provider.MediaStore import androidx.paging.PagingSource.LoadResult import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.data.MediaProviderClient import com.android.photopicker.data.TestMediaProvider import com.android.photopicker.data.model.Group @@ -39,6 +42,7 @@ import org.junit.runner.RunWith class MediaProviderClientTest { private val testContentProvider: TestMediaProvider = TestMediaProvider() private val testContentResolver: ContentResolver = ContentResolver.wrap(testContentProvider) + private val sessionId = generatePickerSessionId() @Test fun testFetchAvailableProviders() = runTest { @@ -63,7 +67,11 @@ class MediaProviderClientTest { pageSize = 5, contentResolver = testContentResolver, availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0, "")), - config = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES) + config = + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + sessionId = sessionId + ) ) assertThat(mediaLoadResult is LoadResult.Page).isTrue() @@ -103,7 +111,11 @@ class MediaProviderClientTest { ) val mimeTypes = arrayListOf("image/gif", "video/*") val config = - PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, mimeTypes = mimeTypes) + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + mimeTypes = mimeTypes, + sessionId = sessionId + ) mediaProviderClient.refreshMedia( providers = providers, @@ -121,6 +133,60 @@ class MediaProviderClientTest { } @Test + fun testRefreshMediaForUserSelectAction() = runTest { + testContentProvider.lastRefreshMediaRequest = null + val mediaProviderClient = MediaProviderClient() + val providers: List<Provider> = + mutableListOf( + Provider( + authority = "local_authority", + mediaSource = MediaSource.LOCAL, + uid = 0, + displayName = "abc" + ), + Provider( + authority = "hypothetical_local_authority", + mediaSource = MediaSource.LOCAL, + uid = 2, + displayName = "xyz" + ), + ) + mediaProviderClient.refreshMedia( + providers = providers, + resolver = testContentResolver, + config = testUserSelectImagesForAppConfiguration + ) + + assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull() + // TODO(b/340246010): Currently, we trigger sync for all available providers. This is + // because UI is responsible for triggering syncs which is sometimes required to enable + // providers. This should be changed to triggering syncs for specific providers once the + // backend takes responsibility for the sync triggers. + assertThat(testContentProvider.lastRefreshMediaRequest?.getBoolean("is_local_only", true)) + .isFalse() + assertThat(testContentProvider.lastRefreshMediaRequest?.getStringArrayList("mime_types")) + .isEqualTo(testUserSelectImagesForAppConfiguration.mimeTypes) + assertThat(testContentProvider.lastRefreshMediaRequest?.getString("intent_action")) + .isEqualTo(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) + assertThat(testContentProvider.lastRefreshMediaRequest?.getInt(Intent.EXTRA_UID)) + .isEqualTo(testUserSelectImagesForAppConfiguration.callingPackageUid) + } + + @Test + fun testFetchMediaGrantsCount() = runTest { + testContentProvider.lastRefreshMediaRequest = null + val mediaProviderClient = MediaProviderClient() + + val countOfGrants = + mediaProviderClient.fetchMediaGrantsCount( + contentResolver = testContentResolver, + callingPackageUid = testUserSelectImagesForAppConfiguration.callingPackageUid ?: -1, + ) + + assertThat(countOfGrants).isEqualTo(testContentProvider.TEST_GRANTS_COUNT) + } + + @Test fun testRefreshLocalOnlyMedia() = runTest { testContentProvider.lastRefreshMediaRequest = null val mediaProviderClient = MediaProviderClient() @@ -141,7 +207,11 @@ class MediaProviderClientTest { ) val mimeTypes = arrayListOf("image/gif", "video/*") val config = - PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, mimeTypes = mimeTypes) + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + mimeTypes = mimeTypes, + sessionId = sessionId + ) mediaProviderClient.refreshMedia( providers = providers, @@ -186,7 +256,11 @@ class MediaProviderClientTest { ) val mimeTypes = arrayListOf("image/gif", "video/*") val config = - PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, mimeTypes = mimeTypes) + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + mimeTypes = mimeTypes, + sessionId = sessionId + ) mediaProviderClient.refreshAlbumMedia( albumId = albumId, @@ -219,7 +293,11 @@ class MediaProviderClientTest { pageSize = 5, contentResolver = testContentResolver, availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0, "")), - config = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES), + config = + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + sessionId = sessionId + ), ) assertThat(albumLoadResult is LoadResult.Page).isTrue() @@ -246,7 +324,11 @@ class MediaProviderClientTest { pageSize = 5, contentResolver = testContentResolver, availableProviders = listOf(Provider(albumAuthority, MediaSource.LOCAL, 0, "")), - config = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES), + config = + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + sessionId = sessionId + ), ) assertThat(mediaLoadResult is LoadResult.Page).isTrue() diff --git a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt index 6a2999d8c..d75873ef0 100644 --- a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt @@ -22,6 +22,7 @@ import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.os.UserManager +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS import android.test.mock.MockContentResolver @@ -44,7 +45,6 @@ import com.android.photopicker.core.ConcurrencyModule import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.ViewModelModule -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.testActionPickImagesConfiguration import com.android.photopicker.core.configuration.testGetContentConfiguration @@ -52,6 +52,7 @@ import com.android.photopicker.core.configuration.testPhotopickerConfiguration import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.navigation.PhotopickerDestinations import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.DataService @@ -67,7 +68,6 @@ import com.android.photopicker.test.utils.MockContentProviderWrapper import com.android.photopicker.tests.HiltTestActivity import com.android.photopicker.tests.utils.mockito.whenever import com.google.common.truth.Truth.assertWithMessage -import dagger.Lazy import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.testing.BindValue @@ -105,6 +105,7 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -139,7 +140,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var events: Events - @Inject lateinit var bannerManager: Lazy<BannerManager> @Inject override lateinit var configurationManager: ConfigurationManager @Inject lateinit var dataService: DataService @@ -193,7 +193,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -228,7 +227,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -257,6 +255,7 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { // Allow the PreviewViewModel to collect flows advanceTimeBy(100) + composeTestRule.waitForIdle() assertWithMessage("Expected route to be albummediagrid") .that(navController.currentBackStackEntry?.destination?.route) @@ -271,7 +270,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -317,7 +315,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -397,7 +394,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -479,7 +475,6 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -518,4 +513,80 @@ class AlbumGridFeatureTest : PhotopickerFeatureBaseTest() { .assertIsDisplayed() } } + + @Test + fun testEmptyStateContentForCamera() { + + val testDataService = dataService as? TestDataServiceImpl + checkNotNull(testDataService) { "Expected a TestDataServiceImpl" } + + // Force the data service to return no data for all test sources during this test. + testDataService.albumMediaSetSize = 0 + testDataService.albumsList = + listOf( + Group.Album( + id = ALBUM_ID_CAMERA, + pickerId = 1234L, + authority = "a", + displayName = "Camera", + coverUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("1234") + } + .build(), + dateTakenMillisLong = 12345678L, + coverMediaSource = MediaSource.LOCAL, + ) + ) + + val resources = getTestableContext().getResources() + + testScope.runTest { + composeTestRule.setContent { + // Set an explicit size to prevent errors in glide being unable to measure + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + advanceTimeBy(100) + + // Navigate on the UI thread (similar to a click handler) + composeTestRule.runOnUiThread({ navController.navigateToAlbumGrid() }) + + assertWithMessage("Expected route to be albumgrid") + .that(navController.currentBackStackEntry?.destination?.route) + .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route) + + advanceTimeBy(100) + composeTestRule.waitForIdle() + + advanceTimeBy(100) + + val testAlbumDisplayName = "Camera" + composeTestRule.onNode(hasText(testAlbumDisplayName)).performClick() + + composeTestRule.waitForIdle() + + // Allow the PreviewViewModel to collect flows + advanceTimeBy(100) + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule + .onNode(hasText(resources.getString(R.string.photopicker_photos_empty_state_title))) + .assertIsDisplayed() + + composeTestRule + .onNode(hasText(resources.getString(R.string.photopicker_camera_empty_state_body))) + .assertIsDisplayed() + } + } } diff --git a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt index a2ca38334..c6130fd22 100644 --- a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.photopicker.features.albumgrid import android.net.Uri +import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.photopicker.core.configuration.PhotopickerConfiguration @@ -24,10 +25,13 @@ import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.Events import com.android.photopicker.core.events.RegisteredEventClass +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureToken.ALBUM_GRID import com.android.photopicker.core.selection.SelectionImpl import com.android.photopicker.data.TestDataServiceImpl +import com.android.photopicker.data.model.Group import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaSource import com.google.common.truth.Truth.assertWithMessage @@ -74,6 +78,27 @@ class AlbumGridViewModelTest { standardMimeTypeExtension = 1, ) + val album = + Group.Album( + id = ALBUM_ID_VIDEOS, + pickerId = 1234L, + authority = "a", + displayName = "Videos", + coverUri = + Uri.EMPTY.buildUpon() + .apply { + scheme("content") + authority("a") + path("1234") + } + .build(), + dateTakenMillisLong = 12345678L, + coverMediaSource = MediaSource.LOCAL, + ) + + val updatedMediaItem = + mediaItem.copy(mediaItemAlbum = album, selectionSource = Telemetry.MediaLocation.ALBUM) + @Test fun testAlbumGridItemClickedUpdatesSelection() { @@ -112,23 +137,24 @@ class AlbumGridViewModelTest { .isEqualTo(0) // Toggle the item into the selection - viewModel.handleAlbumMediaGridItemSelection(mediaItem, "") + viewModel.handleAlbumMediaGridItemSelection(mediaItem, "", album) // Wait for selection update. advanceTimeBy(100) + // The selected media item gets updated with the Selectable interface values assertWithMessage("Selection did not contain expected item") .that(selection.snapshot()) - .contains(mediaItem) + .contains(updatedMediaItem) // Toggle the item out of the selection - viewModel.handleAlbumMediaGridItemSelection(mediaItem, "") + viewModel.handleAlbumMediaGridItemSelection(mediaItem, "", album) advanceTimeBy(100) assertWithMessage("Selection contains unexpected item") .that(selection.snapshot()) - .doesNotContain(mediaItem) + .doesNotContain(updatedMediaItem) } } @@ -146,7 +172,8 @@ class AlbumGridViewModelTest { PhotopickerConfiguration( action = "TEST_ACTION", intent = null, - selectionLimit = 0 + selectionLimit = 0, + sessionId = generatePickerSessionId() ) ) ) @@ -183,7 +210,7 @@ class AlbumGridViewModelTest { // Toggle the item into the selection val errorMessage = "test" - viewModel.handleAlbumMediaGridItemSelection(mediaItem, errorMessage) + viewModel.handleAlbumMediaGridItemSelection(mediaItem, errorMessage, album) // Wait for selection update. advanceTimeBy(100) diff --git a/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt index b1024e9ed..6ae0eef80 100644 --- a/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt @@ -36,7 +36,6 @@ import com.android.photopicker.core.Background import com.android.photopicker.core.ConcurrencyModule import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.testActionPickImagesConfiguration import com.android.photopicker.core.configuration.testEmbeddedPhotopickerConfiguration @@ -46,6 +45,7 @@ import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureToken +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.model.Media import com.android.photopicker.features.PhotopickerFeatureBaseTest @@ -91,6 +91,7 @@ class BrowseFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -110,7 +111,6 @@ class BrowseFeatureTest : PhotopickerFeatureBaseTest() { @Inject lateinit var selection: Lazy<Selection<Media>> @Inject lateinit var featureManager: Lazy<FeatureManager> - @Inject lateinit var bannerManager: Lazy<BannerManager> @Inject lateinit var events: Lazy<Events> @Inject override lateinit var configurationManager: ConfigurationManager @@ -156,7 +156,6 @@ class BrowseFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } @@ -196,7 +195,6 @@ class BrowseFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } diff --git a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt index 517e4c8a1..f554290e3 100644 --- a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt @@ -52,6 +52,7 @@ import com.android.photopicker.core.configuration.testUserSelectImagesForAppConf import com.android.photopicker.core.database.DatabaseManager import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.DataService import com.android.photopicker.data.TestDataServiceImpl @@ -103,6 +104,7 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -186,7 +188,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } @@ -236,7 +237,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } @@ -328,7 +328,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() @@ -397,7 +396,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() @@ -453,7 +451,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() @@ -522,7 +519,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() @@ -567,7 +563,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() @@ -625,7 +620,6 @@ class CloudMediaFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager.get(), selection = selection.get(), events = events.get(), - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() diff --git a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt index ff15fc144..ac0318492 100644 --- a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt @@ -52,9 +52,11 @@ import com.android.photopicker.core.configuration.testGetContentConfiguration import com.android.photopicker.core.configuration.testPhotopickerConfiguration import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.LocalEvents import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.Location import com.android.photopicker.core.features.LocationParams +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaSource @@ -107,6 +109,7 @@ class MediaPreloaderTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -238,7 +241,8 @@ class MediaPreloaderTest : PhotopickerFeatureBaseTest() { composeTestRule.setContent { CompositionLocalProvider( - LocalPhotopickerConfiguration provides testPhotopickerConfiguration + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + LocalEvents provides events.get() ) { featureManager .get() @@ -280,7 +284,8 @@ class MediaPreloaderTest : PhotopickerFeatureBaseTest() { var preloadDeferred = CompletableDeferred<Boolean>() composeTestRule.setContent { CompositionLocalProvider( - LocalPhotopickerConfiguration provides testPhotopickerConfiguration + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + LocalEvents provides events.get() ) { featureManager .get() @@ -326,7 +331,8 @@ class MediaPreloaderTest : PhotopickerFeatureBaseTest() { var preloadDeferred = CompletableDeferred<Boolean>() composeTestRule.setContent { CompositionLocalProvider( - LocalPhotopickerConfiguration provides testPhotopickerConfiguration + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + LocalEvents provides events.get() ) { featureManager .get() @@ -374,7 +380,8 @@ class MediaPreloaderTest : PhotopickerFeatureBaseTest() { var preloadDeferred = CompletableDeferred<Boolean>() composeTestRule.setContent { CompositionLocalProvider( - LocalPhotopickerConfiguration provides testPhotopickerConfiguration + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + LocalEvents provides events.get() ) { featureManager .get() @@ -421,7 +428,8 @@ class MediaPreloaderTest : PhotopickerFeatureBaseTest() { composeTestRule.setContent { CompositionLocalProvider( - LocalPhotopickerConfiguration provides testPhotopickerConfiguration + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, + LocalEvents provides events.get() ) { featureManager .get() diff --git a/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt index 74b0411b0..d869b4cef 100644 --- a/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt @@ -37,7 +37,6 @@ import com.android.photopicker.core.ConcurrencyModule import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.ViewModelModule -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testActionPickImagesConfiguration @@ -46,6 +45,7 @@ import com.android.photopicker.core.configuration.testPhotopickerConfiguration import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.model.Media import com.android.photopicker.features.PhotopickerFeatureBaseTest @@ -54,7 +54,6 @@ import com.android.photopicker.test.utils.MockContentProviderWrapper import com.android.photopicker.tests.HiltTestActivity import com.android.photopicker.tests.utils.mockito.whenever import com.google.common.truth.Truth.assertWithMessage -import dagger.Lazy import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.testing.BindValue @@ -90,6 +89,7 @@ class NavigationBarFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -124,7 +124,6 @@ class NavigationBarFeatureTest : PhotopickerFeatureBaseTest() { @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var events: Events - @Inject lateinit var bannerManager: Lazy<BannerManager> @Inject override lateinit var configurationManager: ConfigurationManager @Before @@ -198,7 +197,6 @@ class NavigationBarFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } diff --git a/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt index db5db7092..f34b328dc 100644 --- a/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt @@ -40,9 +40,11 @@ import com.android.photopicker.core.ConcurrencyModule import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testActionPickImagesConfiguration import com.android.photopicker.core.configuration.testGetContentConfiguration +import com.android.photopicker.core.configuration.testPhotopickerConfiguration import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration import com.android.photopicker.core.events.Events import com.android.photopicker.core.events.LocalEvents @@ -51,6 +53,7 @@ import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureToken.OVERFLOW_MENU import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.features.Location +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.features.PhotopickerFeatureBaseTest import com.android.photopicker.features.simpleuifeature.SimpleUiFeature import com.android.photopicker.inject.PhotopickerTestModule @@ -91,6 +94,7 @@ class OverflowMenuFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -165,6 +169,7 @@ class OverflowMenuFeatureTest : PhotopickerFeatureBaseTest() { CompositionLocalProvider( LocalFeatureManager provides featureManager, LocalEvents provides events, + LocalPhotopickerConfiguration provides testPhotopickerConfiguration, ) { featureManager.composeLocation(Location.OVERFLOW_MENU) } @@ -246,6 +251,7 @@ class OverflowMenuFeatureTest : PhotopickerFeatureBaseTest() { CompositionLocalProvider( LocalFeatureManager provides featureManager, LocalEvents provides events, + LocalPhotopickerConfiguration provides testPhotopickerConfiguration ) { featureManager.composeLocation(Location.OVERFLOW_MENU) } 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 492f110e6..f3b1573a8 100644 --- a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt @@ -45,11 +45,15 @@ import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.ViewModelModule import com.android.photopicker.core.banners.BannerManager +import com.android.photopicker.core.banners.BannerStateDao import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.PhotopickerConfiguration import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.database.DatabaseManager import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.navigation.PhotopickerDestinations import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.DataService @@ -83,6 +87,8 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString import org.mockito.MockitoAnnotations @UninstallModules( @@ -100,6 +106,7 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -137,6 +144,9 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { @Inject lateinit var events: Events @Inject lateinit var bannerManager: Lazy<BannerManager> @Inject lateinit var dataService: DataService + @Inject lateinit var databaseManager: DatabaseManager + + val sessionId = generatePickerSessionId() @Before fun setup() { @@ -162,17 +172,19 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { @Test fun testPhotoGridIsAlwaysEnabled() { - val configOne = PhotopickerConfiguration(action = "TEST_ACTION") + val configOne = PhotopickerConfiguration(action = "TEST_ACTION", sessionId = sessionId) assertWithMessage("PhotoGridFeature is not always enabled for TEST_ACTION") .that(PhotoGridFeature.Registration.isEnabled(configOne)) .isEqualTo(true) - val configTwo = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES) + val configTwo = + PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, sessionId = sessionId) assertWithMessage("PhotoGridFeature is not always enabled") .that(PhotoGridFeature.Registration.isEnabled(configTwo)) .isEqualTo(true) - val configThree = PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT) + val configThree = + PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, sessionId = sessionId) assertWithMessage("PhotoGridFeature is not always enabled") .that(PhotoGridFeature.Registration.isEnabled(configThree)) .isEqualTo(true) @@ -195,7 +207,6 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -219,7 +230,6 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -257,7 +267,6 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -284,7 +293,6 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -321,7 +329,6 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -338,4 +345,39 @@ class PhotoGridFeatureTest : PhotopickerFeatureBaseTest() { .assertIsDisplayed() } } + + @Test + fun testShowsBannersInGrid() { + + testScope.runTest { + val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java) + whenever(bannerStateDao.getBannerState(anyString(), anyInt())) { null } + + configurationManager.setCaller( + callingPackage = "com.android.test.package", + callingPackageUid = 12345, + callingPackageLabel = "Test Package", + ) + advanceTimeBy(100) + + bannerManager.get().refreshBanners() + composeTestRule.setContent { + callPhotopickerMain( + featureManager = featureManager, + selection = selection, + events = events, + ) + } + + val resources = getTestableContext().getResources() + val expectedPrivacyMessage = + resources.getString(R.string.photopicker_privacy_explainer, "Test Package") + + // Wait for the PhotoGridViewModel to load data and for the UI to update. + advanceTimeBy(100) + composeTestRule.waitForIdle() + + composeTestRule.onNode(hasText(expectedPrivacyMessage)).assertIsDisplayed() + } + } } diff --git a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt index a69ef3a4c..04be68e75 100644 --- a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt @@ -16,34 +16,85 @@ package com.android.photopicker.features.photogrid +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.UserProperties import android.net.Uri +import android.os.Parcel +import android.os.UserHandle +import android.os.UserManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.modules.utils.build.SdkLevel +import com.android.photopicker.R +import com.android.photopicker.core.banners.BannerDefinitions +import com.android.photopicker.core.banners.BannerManagerImpl +import com.android.photopicker.core.banners.BannerState +import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.configuration.testActionPickImagesConfiguration +import com.android.photopicker.core.database.DatabaseManagerTestImpl import com.android.photopicker.core.events.Event import com.android.photopicker.core.events.Events import com.android.photopicker.core.events.RegisteredEventClass +import com.android.photopicker.core.events.Telemetry +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureToken.PHOTO_GRID import com.android.photopicker.core.selection.SelectionImpl +import com.android.photopicker.core.user.UserMonitor import com.android.photopicker.data.TestDataServiceImpl import com.android.photopicker.data.model.Media import com.android.photopicker.data.model.MediaSource +import com.android.photopicker.tests.utils.mockito.mockSystemService +import com.android.photopicker.tests.utils.mockito.whenever import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi 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.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class PhotoGridViewModelTest { + private val USER_ID_PRIMARY: Int = 0 + private val USER_HANDLE_PRIMARY: UserHandle + private val PLATFORM_PROVIDED_PROFILE_LABEL = "Platform Label" + private val deviceConfigProxy = TestDeviceConfigProxyImpl() + @Mock lateinit var mockContext: Context + @Mock lateinit var mockUserManager: UserManager + @Mock lateinit var mockPackageManager: PackageManager + @Mock lateinit var mockContentResolver: ContentResolver + + init { + val parcel1 = Parcel.obtain() + parcel1.writeInt(USER_ID_PRIMARY) + parcel1.setDataPosition(0) + USER_HANDLE_PRIMARY = UserHandle(parcel1) + parcel1.recycle() + } + val mediaItem = Media.Image( mediaId = "id", @@ -73,6 +124,46 @@ class PhotoGridViewModelTest { mimeType = "image/png", standardMimeTypeExtension = 1, ) + val updatedMediaItem = + mediaItem.copy(mediaItemAlbum = null, selectionSource = Telemetry.MediaLocation.MAIN_GRID) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + deviceConfigProxy.reset() + val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources() + + mockSystemService(mockContext, UserManager::class.java) { mockUserManager } + whenever(mockContext.packageManager) { mockPackageManager } + whenever(mockContext.contentResolver) { mockContentResolver } + whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext } + whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) { + mockContext + } + + // Initial setup state: Two profiles (Personal/Work), both enabled + whenever(mockUserManager.userProfiles) { listOf(USER_HANDLE_PRIMARY) } + + // Default responses for relevant UserManager apis + whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false } + whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false } + + val mockResolveInfo = mock(ResolveInfo::class.java) + whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true } + whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + listOf(mockResolveInfo) + } + + if (SdkLevel.isAtLeastV()) { + whenever(mockUserManager.getUserBadge()) { + resources.getDrawable(R.drawable.android, /* theme= */ null) + } + whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL } + whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) { + UserProperties.Builder().build() + } + } + } @Test fun testPhotoGridItemClickedUpdatesSelection() { @@ -97,12 +188,46 @@ class PhotoGridViewModelTest { featureManager = featureManager, ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId(), + ) + + val userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = testActionPickImagesConfiguration, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ) + + val bannerManager = + BannerManagerImpl( + scope = this.backgroundScope, + backgroundDispatcher = StandardTestDispatcher(this.testScheduler), + configurationManager = configurationManager, + databaseManager = DatabaseManagerTestImpl(), + featureManager = featureManager, + dataService = TestDataServiceImpl(), + userMonitor = userMonitor, + processOwnerHandle = USER_HANDLE_PRIMARY + ) + val viewModel = PhotoGridViewModel( this.backgroundScope, selection, TestDataServiceImpl(), events, + bannerManager, ) assertWithMessage("Unexpected selection start size") @@ -115,9 +240,10 @@ class PhotoGridViewModelTest { // Wait for selection update. advanceTimeBy(100) + // The selected media item gets updated with the Selectable interface values assertWithMessage("Selection did not contain expected item") .that(selection.snapshot()) - .contains(mediaItem) + .contains(updatedMediaItem) // Toggle the item out of the selection viewModel.handleGridItemSelection(mediaItem, "") @@ -126,7 +252,7 @@ class PhotoGridViewModelTest { assertWithMessage("Selection contains unexpected item") .that(selection.snapshot()) - .doesNotContain(mediaItem) + .doesNotContain(updatedMediaItem) } } @@ -144,7 +270,8 @@ class PhotoGridViewModelTest { PhotopickerConfiguration( action = "TEST_ACTION", intent = null, - selectionLimit = 0 + selectionLimit = 0, + sessionId = generatePickerSessionId() ) ) ) @@ -167,12 +294,46 @@ class PhotoGridViewModelTest { val eventsDispatched = mutableListOf<Event>() backgroundScope.launch { events.flow.toList(eventsDispatched) } + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId(), + ) + + val userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = testActionPickImagesConfiguration, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ) + + val bannerManager = + BannerManagerImpl( + scope = this.backgroundScope, + backgroundDispatcher = StandardTestDispatcher(this.testScheduler), + configurationManager = configurationManager, + databaseManager = DatabaseManagerTestImpl(), + featureManager = featureManager, + dataService = TestDataServiceImpl(), + userMonitor = userMonitor, + processOwnerHandle = USER_HANDLE_PRIMARY + ) + val viewModel = PhotoGridViewModel( this.backgroundScope, selection, TestDataServiceImpl(), events, + bannerManager, ) assertWithMessage("Unexpected selection start size") @@ -182,7 +343,6 @@ class PhotoGridViewModelTest { // Toggle the item into the selection val errorMessage = "test" viewModel.handleGridItemSelection(mediaItem, errorMessage) - // Wait for selection update. advanceTimeBy(100) @@ -191,4 +351,84 @@ class PhotoGridViewModelTest { .contains(Event.ShowSnackbarMessage(PHOTO_GRID.token, errorMessage)) } } + + @Test + fun testPhotoGridBannerDismissedHandler() { + + runTest { + val selection = + SelectionImpl<Media>( + scope = this.backgroundScope, + configuration = provideTestConfigurationFlow(scope = this.backgroundScope) + ) + + val featureManager = + FeatureManager( + configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + scope = this.backgroundScope, + ) + + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager = featureManager, + ) + + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId(), + ) + + val userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = testActionPickImagesConfiguration, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY + ) + + val databaseManager = DatabaseManagerTestImpl() + + val bannerManager = + BannerManagerImpl( + scope = this.backgroundScope, + backgroundDispatcher = StandardTestDispatcher(this.testScheduler), + configurationManager = configurationManager, + databaseManager = databaseManager, + featureManager = featureManager, + dataService = TestDataServiceImpl(), + userMonitor = userMonitor, + processOwnerHandle = USER_HANDLE_PRIMARY + ) + + val viewModel = + PhotoGridViewModel( + this.backgroundScope, + selection, + TestDataServiceImpl(), + events, + bannerManager, + ) + + viewModel.markBannerAsDismissed(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT) + advanceTimeBy(100) + verify(databaseManager.bannerState) + .setBannerState( + BannerState( + bannerId = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id, + uid = 0, + dismissed = true, + ) + ) + } + } } 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 be5b4bd59..f97ba8740 100644 --- a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt @@ -62,10 +62,10 @@ import com.android.photopicker.core.ConcurrencyModule import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.ViewModelModule -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.navigation.PhotopickerDestinations import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.model.Media @@ -80,7 +80,6 @@ import com.android.photopicker.tests.utils.mockito.capture 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.Lazy import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.testing.BindValue @@ -129,6 +128,7 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -168,7 +168,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var events: Events - @Inject lateinit var bannerManager: Lazy<BannerManager> @Inject override lateinit var configurationManager: ConfigurationManager val TEST_MEDIA_IMAGE = @@ -330,7 +329,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } } @@ -371,7 +369,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } } @@ -409,7 +406,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -447,7 +443,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } } @@ -457,6 +452,7 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { // Navigate on the UI thread (similar to a click handler) composeTestRule.runOnUiThread({ navController.navigateToPreviewSelection() }) + composeTestRule.waitForIdle() assertWithMessage("Expected route to be preview/selection") .that(navController.currentBackStackEntry?.destination?.route) @@ -477,7 +473,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } } @@ -549,7 +544,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } } @@ -598,7 +592,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -646,7 +639,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -692,7 +684,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -744,7 +735,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -825,7 +815,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -900,7 +889,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -977,7 +965,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } @@ -1052,7 +1039,6 @@ class PreviewFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } 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 f70e8e26c..9c3aaedbd 100644 --- a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt @@ -55,8 +55,15 @@ import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.modules.utils.build.SdkLevel import com.android.photopicker.R +import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.MULTI_SELECT_CONFIG +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl import com.android.photopicker.core.configuration.provideTestConfigurationFlow +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.core.selection.GrantsAwareSelectionImpl import com.android.photopicker.core.selection.SelectionImpl import com.android.photopicker.core.user.UserMonitor @@ -110,6 +117,7 @@ class PreviewViewModelTest { private val USER_HANDLE_PRIMARY: UserHandle private val USER_ID_PRIMARY: Int = 0 + private val deviceConfigProxy = TestDeviceConfigProxyImpl() init { val parcel1 = Parcel.obtain() @@ -210,6 +218,7 @@ class PreviewViewModelTest { @Before fun setup() { + deviceConfigProxy.reset() MockitoAnnotations.initMocks(this) mockSystemService(mockContext, UserManager::class.java) { mockUserManager } @@ -260,6 +269,26 @@ class PreviewViewModelTest { fun testToggleInSelectionUpdatesSelection() { runTest { + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val selection = SelectionImpl<Media>( scope = this.backgroundScope, @@ -277,7 +306,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) assertWithMessage("Unexpected selection start size") @@ -309,6 +340,26 @@ class PreviewViewModelTest { fun testToggleInSelectionCollectionUpdatesSelection() { runTest { + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val selection = SelectionImpl<Media>( scope = this.backgroundScope, @@ -330,7 +381,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) assertWithMessage("Unexpected selection start size") @@ -369,6 +422,26 @@ class PreviewViewModelTest { configuration = provideTestConfigurationFlow(scope = this.backgroundScope), initialSelection = setOf(TEST_MEDIA_IMAGE), ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = PreviewViewModel( @@ -381,7 +454,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) var snapshot = viewModel.selectionSnapshot.first() @@ -412,6 +487,27 @@ class PreviewViewModelTest { GrantsAwareSelectionImpl<Media>( scope = this.backgroundScope, configuration = provideTestConfigurationFlow(scope = this.backgroundScope), + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount + ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager ) val viewModel = @@ -425,7 +521,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) // remove a pre-granted item and it should be added to the deselection snapshot. @@ -459,6 +557,26 @@ class PreviewViewModelTest { configuration = provideTestConfigurationFlow(scope = this.backgroundScope), initialSelection = setOf(TEST_MEDIA_IMAGE), ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = PreviewViewModel( this.backgroundScope, @@ -470,7 +588,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) val controller = @@ -514,6 +634,26 @@ class PreviewViewModelTest { configuration = provideTestConfigurationFlow(scope = this.backgroundScope), initialSelection = setOf(TEST_MEDIA_IMAGE), ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = PreviewViewModel( this.backgroundScope, @@ -525,7 +665,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) val controller = @@ -613,6 +755,26 @@ class PreviewViewModelTest { configuration = provideTestConfigurationFlow(scope = this.backgroundScope), initialSelection = setOf(TEST_MEDIA_IMAGE), ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = PreviewViewModel( this.backgroundScope, @@ -624,7 +786,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) @@ -645,6 +809,26 @@ class PreviewViewModelTest { configuration = provideTestConfigurationFlow(scope = this.backgroundScope), initialSelection = setOf(TEST_MEDIA_IMAGE), ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + registeredFeatures = setOf(PreviewFeature.Registration), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = PreviewViewModel( this.backgroundScope, @@ -656,7 +840,9 @@ class PreviewViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), - dataService = TestDataServiceImpl() + dataService = TestDataServiceImpl(), + events, + configurationManager ) viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) diff --git a/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt index f84f5ec38..7d67f0430 100644 --- a/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt @@ -44,6 +44,7 @@ import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.database.DatabaseManager import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.model.Media import com.android.photopicker.features.PhotopickerFeatureBaseTest @@ -92,6 +93,7 @@ class PrivacyExplainerFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -177,7 +179,6 @@ class PrivacyExplainerFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() @@ -220,7 +221,6 @@ class PrivacyExplainerFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule.waitForIdle() diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt index 9be1300bb..a45b11759 100644 --- a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt @@ -44,10 +44,10 @@ import com.android.photopicker.core.ConcurrencyModule import com.android.photopicker.core.EmbeddedServiceModule import com.android.photopicker.core.Main import com.android.photopicker.core.ViewModelModule -import com.android.photopicker.core.banners.BannerManager import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.events.Events import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.selection.Selection import com.android.photopicker.data.model.Media import com.android.photopicker.features.PhotopickerFeatureBaseTest @@ -55,7 +55,6 @@ import com.android.photopicker.inject.PhotopickerTestModule import com.android.photopicker.tests.HiltTestActivity import com.android.photopicker.tests.utils.mockito.mockSystemService import com.android.photopicker.tests.utils.mockito.whenever -import dagger.Lazy import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.testing.BindValue @@ -94,6 +93,7 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() @@ -116,7 +116,6 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { @Inject lateinit var selection: Selection<Media> @Inject lateinit var featureManager: FeatureManager @Inject lateinit var userHandle: UserHandle - @Inject lateinit var bannerManager: Lazy<BannerManager> @Inject override lateinit var configurationManager: ConfigurationManager @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver() @@ -160,7 +159,6 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule @@ -182,7 +180,6 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule @@ -270,7 +267,6 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule @@ -349,7 +345,6 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule @@ -392,7 +387,6 @@ class ProfileSelectorFeatureTest : PhotopickerFeatureBaseTest() { featureManager = featureManager, selection = selection, events = events, - bannerManager = bannerManager.get(), ) } composeTestRule diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt index 50e0cac6c..31aba1d8c 100644 --- a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt @@ -31,8 +31,15 @@ import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.modules.utils.build.SdkLevel import com.android.photopicker.R +import com.android.photopicker.core.configuration.ConfigurationManager +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv +import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testActionPickImagesConfiguration +import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId +import com.android.photopicker.core.features.FeatureManager +import com.android.photopicker.core.features.FeatureRegistration import com.android.photopicker.core.selection.SelectionImpl import com.android.photopicker.core.user.UserMonitor import com.android.photopicker.core.user.UserProfile @@ -65,6 +72,7 @@ class ProfileSelectorViewModelTest { @Mock lateinit var mockPackageManager: PackageManager private val mockContentResolver: MockContentResolver = MockContentResolver() + private val deviceConfigProxy = TestDeviceConfigProxyImpl() private val USER_HANDLE_PRIMARY: UserHandle private val USER_ID_PRIMARY: Int = 0 @@ -118,6 +126,7 @@ class ProfileSelectorViewModelTest { @Before fun setup() { + deviceConfigProxy.reset() MockitoAnnotations.initMocks(this) mockSystemService(mockContext, UserManager::class.java) { mockUserManager } @@ -169,6 +178,26 @@ class ProfileSelectorViewModelTest { scope = this.backgroundScope, configuration = provideTestConfigurationFlow(scope = this.backgroundScope) ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + emptySet<FeatureRegistration>(), + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = ProfileSelectorViewModel( @@ -184,6 +213,8 @@ class ProfileSelectorViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), + events, + configurationManager ) assertWithMessage("Expected available number of profiles to be 2.") @@ -217,6 +248,25 @@ class ProfileSelectorViewModelTest { scope = this.backgroundScope, configuration = provideTestConfigurationFlow(scope = this.backgroundScope) ) + val configurationManager = + ConfigurationManager( + runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY, + scope = this.backgroundScope, + dispatcher = StandardTestDispatcher(this.testScheduler), + deviceConfigProxy, + generatePickerSessionId() + ) + val featureManager = + FeatureManager( + configurationManager.configuration, + this.backgroundScope, + ) + val events = + Events( + scope = this.backgroundScope, + provideTestConfigurationFlow(scope = this.backgroundScope), + featureManager + ) val viewModel = ProfileSelectorViewModel( @@ -232,6 +282,8 @@ class ProfileSelectorViewModelTest { StandardTestDispatcher(this.testScheduler), USER_HANDLE_PRIMARY ), + events, + configurationManager ) selection.add(TEST_MEDIA_IMAGE) 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 012efb2c5..394e49bf3 100644 --- a/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt @@ -47,13 +47,16 @@ import com.android.photopicker.core.Main import com.android.photopicker.core.configuration.ConfigurationManager import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration import com.android.photopicker.core.configuration.PhotopickerConfiguration +import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv import com.android.photopicker.core.configuration.provideTestConfigurationFlow import com.android.photopicker.core.configuration.testPhotopickerConfiguration import com.android.photopicker.core.events.Events import com.android.photopicker.core.events.LocalEvents +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.features.LocationParams +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.navigation.LocalNavController import com.android.photopicker.core.selection.LocalSelection import com.android.photopicker.core.selection.Selection @@ -103,11 +106,13 @@ class SelectionBarFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() val testDispatcher = StandardTestDispatcher() + val sessionId = generatePickerSessionId() /* Overrides for ActivityModule */ val testScope: TestScope = TestScope(testDispatcher) @@ -183,44 +188,84 @@ class SelectionBarFeatureTest : PhotopickerFeatureBaseTest() { } @Test - fun testSelectionBarIsEnabledWithSelectionLimit() { - val configOne = PhotopickerConfiguration(action = "TEST_ACTION", selectionLimit = 5) + fun testSelectionBarIsEnabledWithSelectionLimitInActivityMode() { + val configOne = + PhotopickerConfiguration( + action = "TEST_ACTION", + selectionLimit = 5, + sessionId = sessionId + ) assertWithMessage("SelectionBarFeature is not always enabled for TEST_ACTION") .that(SelectionBarFeature.Registration.isEnabled(configOne)) .isEqualTo(true) val configTwo = - PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, selectionLimit = 5) + PhotopickerConfiguration( + action = MediaStore.ACTION_PICK_IMAGES, + selectionLimit = 5, + sessionId = sessionId + ) assertWithMessage("SelectionBarFeature is not always enabled") .that(SelectionBarFeature.Registration.isEnabled(configTwo)) .isEqualTo(true) val configThree = - PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, selectionLimit = 5) + PhotopickerConfiguration( + action = Intent.ACTION_GET_CONTENT, + selectionLimit = 5, + sessionId = sessionId + ) assertWithMessage("SelectionBarFeature is not always enabled") .that(SelectionBarFeature.Registration.isEnabled(configThree)) .isEqualTo(true) } @Test - fun testSelectionBarNotEnabledForSingleSelect() { - val configOne = PhotopickerConfiguration(action = "TEST_ACTION") + fun testSelectionBarNotEnabledForSingleSelectInActivityMode() { + val configOne = PhotopickerConfiguration(action = "TEST_ACTION", sessionId = sessionId) assertWithMessage("SelectionBarFeature is not always enabled for TEST_ACTION") .that(SelectionBarFeature.Registration.isEnabled(configOne)) .isEqualTo(false) - val configTwo = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES) + val configTwo = + PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, sessionId = sessionId) assertWithMessage("SelectionBarFeature is not always enabled") .that(SelectionBarFeature.Registration.isEnabled(configTwo)) .isEqualTo(false) - val configThree = PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT) + val configThree = + PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, sessionId = sessionId) assertWithMessage("SelectionBarFeature is not always enabled") .that(SelectionBarFeature.Registration.isEnabled(configThree)) .isEqualTo(false) } @Test + fun testSelectionBarIsAlwaysEnabledInEmbeddedMode() { + val configOne = + PhotopickerConfiguration( + action = "", + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + selectionLimit = 1, + sessionId = sessionId + ) + assertWithMessage("SelectionBarFeature not always enabled for EMBEDDED mode") + .that(SelectionBarFeature.Registration.isEnabled(configOne)) + .isEqualTo(true) + + val configTwo = + PhotopickerConfiguration( + action = "", + runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED, + selectionLimit = 20, + sessionId = sessionId + ) + assertWithMessage("SelectionBarFeature not always enabled for EMBEDDED mode") + .that(SelectionBarFeature.Registration.isEnabled(configTwo)) + .isEqualTo(true) + } + + @Test fun testSelectionBarIsShown() { testScope.runTest { val photopickerConfiguration: PhotopickerConfiguration = testPhotopickerConfiguration diff --git a/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt index a9fde63e7..10bd21753 100644 --- a/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt @@ -45,6 +45,7 @@ import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.features.FeatureToken.CORE import com.android.photopicker.core.features.LocalFeatureManager import com.android.photopicker.core.features.Location +import com.android.photopicker.core.glide.GlideTestRule import com.android.photopicker.core.navigation.LocalNavController import com.android.photopicker.core.selection.LocalSelection import com.android.photopicker.core.selection.Selection @@ -89,6 +90,7 @@ class SnackbarFeatureTest : PhotopickerFeatureBaseTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) + @get:Rule(order = 2) val glideRule = GlideTestRule() /* Setup dependencies for the UninstallModules for the test class. */ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() diff --git a/photopicker/tests/src/com/android/photopicker/inject/EmbeddedTestModule.kt b/photopicker/tests/src/com/android/photopicker/inject/EmbeddedTestModule.kt index 5f043beae..7b4453ee7 100644 --- a/photopicker/tests/src/com/android/photopicker/inject/EmbeddedTestModule.kt +++ b/photopicker/tests/src/com/android/photopicker/inject/EmbeddedTestModule.kt @@ -32,6 +32,7 @@ import com.android.photopicker.core.database.DatabaseManagerTestImpl import com.android.photopicker.core.embedded.EmbeddedLifecycle import com.android.photopicker.core.embedded.EmbeddedViewModelFactory import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.selection.GrantsAwareSelectionImpl import com.android.photopicker.core.selection.Selection @@ -96,6 +97,7 @@ abstract class EmbeddedTestModule { @Background backgroundDispatcher: CoroutineDispatcher, featureManager: Lazy<FeatureManager>, configurationManager: Lazy<ConfigurationManager>, + bannerManager: Lazy<BannerManager>, selection: Lazy<Selection<Media>>, userMonitor: Lazy<UserMonitor>, dataService: Lazy<DataService>, @@ -105,6 +107,7 @@ abstract class EmbeddedTestModule { EmbeddedViewModelFactory( backgroundDispatcher, configurationManager, + bannerManager, dataService, events, featureManager, @@ -150,6 +153,7 @@ abstract class EmbeddedTestModule { scope, dispatcher, deviceConfigProxy, + generatePickerSessionId() ) } @@ -225,13 +229,15 @@ abstract class EmbeddedTestModule { @Provides fun createSelection( @Background scope: CoroutineScope, - configurationManager: ConfigurationManager + configurationManager: ConfigurationManager, + dataService: DataService ): Selection<Media> { return when (determineSelectionStrategy(configurationManager.configuration.value)) { SelectionStrategy.GRANTS_AWARE_SELECTION -> GrantsAwareSelectionImpl( scope = scope, configuration = configurationManager.configuration, + preGrantedItemsCount = dataService.preGrantedMediaCount, ) SelectionStrategy.DEFAULT -> SelectionImpl( diff --git a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt index ffab11830..1c128b056 100644 --- a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt +++ b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt @@ -32,6 +32,7 @@ import com.android.photopicker.core.database.DatabaseManagerTestImpl import com.android.photopicker.core.embedded.EmbeddedLifecycle import com.android.photopicker.core.embedded.EmbeddedViewModelFactory import com.android.photopicker.core.events.Events +import com.android.photopicker.core.events.generatePickerSessionId import com.android.photopicker.core.features.FeatureManager import com.android.photopicker.core.selection.GrantsAwareSelectionImpl import com.android.photopicker.core.selection.Selection @@ -96,6 +97,7 @@ abstract class PhotopickerTestModule { @Background backgroundDispatcher: CoroutineDispatcher, featureManager: Lazy<FeatureManager>, configurationManager: Lazy<ConfigurationManager>, + bannerManager: Lazy<BannerManager>, selection: Lazy<Selection<Media>>, userMonitor: Lazy<UserMonitor>, dataService: Lazy<DataService>, @@ -105,6 +107,7 @@ abstract class PhotopickerTestModule { EmbeddedViewModelFactory( backgroundDispatcher, configurationManager, + bannerManager, dataService, events, featureManager, @@ -150,6 +153,7 @@ abstract class PhotopickerTestModule { scope, dispatcher, deviceConfigProxy, + generatePickerSessionId() ) } @@ -232,6 +236,7 @@ abstract class PhotopickerTestModule { GrantsAwareSelectionImpl( scope = scope, configuration = configurationManager.configuration, + preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount ) SelectionStrategy.DEFAULT -> SelectionImpl( diff --git a/res/values/strings.xml b/res/values/strings.xml index 46e07b134..a96497df4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -522,10 +522,10 @@ <string name="safety_protection_icon_label">Safety protection</string> <!-- Transcode alert channel name. --> - <string name="transcode_alert_channel">Native Transcode Alerts</string> + <string name="transcode_alert_channel">Transcoding Notifications</string> <!-- Transcode progress channel name. --> - <string name="transcode_progress_channel">Native Transcode Progress</string> + <string name="transcode_progress_channel">Transcoding Progress</string> <!-- Dialog error message--> <string name="dialog_error_message">Try again later. Your photos will be available once the issue is resolved.</string> diff --git a/src/com/android/providers/media/MediaGrants.java b/src/com/android/providers/media/MediaGrants.java index 61d4ab141..649380108 100644 --- a/src/com/android/providers/media/MediaGrants.java +++ b/src/com/android/providers/media/MediaGrants.java @@ -19,6 +19,7 @@ package com.android.providers.media; import static com.android.providers.media.LocalUriMatcher.PICKER_ID; import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar; +import android.annotation.Nullable; import android.content.ContentUris; import android.content.ContentValues; import android.database.Cursor; @@ -156,10 +157,13 @@ public class MediaGrants { * Returns the cursor for file data of items for which the passed package has READ_GRANTS. * * @param packageNames the package name that has access. - * @param packageUserId the user_id of the package + * @param packageUserId the user_id of the package. + * @param mimeTypes the mimeTypes of items for which the grants needs to be returned. + * @param availableVolumes volumes that are available, grants for items only in these volumes + * should be considered. */ - Cursor getMediaGrantsForPackages(String[] packageNames, int packageUserId, - String[] mimeTypes, String[] availableVolumes) + Cursor getMediaGrantsForPackages(@NonNull String[] packageNames, int packageUserId, + @Nullable String[] mimeTypes, @NonNull String[] availableVolumes) throws IllegalArgumentException { Objects.requireNonNull(packageNames); return mExternalDatabase.runWithoutTransaction((db) -> { @@ -178,7 +182,9 @@ public class MediaGrants { .build()); return queryBuilder.query(db, - new String[]{FILE_ID_COLUMN, PACKAGE_USER_ID_COLUMN}, null, + new String[]{FILE_ID_COLUMN, + String.format("%s.%s", MEDIA_GRANTS_TABLE, OWNER_PACKAGE_NAME_COLUMN), + PACKAGE_USER_ID_COLUMN}, null, selectionArgs, null, null, null, null, null); }); } diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java index bafaa67f4..896f22ba7 100644 --- a/src/com/android/providers/media/MediaProvider.java +++ b/src/com/android/providers/media/MediaProvider.java @@ -363,9 +363,9 @@ import java.util.stream.Collectors; /** * Media content provider. See {@link android.provider.MediaStore} for details. - * Separate databases are kept for each external storage card we see (using the - * card's ID as an index). The content visible at content://media/external/... - * changes with the card. + * A single database keep track of media files on external storage + * The content visible at content://media/external/... is a combined view of all media files on all + * available external storage devices */ public class MediaProvider extends ContentProvider { /** @@ -6877,16 +6877,7 @@ public class MediaProvider extends ContentProvider { int userId; final List<Uri> uris; String[] packageNames; - if (checkPermissionSelf(caller)) { - final PackageManager pm = getContext().getPackageManager(); - final int packageUid = extras.getInt(Intent.EXTRA_UID); - packageNames = pm.getPackagesForUid(packageUid); - // Get the userId from packageUid as the initiator could be a cloned app, which - // accesses Media via MP of its parent user and Binder's callingUid reflects - // the latter. - userId = uidToUserId(packageUid); - uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); - } else if (checkPermissionShell(caller)) { + if (checkPermissionShell(caller)) { // If the caller is the shell, the accepted parameter is EXTRA_PACKAGE_NAME // (as string). if (!extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) { @@ -6899,10 +6890,19 @@ public class MediaProvider extends ContentProvider { // Caller is always shell which may not have the desired userId. Hence, use // UserId from the MediaProvider process itself. userId = UserHandle.myUserId(); + } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) { + final PackageManager pm = getContext().getPackageManager(); + final int packageUid = extras.getInt(Intent.EXTRA_UID); + packageNames = pm.getPackagesForUid(packageUid); + // Get the userId from packageUid as the initiator could be a cloned app, which + // accesses Media via MP of its parent user and Binder's callingUid reflects + // the latter. + userId = uidToUserId(packageUid); + uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); } else { // All other callers are unauthorized. throw new SecurityException( - getSecurityExceptionMessage("read media grants")); + getSecurityExceptionMessage("revoke media grants")); } mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId); @@ -7162,7 +7162,21 @@ public class MediaProvider extends ContentProvider { int userId; final List<Uri> uris; String packageName; - if (checkPermissionSelf(caller)) { + if (checkPermissionShell(caller)) { + // If the caller is the shell, the accepted parameters are EXTRA_URI (as string) + // and EXTRA_PACKAGE_NAME (as string). + if (!extras.containsKey(MediaStore.EXTRA_URI) + && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) { + throw new IllegalArgumentException( + "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME"); + } + packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); + uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI))); + // Caller is always shell which may not have the desired userId. Hence, use + // UserId from the MediaProvider process itself. + userId = UserHandle.myUserId(); + + } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) { // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST // and EXTRA_UID. if (!extras.containsKey(MediaStore.EXTRA_URI_LIST) @@ -7189,19 +7203,6 @@ public class MediaProvider extends ContentProvider { // accesses Media via MP of its parent user and Binder's callingUid reflects // the latter. userId = uidToUserId(packageUid); - } else if (checkPermissionShell(caller)) { - // If the caller is the shell, the accepted parameters are EXTRA_URI (as string) - // and EXTRA_PACKAGE_NAME (as string). - if (!extras.containsKey(MediaStore.EXTRA_URI) - && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) { - throw new IllegalArgumentException( - "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME"); - } - packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); - uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI))); - // Caller is always shell which may not have the desired userId. Hence, use - // UserId from the MediaProvider process itself. - userId = UserHandle.myUserId(); } else { // All other callers are unauthorized. diff --git a/src/com/android/providers/media/MediaService.java b/src/com/android/providers/media/MediaService.java index 5d236925b..75a2016b9 100644 --- a/src/com/android/providers/media/MediaService.java +++ b/src/com/android/providers/media/MediaService.java @@ -160,14 +160,12 @@ public class MediaService extends JobIntentService { } private void onPublicVolumeRecovery() { - new Thread(() -> { - try (ContentProviderClient cpc = getContentResolver() - .acquireContentProviderClient(MediaStore.AUTHORITY)) { - ((MediaProvider) cpc.getLocalContentProvider()).recoverPublicVolumes(); - } catch (Exception e) { - Log.e(TAG, "Exception while starting public volume recovery thread", e); - } - }).start(); + try (ContentProviderClient cpc = getContentResolver() + .acquireContentProviderClient(MediaStore.AUTHORITY)) { + ((MediaProvider) cpc.getLocalContentProvider()).recoverPublicVolumes(); + } catch (Exception e) { + Log.e(TAG, "Exception while starting public volume recovery thread", e); + } } public static void onScanVolume(Context context, MediaVolume volume, int reason) diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java index eefd1bc04..6b8f4a779 100644 --- a/src/com/android/providers/media/photopicker/PickerDataLayer.java +++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java @@ -177,7 +177,7 @@ public class PickerDataLayer { getWorkManager(mContext), SyncTrackerRegistry.getLocalSyncTracker(), IMMEDIATE_LOCAL_SYNC_WORK_NAME); - Log.i(TAG, "Local sync is complete"); + Log.i(TAG, "Grants sync and Local sync is complete"); // Wait for on cloud sync with timeout if (!isLocalOnly) { @@ -436,7 +436,7 @@ public class PickerDataLayer { + " Should sync with local provider only: " + syncRequestExtras.shouldSyncLocalOnlyData()); - mSyncManager.syncMediaImmediately(syncRequestExtras.shouldSyncLocalOnlyData()); + mSyncManager.syncMediaImmediately(syncRequestExtras); } else { // Sync album media data Log.i(TAG, String.format("Init data request for album content of: %s" diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java index 0a32618ff..137a7d4cf 100644 --- a/src/com/android/providers/media/photopicker/PickerSyncController.java +++ b/src/com/android/providers/media/photopicker/PickerSyncController.java @@ -17,16 +17,18 @@ package com.android.providers.media.photopicker; import static android.content.ContentResolver.EXTRA_HONORED_ARGS; -import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME; import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID; import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID; import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE; import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN; import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION; +import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT; +import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME; import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION; import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID; -import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT; +import static android.provider.MediaStore.AUTHORITY; import static android.provider.MediaStore.MY_UID; +import static android.provider.MediaStore.PER_USER_RANGE; import static com.android.providers.media.PickerUriResolver.INIT_PATH; import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI; @@ -40,15 +42,19 @@ import static com.android.providers.media.photopicker.NotificationContentObserve import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString; import android.annotation.IntDef; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.database.Cursor; +import android.database.SQLException; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; +import android.os.RemoteException; import android.os.Trace; import android.os.storage.StorageManager; import android.provider.CloudMediaProvider; @@ -75,6 +81,7 @@ import com.android.providers.media.photopicker.sync.PickerSyncLockManager; import com.android.providers.media.photopicker.util.CloudProviderUtils; import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException; import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException; +import com.android.providers.media.photopicker.util.exceptions.WorkCancelledException; import com.android.providers.media.photopicker.v2.PickerNotificationSender; import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo; @@ -161,6 +168,18 @@ public class PickerSyncController { private static PickerSyncController sInstance; /** + * This URI path when used in a MediaProvider.query() method redirects the call to media_grants + * table present in the external database. + */ + private static final String MEDIA_GRANTS_URI_PATH = "content://media/media_grants"; + + /** + * Extra that can be passed in the grants sync query to ensure only the data corresponding to + * the required mimeTypes is synced. + */ + public static final String EXTRA_MEDIA_GRANTS_MIME_TYPES = "media_grant_mime_type_selection"; + + /** * Initialize {@link PickerSyncController} object.{@link PickerSyncController} should only be * initialized from {@link com.android.providers.media.MediaProvider#onCreate}. * @@ -859,7 +878,7 @@ public class PickerSyncController { executeSyncAddAlbum( authority, isLocal, albumId, queryArgs, instanceId, cancellationSignal); } - } catch (RuntimeException | UnableToAcquireLockException e) { + } catch (RuntimeException | UnableToAcquireLockException | WorkCancelledException e) { // Unlike syncAllMediaFromProvider, we don't retry here because any errors would have // occurred in fetching all the album_media since incremental sync is not supported. // A full sync is therefore unlikely to resolve any issue @@ -965,6 +984,10 @@ public class PickerSyncController { default: throw new IllegalArgumentException("Unexpected sync type: " + params.syncType); } + } catch (WorkCancelledException e) { + // Do not reset picker DB here so that the sync operation resumes the next time sync is + // triggered. + Log.e(TAG, "Failed to sync all media because the sync was cancelled.", e); } catch (RequestObsoleteException e) { Log.e(TAG, "Failed to sync all media because authority has changed.", e); try { @@ -1083,7 +1106,7 @@ public class PickerSyncController { Bundle queryArgs, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal) - throws RequestObsoleteException, UnableToAcquireLockException { + throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { final Uri uri = getMediaUri(authority); final List<String> expectedHonoredArgs = new ArrayList<>(); if (isIncrementalSync) { @@ -1133,7 +1156,7 @@ public class PickerSyncController { Bundle queryArgs, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal) - throws RequestObsoleteException, UnableToAcquireLockException { + throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { final Uri uri = getMediaUri(authority); Log.i(TAG, "Executing SyncAddAlbum. " @@ -1184,7 +1207,7 @@ public class PickerSyncController { Bundle queryArgs, InstanceId instanceId, @Nullable CancellationSignal cancellationSignal) - throws RequestObsoleteException, UnableToAcquireLockException { + throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { final Uri uri = getDeletedMediaUri(authority); Log.i(TAG, "Executing SyncRemove. isLocal: " + isLocal + ". authority: " + authority); @@ -1589,7 +1612,7 @@ public class PickerSyncController { String authority, Boolean isLocal, @Nullable CancellationSignal cancellationSignal) - throws RequestObsoleteException, UnableToAcquireLockException { + throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { return executePagedSync( uri, expectedMediaCollectionId, @@ -1634,7 +1657,7 @@ public class PickerSyncController { Boolean isLocal, @Nullable String albumId, @Nullable CancellationSignal cancellationSignal) - throws RequestObsoleteException, UnableToAcquireLockException { + throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException { Trace.beginSection(traceSectionName("executePagedSync")); try { @@ -1655,7 +1678,7 @@ public class PickerSyncController { // At the top of each loop check to see if we've received a CancellationSignal // to stop the paged sync. if (cancellationSignal != null && cancellationSignal.isCanceled()) { - throw new RequestObsoleteException( + throw new WorkCancelledException( "Aborting sync: cancellationSignal was received"); } @@ -2068,4 +2091,98 @@ public class PickerSyncController { } } } + + /** + * Executes a sync for grants from the external database to the picker database. + * + * This should only be called when the picker is in MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP + * action. It requires a valid packageUid and mimeTypes with which the picker was invoked to + * ensure that the sync only happens for the items that: + * <li>match the mimeTypes</li> + * <li>are granted to the package and userId corresponding to the provided packageUid</li> + * + * It fetches the rows from media_grants table in the external.db that matches the criteria and + * inserts them in the media_grants table in picker.db + */ + public void executeGrantsSync( + boolean shouldSyncGrants, int packageUid, + String[] mimeTypes) { + // empty the grants table. + executeClearAllGrants(packageUid); + + // sync all grants into the table + if (shouldSyncGrants) { + final ContentResolver resolver = mContext.getContentResolver(); + try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { + assert client != null; + final Bundle extras = new Bundle(); + extras.putInt(Intent.EXTRA_UID, packageUid); + extras.putStringArray(EXTRA_MEDIA_GRANTS_MIME_TYPES, mimeTypes); + try (Cursor c = client.query(Uri.parse(MEDIA_GRANTS_URI_PATH), + /* projection= */ null, + /* queryArgs= */ extras, + null)) { + Trace.beginSection(traceSectionName( + "executeGrantsSync", /* isLocal */ true)); + try (PickerDbFacade.DbWriteOperation operation = + mDbFacade.beginInsertGrantsOperation()) { + int grantsInsertedCount = operation.execute(c); + operation.setSuccess(); + Log.i(TAG, "Successfully executed grants sync operation operation." + + " Result count: " + grantsInsertedCount); + } finally { + Trace.endSection(); + } + } + } catch (RemoteException e) { + Log.e(TAG, "Remote exception received while fetching grants. " + e.getMessage()); + } + } + } + + /** + * Before a sync for grants is initiated, this method is used to clear any stale grants that + * exists in the database. + * + * This should only be called when the picker is in MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP + * action. It requires a valid packageUid with which the picker was invoked to + * ensure that all the rows that represents the items granted to the package and userId + * corresponding to the provided packageUid are cleared from the media_grants table in picker.db + */ + private void executeClearAllGrants(int packageUid) { + Trace.beginSection(traceSectionName("executeClearAllGrants", /* isLocal */ true)); + int userId = uidToUserId(packageUid); + String[] packageNames = getPackageNameFromUid(mContext, packageUid); + + try (PickerDbFacade.DbWriteOperation operation = + mDbFacade.beginClearGrantsOperation(packageNames, userId)) { + final int clearedGrantsCount = operation.execute(/* cursor */ null); + operation.setSuccess(); + + Log.i(TAG, "Successfully executed clear grants operation." + + " Result count: " + clearedGrantsCount); + } catch (SQLException e) { + Log.e(TAG, "Unable to clear grants for this session: " + e.getMessage()); + } finally { + Trace.endSection(); + } + } + + /** + * Returns an Array of packageNames corresponding to the input package uid. + */ + public static String[] getPackageNameFromUid(Context context, int callingPackageUid) { + final PackageManager pm = context.getPackageManager(); + return pm.getPackagesForUid(callingPackageUid); + } + + /** + * Generates and returns userId from the input package uid. + */ + public static int uidToUserId(int uid) { + // Get the userId from packageUid as the initiator could be a cloned app, which + // accesses Media via MP of its parent user and Binder's callingUid reflects + // the latter. + return uid / PER_USER_RANGE; + } } diff --git a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java index 141807c46..ddeb1de10 100644 --- a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java +++ b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java @@ -40,8 +40,13 @@ public class PickerDatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "PickerDatabaseHelper"; public static final String PICKER_DATABASE_NAME = "picker.db"; + private static final int VERSION_U = 11; - public static final int VERSION_LATEST = VERSION_U; + @VisibleForTesting + public static final int VERSION_INTRODUCING_MEDIA_GRANTS_TABLE = 12; + @VisibleForTesting + public static final int VERSION_INTRODUCING_DE_SELECTIONS_TABLE = 13; + public static final int VERSION_LATEST = VERSION_INTRODUCING_DE_SELECTIONS_TABLE; final Context mContext; final String mName; @@ -71,7 +76,24 @@ public class PickerDatabaseHelper extends SQLiteOpenHelper { public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { Log.v(TAG, "onUpgrade() for " + mName + " from " + oldV + " to " + newV); - resetData(db); + // Minimum compatible version with database migrations is VERSION_U. + // Any database lower than VERSION_U needs to be reset with latest schema. + if (oldV < VERSION_U) { + resetData(db); + return; + } + + // If the version is at least VERSION_U (see block above), then the + // database schema is fine, and all that's required is to add the + // new media_grants table. + if (oldV < VERSION_INTRODUCING_MEDIA_GRANTS_TABLE) { + createMediaGrantsTable(db); + } + if (oldV < VERSION_INTRODUCING_DE_SELECTIONS_TABLE) { + // Create de_selection table in picker.db if we are upgrading from a version where + // de_selection table did not exist. + createDeselectionTable(db); + } } @Override @@ -149,6 +171,30 @@ public class PickerDatabaseHelper extends SQLiteOpenHelper { + "OR (local_id IS NOT NULL AND cloud_id IS NULL))," + "UNIQUE(local_id, album_id)," + "UNIQUE(cloud_id, album_id))"); + createMediaGrantsTable(db); + createDeselectionTable(db); + } + + private static void createMediaGrantsTable(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS media_grants"); + db.execSQL("CREATE TABLE media_grants (" + + "owner_package_name TEXT," + + "file_id INTEGER," + + "package_user_id INTEGER," + + "UNIQUE(owner_package_name, file_id, package_user_id)" + + " ON CONFLICT IGNORE " + + ")"); + } + + private static void createDeselectionTable(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS de_selections"); + db.execSQL("CREATE TABLE de_selections (" + + "owner_package_name TEXT," + + "file_id INTEGER," + + "package_user_id INTEGER," + + "UNIQUE(owner_package_name, file_id, package_user_id)" + + " ON CONFLICT IGNORE " + + ")"); } private static void createLatestIndexes(SQLiteDatabase db) { diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java index 4e248c869..fbb32ef42 100644 --- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java +++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java @@ -22,6 +22,9 @@ import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_ import static android.provider.CloudMediaProviderContract.MediaColumns; import static android.provider.MediaStore.PickerMediaColumns; +import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN; +import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN; +import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; import static com.android.providers.media.photopicker.PickerSyncController.PAGE_SIZE; import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong; import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString; @@ -108,6 +111,8 @@ public class PickerDbFacade { private static final String TABLE_ALBUM_MEDIA = "album_media"; + private static final String TABLE_GRANTS = "media_grants"; + public static final String KEY_ID = "_id"; public static final String KEY_LOCAL_ID = "local_id"; public static final String KEY_CLOUD_ID = "cloud_id"; @@ -127,6 +132,8 @@ public class PickerDbFacade { public static final String KEY_WIDTH = "width"; @VisibleForTesting public static final String KEY_ORIENTATION = "orientation"; + public static final String EXTRA_OWNER_PACKAGE_NAMES = "owner_package_names"; + public static final String EXTRA_PACKAGE_USER_ID = "package_user_id"; private static final String WHERE_ID = KEY_ID + " = ?"; private static final String WHERE_LOCAL_ID = KEY_LOCAL_ID + " = ?"; @@ -275,6 +282,20 @@ public class PickerDbFacade { } /** + * Returns {@link DbWriteOperation} that can be used to insert grants into the database. + */ + public DbWriteOperation beginInsertGrantsOperation() { + return new InsertGrantsOperation(mDatabase, /* isLocal */ true); + } + + /** + * Returns {@link DbWriteOperation} that can be used to clear all grants from the database. + */ + public DbWriteOperation beginClearGrantsOperation(String[] packageNames, int userId) { + return new ClearGrantsOperation(mDatabase, /* isLocal */ true, packageNames, userId); + } + + /** * Returns {@link DbWriteOperation} to add album_media belonging to {@code authority} * into the picker db. */ @@ -431,6 +452,85 @@ public class PickerDbFacade { } /** + * Database operation to insert the grants synced. + */ + public static class InsertGrantsOperation extends DbWriteOperation { + + public InsertGrantsOperation(SQLiteDatabase database, boolean isLocal) { + super(database, isLocal); + } + + @Override + int executeInternal(@Nullable Cursor cursor) { + int numberOfGrantsInserted = 0; + + // fetch ids from thw cursor. + if (cursor == null) { + Log.d(TAG, "No item grants to sync"); + return numberOfGrantsInserted; + } + + ContentValues values = new ContentValues(); + SQLiteQueryBuilder qb = createGrantsQueryBuilder(); + if (cursor.moveToFirst()) { + do { + Integer id = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_ID_COLUMN)); + String packageName = cursor.getString(cursor.getColumnIndexOrThrow( + OWNER_PACKAGE_NAME_COLUMN)); + Integer userId = cursor.getInt( + cursor.getColumnIndexOrThrow(PACKAGE_USER_ID_COLUMN)); + + // insert the grant into the grants table. + values.clear(); + values.put(FILE_ID_COLUMN, id); + values.put(OWNER_PACKAGE_NAME_COLUMN, packageName); + values.put(PACKAGE_USER_ID_COLUMN, userId); + try { + qb.insert(getDatabase(), values); + numberOfGrantsInserted++; + } catch (SQLiteConstraintException ignored) { + Log.e(TAG, "Duplicate row insertion encountered for table media_grants." + + ignored); + // If we hit a constraint exception it means this row is already in media, + // so nothing to do here. + } + } while (cursor.moveToNext()); + } + Log.d(TAG, numberOfGrantsInserted + " grants synced."); + return numberOfGrantsInserted; + } + } + + /** + * Represents an update to the picker database where all grants needs to be cleared. + * + * This needs to be happen before every sync. + */ + public static class ClearGrantsOperation extends DbWriteOperation { + + private final String[] mPackageNames; + private final int mUserId; + + public ClearGrantsOperation(SQLiteDatabase database, boolean isLocal, + @NonNull String[] packageNames, + int userId) { + super(database, isLocal); + mPackageNames = packageNames; + mUserId = userId; + } + + @Override + int executeInternal(@Nullable Cursor cursor) { + // Delete everything from the grants table for the calling package. + SQLiteQueryBuilder qb = createGrantsQueryBuilder(); + Objects.requireNonNull(mPackageNames); + addWhereClausesForMediaGrantsTable(qb, mUserId, mPackageNames); + Log.d(TAG, "Clearing all picker database grants for calling package."); + return qb.delete(getDatabase(), /* selection */ null, /* selectionArgs */ null); + } + } + + /** * Represents an atomic media update operation to the picker database. * * <p>This class is not thread-safe and is meant to be used within a single thread only. @@ -1527,6 +1627,34 @@ public class PickerDbFacade { return qb; } + private static SQLiteQueryBuilder createGrantsQueryBuilder() { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(TABLE_GRANTS); + return qb; + } + + /** + * Appends where clause for package and user id selection to the input query builder. + */ + public static void addWhereClausesForMediaGrantsTable(SQLiteQueryBuilder qb, int userId, + @NonNull String[] packageNames) { + // Add where clause for userId selection. + qb.appendWhereStandalone( + String.format("%s.%s = %s", TABLE_GRANTS, PACKAGE_USER_ID_COLUMN, + String.valueOf(userId))); + + // Add where clause for package name selection. + Objects.requireNonNull(packageNames); + StringBuilder packageSelection = new StringBuilder("("); + for (int itr = 0; itr < packageNames.length; itr++) { + packageSelection.append("\"").append(packageNames[itr]).append("\","); + } + packageSelection.deleteCharAt(packageSelection.length() - 1); + packageSelection.append(")"); + qb.appendWhereStandalone(OWNER_PACKAGE_NAME_COLUMN + " IN " + + packageSelection.toString()); + } + private static SQLiteQueryBuilder createAlbumMediaQueryBuilder(boolean isLocal) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(TABLE_ALBUM_MEDIA); diff --git a/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java b/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java index f479d510e..49e2855e1 100644 --- a/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java +++ b/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java @@ -17,7 +17,9 @@ package com.android.providers.media.photopicker.data; import static com.android.providers.media.photopicker.data.CloudProviderQueryExtras.isMergedAlbum; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXTRA_MIME_TYPES; +import android.content.Intent; import android.os.Bundle; import android.provider.MediaStore; import android.text.TextUtils; @@ -25,23 +27,35 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.ArrayList; import java.util.Objects; /** * Encapsulate all picker sync request arguments related logic. */ public class PickerSyncRequestExtras { + private static final String EXTRA_INTENT_ACTION = "intent_action"; @Nullable private final String mAlbumId; @Nullable private final String mAlbumAuthority; private final boolean mInitLocalOnlyData; + private final int mCallingPackageUid; + private final boolean mShouldSyncGrants; + private final String[] mMimeTypes; + public PickerSyncRequestExtras(@Nullable String albumId, @Nullable String albumAuthority, - boolean initLocalOnlyData) { + boolean initLocalOnlyData, + int callingPackageUid, + boolean shouldSyncGrants, + @Nullable String[] mimeTypes) { mAlbumId = albumId; mAlbumAuthority = albumAuthority; mInitLocalOnlyData = initLocalOnlyData; + mCallingPackageUid = callingPackageUid; + mShouldSyncGrants = shouldSyncGrants; + mMimeTypes = mimeTypes; } /** @@ -54,7 +68,22 @@ public class PickerSyncRequestExtras { final String albumAuthority = extras.getString(MediaStore.EXTRA_ALBUM_AUTHORITY); final boolean initLocalOnlyData = extras.getBoolean(MediaStore.EXTRA_LOCAL_ONLY); - return new PickerSyncRequestExtras(albumId, albumAuthority, initLocalOnlyData); + final int callingPackageUid = extras.getInt(Intent.EXTRA_UID, /* default value */ -1); + // Grants should only be synced when the intent action is + // MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP. + final String intentAction = extras.getString(EXTRA_INTENT_ACTION); + final boolean shouldSyncGrants = intentAction != null + && intentAction.equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP); + final ArrayList<String> mimeTypesList = extras.getStringArrayList(EXTRA_MIME_TYPES); + final String[] mimeTypes; + if (mimeTypesList != null) { + mimeTypes = mimeTypesList.stream().toArray(String[]::new); + } else { + mimeTypes = null; + } + + return new PickerSyncRequestExtras(albumId, albumAuthority, initLocalOnlyData, + callingPackageUid, shouldSyncGrants, mimeTypes); } /** @@ -93,4 +122,25 @@ public class PickerSyncRequestExtras { public String getAlbumAuthority() { return mAlbumAuthority; } + + /** + * Return calling package uid for current picker session. + */ + public int getCallingPackageUid() { + return mCallingPackageUid; + } + + /** + * Returns true if grants should be synced, false otherwise. + */ + public boolean isShouldSyncGrants() { + return mShouldSyncGrants; + } + + /** + * Returns mimeTypes that can be used as a filtering parameter for syncs. + */ + public String[] getMimeTypes() { + return mMimeTypes == null ? new String[]{} : mMimeTypes; + } } diff --git a/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorker.java new file mode 100644 index 000000000..021a7907f --- /dev/null +++ b/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorker.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.providers.media.photopicker.sync; + + +import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXTRA_MIME_TYPES; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_MEDIA_GRANTS; +import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSyncAsComplete; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.ForegroundInfo; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import com.android.providers.media.photopicker.PickerSyncController; +import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException; + +/** + * This is a {@link Worker} class responsible for syncing with the correct sync + * source[SYNC_MEDIA_GRANTS] + */ +public class ImmediateGrantsSyncWorker extends Worker { + private static final String TAG = "ISyncGrantsWorker"; + private final Context mContext; + + /** + * Creates an instance of the {@link Worker}. + * + * @param context the application {@link Context} + * @param workerParams the set of {@link WorkerParameters} + */ + public ImmediateGrantsSyncWorker(@NonNull Context context, + @NonNull WorkerParameters workerParams) { + super(context, workerParams); + mContext = context; + } + + @NonNull + @Override + public Result doWork() { + // Do not allow endless re-runs of this worker, if this isn't the original run, + // just succeed and wait until the next scheduled run. + if (getRunAttemptCount() > 0) { + Log.w(TAG, "Worker retry was detected, ending this run in failure."); + return Result.failure(); + } + + Log.i(TAG, "Starting immediate picker grants sync from external database."); + + try { + final int callingPackageUid = getInputData().getInt(Intent.EXTRA_UID, -1); + final boolean shouldSyncGrants = getInputData().getBoolean(SHOULD_SYNC_GRANTS, false); + final String[] mimeTypes = getInputData().getStringArray(EXTRA_MIME_TYPES); + checkIsWorkerStopped(); + if (callingPackageUid != -1) { + PickerSyncController.getInstanceOrThrow().executeGrantsSync( + shouldSyncGrants, + callingPackageUid, + mimeTypes); + Log.i(TAG, "Completed immediate picker grants sync from external database. "); + } else { + // Having package uid is a must to execute sync for grants. + return Result.failure(); + } + return Result.success(); + } catch (IllegalStateException | RequestObsoleteException e) { + Log.i(TAG, "Could not complete immediate sync for grants"); + return Result.failure(); + } finally { + // Mark all pending syncs as finished. + markSyncAsComplete(SYNC_MEDIA_GRANTS, getId()); + } + } + + private void checkIsWorkerStopped() throws RequestObsoleteException { + if (isStopped()) { + throw new RequestObsoleteException("Work is stopped " + getId()); + } + } + + @Override + @NonNull + public ForegroundInfo getForegroundInfo() { + return PickerSyncNotificationHelper.getForegroundInfo(mContext); + } + + @Override + public void onStopped() { + Log.w(TAG, "Worker is stopped. Clearing all pending futures. It's possible that the sync " + + "still finishes running if it has started already."); + // Send CancellationSignal to any running tasks. + markSyncAsComplete(SYNC_MEDIA_GRANTS, getId()); + } +} diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java index bce961f62..61d45d38b 100644 --- a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java +++ b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java @@ -24,6 +24,7 @@ import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.t import static java.util.Objects.requireNonNull; import android.content.Context; +import android.content.Intent; import android.util.Log; import androidx.annotation.IntDef; @@ -41,6 +42,7 @@ import androidx.work.Worker; import com.android.modules.utils.BackgroundThread; import com.android.providers.media.ConfigStore; +import com.android.providers.media.photopicker.data.PickerSyncRequestExtras; import org.jetbrains.annotations.NotNull; @@ -67,8 +69,9 @@ public class PickerSyncManager { public static final int SYNC_LOCAL_ONLY = 1; public static final int SYNC_CLOUD_ONLY = 2; public static final int SYNC_LOCAL_AND_CLOUD = 3; + public static final int SYNC_MEDIA_GRANTS = 4; - @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD }) + @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD, SYNC_MEDIA_GRANTS }) @Retention(RetentionPolicy.SOURCE) public @interface SyncSource {} @@ -96,6 +99,9 @@ public class PickerSyncManager { public static final String IMMEDIATE_ALBUM_SYNC_WORK_NAME; public static final String PERIODIC_ALBUM_RESET_WORK_NAME; private static final String ENDLESS_WORK_NAME; + public static final String IMMEDIATE_GRANTS_SYNC_WORK_NAME; + public static final String SHOULD_SYNC_GRANTS; + public static final String EXTRA_MIME_TYPES; static { final String syncPeriodicPrefix = "SYNC_MEDIA_PERIODIC_"; @@ -104,15 +110,19 @@ public class PickerSyncManager { final String syncAllSuffix = "ALL"; final String syncLocalSuffix = "LOCAL"; final String syncCloudSuffix = "CLOUD"; + final String syncGrantsSuffix = "GRANTS"; PERIODIC_ALBUM_RESET_WORK_NAME = "RESET_ALBUM_MEDIA_PERIODIC"; PERIODIC_SYNC_WORK_NAME = syncPeriodicPrefix + syncAllSuffix; PROACTIVE_LOCAL_SYNC_WORK_NAME = syncProactivePrefix + syncLocalSuffix; PROACTIVE_SYNC_WORK_NAME = syncProactivePrefix + syncAllSuffix; + IMMEDIATE_GRANTS_SYNC_WORK_NAME = syncImmediatePrefix + syncGrantsSuffix; IMMEDIATE_LOCAL_SYNC_WORK_NAME = syncImmediatePrefix + syncLocalSuffix; IMMEDIATE_CLOUD_SYNC_WORK_NAME = syncImmediatePrefix + syncCloudSuffix; IMMEDIATE_ALBUM_SYNC_WORK_NAME = "SYNC_ALBUM_MEDIA_IMMEDIATE"; ENDLESS_WORK_NAME = "ENDLESS_WORK"; + SHOULD_SYNC_GRANTS = "SHOULD_SYNC_GRANTS"; + EXTRA_MIME_TYPES = "mime_types"; } private final WorkManager mWorkManager; @@ -246,27 +256,68 @@ public class PickerSyncManager { final OneTimeWorkRequest syncRequest = getOneTimeProactiveSyncRequest(inputData); // Don't wait for the sync operation to enqueue so that Picker sync enqueue - // requests in - // order to avoid adding latency to critical MP code paths. - - mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, syncRequest); + // requests in order to avoid adding latency to critical MP code paths. + mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, syncRequest); } /** * Use this method for reactive syncs which are user triggered. * - * @param shouldSyncLocalOnlyData if true indicates that the sync should only be triggered with - * the local provider. Otherwise, sync will be triggered for both - * local and cloud provider. + * @param pickerSyncRequestExtras extras used to figure out which all syncs to trigger. */ - public void syncMediaImmediately(boolean shouldSyncLocalOnlyData) { + public void syncMediaImmediately(PickerSyncRequestExtras pickerSyncRequestExtras) { + + if (mConfigStore.isModernPickerEnabled()) { + // sync for grants is only required for the modern picker, the java picker uses + // MediaStore to directly fetch the grants for all purposes of selection. + syncGrantsImmediately( + IMMEDIATE_GRANTS_SYNC_WORK_NAME, + pickerSyncRequestExtras.getCallingPackageUid(), + pickerSyncRequestExtras.isShouldSyncGrants(), + pickerSyncRequestExtras.getMimeTypes()); + } + syncMediaImmediately(PickerSyncManager.SYNC_LOCAL_ONLY, IMMEDIATE_LOCAL_SYNC_WORK_NAME); - if (!shouldSyncLocalOnlyData) { + if (!pickerSyncRequestExtras.shouldSyncLocalOnlyData()) { syncMediaImmediately(PickerSyncManager.SYNC_CLOUD_ONLY, IMMEDIATE_CLOUD_SYNC_WORK_NAME); } } /** + * Use this method for reactive syncs for grants from the external database. + */ + private void syncGrantsImmediately(@NonNull String workName, int callingPackageUid, + boolean shouldSyncGrants, String[] mimeTypes) { + final Data inputData = new Data( + Map.of( + Intent.EXTRA_UID, callingPackageUid, + SHOULD_SYNC_GRANTS, shouldSyncGrants, + EXTRA_MIME_TYPES, mimeTypes + ) + ); + + final OneTimeWorkRequest syncRequestForGrants = + buildOneTimeWorkerRequest(ImmediateGrantsSyncWorker.class, inputData); + + // Track the new sync request(s) + trackNewSyncRequests(PickerSyncManager.SYNC_MEDIA_GRANTS, syncRequestForGrants.getId()); + + // Enqueue grants sync request + try { + final Operation enqueueOperation = mWorkManager + .enqueueUniqueWork(workName, ExistingWorkPolicy.APPEND_OR_REPLACE, + syncRequestForGrants); + + // Check that the request has been successfully enqueued. + enqueueOperation.getResult().get(); + } catch (Exception e) { + Log.e(TAG, "Could not enqueue expedited picker grants sync request", e); + markSyncAsComplete(PickerSyncManager.SYNC_MEDIA_GRANTS, + syncRequestForGrants.getId()); + } + } + + /** * Use this method for reactive syncs with either, local and cloud providers, or both. */ private void syncMediaImmediately(@SyncSource int syncSource, @NonNull String workName) { diff --git a/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java b/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java index 5a5f6c90f..3eaa43504 100644 --- a/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java +++ b/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java @@ -19,6 +19,7 @@ package com.android.providers.media.photopicker.sync; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_MEDIA_GRANTS; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -33,6 +34,7 @@ public class SyncTrackerRegistry { private static SyncTracker sLocalAlbumSyncTracker = new SyncTracker(); private static SyncTracker sCloudSyncTracker = new SyncTracker(); private static SyncTracker sCloudAlbumSyncTracker = new SyncTracker(); + private static SyncTracker sGrantsSyncTracker = new SyncTracker(); public static SyncTracker getLocalSyncTracker() { return sLocalSyncTracker; @@ -59,6 +61,15 @@ public class SyncTrackerRegistry { sLocalAlbumSyncTracker = localAlbumSyncTracker; } + /** + * This setter is required to inject mock data for tests. Do not use this anywhere else. + */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public static void setGrantsSyncTracker( + SyncTracker grantsSyncTracker) { + sGrantsSyncTracker = grantsSyncTracker; + } + public static SyncTracker getCloudSyncTracker() { return sCloudSyncTracker; } @@ -85,6 +96,10 @@ public class SyncTrackerRegistry { sCloudAlbumSyncTracker = cloudAlbumSyncTracker; } + public static SyncTracker getGrantsSyncTracker() { + return sGrantsSyncTracker; + } + /** * Return the appropriate sync tracker. * @param isLocal is true when sync with local provider needs to be tracked. It is false for @@ -125,6 +140,9 @@ public class SyncTrackerRegistry { if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) { getCloudSyncTracker().createSyncFuture(syncRequestId); } + if (syncSource == SYNC_MEDIA_GRANTS) { + getGrantsSyncTracker().createSyncFuture(syncRequestId); + } } /** @@ -154,6 +172,9 @@ public class SyncTrackerRegistry { if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) { getCloudSyncTracker().markSyncCompleted(syncRequestId); } + if (syncSource == SYNC_MEDIA_GRANTS) { + getGrantsSyncTracker().markSyncCompleted(syncRequestId); + } } /** diff --git a/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java b/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java index 638b07105..b8309d210 100644 --- a/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java +++ b/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java @@ -33,7 +33,7 @@ public class WorkManagerInitializer { // {@link PickerSyncManager} to ensure that any request type is not blocked on other request // types. It is advisable to use unique work requests because in case the number of queued // requests grows, they should not block other work requests. - private static final int WORK_MANAGER_THREAD_POOL_SIZE = 6; + private static final int WORK_MANAGER_THREAD_POOL_SIZE = 7; @Nullable private static volatile Executor sWorkManagerExecutor; diff --git a/src/com/android/providers/media/photopicker/util/exceptions/WorkCancelledException.java b/src/com/android/providers/media/photopicker/util/exceptions/WorkCancelledException.java new file mode 100644 index 000000000..9ee7ce4f5 --- /dev/null +++ b/src/com/android/providers/media/photopicker/util/exceptions/WorkCancelledException.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 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.providers.media.photopicker.util.exceptions; + +/** + * {@code WorkCancelledException} is thrown when the work in progress is cancelled. + */ +public class WorkCancelledException extends Exception { + public WorkCancelledException(String message) { + super(message); + } +} diff --git a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java index 1a1a55b74..1a082aaa2 100644 --- a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java +++ b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java @@ -16,7 +16,13 @@ package com.android.providers.media.photopicker.v2; +import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE; +import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN; +import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; import static com.android.providers.media.PickerUriResolver.getAlbumUri; +import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid; +import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_GRANTS_SYNC_WORK_NAME; import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_LOCAL_SYNC_WORK_NAME; import static com.android.providers.media.photopicker.sync.WorkManagerInitializer.getWorkManager; import static com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper.EMPTY_MEDIA_ID; @@ -25,6 +31,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.UserIdInt; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ProviderInfo; @@ -32,9 +39,11 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; import android.os.Bundle; import android.os.Process; import android.provider.CloudMediaProviderContract.AlbumColumns; +import android.provider.MediaStore; import android.util.Log; import androidx.annotation.NonNull; @@ -50,6 +59,7 @@ import com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper; import com.android.providers.media.photopicker.v2.model.FavoritesMediaQuery; import com.android.providers.media.photopicker.v2.model.MediaQuery; import com.android.providers.media.photopicker.v2.model.MediaSource; +import com.android.providers.media.photopicker.v2.model.PreviewMediaQuery; import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo; import com.android.providers.media.photopicker.v2.model.VideoMediaQuery; @@ -57,7 +67,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -80,6 +92,7 @@ public class PickerDataLayerV2 { AlbumColumns.ALBUM_ID_FAVORITES, AlbumColumns.ALBUM_ID_VIDEOS ); + // Set of known local albums. public static final Set<String> LOCAL_ALBUMS = Set.of( AlbumColumns.ALBUM_ID_CAMERA, @@ -88,6 +101,32 @@ public class PickerDataLayerV2 { ); /** + * Table used to store the items for which the app hold read grants but have been de-selected + * by the user in the current photo-picker session. + */ + public static final String DE_SELECTIONS_TABLE = "de_selections"; + + /** + * Table used to store the items for which the app hold read grants but have been de-selected + * by the user in the current photo-picker session, filtered by calling package name and userId. + */ + public static final String CURRENT_DE_SELECTIONS_TABLE = "current_de_selections"; + + private static final String IS_FIRST_PAGE = "is_first_page"; + /** + * In SQL joins for media_grants table, it is filtered to only provide the rows corresponding to + * the current package and userId. This is the name for the filtered table that is computed in a + * sub-query. Any references to the columns for media_grants table should use this table name + * instead. + */ + public static final String CURRENT_GRANTS_TABLE = "current_media_grants"; + + public static final String COLUMN_GRANTS_COUNT = "grants_count"; + + private static final String PROJECTION_GRANTS_COUNT = String.format("COUNT(*) AS %s", + COLUMN_GRANTS_COUNT); + + /** * Refresh the cloud provider in-memory cache in PickerSyncController. */ public static void ensureProviders() { @@ -131,6 +170,42 @@ public class PickerDataLayerV2 { } /** + * Returns a cursor with the Photo Picker media in response. + * + * @param appContext The application context. + * @param queryArgs The arguments help us filter on the media query to yield the desired + * results. + */ + @NonNull + static Cursor queryPreviewMedia(@NonNull Context appContext, @NonNull Bundle queryArgs) { + final PreviewMediaQuery query = new PreviewMediaQuery(queryArgs); + final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); + final String effectiveLocalAuthority = + query.getProviders().contains(syncController.getLocalProvider()) + ? syncController.getLocalProvider() + : null; + final String cloudAuthority = syncController + .getCloudProviderOrDefault(/* defaultValue */ null); + final String effectiveCloudAuthority = + syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority) + ? cloudAuthority + : null; + + if (queryArgs.getBoolean(IS_FIRST_PAGE)) { + PreviewMediaQuery.insertDeSelections(appContext, syncController, + query.getCallingPackageUid(), query.getCurrentDeSelection()); + } + + return queryMediaInternal( + appContext, + syncController, + query, + effectiveLocalAuthority, + effectiveCloudAuthority + ); + } + + /** * Returns a cursor with the Photo Picker albums in response. * * @param appContext The application context. @@ -163,7 +238,7 @@ public class PickerDataLayerV2 { for (String albumId: PINNED_ALBUMS_ORDER) { final AlbumsCursorWrapper albumCursor; if (MERGED_ALBUMS.contains(albumId)) { - albumCursor = getMergedAlbumsCursor(albumId, queryArgs, database, + albumCursor = getMergedAlbumsCursor(albumId, appContext, queryArgs, database, effectiveLocalAuthority, effectiveCloudAuthority); } else if (LOCAL_ALBUMS.contains(albumId)) { albumCursor = localAlbums.getOrDefault(albumId, null); @@ -237,6 +312,52 @@ public class PickerDataLayerV2 { } /** + * Queries the picker database and fetches the count of pre-granted media for the current + * package and userId. + * + * @return a [Cursor] containing only one column [COLUMN_GRANTS_COUNT] which have a single + * row representing the count. + */ + static Cursor fetchMediaGrantsCount( + @NonNull Context appContext, + @NonNull Bundle queryArgs) { + String[] projectionIn = new String[]{PROJECTION_GRANTS_COUNT}; + final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow(); + final SQLiteDatabase database = syncController.getDbFacade().getDatabase(); + + waitForOngoingGrantsSync(appContext); + + int packageUid = queryArgs.getInt(Intent.EXTRA_UID); + int userId = uidToUserId(packageUid); + String[] packageNames = getPackageNameFromUid(appContext, + packageUid); + + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(MEDIA_GRANTS_TABLE); + addWhereClausesForPackageAndUserIdSelection(userId, packageNames, MEDIA_GRANTS_TABLE, qb); + + Cursor result = qb.query(database, projectionIn, null, + null, null, null, null); + return result; + } + + /** + * Adds the clause to select rows based on calling packageName and userId. + */ + public static void addWhereClausesForPackageAndUserIdSelection(int userId, + @NonNull String[] packageNames, String table, SQLiteQueryBuilder qb) { + // Add where clause for userId selection. + qb.appendWhereStandalone( + String.format(Locale.ROOT, + "%s.%s = %d", table, PACKAGE_USER_ID_COLUMN, userId)); + + // Add where clause for package name selection. + Objects.requireNonNull(packageNames); + qb.appendWhereStandalone(getPackageSelectionWhereClause(packageNames, + table).toString()); + } + + /** * Query media from the database and prepare a cursor in response. * * We need to make multiple queries to prepare a response for the media query. @@ -266,13 +387,13 @@ public class PickerDataLayerV2 { ) { try { final SQLiteDatabase database = syncController.getDbFacade().getDatabase(); - - waitForOngoingSync(appContext, localAuthority, cloudAuthority); + waitForOngoingSync(appContext, localAuthority, cloudAuthority, query.getIntentAction()); try { database.beginTransactionNonExclusive(); Cursor pageData = database.rawQuery( getMediaPageQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, @@ -281,10 +402,10 @@ public class PickerDataLayerV2 { ), /* selectionArgs */ null ); - Bundle extraArgs = new Bundle(); Cursor nextPageKeyCursor = database.rawQuery( getMediaNextPageKeyQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, @@ -297,6 +418,7 @@ public class PickerDataLayerV2 { Cursor prevPageKeyCursor = database.rawQuery( getMediaPreviousPageQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, @@ -306,17 +428,13 @@ public class PickerDataLayerV2 { /* selectionArgs */ null ); addPrevPageKey(extraArgs, prevPageKeyCursor); - database.setTransactionSuccessful(); - pageData.setExtras(extraArgs); Log.i(TAG, "Returning " + pageData.getCount() + " media metadata"); return pageData; } finally { database.endTransaction(); } - - } catch (Exception e) { throw new RuntimeException("Could not fetch media", e); } @@ -325,16 +443,28 @@ public class PickerDataLayerV2 { private static void waitForOngoingSync( @NonNull Context appContext, @Nullable String localAuthority, - @Nullable String cloudAuthority) { + @Nullable String cloudAuthority, String intentAction) { + // when the intent action is ACTION_USER_SELECT_IMAGES_FOR_APP, the flow should wait for + // the sync of grants and since this is a localOnly session. It should not wait or check + // cloud media. + boolean isUserSelectAction = MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals( + intentAction); if (localAuthority != null) { SyncCompletionWaiter.waitForSync( getWorkManager(appContext), - SyncTrackerRegistry.getLocalSyncTracker(), - IMMEDIATE_LOCAL_SYNC_WORK_NAME + SyncTrackerRegistry.getGrantsSyncTracker(), + IMMEDIATE_GRANTS_SYNC_WORK_NAME ); + if (isUserSelectAction) { + SyncCompletionWaiter.waitForSync( + getWorkManager(appContext), + SyncTrackerRegistry.getLocalSyncTracker(), + IMMEDIATE_LOCAL_SYNC_WORK_NAME + ); + } } - if (cloudAuthority != null) { + if (cloudAuthority != null && !isUserSelectAction) { boolean syncIsComplete = SyncCompletionWaiter.waitForSyncWithTimeout( SyncTrackerRegistry.getCloudSyncTracker(), CLOUD_SYNC_TIMEOUT_MILLIS); @@ -343,6 +473,15 @@ public class PickerDataLayerV2 { } } + private static void waitForOngoingGrantsSync( + @NonNull Context appContext) { + SyncCompletionWaiter.waitForSync( + getWorkManager(appContext), + SyncTrackerRegistry.getGrantsSyncTracker(), + IMMEDIATE_GRANTS_SYNC_WORK_NAME + ); + } + /** * @param appContext The application context. * @param query The AlbumMediaQuery object instance that tells us about the media query args. @@ -431,13 +570,15 @@ public class PickerDataLayerV2 { * Builds and returns the SQL query to get the page contents from the Media table in Picker DB. */ private static String getMediaPageQuery( + @Nullable Context appContext, @NonNull MediaQuery query, @NonNull SQLiteDatabase database, @NonNull PickerSQLConstants.Table table, @Nullable String localAuthority, @Nullable String cloudAuthority) { SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database) - .setTables(table.name()) + .setTables(query.getTableWithRequiredJoins(table.toString(), appContext, + query.getCallingPackageUid(), query.getIntentAction())) .setProjection(List.of( PickerSQLConstants.MediaResponse.MEDIA_ID.getProjection(), PickerSQLConstants.MediaResponse.PICKER_ID.getProjection(), @@ -452,7 +593,9 @@ public class PickerDataLayerV2 { PickerSQLConstants.MediaResponse.SIZE_IN_BYTES.getProjection(), PickerSQLConstants.MediaResponse.MIME_TYPE.getProjection(), PickerSQLConstants.MediaResponse.STANDARD_MIME_TYPE.getProjection(), - PickerSQLConstants.MediaResponse.DURATION_MS.getProjection() + PickerSQLConstants.MediaResponse.DURATION_MS.getProjection(), + PickerSQLConstants.MediaResponse.IS_PRE_GRANTED.getProjection( + query.getIntentAction()) )) .setSortOrder( String.format( @@ -478,6 +621,7 @@ public class PickerDataLayerV2 { */ @Nullable private static String getMediaNextPageKeyQuery( + @Nullable Context appContext, @NonNull MediaQuery query, @NonNull SQLiteDatabase database, @NonNull PickerSQLConstants.Table table, @@ -488,7 +632,9 @@ public class PickerDataLayerV2 { } SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database) - .setTables(table.name()) + .setTables( + query.getTableWithRequiredJoins(table.toString(), appContext, + query.getCallingPackageUid(), query.getIntentAction())) .setProjection(List.of( PickerSQLConstants.MediaResponse.PICKER_ID.getProjection(), PickerSQLConstants.MediaResponse.DATE_TAKEN_MS.getProjection() @@ -522,13 +668,16 @@ public class PickerDataLayerV2 { * get the previous page key. */ private static String getMediaPreviousPageQuery( + @Nullable Context appContext, @NonNull MediaQuery query, @NonNull SQLiteDatabase database, @NonNull PickerSQLConstants.Table table, @Nullable String localAuthority, @Nullable String cloudAuthority) { SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database) - .setTables(table.name()) + .setTables( + query.getTableWithRequiredJoins(table.toString(), appContext, + query.getCallingPackageUid(), query.getIntentAction())) .setProjection(List.of( PickerSQLConstants.MediaResponse.PICKER_ID.getProjection(), PickerSQLConstants.MediaResponse.DATE_TAKEN_MS.getProjection() @@ -552,11 +701,28 @@ public class PickerDataLayerV2 { } /** + * Returns a clause that can be used to filter OWNER_PACKAGE_NAME_COLUMN using the input + * packageNames in a query. + */ + public static @NonNull StringBuilder getPackageSelectionWhereClause(String[] packageNames, + String table) { + StringBuilder packageSelection = new StringBuilder(); + String packageColumn = String.format("%s.%s", table, OWNER_PACKAGE_NAME_COLUMN); + packageSelection.append(packageColumn).append(" IN (\'"); + + String joinedPackageNames = String.join("\',\'", packageNames); + packageSelection.append(joinedPackageNames); + + packageSelection.append("\')"); + return packageSelection; + } + + /** * Return merged albums cursor for the given merged album id. * - * @param albumId Merged album id. - * @param queryArgs Query arguments bundle that will be used to filter albums. - * @param database Instance of Picker SQLiteDatabase. + * @param albumId Merged album id. + * @param queryArgs Query arguments bundle that will be used to filter albums. + * @param database Instance of Picker SQLiteDatabase. * @param localAuthority The local authority if local albums should be returned, otherwise this * argument should be null. * @param cloudAuthority The cloud authority if cloud albums should be returned, otherwise this @@ -564,6 +730,7 @@ public class PickerDataLayerV2 { */ private static AlbumsCursorWrapper getMergedAlbumsCursor( @NonNull String albumId, + Context appContext, @NonNull Bundle queryArgs, @NonNull SQLiteDatabase database, @Nullable String localAuthority, @@ -591,6 +758,7 @@ public class PickerDataLayerV2 { database.beginTransactionNonExclusive(); Cursor pickerDBResponse = database.rawQuery( getMediaPageQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, @@ -620,22 +788,21 @@ public class PickerDataLayerV2 { return new AlbumsCursorWrapper(result, authority, localAuthority); } - // Show merged albums even if no data is currently available in the DB when cloud media - // feature is enabled. - if (cloudAuthority != null) { - // Conform to the album response projection. Temporary code, this will change once - // we start caching album metadata. - final MatrixCursor result = new MatrixCursor(AlbumColumns.ALL_PROJECTION); - final String[] projectionValue = new String[]{ - /* albumId */ albumId, - /* dateTakenMillis */ Long.toString(Long.MAX_VALUE), - /* displayName */ albumId, - /* mediaId */ EMPTY_MEDIA_ID, - /* count */ "0", // This value is not used anymore - localAuthority, - }; - result.addRow(projectionValue); - return new AlbumsCursorWrapper(result, localAuthority, localAuthority); + // Always show Videos album if cloud feature is turned on and the MIME types filter + // would allow for video format(s). + if (albumId.equals(AlbumColumns.ALBUM_ID_VIDEOS) && cloudAuthority != null) { + return new AlbumsCursorWrapper( + getDefaultEmptyAlbum(albumId), + /* albumAuthority */ localAuthority, + /* localAuthority */ localAuthority); + } + + // Always show Favorites album. + if (albumId.equals(AlbumColumns.ALBUM_ID_FAVORITES)) { + return new AlbumsCursorWrapper( + getDefaultEmptyAlbum(albumId), + /* albumAuthority */ localAuthority, + /* localAuthority */ localAuthority); } return null; @@ -643,7 +810,22 @@ public class PickerDataLayerV2 { database.setTransactionSuccessful(); database.endTransaction(); } + } + private static Cursor getDefaultEmptyAlbum(@NonNull String albumId) { + // Conform to the album response projection. Temporary code, this will change once we start + // caching album metadata. + final MatrixCursor result = new MatrixCursor(AlbumColumns.ALL_PROJECTION); + final String[] projectionValue = new String[]{ + /* albumId */ albumId, + /* dateTakenMillis */ Long.toString(Long.MAX_VALUE), + /* displayName */ albumId, + /* mediaId */ EMPTY_MEDIA_ID, + /* count */ "0", // This value is not used anymore + /* authority */ null, // Authority is populated in AlbumsCursorWrapper + }; + result.addRow(projectionValue); + return result; } /** @@ -718,6 +900,17 @@ public class PickerDataLayerV2 { // be used again. if (localAlbumsCursor != null) localAlbumsCursor.close(); + // Always show Camera album. + if (!localAlbumsMap.containsKey(AlbumColumns.ALBUM_ID_CAMERA)) { + localAlbumsMap.put( + AlbumColumns.ALBUM_ID_CAMERA, + new AlbumsCursorWrapper( + getDefaultEmptyAlbum(AlbumColumns.ALBUM_ID_CAMERA), + /* albumAuthority */ localAuthority, + /* localAuthority */ localAuthority) + ); + } + return localAlbumsMap; } @@ -796,6 +989,7 @@ public class PickerDataLayerV2 { database.beginTransactionNonExclusive(); Cursor pageData = database.rawQuery( getMediaPageQuery( + appContext, query, database, PickerSQLConstants.Table.ALBUM_MEDIA, @@ -808,6 +1002,7 @@ public class PickerDataLayerV2 { Bundle extraArgs = new Bundle(); Cursor nextPageKeyCursor = database.rawQuery( getMediaNextPageKeyQuery( + appContext, query, database, PickerSQLConstants.Table.ALBUM_MEDIA, @@ -820,6 +1015,7 @@ public class PickerDataLayerV2 { Cursor prevPageKeyCursor = database.rawQuery( getMediaPreviousPageQuery( + appContext, query, database, PickerSQLConstants.Table.ALBUM_MEDIA, @@ -884,12 +1080,13 @@ public class PickerDataLayerV2 { final SQLiteDatabase database = syncController.getDbFacade().getDatabase(); - waitForOngoingSync(appContext, localAuthority, cloudAuthority); + waitForOngoingSync(appContext, localAuthority, cloudAuthority, query.getIntentAction()); try { database.beginTransactionNonExclusive(); Cursor pageData = database.rawQuery( getMediaPageQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, @@ -902,6 +1099,7 @@ public class PickerDataLayerV2 { Bundle extraArgs = new Bundle(); Cursor nextPageKeyCursor = database.rawQuery( getMediaNextPageKeyQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, @@ -914,6 +1112,7 @@ public class PickerDataLayerV2 { Cursor prevPageKeyCursor = database.rawQuery( getMediaPreviousPageQuery( + appContext, query, database, PickerSQLConstants.Table.MEDIA, diff --git a/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java b/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java index ff7f33f38..1354830e1 100644 --- a/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java +++ b/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java @@ -34,6 +34,7 @@ import android.provider.MediaStore; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.providers.media.MediaGrants; import com.android.providers.media.photopicker.v2.model.MediaSource; import java.util.Arrays; @@ -142,7 +143,8 @@ public class PickerSQLConstants { MIME_TYPE(KEY_MIME_TYPE, CloudMediaProviderContract.MediaColumns.MIME_TYPE), STANDARD_MIME_TYPE(KEY_STANDARD_MIME_TYPE_EXTENSION, CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION), - DURATION_MS(KEY_DURATION_MS, CloudMediaProviderContract.MediaColumns.DURATION_MILLIS); + DURATION_MS(KEY_DURATION_MS, CloudMediaProviderContract.MediaColumns.DURATION_MILLIS), + IS_PRE_GRANTED("is_pre_granted"); private static final String DEFAULT_PROJECTION = "%s AS %s"; @Nullable @@ -236,6 +238,22 @@ public class PickerSQLConstants { } } + @NonNull + public String getProjection(String intentAction) { + switch (this) { + case IS_PRE_GRANTED: + return String.format(DEFAULT_PROJECTION, getIsPregranted(intentAction), + mProjectedName); + default: + if (mColumnName == null) { + throw new IllegalArgumentException( + "Could not get projection for " + this.name() + ); + } + return String.format(DEFAULT_PROJECTION, mColumnName, mProjectedName); + } + } + private String getMediaId() { return String.format( "IFNULL(%s, %s)", @@ -295,6 +313,15 @@ public class PickerSQLConstants { getMediaId() ); } + + private String getIsPregranted(String intentAction) { + if (MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intentAction)) { + return String.format("CASE WHEN %s.%s IS NOT NULL THEN 1 ELSE 0 END", + PickerDataLayerV2.CURRENT_GRANTS_TABLE, MediaGrants.FILE_ID_COLUMN); + } else { + return "0"; // default case for other intent actions + } + } } enum MediaResponseExtras { diff --git a/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java b/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java index 0364848f6..0e9e1aa90 100644 --- a/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java +++ b/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java @@ -42,6 +42,9 @@ public class PickerUriResolverV2 { public static final String MEDIA_PATH_SEGMENT = "media"; public static final String ALBUM_PATH_SEGMENT = "album"; public static final String UPDATE_PATH_SEGMENT = "update"; + public static final String MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count"; + public static final String PREVIEW_PATH_SEGMENT = "preview"; + static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static final int PICKER_INTERNAL_MEDIA = 1; @@ -49,6 +52,8 @@ public class PickerUriResolverV2 { static final int PICKER_INTERNAL_ALBUM_CONTENT = 3; static final int PICKER_INTERNAL_AVAILABLE_PROVIDERS = 4; static final int PICKER_INTERNAL_COLLECTION_INFO = 5; + static final int PICKER_INTERNAL_MEDIA_GRANTS_COUNT = 6; + static final int PICKER_INTERNAL_MEDIA_PREVIEW = 7; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -58,6 +63,8 @@ public class PickerUriResolverV2 { PICKER_INTERNAL_ALBUM_CONTENT, PICKER_INTERNAL_AVAILABLE_PROVIDERS, PICKER_INTERNAL_COLLECTION_INFO, + PICKER_INTERNAL_MEDIA_GRANTS_COUNT, + PICKER_INTERNAL_MEDIA_PREVIEW }) private @interface PickerQuery {} @@ -81,6 +88,11 @@ public class PickerUriResolverV2 { BASE_PICKER_PATH + COLLECTION_INFO_PATH_SEGMENT, PICKER_INTERNAL_COLLECTION_INFO ); + sUriMatcher.addURI(MediaStore.AUTHORITY, BASE_PICKER_PATH + MEDIA_GRANTS_COUNT_PATH_SEGMENT, + PICKER_INTERNAL_MEDIA_GRANTS_COUNT); + sUriMatcher.addURI(MediaStore.AUTHORITY, + BASE_PICKER_PATH + MEDIA_PATH_SEGMENT + "/" + PREVIEW_PATH_SEGMENT, + PICKER_INTERNAL_MEDIA_PREVIEW); } /** @@ -110,6 +122,11 @@ public class PickerUriResolverV2 { return PickerDataLayerV2.queryAvailableProviders(appContext); case PICKER_INTERNAL_COLLECTION_INFO: return PickerDataLayerV2.queryCollectionInfo(); + case PICKER_INTERNAL_MEDIA_GRANTS_COUNT: + return PickerDataLayerV2.fetchMediaGrantsCount(appContext, + requireNonNull(queryArgs)); + case PICKER_INTERNAL_MEDIA_PREVIEW: + return PickerDataLayerV2.queryPreviewMedia(appContext, queryArgs); default: throw new UnsupportedOperationException("Could not recognize content URI " + uri); } diff --git a/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java index e13bcf8a1..28e770d64 100644 --- a/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java +++ b/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java @@ -16,24 +16,35 @@ package com.android.providers.media.photopicker.v2.model; +import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN; +import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE; +import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; +import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid; +import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId; import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID; import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_DATE_TAKEN_MS; import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_ID; import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_IS_VISIBLE; import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID; import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_MIME_TYPE; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.CURRENT_GRANTS_TABLE; +import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.provider.MediaStore; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.providers.media.MediaGrants; +import com.android.providers.media.photopicker.v2.PickerDataLayerV2; import com.android.providers.media.photopicker.v2.SelectSQLiteQueryBuilder; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Objects; /** @@ -46,6 +57,7 @@ public class MediaQuery { private final String mIntentAction; @NonNull private final List<String> mProviders; + private final int mCallingPackageUid; // If this is not null or empty, only fetch the rows that match at least one of the // given mime types. @Nullable @@ -64,11 +76,11 @@ public class MediaQuery { mProviders = new ArrayList<>( Objects.requireNonNull(queryArgs.getStringArrayList("providers"))); mMimeTypes = queryArgs.getStringArrayList("mime_types") != null - ? new ArrayList<>(queryArgs.getStringArrayList("mime_types")) - : null; + ? new ArrayList<>(queryArgs.getStringArrayList("mime_types")) : null; // This is true by default. mShouldDedupe = true; + mCallingPackageUid = queryArgs.getInt(Intent.EXTRA_UID, -1); } @NonNull @@ -91,6 +103,11 @@ public class MediaQuery { return mIntentAction; } + public int getCallingPackageUid() { + return mCallingPackageUid; + } + + /** * Create and return a bundle for extras for CMP queries made from Media Provider. */ @@ -104,6 +121,62 @@ public class MediaQuery { } /** + * Returns the table that should be used in the query operations including any joins that are + * required with other tables in the database. + */ + public String getTableWithRequiredJoins(String table, + @NonNull Context appContext, int callingPackageUid, String intentAction) { + + if (!MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intentAction)) { + // No joins are required for a ACTION_USER_SELECT_IMAGES_FOR_APP action query. + return table; + } + Objects.requireNonNull(appContext); + if (callingPackageUid == -1) { + throw new IllegalArgumentException("Calling package uid in" + + "ACTION_USER_SELECT_IMAGES_FOR_APP mode should not be -1. Invalid UID"); + } + + int userId = uidToUserId(callingPackageUid); + String[] packageNames = getPackageNameFromUid(appContext, + callingPackageUid); + Objects.requireNonNull(packageNames); + StringBuilder packageSelection = + PickerDataLayerV2.getPackageSelectionWhereClause(packageNames, MEDIA_GRANTS_TABLE); + + // The following join is performed for the query media operation to obtain information on + // which items are preGranted. + String filterQueryBasedOnPackageNameAndUserId = + "(SELECT %s.%s FROM %s " + + "WHERE " + + " %s AND " + + "%s = %d) " + + "AS %s"; + + String filteredMediaGrantsTable = String.format( + Locale.ROOT, + filterQueryBasedOnPackageNameAndUserId, + MEDIA_GRANTS_TABLE, + FILE_ID_COLUMN, + MEDIA_GRANTS_TABLE, + packageSelection, + PACKAGE_USER_ID_COLUMN, + userId, + CURRENT_GRANTS_TABLE); + + return String.format( + "%s LEFT JOIN %s" + + " ON %s.%s = %s.%s ", + table, + filteredMediaGrantsTable, + table, + KEY_LOCAL_ID, + CURRENT_GRANTS_TABLE, + MediaGrants.FILE_ID_COLUMN + ); + } + + /** * @param queryBuilder Adds SQL query where clause based on the Media query arguments to the * given query builder. * @param localAuthority the authority of the local provider if we should include local media in diff --git a/src/com/android/providers/media/photopicker/v2/model/PreviewMediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/PreviewMediaQuery.java new file mode 100644 index 000000000..76aa8de5d --- /dev/null +++ b/src/com/android/providers/media/photopicker/v2/model/PreviewMediaQuery.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 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.providers.media.photopicker.v2.model; + +import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN; +import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE; +import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN; +import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN; +import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid; +import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId; +import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.CURRENT_DE_SELECTIONS_TABLE; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.CURRENT_GRANTS_TABLE; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.DE_SELECTIONS_TABLE; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.addWhereClausesForPackageAndUserIdSelection; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.getPackageSelectionWhereClause; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.providers.media.MediaGrants; +import com.android.providers.media.photopicker.PickerSyncController; +import com.android.providers.media.photopicker.v2.SelectSQLiteQueryBuilder; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.Objects; + +/** + * This is a convenience class for Preview media content related SQL queries performed on the Picker + * Database. + */ +public class PreviewMediaQuery extends MediaQuery { + private final ArrayList<String> mCurrentSelection; + private final ArrayList<String> mCurrentDeSelection; + + public PreviewMediaQuery( + @NonNull Bundle queryArgs) { + super(queryArgs); + mCurrentSelection = queryArgs.getStringArrayList("current_selection"); + mCurrentDeSelection = queryArgs.getStringArrayList("current_de_selection"); + } + + public ArrayList<String> getCurrentSelection() { + return mCurrentSelection; + } + + public ArrayList<String> getCurrentDeSelection() { + return mCurrentDeSelection; + } + + @Override + public void addWhereClause( + @NonNull SelectSQLiteQueryBuilder queryBuilder, + @Nullable String localAuthority, + @Nullable String cloudAuthority, + boolean reverseOrder + ) { + super.addWhereClause(queryBuilder, localAuthority, cloudAuthority, reverseOrder); + + addIdSelectionClause(queryBuilder); + } + + private void addIdSelectionClause(@NonNull SelectSQLiteQueryBuilder queryBuilder) { + StringBuilder idSelectionPlaceholder = new StringBuilder(); + if (mCurrentSelection != null && !mCurrentSelection.isEmpty()) { + idSelectionPlaceholder.append("local_id IN ("); + String joinedIds = String.join(",", mCurrentSelection); + idSelectionPlaceholder.append(joinedIds); + idSelectionPlaceholder.append(")"); + } + + if (!idSelectionPlaceholder.toString().isEmpty()) { + idSelectionPlaceholder.append(" OR "); + } + + idSelectionPlaceholder.append( + String.format("(%s.%s IS NOT NULL AND %s.%s IS NULL)", + // current_media_grants.file_id IS NOT NULL + CURRENT_GRANTS_TABLE, MediaGrants.FILE_ID_COLUMN, + // current_de_selections.file_id IS NULL + CURRENT_DE_SELECTIONS_TABLE, MediaGrants.FILE_ID_COLUMN)); + queryBuilder.appendWhereStandalone(idSelectionPlaceholder.toString()); + } + + /** + * Returns the table that should be used in the query operations including any joins that are + * required with other tables in the database. + */ + @Override + public String getTableWithRequiredJoins(String table, + @NonNull Context appContext, int callingPackageUid, String intentAction) { + Objects.requireNonNull(appContext); + if (callingPackageUid == -1) { + throw new IllegalArgumentException("Calling package uid in" + + "ACTION_USER_SELECT_IMAGES_FOR_APP mode should not be -1. Invalid UID"); + } + int userId = uidToUserId(callingPackageUid); + String[] packageNames = getPackageNameFromUid(appContext, + callingPackageUid); + Objects.requireNonNull(packageNames); + + // The following joins for the table is performed for the preview request. + // Media items needs to be filtered based on: + // 1. if they are selected by the user in the current session + // 2. if they have been pre-granted i.e. their grant is in media_grants table AND they have + // not be de-selected by the user in the current session i.e. the item is not part of + // the de_selections table. + // To find such a union of items, the current selection mentioned in point one can be + // handled with a where clause on media table itself but for point 2 to be satisfied the + // media table needs to be joined with media_grants and de_selections tables. + // These tables can contain data for multiple apps hence they need to be separately + // filtered with the help of a sub-query based on the current calling package name and + // userId. + + String filterQueryBasedOnPackageNameAndUserId = "(SELECT %s.%s FROM %s " + + "WHERE " + + " %s AND " + + "%s = %d) " + + "AS %s"; + + String filteredMediaGrantsTable = String.format(Locale.ROOT, + filterQueryBasedOnPackageNameAndUserId, + MEDIA_GRANTS_TABLE, + FILE_ID_COLUMN, + MEDIA_GRANTS_TABLE, + getPackageSelectionWhereClause(packageNames, MEDIA_GRANTS_TABLE), + PACKAGE_USER_ID_COLUMN, + userId, + CURRENT_GRANTS_TABLE); + + String filteredDeSelectionsTable = String.format(Locale.ROOT, + filterQueryBasedOnPackageNameAndUserId, + DE_SELECTIONS_TABLE, + FILE_ID_COLUMN, + DE_SELECTIONS_TABLE, + getPackageSelectionWhereClause(packageNames, DE_SELECTIONS_TABLE), + PACKAGE_USER_ID_COLUMN, + userId, + CURRENT_DE_SELECTIONS_TABLE); + + return String.format( + "%s LEFT JOIN %s" + + " ON %s.%s = %s.%s " + + "LEFT JOIN %s" + + " ON %s.%s = %s.%s", + table, + filteredMediaGrantsTable, + table, + KEY_LOCAL_ID, + CURRENT_GRANTS_TABLE, + MediaGrants.FILE_ID_COLUMN, + filteredDeSelectionsTable, + table, + KEY_LOCAL_ID, + CURRENT_DE_SELECTIONS_TABLE, + FILE_ID_COLUMN + ); + } + + /** + * Insert ids in 'de_selection' table in the picker.db to be used for exclusions in the query + * operation. + */ + public static void insertDeSelections( + @NonNull Context appContext, + @NonNull PickerSyncController syncController, + int callingUid, + @NonNull ArrayList<String> currentDeSelection + ) { + + final SQLiteDatabase database = syncController.getDbFacade().getDatabase(); + try { + database.beginTransaction(); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(DE_SELECTIONS_TABLE); + String[] ownerPackageName = getPackageNameFromUid(appContext, + callingUid); + int userId = uidToUserId(callingUid); + addWhereClausesForPackageAndUserIdSelection(userId, ownerPackageName, + DE_SELECTIONS_TABLE, qb); + qb.delete(database, null, null); + + qb = new SQLiteQueryBuilder(); + qb.setTables(DE_SELECTIONS_TABLE); + + if (!currentDeSelection.isEmpty()) { + ContentValues cv = new ContentValues(); + for (int i = 0; i < currentDeSelection.size(); i++) { + cv.clear(); + cv.put(FILE_ID_COLUMN, currentDeSelection.get(i)); + cv.put(OWNER_PACKAGE_NAME_COLUMN, ownerPackageName[0]); + cv.put(PACKAGE_USER_ID_COLUMN, userId); + qb.insert(database, cv); + } + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } +} diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java index faac7a09d..c791cc1b5 100644 --- a/src/com/android/providers/media/scan/ModernMediaScanner.java +++ b/src/com/android/providers/media/scan/ModernMediaScanner.java @@ -49,6 +49,7 @@ import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.providers.media.util.FileUtils.canonicalize; +import static com.android.providers.media.util.IsoInterface.MAX_XMP_SIZE_BYTES; import static com.android.providers.media.util.Metrics.translateReason; import static java.util.Objects.requireNonNull; @@ -175,7 +176,6 @@ public class ModernMediaScanner implements MediaScanner { } private static final int BATCH_SIZE = 32; - private static final int MAX_XMP_SIZE_BYTES = 1024 * 1024; // |excludeDirs * 2| < 1000 which is the max SQL expression size // Because we add |excludeDir| and |excludeDir/| in the SQL expression to match dir and subdirs // See SQLITE_MAX_EXPR_DEPTH in sqlite3.c diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java index 5fb5130a8..43a00f00c 100644 --- a/src/com/android/providers/media/util/IsoInterface.java +++ b/src/com/android/providers/media/util/IsoInterface.java @@ -42,12 +42,15 @@ import java.util.UUID; /** * Simple parser for ISO base media file format. Designed to mirror ergonomics - * of {@link ExifInterface}. + * of {@link ExifInterface}. Stores boxes related to xmp and gps in order to + * prevent from {@link OutOfMemoryError}. */ public class IsoInterface { private static final String TAG = "IsoInterface"; private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); + public static final int MAX_XMP_SIZE_BYTES = 1024 * 1024; + public static final int BOX_ILST = 0x696c7374; public static final int BOX_FTYP = 0x66747970; public static final int BOX_HDLR = 0x68646c72; @@ -60,6 +63,9 @@ public class IsoInterface { public static final int BOX_GPS = 0x67707320; public static final int BOX_GPS0 = 0x67707330; + public static final UUID XMP_UUID = + UUID.fromString("be7acfcb-97a9-42e8-9c71-999491e3afac"); + /** * Test if given box type is a well-known parent box type. */ @@ -90,17 +96,14 @@ public class IsoInterface { } } - /** Top-level boxes */ - private List<Box> mRoots = new ArrayList<>(); - /** Flattened view of all boxes */ - private List<Box> mFlattened = new ArrayList<>(); + /** Flattened view of some boxes */ + private final List<Box> mFlattened = new ArrayList<>(); private static class Box { public final int type; public long[] range; public UUID uuid; public byte[] data; - public List<Box> children; public int headerSize; public Box(int type, long[] range) { @@ -133,13 +136,26 @@ public class IsoInterface { return new UUID(high, low); } - private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end, int parentType, - @NonNull String prefix) throws ErrnoException, IOException { + private static @Nullable byte[] allocateBuffer(int type, int size) { + try { + if (size > MAX_XMP_SIZE_BYTES) { + Log.w(TAG, "Iso box(" + type + ") data size is too large: " + size); + return null; + } + return new byte[size]; + } catch (OutOfMemoryError e) { + Log.w(TAG, "Couldn't read large box(" + type + "), size: " + size, e); + return null; + } + } + + private static boolean parseNextBox(@NonNull List<Box> flatten, @NonNull FileDescriptor fd, + long end, int parentType, @NonNull String prefix) throws ErrnoException, IOException { final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR); int headerSize = 8; if (end - pos < headerSize) { - return null; + return false; } long len = Integer.toUnsignedLong(readInt(fd)); @@ -159,7 +175,7 @@ public class IsoInterface { if (len < headerSize || pos + len > end) { Log.w(TAG, "Invalid box at " + pos + " of length " + len + ". End of parent " + end); - return null; + return false; } final Box box = new Box(type, new long[] { pos, len }); @@ -173,30 +189,16 @@ public class IsoInterface { Log.v(TAG, prefix + " UUID " + box.uuid); } - if (len > Integer.MAX_VALUE) { - Log.w(TAG, "Skipping abnormally large uuid box"); - return null; - } + if (Objects.equals(box.uuid, XMP_UUID)) { + box.data = allocateBuffer(type, (int) (len - box.headerSize)); + if (box.data == null) return false; - try { - box.data = new byte[(int) (len - box.headerSize)]; - } catch (OutOfMemoryError e) { - Log.w(TAG, "Couldn't read large uuid box", e); - return null; + Os.read(fd, box.data, 0, box.data.length); } - Os.read(fd, box.data, 0, box.data.length); } else if (type == BOX_XMP) { - if (len > Integer.MAX_VALUE) { - Log.w(TAG, "Skipping abnormally large xmp box"); - return null; - } + box.data = allocateBuffer(type, (int) (len - box.headerSize)); + if (box.data == null) return false; - try { - box.data = new byte[(int) (len - box.headerSize)]; - } catch (OutOfMemoryError e) { - Log.w(TAG, "Couldn't read large xmp box", e); - return null; - } Os.read(fd, box.data, 0, box.data.length); } else if (type == BOX_META && len != headerSize) { // The format of this differs in ISO and QT encoding: @@ -221,19 +223,29 @@ public class IsoInterface { + " at " + pos + " hdr " + box.headerSize + " length " + len); } + switch (type) { + case BOX_UUID: + if (!Objects.equals(box.uuid, XMP_UUID)) break; + // fall through + case BOX_META: + case BOX_HDLR: + case BOX_XYZ: + case BOX_LOCI: + case BOX_GPS: + case BOX_GPS0: + flatten.add(box); + break; + } + // Recursively parse any children boxes if (isBoxParent(type)) { - box.children = new ArrayList<>(); - - Box child; - while ((child = parseNextBox(fd, pos + len, type, prefix + " ")) != null) { - box.children.add(child); - } + //noinspection StatementWithEmptyBody + while (parseNextBox(flatten, fd, pos + len, type, prefix + " ")) {} } // Skip completely over ourselves Os.lseek(fd, pos + len, OsConstants.SEEK_SET); - return box; + return true; } private IsoInterface(@NonNull FileDescriptor fd) throws IOException { @@ -255,26 +267,15 @@ public class IsoInterface { final long end = Os.lseek(fd, 0, OsConstants.SEEK_END); Os.lseek(fd, 0, OsConstants.SEEK_SET); - Box box; - while ((box = parseNextBox(fd, end, -1, "")) != null) { - mRoots.add(box); - } + + //noinspection StatementWithEmptyBody + while (parseNextBox(mFlattened, fd, end, -1, "")) {} } catch (ErrnoException e) { throw e.rethrowAsIOException(); } catch (OutOfMemoryError e) { Log.e(TAG, "Too many boxes in file. This might imply a corrupted file.", e); throw new IOException(e.getMessage()); } - - // Also create a flattened structure to speed up searching - final Queue<Box> queue = new ArrayDeque<>(mRoots); - while (!queue.isEmpty()) { - final Box box = queue.poll(); - mFlattened.add(box); - if (box.children != null) { - queue.addAll(box.children); - } - } } public static @NonNull IsoInterface fromFile(@NonNull File file) @@ -308,10 +309,10 @@ public class IsoInterface { return res.toArray(); } - public @NonNull long[] getBoxRanges(@NonNull UUID uuid) { + public @NonNull long[] getBoxRangesForXmpUuid() { LongArray res = new LongArray(); for (Box box : mFlattened) { - if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) { + if (box.type == BOX_UUID && Objects.equals(box.uuid, XMP_UUID)) { for (int i = 0; i < box.range.length; i += 2) { res.add(box.range[i] + box.headerSize); res.add(box.range[i] + box.range[i + 1]); @@ -334,11 +335,11 @@ public class IsoInterface { } /** - * Return contents of the first UUID box of requested type. + * Return contents of the first XMP UUID box of requested type. */ - public @Nullable byte[] getBoxBytes(@NonNull UUID uuid) { + public @Nullable byte[] getBoxBytesForXmpUuid() { for (Box box : mFlattened) { - if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) { + if (box.type == BOX_UUID && Objects.equals(box.uuid, XMP_UUID)) { return box.data; } } diff --git a/src/com/android/providers/media/util/XmpDataParser.java b/src/com/android/providers/media/util/XmpDataParser.java index 7ee0adda0..cbd4bcf59 100644 --- a/src/com/android/providers/media/util/XmpDataParser.java +++ b/src/com/android/providers/media/util/XmpDataParser.java @@ -36,7 +36,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.UUID; public final class XmpDataParser implements Closeable { @@ -223,9 +222,8 @@ public final class XmpDataParser implements Closeable { } static @NonNull XmpData extractXmpData(@NonNull IsoInterface iso) { - UUID uuid = UUID.fromString("be7acfcb-97a9-42e8-9c71-999491e3afac"); - byte[] buf = iso.getBoxBytes(uuid); - long[] xmpOffsets = iso.getBoxRanges(uuid); + byte[] buf = iso.getBoxBytesForXmpUuid(); + long[] xmpOffsets = iso.getBoxRangesForXmpUuid(); if (buf == null) { buf = iso.getBoxBytes(IsoInterface.BOX_XMP); diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml index 55d21a6fe..31d9e1535 100644 --- a/tests/AndroidTest.xml +++ b/tests/AndroidTest.xml @@ -14,6 +14,13 @@ limitations under the License. --> <configuration description="Runs Tests for MediaProvder."> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="force-skip-system-props" value="true" /> + <option name="set-global-setting" key="verifier_engprod" value="1" /> + <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="test-file-name" value="MediaProviderTests.apk" /> <option name="test-file-name" value="MediaProviderTestAppForPermissionActivity.apk" /> diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java index 197556261..02a1999af 100644 --- a/tests/src/com/android/providers/media/MediaProviderTest.java +++ b/tests/src/com/android/providers/media/MediaProviderTest.java @@ -16,6 +16,8 @@ package com.android.providers.media; +import static android.provider.MediaStore.getGeneration; + import static com.android.providers.media.scan.MediaScannerTest.stage; import static com.android.providers.media.util.FileUtils.extractDisplayName; import static com.android.providers.media.util.FileUtils.extractRelativePath; @@ -36,6 +38,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.Manifest; +import android.content.ContentInterface; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; @@ -53,6 +56,7 @@ import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Environment; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.MediaStore; @@ -88,6 +92,7 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.io.ByteArrayOutputStream; import java.io.File; @@ -304,7 +309,7 @@ public class MediaProviderTest { public void testMetadata() { assertNotNull(MediaStore.getVersion(sIsolatedContext, MediaStore.VOLUME_EXTERNAL_PRIMARY)); - assertNotNull(MediaStore.getGeneration(sIsolatedResolver, + assertNotNull(getGeneration(sIsolatedResolver, MediaStore.VOLUME_EXTERNAL_PRIMARY)); } @@ -1830,6 +1835,25 @@ public class MediaProviderTest { } } + @Test + public void testIllegalStateExceptionOnGetGenerationForNullValue() throws RemoteException { + ContentInterface contentInterface = Mockito.mock(MediaProvider.class); + Mockito.doReturn(null).when(contentInterface).call(Mockito.anyString(), + Mockito.anyString(), Mockito.any(String.class), Mockito.any(Bundle.class)); + String volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY; + + ContentResolver contentResolver = ContentResolver.wrap(contentInterface); + + try { + getGeneration(contentResolver, volumeName); + fail("Expected a IllegalStateException Exception"); + } catch (IllegalStateException e) { + assertEquals("Failed to get generation for volume '" + volumeName + + "'. The ContentResolver call returned null.", e.getMessage()); + } + + } + private void testRedactionForFileExtension(int resId, String extension) throws Exception { final File dir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); diff --git a/tests/src/com/android/providers/media/TestConfigStore.java b/tests/src/com/android/providers/media/TestConfigStore.java index c3c71ba34..9a173878a 100644 --- a/tests/src/com/android/providers/media/TestConfigStore.java +++ b/tests/src/com/android/providers/media/TestConfigStore.java @@ -37,6 +37,7 @@ public class TestConfigStore implements ConfigStore { private boolean mCloudMediaInPhotoPickerEnabled = false; private boolean mPrivateSpaceEnabled = false; + private boolean mIsModernPickerEnabled = false; private boolean mPickerChoiceManagedSelectionEnabled = false; private List<String> mAllowedCloudProviderPackages = Collections.emptyList(); private @Nullable String mDefaultCloudProviderPackage = null; @@ -48,6 +49,10 @@ public class TestConfigStore implements ConfigStore { notifyObservers(); } + public void setIsModernPickerEnabled(boolean isModernPickerEnabled) { + mIsModernPickerEnabled = isModernPickerEnabled; + } + /** * Enables private space flag for PhotoPicker in test config */ @@ -68,6 +73,11 @@ public class TestConfigStore implements ConfigStore { return mPrivateSpaceEnabled; } + @Override + public boolean isModernPickerEnabled() { + return mIsModernPickerEnabled; + } + public void enableCloudMediaFeature() { mCloudMediaInPhotoPickerEnabled = true; notifyObservers(); diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java index 8d1a16387..ba98432fc 100644 --- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java +++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java @@ -40,6 +40,7 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; +import android.os.CancellationSignal; import android.os.Handler; import android.os.Process; import android.os.storage.StorageManager; @@ -602,6 +603,81 @@ public class PickerSyncControllerTest { } @Test + public void testCancelledLocalSyncWork() { + // Init picker DB with one local media item and verify it. + addMedia(mLocalMediaGenerator, LOCAL_ONLY_1); + mController.syncAllMediaFromLocalProvider(/* cancellationSignal=*/ null); + try (Cursor cr = queryMedia()) { + assertWithMessage( + "Unexpected number of media on queryMedia() after syncing local media.") + .that(cr.getCount()).isEqualTo(1); + assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY); + } + + // Create a cancellation signal and mark it as cancelled + final CancellationSignal cancellationSignal = new CancellationSignal(); + cancellationSignal.cancel(); + + // Add another local media item in local media generator + addMedia(mLocalMediaGenerator, LOCAL_ONLY_2); + + // Check that running the sync with the cancellation does not add the new local item to the + // Picker DB and also does not clear the existing items in the Picker DB. + mController.syncAllMediaFromLocalProvider(cancellationSignal); + + try (Cursor cr = queryMedia()) { + assertWithMessage( + "Unexpected number of media on queryMedia() after syncing local media.") + .that(cr.getCount()).isEqualTo(1); + assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY); + } + } + + @Test + public void testCancelledCloudSyncWork() { + // Init picker DB with one cloud media item and verify it. + addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1); + setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY); + try (Cursor cr = queryMedia()) { + assertWithMessage( + "Unexpected number of media on queryMedia() after syncing all media.") + .that(cr.getCount()).isEqualTo(1); + assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY); + } + + // Create a cancellation signal and mark it as cancelled + final CancellationSignal cancellationSignal = new CancellationSignal(); + cancellationSignal.cancel(); + + // Add another cloud media item in cloud media generator + addMedia(mLocalMediaGenerator, CLOUD_ONLY_2); + + // Check that running the sync with the cancellation does not add the new cloud item to the + // Picker DB and also does not clear the existing items in the Picker DB. + mController.syncAllMediaFromCloudProvider(cancellationSignal); + + try (Cursor cr = queryMedia()) { + assertWithMessage( + "Unexpected number of media on queryMedia() after syncing cloud media.") + .that(cr.getCount()).isEqualTo(1); + assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY); + } + } + + @Test + public void testCancelledAlbumSyncWork() { + // Create a cancellation signal and mark it as cancelled + final CancellationSignal cancellationSignal = new CancellationSignal(); + cancellationSignal.cancel(); + + // Check that running the sync with the cancellation does not add the new local item to the + // Picker DB. + addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_1.first, LOCAL_ONLY_1.second, ALBUM_ID_1); + mController.syncAlbumMediaFromLocalProvider(ALBUM_ID_1, cancellationSignal); + assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, true); + } + + @Test public void testCloudResetSync() { setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY); diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java index 6d1d4de64..39d9f71d6 100644 --- a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java +++ b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java @@ -28,6 +28,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.providers.media.IsolatedContext; +import com.android.providers.media.MediaGrants; import org.junit.Before; import org.junit.Test; @@ -40,6 +41,7 @@ public class PickerDatabaseHelperTest { private static final String TEST_PICKER_DB = "test_picker"; static final String MEDIA_TABLE = "media"; static final String ALBUM_MEDIA_TABLE = "album_media"; + static final String GRANTS_TABLE = "media_grants"; private static final String KEY_LOCAL_ID = "local_id"; private static final String KEY_CLOUD_ID = "cloud_id"; @@ -155,6 +157,38 @@ public class PickerDatabaseHelperTest { } @Test + public void testGrantsColumns() { + String[] projection = new String[] { + MediaGrants.FILE_ID_COLUMN, + MediaGrants.OWNER_PACKAGE_NAME_COLUMN, + MediaGrants.PACKAGE_USER_ID_COLUMN + }; + + try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) { + SQLiteDatabase db = helper.getWritableDatabase(); + + int testInputId = 1234; + // All fields specified + ContentValues values = new ContentValues(); + values.put(MediaGrants.FILE_ID_COLUMN, testInputId); + values.put(MediaGrants.OWNER_PACKAGE_NAME_COLUMN, "abc"); + values.put(MediaGrants.PACKAGE_USER_ID_COLUMN, 123); + assertThat(db.insert(GRANTS_TABLE, null, values)) + .isNotEqualTo(-1); + + try (Cursor cr = db.query(GRANTS_TABLE, projection, null, + null, null, null, null)) { + assertThat(cr.getCount()).isEqualTo(1); + while (cr.moveToNext()) { + assertThat(cr.getInt(0)).isEqualTo(testInputId); + assertThat(cr.getString(1)).isEqualTo("abc"); + assertThat(cr.getInt(2)).isEqualTo(123); + } + } + } + } + + @Test public void testCheck_cloudOrLocal() throws Exception { try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) { SQLiteDatabase db = helper.getWritableDatabase(); diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java index 1283dd133..68438b176 100644 --- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java +++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java @@ -43,13 +43,17 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.MPE import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.SIZE_BYTES; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.TEST_PACKAGE_NAME; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.VIDEO_MIME_TYPES_QUERY; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.WEBM_VIDEO_MIME_TYPE; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddAlbumMediaOperation; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAllMediaCursor; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertClearGrantsOperation; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertCloudAlbumCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertCloudMediaCursor; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertGrantsCursor; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertInsertGrantsOperation; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertMediaStoreCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertRemoveMediaOperation; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertResetAlbumMediaOperation; @@ -60,7 +64,9 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.get import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getDeletedMediaCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCursor; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaGrantsCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.queryAlbumMedia; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.queryGrants; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.queryMediaAll; import static com.google.common.truth.Truth.assertWithMessage; @@ -72,6 +78,8 @@ import static org.mockito.MockitoAnnotations.initMocks; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.sqlite.SQLiteQueryBuilder; +import android.os.UserHandle; import android.provider.CloudMediaProviderContract.MediaColumns; import android.provider.Column; import android.provider.ExportedSince; @@ -80,6 +88,7 @@ import android.provider.MediaStore.PickerMediaColumns; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.providers.media.MediaGrants; import com.android.providers.media.PickerUriResolver; import com.android.providers.media.ProjectionHelper; import com.android.providers.media.photopicker.sync.PickerSyncLockManager; @@ -342,6 +351,55 @@ public class PickerDbFacadeTest { } @Test + public void testAddAndClearGrants() { + Cursor cursor1 = getMediaGrantsCursor(LOCAL_ID); + + // insert a grants. + assertInsertGrantsOperation(mFacade, cursor1, 1); + // verify the grants is present in the database. + try (Cursor cr = queryGrants(mFacade)) { + assertWithMessage( + "Unexpected number of grants ") + .that(cr.getCount()).isEqualTo(1); + cr.moveToFirst(); + assertGrantsCursor(cr, LOCAL_ID); + } + + // clear all grants. + assertClearGrantsOperation(mFacade, 1, new String[]{TEST_PACKAGE_NAME}, + UserHandle.myUserId()); + // verify that the grants have been cleared. + try (Cursor cr = queryGrants(mFacade)) { + assertWithMessage( + "Unexpected number of grants ") + .that(cr.getCount()).isEqualTo(0); + } + } + + @Test + public void testAddWhereClausesForMediaGrantsTable() { + // set up + SQLiteQueryBuilder sqb = new SQLiteQueryBuilder(); + int testUserId = 1; + String[] testPackageNames = {"com.test.example"}; + + // adding where clause + PickerDbFacade.addWhereClausesForMediaGrantsTable(sqb, testUserId, testPackageNames); + + // verify where clauses have been added to the query. + String resultQuery = sqb.buildQuery(null, null, null, null, null, null); + + assertWithMessage("Query should contain clause for userId.").that( + resultQuery.contains(String.format("%s = %d", MediaGrants.PACKAGE_USER_ID_COLUMN, + testUserId))).isEqualTo(true); + assertWithMessage("Query should contain clause for packageNames.") + .that(resultQuery.contains(String.format("%s IN (\"%s\")", + MediaGrants.OWNER_PACKAGE_NAME_COLUMN, + testPackageNames[0]))).isEqualTo( + true); + } + + @Test public void testAddCloudAlbumMediaWhileCloudSyncIsRunning() { diff --git a/tests/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorkerTest.java new file mode 100644 index 000000000..f16bd2331 --- /dev/null +++ b/tests/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorkerTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.providers.media.photopicker.sync; + +import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_CHANNEL_ID; +import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_ID; +import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.buildGrantsTestWorker; +import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getGrantsSyncInputData; +import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.content.Context; +import android.os.Build; + +import androidx.test.filters.SdkSuppress; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.work.ForegroundInfo; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; + +import com.android.providers.media.photopicker.PickerSyncController; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.util.concurrent.ExecutionException; + + +/** + * Tests to verify sync of grants used in photopicker when invoked with + * MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP. + * + * This action is available SDK T and above hence this test has a minSdkVersion to respect this + * restriction. + */ +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU) +public class ImmediateGrantsSyncWorkerTest { + @Mock + private PickerSyncController mMockPickerSyncController; + + @Mock + private SyncTracker mMockGrantsSyncTracker; + + private Context mContext; + + @Before + public void setup() { + initMocks(this); + + // Inject mock tracker + SyncTrackerRegistry.setGrantsSyncTracker(mMockGrantsSyncTracker); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + initializeTestWorkManager(mContext); + } + + @After + public void teardown() { + // Reset mock trackers + SyncTrackerRegistry.setLocalSyncTracker(new SyncTracker()); + SyncTrackerRegistry.setCloudSyncTracker(new SyncTracker()); + SyncTrackerRegistry.setGrantsSyncTracker(new SyncTracker()); + } + + @Test + public void testGrantsImmediateSync() throws ExecutionException, InterruptedException { + // Setup + PickerSyncController.setInstance(mMockPickerSyncController); + final OneTimeWorkRequest request = + new OneTimeWorkRequest.Builder(ImmediateGrantsSyncWorker.class) + .setInputData(getGrantsSyncInputData()) + .build(); + + // Test run + final WorkManager workManager = WorkManager.getInstance(mContext); + workManager.enqueue(request).getResult().get(); + + // Verify + final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get(); + assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED); + + verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1)) + .executeGrantsSync(true, 1, null); + + verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 0)) + .createSyncFuture(any()); + verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 1)) + .markSyncCompleted(any()); + } + + @Test + public void testLocalAndCloudImmediateSyncFailure() + throws ExecutionException, InterruptedException { + // Setup + PickerSyncController.setInstance(null); + final OneTimeWorkRequest request = + new OneTimeWorkRequest.Builder(ImmediateGrantsSyncWorker.class) + .setInputData(getGrantsSyncInputData()) + .build(); + + // Test run + final WorkManager workManager = WorkManager.getInstance(mContext); + workManager.enqueue(request).getResult().get(); + + // Verify + final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get(); + assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED); + + verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0)) + .executeGrantsSync(true, 1, null); + verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0)) + .executeGrantsSync(true, 1, null); + + verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 0)) + .createSyncFuture(any()); + verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 1)) + .markSyncCompleted(any()); + } + + @Test + public void testImmediateSyncWorkerOnStopped() { + // Setup + final ImmediateGrantsSyncWorker immediateGrantsSyncWorker = + buildGrantsTestWorker(mContext, ImmediateGrantsSyncWorker.class); + + // Test onStopped + immediateGrantsSyncWorker.onStopped(); + + verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 0)) + .createSyncFuture(any()); + verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 1)) + .markSyncCompleted(any()); + } + + @Test + public void testGetForegroundInfo() { + final ForegroundInfo foregroundInfo = + buildGrantsTestWorker(mContext, ImmediateGrantsSyncWorker.class) + .getForegroundInfo(); + + assertThat(foregroundInfo.getNotificationId()).isEqualTo(NOTIFICATION_ID); + assertThat(foregroundInfo.getNotification().getChannelId()) + .isEqualTo(NOTIFICATION_CHANNEL_ID); + } +} diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java index 433c35fdc..857d14359 100644 --- a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java +++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java @@ -16,6 +16,7 @@ package com.android.providers.media.photopicker.sync; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY; @@ -34,6 +35,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import androidx.work.ExistingPeriodicWorkPolicy; @@ -47,6 +49,7 @@ import androidx.work.WorkRequest; import com.android.providers.media.TestConfigStore; import com.android.providers.media.photopicker.PickerSyncController; +import com.android.providers.media.photopicker.data.PickerSyncRequestExtras; import com.google.common.util.concurrent.ListenableFuture; @@ -254,15 +257,55 @@ public class PickerSyncManagerTest { } @Test + public void testImmediateGrantsSync() { + setupPickerSyncManager(/* schedulePeriodicSyncs */ false); + + mConfigStore.setIsModernPickerEnabled(true); + reset(mMockWorkManager); + mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null, + /* albumAuthority */ null, /* initLocalDataOnly */ true, + /* callingPackageUid */ 0, /* shouldSyncGrants */ true, null)); + verify(mMockWorkManager, times(2)) + .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture()); + + final List<OneTimeWorkRequest> workRequestList = + mOneTimeWorkRequestArgumentCaptor.getAllValues(); + assertThat(workRequestList.size()).isEqualTo(2); + + // work request 0 is for grants sync. + WorkRequest workRequest = workRequestList.get(0); + assertThat(workRequest.getWorkSpec().workerClassName) + .isEqualTo(ImmediateGrantsSyncWorker.class.getName()); + assertThat(workRequest.getWorkSpec().expedited).isTrue(); + assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse(); + assertThat(workRequest.getWorkSpec().id).isNotNull(); + assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse(); + assertThat(workRequest.getWorkSpec().input + .getInt(Intent.EXTRA_UID, -1)) + .isEqualTo(0); + assertThat(workRequest.getWorkSpec().input + .getBoolean(SHOULD_SYNC_GRANTS, false)) + .isEqualTo(true); + } + + @Test public void testImmediateLocalSync() { + mConfigStore.setIsModernPickerEnabled(true); setupPickerSyncManager(/* schedulePeriodicSyncs */ false); reset(mMockWorkManager); - mPickerSyncManager.syncMediaImmediately(true); - verify(mMockWorkManager, times(1)) + mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null, + /* albumAuthority */ null, /* initLocalDataOnly */ true, + /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null)); + verify(mMockWorkManager, times(2)) .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture()); - final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue(); + final List<OneTimeWorkRequest> workRequestList = + mOneTimeWorkRequestArgumentCaptor.getAllValues(); + assertThat(workRequestList.size()).isEqualTo(2); + + // work request 0 is for grants sync, so use request number 1 for local syncs. + WorkRequest workRequest = workRequestList.get(1); assertThat(workRequest.getWorkSpec().workerClassName) .isEqualTo(ImmediateSyncWorker.class.getName()); assertThat(workRequest.getWorkSpec().expedited).isTrue(); @@ -276,19 +319,24 @@ public class PickerSyncManagerTest { @Test public void testImmediateCloudSync() { + mConfigStore.setIsModernPickerEnabled(true); setupPickerSyncManager(/* schedulePeriodicSyncs */ false); reset(mMockWorkManager); - mPickerSyncManager.syncMediaImmediately(false); - verify(mMockWorkManager, times(2)) + mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null, + /* albumAuthority */ null, /* initLocalDataOnly */ false, + /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null)); + verify(mMockWorkManager, times(3)) .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture()); final List<OneTimeWorkRequest> workRequestList = mOneTimeWorkRequestArgumentCaptor.getAllValues(); - assertThat(workRequestList.size()).isEqualTo(2); + assertThat(workRequestList.size()).isEqualTo(3); + + // work request 0 is for grants sync, 1 for local syncs and 2 for cloud syncs. - WorkRequest localWorkRequest = workRequestList.get(0); + WorkRequest localWorkRequest = workRequestList.get(1); assertThat(localWorkRequest.getWorkSpec().workerClassName) .isEqualTo(ImmediateSyncWorker.class.getName()); assertThat(localWorkRequest.getWorkSpec().expedited).isTrue(); @@ -299,7 +347,7 @@ public class PickerSyncManagerTest { .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1)) .isEqualTo(SYNC_LOCAL_ONLY); - WorkRequest cloudWorkRequest = workRequestList.get(1); + WorkRequest cloudWorkRequest = workRequestList.get(2); assertThat(cloudWorkRequest.getWorkSpec().workerClassName) .isEqualTo(ImmediateSyncWorker.class.getName()); assertThat(cloudWorkRequest.getWorkSpec().expedited).isTrue(); diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java index ed1d117b7..1eac73a78 100644 --- a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java +++ b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java @@ -64,6 +64,8 @@ public class SyncTrackerTests { @Test public void getSyncTrackerFromRegistry() { + assertThat(SyncTrackerRegistry.getGrantsSyncTracker()) + .isNotNull(); assertThat(SyncTrackerRegistry.getSyncTracker(/* isLocal */ true)) .isEqualTo(SyncTrackerRegistry.getLocalSyncTracker()); assertThat(SyncTrackerRegistry.getSyncTracker(/* isLocal */ false)) diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java index 7eb9326a5..52702ac7f 100644 --- a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java +++ b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java @@ -16,6 +16,7 @@ package com.android.providers.media.photopicker.sync; +import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD; import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY; @@ -26,6 +27,7 @@ import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYN import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE; import android.content.Context; +import android.content.Intent; import android.util.Log; import androidx.annotation.NonNull; @@ -63,6 +65,14 @@ public class SyncWorkerTestUtils { } @NonNull + public static Data getGrantsSyncInputData() { + return new Data(Map.of( + Intent.EXTRA_UID, /* test uid */ 1, + SHOULD_SYNC_GRANTS, true + )); + } + + @NonNull public static Data getCloudSyncInputData() { return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY)); } @@ -104,4 +114,11 @@ public class SyncWorkerTestUtils { .setInputData(getLocalAndCloudSyncInputData()) .build(); } + + static <W extends Worker> W buildGrantsTestWorker(@NonNull Context context, + @NonNull Class<W> workerClass) { + return TestWorkerBuilder.from(context, workerClass) + .setInputData(getGrantsSyncInputData()) + .build(); + } } diff --git a/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java b/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java index 989e333c7..df29f9d40 100644 --- a/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java +++ b/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java @@ -22,9 +22,11 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.database.Cursor; import android.database.MatrixCursor; +import android.os.UserHandle; import android.provider.CloudMediaProviderContract; import android.provider.MediaStore; +import com.android.providers.media.MediaGrants; import com.android.providers.media.PickerUriResolver; import com.android.providers.media.photopicker.data.PickerDbFacade; @@ -59,6 +61,7 @@ public class PickerDbTestUtils { public static final String[] IMAGE_MIME_TYPES_QUERY = new String[]{"image/jpeg"}; public static final int STANDARD_MIME_TYPE_EXTENSION = CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION_GIF; + public static final String TEST_PACKAGE_NAME = "com.test.package"; public static final String LOCAL_PROVIDER = "com.local.provider"; public static final String CLOUD_PROVIDER = "com.cloud.provider"; @@ -76,6 +79,11 @@ public class PickerDbTestUtils { authority); } + public static Cursor queryGrants(PickerDbFacade mFacade) { + return mFacade.getDatabase().query( + "media_grants", null, null, null, null, null, null); + } + public static void assertAddMediaOperation(PickerDbFacade mFacade, String authority, Cursor cursor, int writeCount) { try (PickerDbFacade.DbWriteOperation operation = @@ -94,6 +102,24 @@ public class PickerDbTestUtils { } } + public static void assertInsertGrantsOperation(PickerDbFacade mFacade, + Cursor cursor, int writeCount) { + try (PickerDbFacade.DbWriteOperation operation = + mFacade.beginInsertGrantsOperation()) { + assertWriteOperation(operation, cursor, writeCount); + operation.setSuccess(); + } + } + + public static void assertClearGrantsOperation(PickerDbFacade mFacade, + int writeCount, String[] packageNames, int userId) { + try (PickerDbFacade.DbWriteOperation operation = + mFacade.beginClearGrantsOperation(packageNames, userId)) { + assertWriteOperation(operation, null, writeCount); + operation.setSuccess(); + } + } + public static void assertRemoveMediaOperation(PickerDbFacade mFacade, String authority, Cursor cursor, int writeCount) { try (PickerDbFacade.DbWriteOperation operation = @@ -236,6 +262,32 @@ public class PickerDbTestUtils { return c; } + public static Cursor getMediaGrantsCursor( + String id) { + return getMediaGrantsCursor(id, TEST_PACKAGE_NAME, UserHandle.myUserId()); + } + + public static Cursor getMediaGrantsCursor( + String id, String packageName, int userId) { + String[] projectionKey = + new String[]{ + MediaGrants.FILE_ID_COLUMN, + MediaGrants.OWNER_PACKAGE_NAME_COLUMN, + MediaGrants.PACKAGE_USER_ID_COLUMN + }; + + String[] projectionValue = + new String[]{ + id, + packageName, + String.valueOf(userId) + }; + + MatrixCursor c = new MatrixCursor(projectionKey); + c.addRow(projectionValue); + return c; + } + public static Cursor getLocalMediaCursor(String localId, long dateTakenMs) { return getMediaCursor(localId, dateTakenMs, GENERATION_MODIFIED, toMediaStoreUri(localId), SIZE_BYTES, MP4_VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, @@ -357,6 +409,19 @@ public class PickerDbTestUtils { .isEqualTo(DURATION_MS); } + public static void assertGrantsCursor(Cursor cursor, String fileId) { + assertWithMessage("Unexpected value of grants.file_id") + .that(cursor.getString(cursor.getColumnIndexOrThrow( + MediaGrants.FILE_ID_COLUMN))).isEqualTo(fileId); + assertWithMessage("Unexpected value of grants.owner_package_name") + .that(cursor.getString(cursor.getColumnIndexOrThrow( + MediaGrants.OWNER_PACKAGE_NAME_COLUMN))).isEqualTo(TEST_PACKAGE_NAME); + assertWithMessage("Unexpected value of grants.package_user_id") + .that(cursor.getInt(cursor.getColumnIndexOrThrow( + MediaGrants.PACKAGE_USER_ID_COLUMN))).isEqualTo( + UserHandle.myUserId()); + } + public static void assertCloudMediaCursor( Cursor cursor, String id, long dateTakenMs, String mimeType) { assertCloudMediaCursor(cursor, id, mimeType); diff --git a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java index f637858d5..fe3268148 100644 --- a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java +++ b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java @@ -25,6 +25,8 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLO import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.DATE_TAKEN_MS; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GENERATION_MODIFIED; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2; @@ -32,17 +34,19 @@ import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOC import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_4; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_PROVIDER; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.MP4_VIDEO_MIME_TYPE; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION; -import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.TEST_PACKAGE_NAME; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddAlbumMediaOperation; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertInsertGrantsOperation; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getAlbumCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getAlbumMediaCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor; import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCursor; -import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE; -import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE; -import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE; +import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaGrantsCursor; +import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.COLUMN_GRANTS_COUNT; import static com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper.EMPTY_MEDIA_ID; import static com.google.common.truth.Truth.assertWithMessage; @@ -64,6 +68,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Process; +import android.os.UserHandle; import android.provider.CloudMediaProviderContract; import android.provider.MediaStore; import android.test.mock.MockContentProvider; @@ -417,6 +422,181 @@ public class PickerDataLayerV2Test { } @Test + public void testQueryLocalMediaWithGrants() { + Cursor cursorForMediaWithoutGrants = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS + 1, + GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1, + MP4_VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false); + Cursor cursorForMediaWithGrants = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS, + GENERATION_MODIFIED, + /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE, + STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false); + + assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithoutGrants, + /* writeCount */1); + assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrants, + /* writeCount */1); + int testUid = 123; + doReturn(mMockPackageManager) + .when(mMockContext).getPackageManager(); + String[] packageNames = new String[]{TEST_PACKAGE_NAME}; + doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(testUid); + // insert a grant for the second item inserted in media. + assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2), /* writeCount */1); + + doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any()); + + try (Cursor cr = PickerDataLayerV2.queryMedia( + mMockContext, getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 3, + new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)), + new ArrayList<>(Arrays.asList("video/*")), + MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, + testUid))) { + assertWithMessage( + "Unexpected number of rows in media query result") + .that(cr.getCount()).isEqualTo(2); + + // verify item with isPreGranted as false. + cr.moveToFirst(); + assertMediaCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER, DATE_TAKEN_MS + 1, + MP4_VIDEO_MIME_TYPE, MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, + /* isPreGranted */ false); + + // verify item with isPreGranted as true. + cr.moveToNext(); + assertMediaCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE, + MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, + /* isPreGranted */ true); + } + } + + @Test + public void testQueryLocalMediaForPreview() { + Cursor cursorForMediaWithoutGrants = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS + 1, + GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1, + MP4_VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false); + Cursor cursorForMediaWithGrants = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS, + GENERATION_MODIFIED, + /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE, + STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false); + Cursor cursorForMediaWithGrantsButDeSelected = getMediaCursor(LOCAL_ID_3, DATE_TAKEN_MS, + GENERATION_MODIFIED, + /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE, + STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false); + + assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithoutGrants, + /* writeCount */1); + assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrants, + /* writeCount */1); + assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrantsButDeSelected, + /* writeCount */1); + + int testUid = 123; + doReturn(mMockPackageManager) + .when(mMockContext).getPackageManager(); + String[] packageNames = new String[]{TEST_PACKAGE_NAME}; + doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(testUid); + // insert a grant for the second item inserted in media. + assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2), /* writeCount */1); + // insert a grant for the third item inserted in media. + assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_3), /* writeCount */1); + + doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any()); + + Bundle extras = getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 3, + new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)), + new ArrayList<>(Arrays.asList("video/*")), + MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, + testUid); + + extras.putBoolean("is_preview_session", true); + extras.putBoolean("is_first_page", true); + extras.putStringArrayList("current_de_selection", new ArrayList<>(List.of(LOCAL_ID_3))); + extras.putStringArrayList("current_selection", new ArrayList<>(List.of(LOCAL_ID_1))); + + // Expected result: + // 1. one item with LOCAL_ID_1 that has been added as current selection. + // 2. one item with LOCAL_ID_2 which is a pre-granted item. + // 3. item with LOCAL_ID_3 should not be included in the cursor because it is de-selected. + + try (Cursor cr = PickerDataLayerV2.queryPreviewMedia( + mMockContext, extras)) { + assertWithMessage( + "Unexpected number of rows in media query result") + .that(cr.getCount()).isEqualTo(2); + + // verify item with isPreGranted as false. + cr.moveToFirst(); + assertMediaCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER, DATE_TAKEN_MS + 1, + MP4_VIDEO_MIME_TYPE, MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, + /* isPreGranted */ false); + + // verify item with isPreGranted as true. + cr.moveToNext(); + assertMediaCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE, + MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP, + /* isPreGranted */ true); + } + } + + @Test + public void testFetchMediaGrantsCount() { + int testUid = 123; + int userId = PickerSyncController.uidToUserId(testUid); + doReturn(mMockPackageManager) + .when(mMockContext).getPackageManager(); + String[] packageNames = new String[]{TEST_PACKAGE_NAME}; + doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(testUid); + + + // insert 2 grants corresponding to testUid. + assertInsertGrantsOperation(mFacade, + getMediaGrantsCursor(LOCAL_ID_1, TEST_PACKAGE_NAME, userId), /* writeCount */1); + assertInsertGrantsOperation(mFacade, + getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME, userId), /* writeCount */1); + + // insert grants with different packageName or userIds. + String TEST_PACKAGE_NAME_2 = "package.name.two"; + int TEST_USER_ID_2 = 10; + + // same id but different packageName + assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME_2, + UserHandle.myUserId()), /* writeCount */1); + // same id but different userId + assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME, + TEST_USER_ID_2), /* writeCount */1); + // both packageName and userId different + assertInsertGrantsOperation(mFacade, + getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME_2, TEST_USER_ID_2), 1); + // every aspect different + assertInsertGrantsOperation(mFacade, + getMediaGrantsCursor(LOCAL_ID_3, TEST_PACKAGE_NAME_2, TEST_USER_ID_2), 1); + + Bundle input = new Bundle(); + input.putInt(Intent.EXTRA_UID, testUid); + + try (Cursor cr = PickerDataLayerV2.fetchMediaGrantsCount( + mMockContext, input)) { + + // cursor should only contain 1 row that represents the count. + assertWithMessage( + "Unexpected number of rows in media query result") + .that(cr.getCount()).isEqualTo(1); + + // verify that the cursor contains the count. Ensure that only 2 grants are considered + // even when there were total 4 grants inserted. This ensures that the grants were + // filtered properly based on the packageName and UserId. + cr.moveToFirst(); + int columnIndexForCount = cr.getColumnIndex(COLUMN_GRANTS_COUNT); + assertWithMessage( + "column index should not be -1.") + .that(columnIndexForCount).isNotEqualTo(-1); + assertWithMessage( + "Unexpected number grants count, expected to be 2.") + .that(cr.getInt(columnIndexForCount)).isEqualTo(2); + } + } + + @Test public void queryMediaWithCloudQueryEnabled() { Cursor cursor1 = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE, @@ -756,7 +936,7 @@ public class PickerDataLayerV2Test { @Test - public void testMergedAlbumsWithCloudQueriesDisabled() { + public void testDefaultAlbumsWithCloudQueriesDisabled() { Cursor cursor1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1, JPEG_IMAGE_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false); @@ -781,11 +961,23 @@ public class PickerDataLayerV2Test { try (Cursor cr = PickerDataLayerV2.queryAlbums( mMockContext, getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 10, new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { - // Verify that merged albums are not displayed by default when cloud albums are - // disabled. assertWithMessage( "Unexpected number of rows in media query result") - .that(cr).isNull(); + .that(cr.getCount()).isEqualTo(2); + + // Favorites album will be displayed by default + cr.moveToFirst(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + + // Camera album will be displayed by default + cr.moveToNext(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); } } @@ -1073,10 +1265,24 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(1); + .that(cr.getCount()).isEqualTo(3); + // Favorites album will be displayed by default cr.moveToFirst(); assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + + // Camera album will be displayed by default + cr.moveToNext(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + + cr.moveToNext(); + assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS, LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2); } @@ -1110,7 +1316,7 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(2); + .that(cr.getCount()).isEqualTo(3); cr.moveToFirst(); // Favorites albums will be displayed by default @@ -1119,6 +1325,13 @@ public class PickerDataLayerV2Test { LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ EMPTY_MEDIA_ID, MediaSource.LOCAL); + // Camera album will be displayed by default + cr.moveToNext(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + cr.moveToNext(); assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS, @@ -1149,7 +1362,7 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(2); + .that(cr.getCount()).isEqualTo(3); cr.moveToFirst(); // Favorites albums will be displayed by default @@ -1158,6 +1371,13 @@ public class PickerDataLayerV2Test { LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ EMPTY_MEDIA_ID); + // Camera album will be displayed by default + cr.moveToNext(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + cr.moveToNext(); assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS, @@ -1193,13 +1413,20 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(1); + .that(cr.getCount()).isEqualTo(2); cr.moveToFirst(); // Favorites albums will be displayed by default assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES, LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2); + + // Camera album will be displayed by default + cr.moveToNext(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); } } @@ -1231,7 +1458,7 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(2); + .that(cr.getCount()).isEqualTo(3); cr.moveToFirst(); assertAlbumCursor(cr, @@ -1239,6 +1466,13 @@ public class PickerDataLayerV2Test { LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ CLOUD_ID_1, MediaSource.REMOTE); + // Camera album will be displayed by default + cr.moveToNext(); + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + cr.moveToNext(); // Videos album will be displayed by default assertAlbumCursor(cr, @@ -1272,7 +1506,7 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(2); + .that(cr.getCount()).isEqualTo(3); cr.moveToFirst(); assertAlbumCursor(cr, @@ -1280,6 +1514,13 @@ public class PickerDataLayerV2Test { LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1); cr.moveToNext(); + // Camera album will be displayed by default + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + + cr.moveToNext(); // Videos album will be displayed by default assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS, @@ -1308,9 +1549,17 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(2); + .that(cr.getCount()).isEqualTo(3); cr.moveToFirst(); + // Favorites album will be displayed by default + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + + cr.moveToNext(); + // Camera album will be displayed by default assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2); @@ -1341,7 +1590,7 @@ public class PickerDataLayerV2Test { new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) { assertWithMessage( "Unexpected number of rows in media query result") - .that(cr.getCount()).isEqualTo(3); + .that(cr.getCount()).isEqualTo(4); cr.moveToFirst(); // Favorites albums will be displayed by default @@ -1351,6 +1600,13 @@ public class PickerDataLayerV2Test { /* coverMediaId */ EMPTY_MEDIA_ID); cr.moveToNext(); + // Camera album will be displayed by default + assertAlbumCursor(cr, + /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA, + LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, + /* coverMediaId */ EMPTY_MEDIA_ID); + + cr.moveToNext(); assertAlbumCursor(cr, /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS, LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1); @@ -1660,11 +1916,16 @@ public class PickerDataLayerV2Test { private static void assertMediaCursor(Cursor cursor, String id, String authority, Long dateTaken, String mimeType) { assertMediaCursor(cursor, id, authority, dateTaken, mimeType, - MediaStore.ACTION_PICK_IMAGES); + MediaStore.ACTION_PICK_IMAGES, /* isPreGranted */ false); } - private static void assertMediaCursor(Cursor cursor, String id, String authority, Long dateTaken, String mimeType, String intent) { + assertMediaCursor(cursor, id, authority, dateTaken, mimeType, + intent, /* isPreGranted */ false); + } + + private static void assertMediaCursor(Cursor cursor, String id, String authority, + Long dateTaken, String mimeType, String intent, boolean isPreGranted) { assertWithMessage("Unexpected value of id in the media cursor.") .that(cursor.getString(cursor.getColumnIndexOrThrow( PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName()))) @@ -1691,6 +1952,11 @@ public class PickerDataLayerV2Test { .that(cursor.getString(cursor.getColumnIndexOrThrow( PickerSQLConstants.MediaResponse.WRAPPED_URI.getProjectedName()))) .isEqualTo(expectedUri.toString()); + + assertWithMessage("Unexpected value of grants in the media cursor.") + .that(cursor.getInt(cursor.getColumnIndexOrThrow( + PickerSQLConstants.MediaResponse.IS_PRE_GRANTED.getProjectedName()))) + .isEqualTo(isPreGranted ? 1 : 0); } private static void assertAlbumCursor(Cursor cursor, String albumId, String authority, @@ -1748,30 +2014,46 @@ public class PickerDataLayerV2Test { } private Bundle getMediaQueryExtras(Long pickerId, Long dateTakenMillis, int pageSize, - ArrayList<String> providers) { + List<String> providers) { Bundle extras = new Bundle(); extras.putLong("picker_id", pickerId); extras.putLong("date_taken_millis", dateTakenMillis); extras.putInt("page_size", pageSize); - extras.putStringArrayList("providers", providers); + extras.putStringArrayList("providers", new ArrayList<>(providers)); extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES); return extras; } private Bundle getMediaQueryExtras(Long pickerId, Long dateTakenMillis, int pageSize, - ArrayList<String> providers, ArrayList<String> mimeTypes) { + List<String> providers, List<String> mimeTypes) { Bundle extras = getMediaQueryExtras( pickerId, dateTakenMillis, pageSize, providers ); - extras.putStringArrayList("mime_types", mimeTypes); + extras.putStringArrayList("mime_types", new ArrayList<>(mimeTypes)); + return extras; + } + + private Bundle getMediaQueryExtras( + Long pickerId, Long dateTakenMillis, int pageSize, + List<String> providers, List<String> mimeTypes, + String intentAction, int callingUid) { + Bundle extras = getMediaQueryExtras( + pickerId, + dateTakenMillis, + pageSize, + providers, + mimeTypes + ); + extras.putInt(Intent.EXTRA_UID, callingUid); + extras.putString("intent_action", intentAction); return extras; } private Bundle getAlbumMediaQueryExtras(Long pickerId, Long dateTakenMillis, int pageSize, - ArrayList<String> providers, String albumAuthority) { + List<String> providers, String albumAuthority) { Bundle extras = getMediaQueryExtras( pickerId, dateTakenMillis, diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java index 8805881c7..0a2a3c795 100644 --- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java +++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java @@ -53,12 +53,14 @@ public class IsoInterfaceTest { final File file = stageMp4File(R.raw.test_video); final IsoInterface mp4 = IsoInterface.fromFile(file); - final long[] ranges = mp4.getBoxRanges(0x746b6864); // tkhd - assertThat(ranges.length).isEqualTo(4); - assertThat(ranges[0]).isEqualTo(105534 + 8); - assertThat(ranges[1]).isEqualTo(105534 + 92); - assertThat(ranges[2]).isEqualTo(118275 + 8); - assertThat(ranges[3]).isEqualTo(118275 + 92); + final long[] ranges = mp4.getBoxRanges(0x68646c72); // hdlr + assertThat(ranges.length).isEqualTo(6); + assertThat(ranges[0]).isEqualTo(105702 + 8); + assertThat(ranges[1]).isEqualTo(105702 + 45); + assertThat(ranges[2]).isEqualTo(118407 + 8); + assertThat(ranges[3]).isEqualTo(118407 + 45); + assertThat(ranges[4]).isEqualTo(135507 + 8); + assertThat(ranges[5]).isEqualTo(135507 + 33); } @Test diff --git a/tools/photopickerV2/Android.bp b/tools/photopickerV2/Android.bp index 7b6d84afe..a8e6ac5aa 100644 --- a/tools/photopickerV2/Android.bp +++ b/tools/photopickerV2/Android.bp @@ -14,6 +14,7 @@ android_app { "androidx.activity_activity-compose", "androidx.compose.foundation_foundation", "androidx.compose.material3_material3", + "androidx.compose.runtime_runtime-livedata", "androidx.compose.runtime_runtime", "androidx.compose.ui_ui", "androidx.core_core-ktx", @@ -40,6 +41,6 @@ android_app { ], srcs: ["src/**/*.kt"], sdk_version: "module_current", - target_sdk_version: "30", - min_sdk_version: "30", + target_sdk_version: "34", + min_sdk_version: "34", } diff --git a/tools/photopickerV2/AndroidManifest.xml b/tools/photopickerV2/AndroidManifest.xml index f176fa716..f861aa5ed 100644 --- a/tools/photopickerV2/AndroidManifest.xml +++ b/tools/photopickerV2/AndroidManifest.xml @@ -2,6 +2,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.android.providers.media.tools.photopickerv2"> + <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> + <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> + <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> <application android:allowBackup="false" @@ -10,7 +13,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/Theme.PhotoPickerToolV2" - tools:targetApi="30"> + tools:targetApi="34"> <activity android:name=".MainActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" diff --git a/tools/photopickerV2/res/values-af/strings.xml b/tools/photopickerV2/res/values-af/strings.xml index 298703297..ab5655d0c 100644 --- a/tools/photopickerV2/res/values-af/strings.xml +++ b/tools/photopickerV2/res/values-af/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"Fotokieser V2"</string> <string name="pick_images" msgid="5326258471545526911">"Kies prente"</string> <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Wys prente in volgorde"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Laat veelvuldige keuse toe"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimum aantal media-items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Voer ’n geldige telling groter as een in"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Kies media"</string> <string name="working_on_it" msgid="1373762827081252341">"Werk tans daaraan"</string> </resources> diff --git a/tools/photopickerV2/res/values-am/strings.xml b/tools/photopickerV2/res/values-am/strings.xml index 5b7f354aa..bb205eae1 100644 --- a/tools/photopickerV2/res/values-am/strings.xml +++ b/tools/photopickerV2/res/values-am/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"ፎቶ መራጭV2"</string> <string name="pick_images" msgid="5326258471545526911">"ምስሎችን ይምረጡ"</string> <string name="action_get_content" msgid="4319210475508093083">"እርምጃ ይዘትን አግኝ"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ምስሎችን በቅደም ተከተል አሳይ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"በርካታ ምርጫን ይፍቀዱ"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"ከፍተኛ የሚዲያ ንጥሎች ቁጥር"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ከአንድ የሚበልጥ ትክክለኛ ቁጥር ያስገቡ"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"ሚዲያ ይምረጡ"</string> <string name="working_on_it" msgid="1373762827081252341">"በእሱ ላይ እየሰራን ነው"</string> </resources> diff --git a/tools/photopickerV2/res/values-ar/strings.xml b/tools/photopickerV2/res/values-ar/strings.xml index 0cabcc8c9..f885a8666 100644 --- a/tools/photopickerV2/res/values-ar/strings.xml +++ b/tools/photopickerV2/res/values-ar/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"اختيار الصور"</string> <string name="action_get_content" msgid="4319210475508093083">"إجراء الحصول على محتوى"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"عرض الصور بالترتيب"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"السماع بتحديد خيارات متعددة"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"أقصى عدد من ملفات الوسائط"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"أدخِل عددًا صالحًا أكبر من واحد"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"اختيار ملفات الوسائط"</string> <string name="working_on_it" msgid="1373762827081252341">"تجري الآن المعالجة"</string> </resources> diff --git a/tools/photopickerV2/res/values-as/strings.xml b/tools/photopickerV2/res/values-as/strings.xml index abd12f516..5b21ca845 100644 --- a/tools/photopickerV2/res/values-as/strings.xml +++ b/tools/photopickerV2/res/values-as/strings.xml @@ -8,10 +8,18 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"প্ৰতিচ্ছবিসমূহ বাছনি কৰক"</string> <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string> + <string name="open_document" msgid="8593796561386540777">"নথি খোলক"</string> + <string name="open_document_tree" msgid="8979404185180480396">"নথি ট্ৰী খোলক"</string> <string name="display_images_in_order" msgid="7880116254468536174">"প্ৰতিচ্ছবিসমূহ ক্ৰম অনুসৰি প্ৰদৰ্শন কৰক"</string> + <string name="show_images_only" msgid="6365019348435132030">"কেৱল প্ৰতিচ্ছবি দেখুৱাওক"</string> + <string name="show_videos_only" msgid="7302756380142587762">"কেৱল ভিডিঅ’ দেখুৱাওক"</string> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <string name="select_launch_tab" msgid="1219436289162294907">"লঞ্চ কৰা টেব বাছনি কৰক"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"একাধিক বাছনি কৰাৰ অনুমতি দিয়ক"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"কাষ্টম MIMEৰ ধৰণৰ অনুমতি দিয়ক"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"মিডিয়া বস্তুৰ সৰ্বাধিক সংখ্যা"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"একতকৈ ডাঙৰ এটা বৈধ সংখ্যা দিয়ক"</string> + <string name="enter_valid_number" msgid="650407643348891234">"একতকৈ ডাঙৰ এটা মান্য সংখ্যা দিয়ক"</string> <string name="pick_media" msgid="5269447618857205416">"মিডিয়া বাছনি কৰক"</string> <string name="working_on_it" msgid="1373762827081252341">"এইটোত কাম কৰি আছোঁ"</string> </resources> diff --git a/tools/photopickerV2/res/values-az/strings.xml b/tools/photopickerV2/res/values-az/strings.xml index 1f497b565..899f5becc 100644 --- a/tools/photopickerV2/res/values-az/strings.xml +++ b/tools/photopickerV2/res/values-az/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Şəkillər seçin"</string> <string name="action_get_content" msgid="4319210475508093083">"Kontent əldə etmə əməliyyatı"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Şəkilləri ardıcıllıqla göstərin"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Bir neçə seçimə icazə verin"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimum sayda media elementi"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Birdən böyük etibarlı say daxil edin"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Media seçin"</string> <string name="working_on_it" msgid="1373762827081252341">"Üzərində işləyirik"</string> </resources> diff --git a/tools/photopickerV2/res/values-b+sr+Latn/strings.xml b/tools/photopickerV2/res/values-b+sr+Latn/strings.xml index 21cafd1a4..619eba483 100644 --- a/tools/photopickerV2/res/values-b+sr+Latn/strings.xml +++ b/tools/photopickerV2/res/values-b+sr+Latn/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker verzije 2"</string> <string name="pick_images" msgid="5326258471545526911">"Izaberite slike"</string> <string name="action_get_content" msgid="4319210475508093083">"Radnja preuzimanja sadržaja"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Prikazujte slike u navedenom redosledu"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Dozvolite izbor više stavki"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalan broj medijskih elemenata"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Unesite važeći broj veći od jedan"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Izaberite medije"</string> <string name="working_on_it" msgid="1373762827081252341">"Radimo na tome"</string> </resources> diff --git a/tools/photopickerV2/res/values-be/strings.xml b/tools/photopickerV2/res/values-be/strings.xml index 4cfb743e9..87d2ddf4f 100644 --- a/tools/photopickerV2/res/values-be/strings.xml +++ b/tools/photopickerV2/res/values-be/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker, версія 2"</string> <string name="pick_images" msgid="5326258471545526911">"Выберыце відарысы"</string> <string name="action_get_content" msgid="4319210475508093083">"Дзеянне \"Атрымаць змесціва\""</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Паказваць відарысы па парадку"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Дазволіць выбар некалькіх фота"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максімальная колькасць медыяфайлаў"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Увядзіце дапушчальную лічбу, якая перавышае 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Выберыце медыяфайлы"</string> <string name="working_on_it" msgid="1373762827081252341">"Ідзе апрацоўка"</string> </resources> diff --git a/tools/photopickerV2/res/values-bg/strings.xml b/tools/photopickerV2/res/values-bg/strings.xml index 891277a3b..844129735 100644 --- a/tools/photopickerV2/res/values-bg/strings.xml +++ b/tools/photopickerV2/res/values-bg/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker версия 2"</string> <string name="pick_images" msgid="5326258471545526911">"Избиране на изображения"</string> <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Показване на изображенията в определен ред"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Разрешаване на множествен избор"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимален брой мултимедийни елементи"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Въведете валиден брой, по-голям от едно"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Избиране на мултимедия"</string> <string name="working_on_it" msgid="1373762827081252341">"Работим по въпроса"</string> </resources> diff --git a/tools/photopickerV2/res/values-bn/strings.xml b/tools/photopickerV2/res/values-bn/strings.xml index 1b058ec96..558c86864 100644 --- a/tools/photopickerV2/res/values-bn/strings.xml +++ b/tools/photopickerV2/res/values-bn/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ছবি বেছে নিন"</string> <string name="action_get_content" msgid="4319210475508093083">"কন্টেন্ট পাওয়ার অ্যাকশন"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ক্রম অনুযায়ী ছবি দেখান"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"একাধিক বেছে নেওয়ার অনুমতি দিন"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"মিডিয়া আইটেমের সর্বোচ্চ সংখ্যা"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"একের থেকে বড় একটি সঠিক সংখ্যা লিখুন"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"মিডিয়া বেছে নিন"</string> <string name="working_on_it" msgid="1373762827081252341">"এটির জন্য কাজ চলছে"</string> </resources> diff --git a/tools/photopickerV2/res/values-bs/strings.xml b/tools/photopickerV2/res/values-bs/strings.xml index e3667378f..80e5e23a2 100644 --- a/tools/photopickerV2/res/values-bs/strings.xml +++ b/tools/photopickerV2/res/values-bs/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Odaberite slike"</string> <string name="action_get_content" msgid="4319210475508093083">"Radnja dohvatanja sadržaja"</string> - <string name="display_images_in_order" msgid="7880116254468536174">"Prikazuj slike prema redoslijedu"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> + <string name="display_images_in_order" msgid="7880116254468536174">"Prikaži slike prema redoslijedu"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Dozvoli višestruki odabir"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalni broj medijskih fajlova"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Unesite važeći broj veći od jedan"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Odaberite medij"</string> <string name="working_on_it" msgid="1373762827081252341">"Radimo na tome"</string> </resources> diff --git a/tools/photopickerV2/res/values-ca/strings.xml b/tools/photopickerV2/res/values-ca/strings.xml index b81f5c8e2..1c4aee51e 100644 --- a/tools/photopickerV2/res/values-ca/strings.xml +++ b/tools/photopickerV2/res/values-ca/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Tria imatges"</string> <string name="action_get_content" msgid="4319210475508093083">"Acció per obtenir contingut"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Mostra les imatges en ordre"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permet la selecció múltiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Nombre màxim d\'elements multimèdia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Introdueix un recompte vàlid superior a u"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Tria contingut multimèdia"</string> <string name="working_on_it" msgid="1373762827081252341">"Hi estem treballant"</string> </resources> diff --git a/tools/photopickerV2/res/values-cs/strings.xml b/tools/photopickerV2/res/values-cs/strings.xml index 8b19e6ad9..4048de9f7 100644 --- a/tools/photopickerV2/res/values-cs/strings.xml +++ b/tools/photopickerV2/res/values-cs/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"Výběr fotek V2"</string> <string name="pick_images" msgid="5326258471545526911">"Vyberte obrázky"</string> <string name="action_get_content" msgid="4319210475508093083">"Akce načtení obsahu"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Zobrazit obrázky popořadě"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Povolit více výběrů"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximální počet mediálních položek"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Zadejte platný počet vyšší než jedna"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Vyberte média"</string> <string name="working_on_it" msgid="1373762827081252341">"Pracujeme na tom"</string> </resources> diff --git a/tools/photopickerV2/res/values-da/strings.xml b/tools/photopickerV2/res/values-da/strings.xml index 7d19bfa0e..e4401bfe1 100644 --- a/tools/photopickerV2/res/values-da/strings.xml +++ b/tools/photopickerV2/res/values-da/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Vælg billeder"</string> <string name="action_get_content" msgid="4319210475508093083">"Handlingsknap for at hente indhold"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Vis billeder i rækkefølge"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Tillad flere valg"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Det maksimale antal medieelementer"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Angiv et gyldigt antal, der er større end ét"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Vælg medie"</string> <string name="working_on_it" msgid="1373762827081252341">"Vi arbejder på sagen"</string> </resources> diff --git a/tools/photopickerV2/res/values-de/strings.xml b/tools/photopickerV2/res/values-de/strings.xml index 366a61795..503e44ec0 100644 --- a/tools/photopickerV2/res/values-de/strings.xml +++ b/tools/photopickerV2/res/values-de/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Bilder auswählen"</string> <string name="action_get_content" msgid="4319210475508093083">"Aktion „Inhalte erhalten“"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Bilder in Reihenfolge anzeigen"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Mehrfachauswahl zulassen"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximale Anzahl an Mediendateien"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Gib eine gültige Zahl größer als eins ein"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Medien auswählen"</string> <string name="working_on_it" msgid="1373762827081252341">"Wird bearbeitet"</string> </resources> diff --git a/tools/photopickerV2/res/values-el/strings.xml b/tools/photopickerV2/res/values-el/strings.xml index 97cbee31a..00095ee18 100644 --- a/tools/photopickerV2/res/values-el/strings.xml +++ b/tools/photopickerV2/res/values-el/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Επιλογή εικόνων"</string> <string name="action_get_content" msgid="4319210475508093083">"Ενέργεια για τη λήψη περιεχομένου"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Προβολή εικόνων σε σειρά"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Να επιτρέπεται η πολλαπλή επιλογή"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Μέγιστος αριθμός στοιχείων μέσων"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Εισαγωγή έγκυρου πλήθους μεγαλύτερου από ένα"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Επιλογή μέσων"</string> <string name="working_on_it" msgid="1373762827081252341">"Γίνεται επεξεργασία"</string> </resources> diff --git a/tools/photopickerV2/res/values-en-rAU/strings.xml b/tools/photopickerV2/res/values-en-rAU/strings.xml index 0a75e6e24..1028a2e98 100644 --- a/tools/photopickerV2/res/values-en-rAU/strings.xml +++ b/tools/photopickerV2/res/values-en-rAU/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pick images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action get content"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Display images in order"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow multiple selection"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Enter a valid count greater than one"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Pick media"</string> <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string> </resources> diff --git a/tools/photopickerV2/res/values-en-rCA/strings.xml b/tools/photopickerV2/res/values-en-rCA/strings.xml index 58c8ff8d2..17bef980d 100644 --- a/tools/photopickerV2/res/values-en-rCA/strings.xml +++ b/tools/photopickerV2/res/values-en-rCA/strings.xml @@ -8,10 +8,17 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pick Images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string> + <string name="open_document" msgid="8593796561386540777">"Open Document"</string> + <string name="open_document_tree" msgid="8979404185180480396">"Open Document Tree"</string> <string name="display_images_in_order" msgid="7880116254468536174">"Display Images in Order"</string> + <string name="show_images_only" msgid="6365019348435132030">"Show Images Only"</string> + <string name="show_videos_only" msgid="7302756380142587762">"Show Videos Only"</string> + <string name="enter_mime_type" msgid="6599304148898478294">"Enter Mime Type"</string> + <string name="select_launch_tab" msgid="1219436289162294907">"Select Launch Tab"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow Multiple Selection"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow Custom Mime Type"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Enter a valid count greater than one"</string> + <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string> <string name="pick_media" msgid="5269447618857205416">"Pick Media"</string> <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string> </resources> diff --git a/tools/photopickerV2/res/values-en-rGB/strings.xml b/tools/photopickerV2/res/values-en-rGB/strings.xml index 0a75e6e24..1028a2e98 100644 --- a/tools/photopickerV2/res/values-en-rGB/strings.xml +++ b/tools/photopickerV2/res/values-en-rGB/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pick images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action get content"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Display images in order"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow multiple selection"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Enter a valid count greater than one"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Pick media"</string> <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string> </resources> diff --git a/tools/photopickerV2/res/values-en-rIN/strings.xml b/tools/photopickerV2/res/values-en-rIN/strings.xml index 0a75e6e24..1028a2e98 100644 --- a/tools/photopickerV2/res/values-en-rIN/strings.xml +++ b/tools/photopickerV2/res/values-en-rIN/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pick images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action get content"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Display images in order"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow multiple selection"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Enter a valid count greater than one"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Pick media"</string> <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string> </resources> diff --git a/tools/photopickerV2/res/values-en-rXC/strings.xml b/tools/photopickerV2/res/values-en-rXC/strings.xml index d35df81ed..5bab75f26 100644 --- a/tools/photopickerV2/res/values-en-rXC/strings.xml +++ b/tools/photopickerV2/res/values-en-rXC/strings.xml @@ -8,10 +8,17 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pick Images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string> + <string name="open_document" msgid="8593796561386540777">"Open Document"</string> + <string name="open_document_tree" msgid="8979404185180480396">"Open Document Tree"</string> <string name="display_images_in_order" msgid="7880116254468536174">"Display Images in Order"</string> + <string name="show_images_only" msgid="6365019348435132030">"Show Images Only"</string> + <string name="show_videos_only" msgid="7302756380142587762">"Show Videos Only"</string> + <string name="enter_mime_type" msgid="6599304148898478294">"Enter Mime Type"</string> + <string name="select_launch_tab" msgid="1219436289162294907">"Select Launch Tab"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow Multiple Selection"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow Custom Mime Type"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Enter a valid count greater than one"</string> + <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string> <string name="pick_media" msgid="5269447618857205416">"Pick Media"</string> <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string> </resources> diff --git a/tools/photopickerV2/res/values-es-rUS/strings.xml b/tools/photopickerV2/res/values-es-rUS/strings.xml index 5b7d754b5..50966a66e 100644 --- a/tools/photopickerV2/res/values-es-rUS/strings.xml +++ b/tools/photopickerV2/res/values-es-rUS/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Elegir imágenes"</string> <string name="action_get_content" msgid="4319210475508093083">"Acción para obtener el contenido"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Mostrar imágenes en orden"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir selección múltiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de elementos multimedia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Ingresa un número válido mayor que uno"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Elegir contenido multimedia"</string> <string name="working_on_it" msgid="1373762827081252341">"Estamos trabajando en ello"</string> </resources> diff --git a/tools/photopickerV2/res/values-es/strings.xml b/tools/photopickerV2/res/values-es/strings.xml index 44615897f..70abf5001 100644 --- a/tools/photopickerV2/res/values-es/strings.xml +++ b/tools/photopickerV2/res/values-es/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Elegir imágenes"</string> <string name="action_get_content" msgid="4319210475508093083">"Acción para obtener el contenido"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Mostrar imágenes en orden"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir selección múltiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de elementos multimedia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Introduce un número válido mayor que uno"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Elegir contenido multimedia"</string> <string name="working_on_it" msgid="1373762827081252341">"Estamos trabajando en ello"</string> </resources> diff --git a/tools/photopickerV2/res/values-et/strings.xml b/tools/photopickerV2/res/values-et/strings.xml index 6a1c2cbcb..9f1cf52af 100644 --- a/tools/photopickerV2/res/values-et/strings.xml +++ b/tools/photopickerV2/res/values-et/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Valige pildid"</string> <string name="action_get_content" msgid="4319210475508093083">"ACTION_GET_CONTENT"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Piltide kuvamine järjekorras"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Mitme valimise lubamine"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Meediaüksuste maksimaalne arv"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Sisestage kehtiv arv, mis on suurem kui 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Valige meedia"</string> <string name="working_on_it" msgid="1373762827081252341">"Toiming on pooleli"</string> </resources> diff --git a/tools/photopickerV2/res/values-eu/strings.xml b/tools/photopickerV2/res/values-eu/strings.xml index 57f2ef19a..7f1caa22f 100644 --- a/tools/photopickerV2/res/values-eu/strings.xml +++ b/tools/photopickerV2/res/values-eu/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker (2. bertsioa)"</string> <string name="pick_images" msgid="5326258471545526911">"Hautatu irudiak"</string> <string name="action_get_content" msgid="4319210475508093083">"Edukia eskuratzeko ekintza"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Bistaratu irudiak ordenatuta"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Eman bat baino gehiago hautatzeko baimena"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Multimedia-elementuen gehieneko kopurua"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Idatzi bat baino handiagoa den zenbaki bat"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Hautatu multimedia-elementuak"</string> <string name="working_on_it" msgid="1373762827081252341">"Lanean ari gara"</string> </resources> diff --git a/tools/photopickerV2/res/values-fa/strings.xml b/tools/photopickerV2/res/values-fa/strings.xml index 8f1ae3090..8d70640f8 100644 --- a/tools/photopickerV2/res/values-fa/strings.xml +++ b/tools/photopickerV2/res/values-fa/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"انتخابگر عکس نسخه ۲"</string> <string name="pick_images" msgid="5326258471545526911">"انتخاب تصویر"</string> <string name="action_get_content" msgid="4319210475508093083">"محتوای مربوط به کنش دریافت"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"نمایش تصاویر به ترتیب"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"مجاز کردن انتخاب چند گزینه"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"حداکثر تعداد فایل رسانهای"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"عدد معتبری که بزرگتر از یک باشد وارد کنید"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"انتخاب رسانه"</string> <string name="working_on_it" msgid="1373762827081252341">"مشغول تهیه پاسخ"</string> </resources> diff --git a/tools/photopickerV2/res/values-fi/strings.xml b/tools/photopickerV2/res/values-fi/strings.xml index 4de533d37..7d77953ab 100644 --- a/tools/photopickerV2/res/values-fi/strings.xml +++ b/tools/photopickerV2/res/values-fi/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"Kuvanvalitsin versio 2"</string> <string name="pick_images" msgid="5326258471545526911">"Valitse kuvia"</string> <string name="action_get_content" msgid="4319210475508093083">"Sisällön hakutoiminto"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Näytä kuvat järjestyksessä"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Salli usean valinta"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Mediakohteiden enimmäismäärä"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Lisää sallittu määrä, joka on enemmän kuin yksi"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Valitse mediaa"</string> <string name="working_on_it" msgid="1373762827081252341">"Työn alla"</string> </resources> diff --git a/tools/photopickerV2/res/values-fr-rCA/strings.xml b/tools/photopickerV2/res/values-fr-rCA/strings.xml index 0b2b3d7d4..e0d14e91a 100644 --- a/tools/photopickerV2/res/values-fr-rCA/strings.xml +++ b/tools/photopickerV2/res/values-fr-rCA/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Choisir des images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action Obtenir du contenu"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Afficher les images en ordre"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permettre la sélection multiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Nombre maximum d\'éléments multimédias"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Saisir un compte valide supérieur au nombre un"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Choisir un média"</string> <string name="working_on_it" msgid="1373762827081252341">"Traitement en cours…"</string> </resources> diff --git a/tools/photopickerV2/res/values-fr/strings.xml b/tools/photopickerV2/res/values-fr/strings.xml index 285813546..18087b459 100644 --- a/tools/photopickerV2/res/values-fr/strings.xml +++ b/tools/photopickerV2/res/values-fr/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Sélectionnez des images"</string> <string name="action_get_content" msgid="4319210475508093083">"Action \"Obtenir du contenu\""</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Afficher les images dans l\'ordre"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Autoriser la sélection multiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Nombre maximal d\'éléments multimédias"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Saisissez un nombre valide supérieur à un"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Sélectionnez des éléments multimédias"</string> <string name="working_on_it" msgid="1373762827081252341">"Opération en cours"</string> </resources> diff --git a/tools/photopickerV2/res/values-gl/strings.xml b/tools/photopickerV2/res/values-gl/strings.xml index d7a3abc07..0663f5ed0 100644 --- a/tools/photopickerV2/res/values-gl/strings.xml +++ b/tools/photopickerV2/res/values-gl/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Escoller imaxes"</string> <string name="action_get_content" msgid="4319210475508093083">"Acción de obter contido"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Mostrar imaxes por orde"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir selección múltiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de elementos multimedia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Pon un número válido superior a un"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Escoller elementos multimedia"</string> <string name="working_on_it" msgid="1373762827081252341">"Estamos niso"</string> </resources> diff --git a/tools/photopickerV2/res/values-gu/strings.xml b/tools/photopickerV2/res/values-gu/strings.xml index c3130ab85..10d427198 100644 --- a/tools/photopickerV2/res/values-gu/strings.xml +++ b/tools/photopickerV2/res/values-gu/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"છબીઓ પસંદ કરો"</string> <string name="action_get_content" msgid="4319210475508093083">"ઍક્શન કન્ટેન્ટ મેળવો"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ક્રમમાં છબીઓ બતાવો"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"એકથી વધુ પસંદગીની મંજૂરી આપો"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"મીડિયા આઇટમની મહત્તમ સંખ્યા"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"એકથી મોટી માન્ય ગણતરી દાખલ કરો"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"મીડિયા પસંદ કરો"</string> <string name="working_on_it" msgid="1373762827081252341">"તેના પર જ કામ કરી રહ્યાં છીએ"</string> </resources> diff --git a/tools/photopickerV2/res/values-hi/strings.xml b/tools/photopickerV2/res/values-hi/strings.xml index 5a062d535..32219e811 100644 --- a/tools/photopickerV2/res/values-hi/strings.xml +++ b/tools/photopickerV2/res/values-hi/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"इमेज चुनें"</string> <string name="action_get_content" msgid="4319210475508093083">"ऐक्शन गेट कॉन्टेंट"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"इमेज क्रम से दिखाएं"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"एक से ज़्यादा विकल्प चुनने की अनुमति दें"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"मीडिया आइटम की ज़्यादा से ज़्यादा संख्या"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"एक से बड़ी ऐसी संख्या डालें जो मान्य हो"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"मीडिया चुनें"</string> <string name="working_on_it" msgid="1373762827081252341">"प्रोसेस जारी है"</string> </resources> diff --git a/tools/photopickerV2/res/values-hr/strings.xml b/tools/photopickerV2/res/values-hr/strings.xml index 1e4ce3088..70decfe13 100644 --- a/tools/photopickerV2/res/values-hr/strings.xml +++ b/tools/photopickerV2/res/values-hr/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Odaberite slike"</string> <string name="action_get_content" msgid="4319210475508093083">"Radnja dohvaćanja sadržaja"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Prikaži slike prema redoslijedu"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Dopusti višestruki izbor"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalan broj medijskih datoteka"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Unesite važeći broj koji je veći od jedan"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Odaberite medije"</string> <string name="working_on_it" msgid="1373762827081252341">"Radimo na tome"</string> </resources> diff --git a/tools/photopickerV2/res/values-hu/strings.xml b/tools/photopickerV2/res/values-hu/strings.xml index 0d1381486..670b8e73c 100644 --- a/tools/photopickerV2/res/values-hu/strings.xml +++ b/tools/photopickerV2/res/values-hu/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Képek kiválasztása"</string> <string name="action_get_content" msgid="4319210475508093083">"Tartalom kérése művelet"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Képek megjelenítése sorrendben"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Többszörös kiválasztás engedélyezése"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Médiaelemek maximális száma"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Egynél nagyobb érvényes számot adjon meg"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Média kiválasztása"</string> <string name="working_on_it" msgid="1373762827081252341">"Dolgozunk rajta"</string> </resources> diff --git a/tools/photopickerV2/res/values-hy/strings.xml b/tools/photopickerV2/res/values-hy/strings.xml index ffc716ae4..5e0adb167 100644 --- a/tools/photopickerV2/res/values-hy/strings.xml +++ b/tools/photopickerV2/res/values-hy/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Ընտրեք պատկերներ"</string> <string name="action_get_content" msgid="4319210475508093083">"«Ստանալ բովանդակություն» գործողություն"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Ցուցադրել պատկերները հերթականությամբ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Թույլատրել բազմակի ընտրությունը"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Մուլտիմեդիա ֆայլերի առավելագույն քանակը"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Մուտքագրեք մեկից մեծ վավեր թիվ"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Ընտրեք մուլտիմեդիա ֆայլեր"</string> <string name="working_on_it" msgid="1373762827081252341">"Մշակվում է"</string> </resources> diff --git a/tools/photopickerV2/res/values-in/strings.xml b/tools/photopickerV2/res/values-in/strings.xml index 79958fd05..ab31cc41c 100644 --- a/tools/photopickerV2/res/values-in/strings.xml +++ b/tools/photopickerV2/res/values-in/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pilih Gambar"</string> <string name="action_get_content" msgid="4319210475508093083">"Konten Mendapatkan Tindakan"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Tampilkan Gambar secara Berurutan"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Izinkan Lebih dari Satu Pilihan"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Jumlah maksimal item media"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Masukkan jumlah valid yang lebih besar dari satu"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Pilih Media"</string> <string name="working_on_it" msgid="1373762827081252341">"Sedang dalam proses"</string> </resources> diff --git a/tools/photopickerV2/res/values-is/strings.xml b/tools/photopickerV2/res/values-is/strings.xml index 2b073a968..bb93f0ed0 100644 --- a/tools/photopickerV2/res/values-is/strings.xml +++ b/tools/photopickerV2/res/values-is/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"Myndaval, 2. útg."</string> <string name="pick_images" msgid="5326258471545526911">"Velja myndir"</string> <string name="action_get_content" msgid="4319210475508093083">"Aðgerðin „Sækja efni“"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Birta myndir í röð"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Leyfa val á mörgum atriðum"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Hámarksfjöldi margmiðlunarskráa"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Sláðu inn gildan fjölda sem er meiri en eitt atriði"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Velja margmiðlunarefni"</string> <string name="working_on_it" msgid="1373762827081252341">"Við erum að vinna í þessu"</string> </resources> diff --git a/tools/photopickerV2/res/values-it/strings.xml b/tools/photopickerV2/res/values-it/strings.xml index 3e14b7111..2f5d72ad2 100644 --- a/tools/photopickerV2/res/values-it/strings.xml +++ b/tools/photopickerV2/res/values-it/strings.xml @@ -8,10 +8,18 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Scegli le immagini"</string> <string name="action_get_content" msgid="4319210475508093083">"Azione Ricevi contenuti"</string> + <string name="open_document" msgid="8593796561386540777">"Apri documento"</string> + <string name="open_document_tree" msgid="8979404185180480396">"Apri documento con struttura ad albero"</string> <string name="display_images_in_order" msgid="7880116254468536174">"Mostra le immagini in ordine"</string> + <string name="show_images_only" msgid="6365019348435132030">"Mostra solo immagini"</string> + <string name="show_videos_only" msgid="7302756380142587762">"Mostra solo video"</string> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <string name="select_launch_tab" msgid="1219436289162294907">"Seleziona scheda di lancio"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"Consenti la selezione multipla"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"Consenti tipo MIME personalizzato"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"Numero massimo di elementi multimediali"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Inserisci un numero valido maggiore di uno"</string> + <string name="enter_valid_number" msgid="650407643348891234">"Inserisci un numero valido maggiore di uno"</string> <string name="pick_media" msgid="5269447618857205416">"Scegli i contenuti multimediali"</string> <string name="working_on_it" msgid="1373762827081252341">"Operazione in corso…"</string> </resources> diff --git a/tools/photopickerV2/res/values-iw/strings.xml b/tools/photopickerV2/res/values-iw/strings.xml index f50f32dea..031b0121d 100644 --- a/tools/photopickerV2/res/values-iw/strings.xml +++ b/tools/photopickerV2/res/values-iw/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"בחירת תמונות"</string> <string name="action_get_content" msgid="4319210475508093083">"תוכן להשגת פעולה"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"הצגת התמונות לפי סדר"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"הפעלת האפשרות לבחירה מרובה"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"מספר מקסימלי של קובצי מדיה"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"צריך להזין מספר תקין שגדול מאחד"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"בחירת מדיה"</string> <string name="working_on_it" msgid="1373762827081252341">"בטיפול"</string> </resources> diff --git a/tools/photopickerV2/res/values-ja/strings.xml b/tools/photopickerV2/res/values-ja/strings.xml index d829279b1..d34cc1986 100644 --- a/tools/photopickerV2/res/values-ja/strings.xml +++ b/tools/photopickerV2/res/values-ja/strings.xml @@ -8,10 +8,18 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"画像の選択"</string> <string name="action_get_content" msgid="4319210475508093083">"アクション - コンテンツの取得"</string> + <string name="open_document" msgid="8593796561386540777">"オープン ドキュメント"</string> + <string name="open_document_tree" msgid="8979404185180480396">"オープン ドキュメント ツリー"</string> <string name="display_images_in_order" msgid="7880116254468536174">"一連の画像の表示"</string> + <string name="show_images_only" msgid="6365019348435132030">"画像のみを表示"</string> + <string name="show_videos_only" msgid="7302756380142587762">"動画のみを表示"</string> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <string name="select_launch_tab" msgid="1219436289162294907">"起動タブを選択"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"複数の選択を許可する"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"カスタムの MIME タイプを許可する"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"メディア項目数の上限"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"1 より大きい有効な数値を入力してください"</string> + <string name="enter_valid_number" msgid="650407643348891234">"1 より大きい有効な数値を入力してください"</string> <string name="pick_media" msgid="5269447618857205416">"メディアの選択"</string> <string name="working_on_it" msgid="1373762827081252341">"処理しています"</string> </resources> diff --git a/tools/photopickerV2/res/values-ka/strings.xml b/tools/photopickerV2/res/values-ka/strings.xml index cf1f976d2..dfe5e143e 100644 --- a/tools/photopickerV2/res/values-ka/strings.xml +++ b/tools/photopickerV2/res/values-ka/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"სურათების არჩევა"</string> <string name="action_get_content" msgid="4319210475508093083">"მოქმედება კონტენტის მისაღებად"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"სურათების ჩვენება თანმიმდევრობით"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"რამდენიმე არჩევის დაშვება"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"მედია ერთეულების მაქსიმალური რაოდენობა"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"შეიყვანეთ ვალიდური ოდენობა, რომელიც აღემატება ერთს"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"მედიის არჩევა"</string> <string name="working_on_it" msgid="1373762827081252341">"მუშავდება"</string> </resources> diff --git a/tools/photopickerV2/res/values-kk/strings.xml b/tools/photopickerV2/res/values-kk/strings.xml index 4004111de..3dfa78412 100644 --- a/tools/photopickerV2/res/values-kk/strings.xml +++ b/tools/photopickerV2/res/values-kk/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Суреттерді таңдау"</string> <string name="action_get_content" msgid="4319210475508093083">"\"Контент алу\" әрекеті"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Суреттерді ретпен көрсету"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Бірнеше элемент таңдауға рұқсат ету"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максималды медиафайлдар саны"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Бірден үлкен жарамды сан енгізіңіз."</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Медиафайл таңдау"</string> <string name="working_on_it" msgid="1373762827081252341">"Орындалып жатыр"</string> </resources> diff --git a/tools/photopickerV2/res/values-km/strings.xml b/tools/photopickerV2/res/values-km/strings.xml index 00ee9153b..cd4bf257a 100644 --- a/tools/photopickerV2/res/values-km/strings.xml +++ b/tools/photopickerV2/res/values-km/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ជ្រើសរើសរូបភាព"</string> <string name="action_get_content" msgid="4319210475508093083">"សកម្មភាពយកខ្លឹមសារ"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"បង្ហាញរូបភាពតាមលំដាប់"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"អនុញ្ញាតការជ្រើសរើសច្រើន"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"ចំនួនធាតុមេឌៀអតិបរមា"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"បញ្ចូលចំនួនធំជាងមួយដែលមានសុពលភាព"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"ជ្រើសរើសមេឌៀ"</string> <string name="working_on_it" msgid="1373762827081252341">"កំពុងដោះស្រាយបញ្ហានេះ"</string> </resources> diff --git a/tools/photopickerV2/res/values-kn/strings.xml b/tools/photopickerV2/res/values-kn/strings.xml index b79a9e086..020950c83 100644 --- a/tools/photopickerV2/res/values-kn/strings.xml +++ b/tools/photopickerV2/res/values-kn/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ಚಿತ್ರಗಳನ್ನು ಆರಿಸಿ"</string> <string name="action_get_content" msgid="4319210475508093083">"ಆ್ಯಕ್ಷನ್ ಗೆಟ್ ಕಂಟೆಂಟ್"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ಕ್ರಮವಾಗಿ ಚಿತ್ರಗಳನ್ನು ಪ್ರದರ್ಶಿಸಿ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"ಬಹು ಆಯ್ಕೆಗಳನ್ನು ಆರಿಸಲು ಅನುಮತಿ ನೀಡಿ"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"ಗರಿಷ್ಠ ಸಂಖ್ಯೆಯ ಮಾಧ್ಯಮ ಐಟಂಗಳು"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚಿನ ಮೌಲ್ಯದ ಮಾನ್ಯವಾದ ಸಂಖ್ಯೆಯನ್ನು ನಮೂದಿಸಿ"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"ಮಾಧ್ಯಮವನ್ನು ಆರಿಸಿ"</string> <string name="working_on_it" msgid="1373762827081252341">"ಪ್ರಯತ್ನಿಸಲಾಗುತ್ತಿದೆ"</string> </resources> diff --git a/tools/photopickerV2/res/values-ko/strings.xml b/tools/photopickerV2/res/values-ko/strings.xml index e88448327..1c475a839 100644 --- a/tools/photopickerV2/res/values-ko/strings.xml +++ b/tools/photopickerV2/res/values-ko/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"이미지 선택"</string> <string name="action_get_content" msgid="4319210475508093083">"콘텐츠 가져오기 작업"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"순서대로 이미지 표시"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"다중 선택 허용"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"최대 미디어 항목 수"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"1보다 큰 유효한 개수 입력"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"미디어 선택"</string> <string name="working_on_it" msgid="1373762827081252341">"진행 중"</string> </resources> diff --git a/tools/photopickerV2/res/values-ky/strings.xml b/tools/photopickerV2/res/values-ky/strings.xml index 582d4ff7c..633404f52 100644 --- a/tools/photopickerV2/res/values-ky/strings.xml +++ b/tools/photopickerV2/res/values-ky/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Сүрөттөрдү тандоо"</string> <string name="action_get_content" msgid="4319210475508093083">"\"Контент алуу\" аракети"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Сүрөттөрдү ирети менен көрсөтүү"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Бир нече объектти тандоого уруксат берүү"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Медиа файлдардын макcималдуу саны"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Бирден чоңураак болгон жарамдуу санды киргизиңиз"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Медиа тандоо"</string> <string name="working_on_it" msgid="1373762827081252341">"Иштетилип жатат"</string> </resources> diff --git a/tools/photopickerV2/res/values-lo/strings.xml b/tools/photopickerV2/res/values-lo/strings.xml index d9a7d854c..445a7327e 100644 --- a/tools/photopickerV2/res/values-lo/strings.xml +++ b/tools/photopickerV2/res/values-lo/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ເລືອກຮູບພາບ"</string> <string name="action_get_content" msgid="4319210475508093083">"ຄຳສັ່ງຮັບເນື້ອຫາ"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ສະແດງຮູບພາບຕາມລຳດັບ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"ອະນຸຍາດໃຫ້ເລືອກຫຼາຍລາຍການ"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"ຈຳນວນລາຍການມີເດຍສູງສຸດ"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ລະບຸຄ່າທີ່ຖືກຕ້ອງທີ່ຫຼາຍກວ່າ 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"ເລືອກມີເດຍ"</string> <string name="working_on_it" msgid="1373762827081252341">"ກຳລັງດຳເນີນການ"</string> </resources> diff --git a/tools/photopickerV2/res/values-lt/strings.xml b/tools/photopickerV2/res/values-lt/strings.xml index c80797b09..5eb5dce5b 100644 --- a/tools/photopickerV2/res/values-lt/strings.xml +++ b/tools/photopickerV2/res/values-lt/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pasirinkite vaizdus"</string> <string name="action_get_content" msgid="4319210475508093083">"Veiksmas „Gauti turinį“"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Rodyti vaizdus eilės tvarka"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Leisti kelis pasirinkimus"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maks. medijos elementų skaičius"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Įveskite tinkamą skaičių, didesnį nei 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Pasirinkite mediją"</string> <string name="working_on_it" msgid="1373762827081252341">"Vykdoma"</string> </resources> diff --git a/tools/photopickerV2/res/values-lv/strings.xml b/tools/photopickerV2/res/values-lv/strings.xml index 3f4ab8a0b..575d43e3e 100644 --- a/tools/photopickerV2/res/values-lv/strings.xml +++ b/tools/photopickerV2/res/values-lv/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Atlasīt attēlus"</string> <string name="action_get_content" msgid="4319210475508093083">"Satura iegūšanas darbība"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Rādīt attēlus norādītajā secībā"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Atļaut vairāku vienumu atlasi"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimālais mediju skaits"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Ievadiet derīgu skaitu, kas ir lielāks par 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Atlasīt multivides saturu"</string> <string name="working_on_it" msgid="1373762827081252341">"Notiek darbība…"</string> </resources> diff --git a/tools/photopickerV2/res/values-mk/strings.xml b/tools/photopickerV2/res/values-mk/strings.xml index f93c5b0e4..9d93015aa 100644 --- a/tools/photopickerV2/res/values-mk/strings.xml +++ b/tools/photopickerV2/res/values-mk/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Изберете слики"</string> <string name="action_get_content" msgid="4319210475508093083">"Дејство за добивање содржини"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Прикажувајте слики по редослед"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Дозволете повеќекратен избор"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимален број аудиовизуелни ставки"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Внесете важечки број поголем од еден"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Изберете аудиовизуелни содржини"</string> <string name="working_on_it" msgid="1373762827081252341">"Се работи на тоа"</string> </resources> diff --git a/tools/photopickerV2/res/values-ml/strings.xml b/tools/photopickerV2/res/values-ml/strings.xml index 737d1efb2..b56121f07 100644 --- a/tools/photopickerV2/res/values-ml/strings.xml +++ b/tools/photopickerV2/res/values-ml/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ചിത്രങ്ങൾ തിരഞ്ഞെടുക്കുക"</string> <string name="action_get_content" msgid="4319210475508093083">"ആക്ഷൻ ഉള്ളടക്കം നേടുക"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ചിത്രങ്ങൾ ക്രമത്തിൽ പ്രദർശിപ്പിക്കുക"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"ഒന്നിലധികം സെലക്ഷൻ അനുവദിക്കുക"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"മീഡിയ ഇനങ്ങളുടെ പരമാവധി എണ്ണം"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ഒന്നിനേക്കാൾ വലുതും സാധുതയുള്ളതുമായ എണ്ണം നൽകുക"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"മീഡിയ തിരഞ്ഞെടുക്കുക"</string> <string name="working_on_it" msgid="1373762827081252341">"ശ്രമിച്ചുകൊണ്ടിരിക്കുകയാണ്"</string> </resources> diff --git a/tools/photopickerV2/res/values-mn/strings.xml b/tools/photopickerV2/res/values-mn/strings.xml index 387330a0d..41aa8ea8f 100644 --- a/tools/photopickerV2/res/values-mn/strings.xml +++ b/tools/photopickerV2/res/values-mn/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Зураг сонгох"</string> <string name="action_get_content" msgid="4319210475508093083">"Контент авах үйлдэл"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Зургийг дарааллаар нь үзүүлэх"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Олон сонголтыг зөвшөөрөх"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Медиа зүйлийн дээд тоо"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Нэгээс дээш хүчинтэй тоо оруулна уу"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Медиа сонгох"</string> <string name="working_on_it" msgid="1373762827081252341">"Үүн дээр ажиллаж байна"</string> </resources> diff --git a/tools/photopickerV2/res/values-mr/strings.xml b/tools/photopickerV2/res/values-mr/strings.xml index 6043f0f3d..5f3829807 100644 --- a/tools/photopickerV2/res/values-mr/strings.xml +++ b/tools/photopickerV2/res/values-mr/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"इमेज निवडा"</string> <string name="action_get_content" msgid="4319210475508093083">"आशय मिळवण्याची कृती"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"इमेजना क्रम यानुसार प्रदर्शित करा"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"एकाहून अधिक निवड करण्यास अनुमती द्या"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"मीडिया आयटमची कमाल संख्या"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"एकाहून मोठी असलेली वैध संख्या एंटर करा"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"मीडिया निवडा"</string> <string name="working_on_it" msgid="1373762827081252341">"त्यावर प्रक्रिया सुरू आहे"</string> </resources> diff --git a/tools/photopickerV2/res/values-ms/strings.xml b/tools/photopickerV2/res/values-ms/strings.xml index 4f7f228de..aa7dbf2f7 100644 --- a/tools/photopickerV2/res/values-ms/strings.xml +++ b/tools/photopickerV2/res/values-ms/strings.xml @@ -8,10 +8,18 @@ <string name="title_photopicker" msgid="3154038243490840415">"PemilihFoto V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pilih Imej"</string> <string name="action_get_content" msgid="4319210475508093083">"Tindakan Mendapatkan Kandungan"</string> + <string name="open_document" msgid="8593796561386540777">"Buka Dokumen"</string> + <string name="open_document_tree" msgid="8979404185180480396">"Buka Pohon Dokumen"</string> <string name="display_images_in_order" msgid="7880116254468536174">"Paparkan Imej mengikut Urutan"</string> + <string name="show_images_only" msgid="6365019348435132030">"Tunjukkan Imej Sahaja"</string> + <string name="show_videos_only" msgid="7302756380142587762">"Tunjukkan Video Sahaja"</string> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <string name="select_launch_tab" msgid="1219436289162294907">"Pilih Tab Pelancaran"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"Benarkan Berbilang Pilihan"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"Benarkan Jenis MIME Tersuai"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"Bilangan maksimum item media"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Masukkan kiraan sah yang lebih banyak daripada satu"</string> + <string name="enter_valid_number" msgid="650407643348891234">"Masukkan nombor sah yang lebih banyak daripada satu"</string> <string name="pick_media" msgid="5269447618857205416">"Pilih Media"</string> <string name="working_on_it" msgid="1373762827081252341">"Sedang diusahakan"</string> </resources> diff --git a/tools/photopickerV2/res/values-my/strings.xml b/tools/photopickerV2/res/values-my/strings.xml index 545c5d1cf..53ea38b03 100644 --- a/tools/photopickerV2/res/values-my/strings.xml +++ b/tools/photopickerV2/res/values-my/strings.xml @@ -8,10 +8,18 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ကြိုက်ရာ ပုံများ"</string> <string name="action_get_content" msgid="4319210475508093083">"လုပ်ဆောင်ချက်ရယူခြင်း အကြောင်းအရာ"</string> + <string name="open_document" msgid="8593796561386540777">"မှတ်တမ်း ဖွင့်ရန်"</string> + <string name="open_document_tree" msgid="8979404185180480396">"မှတ်တမ်း ဆက်နွယ်စနစ် ဖွင့်ရန်"</string> <string name="display_images_in_order" msgid="7880116254468536174">"ပုံများကို အစဉ်လိုက် ပြရန်"</string> + <string name="show_images_only" msgid="6365019348435132030">"ပုံများသာ ပြပါ"</string> + <string name="show_videos_only" msgid="7302756380142587762">"ဗီဒီယိုများသာ ပြပါ"</string> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <string name="select_launch_tab" msgid="1219436289162294907">"စတင်ရန်တဘ် ရွေးရန်"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"ရွေးချယ်မှုများစွာ ခွင့်ပြုရန်"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"စိတ်ကြိုက် MIME အမျိုးအစားကို ခွင့်ပြုရန်"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"အများဆုံး မီဒီယာဖိုင်အရေအတွက်"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"တစ်ခုထက်ပိုများသော မှန်ကန်သည့်အရေအတွက် ထည့်ရန်"</string> + <string name="enter_valid_number" msgid="650407643348891234">"တစ်ထက်ကြီးသော မှန်ကန်သည့်နံပါတ် ထည့်ရန်"</string> <string name="pick_media" msgid="5269447618857205416">"ကြိုက်ရာ မီဒီယာ"</string> <string name="working_on_it" msgid="1373762827081252341">"လုပ်ဆောင်နေသည်"</string> </resources> diff --git a/tools/photopickerV2/res/values-nb/strings.xml b/tools/photopickerV2/res/values-nb/strings.xml index f3f5008b3..bbfb221b1 100644 --- a/tools/photopickerV2/res/values-nb/strings.xml +++ b/tools/photopickerV2/res/values-nb/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Velg bilder"</string> <string name="action_get_content" msgid="4319210475508093083">"Handling, hent innhold"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Vis bilder i rekkefølge"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Tillat flere valg"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalt antall medieelementer"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Legg inn et gyldig tall på mer enn én"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Velg medieinnhold"</string> <string name="working_on_it" msgid="1373762827081252341">"Vi jobber med saken"</string> </resources> diff --git a/tools/photopickerV2/res/values-ne/strings.xml b/tools/photopickerV2/res/values-ne/strings.xml index 8eccf250e..b7507f67f 100644 --- a/tools/photopickerV2/res/values-ne/strings.xml +++ b/tools/photopickerV2/res/values-ne/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"फोटोहरू चयन गर्नुहोस्"</string> <string name="action_get_content" msgid="4319210475508093083">"\"सामग्री प्राप्त गर्नुहोस्\" नामक कारबाही"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"फोटोहरू क्रमबद्ध रूपमा देखाउनुहोस्"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"एकभन्दा बढी मिडिया चयन गर्ने अनुमति दिनुहोस्"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"मिडिया सामग्रीहरूको अधिकतम सङ्ख्या"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"एकभन्दा ठुलो वैध अङ्क हाल्नुहोस्"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"मिडिया चयन गर्नुहोस्"</string> <string name="working_on_it" msgid="1373762827081252341">"हामी यो कार्य गरिरहेका छौँ"</string> </resources> diff --git a/tools/photopickerV2/res/values-nl/strings.xml b/tools/photopickerV2/res/values-nl/strings.xml index e46074f63..c535c155b 100644 --- a/tools/photopickerV2/res/values-nl/strings.xml +++ b/tools/photopickerV2/res/values-nl/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Afbeeldingen kiezen"</string> <string name="action_get_content" msgid="4319210475508093083">"Actie voor content ophalen"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Afbeeldingen op volgorde tonen"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Meerdere selecties toestaan"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximumaantal media-items"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Voer een geldig aantal in groter dan één"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Media kiezen"</string> <string name="working_on_it" msgid="1373762827081252341">"Ik ben ermee bezig"</string> </resources> diff --git a/tools/photopickerV2/res/values-or/strings.xml b/tools/photopickerV2/res/values-or/strings.xml index 2d1c8dc64..e6d71919f 100644 --- a/tools/photopickerV2/res/values-or/strings.xml +++ b/tools/photopickerV2/res/values-or/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ଇମେଜ ବାଛନ୍ତୁ"</string> <string name="action_get_content" msgid="4319210475508093083">"ବିଷୟବସ୍ତୁ ପାଇବା ପାଇଁ କାର୍ଯ୍ୟ"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ଇମେଜଗୁଡ଼ିକୁ କ୍ରମରେ ଡିସପ୍ଲେ କରନ୍ତୁ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"ଏକାଧିକ ଚୟନକୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"ସର୍ବାଧିକ ସଂଖ୍ୟକ ମିଡିଆ ଆଇଟମ"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ଏକରୁ ବଡ଼ ଏକ ବୈଧ ଗଣନା ଲେଖନ୍ତୁ"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"ମିଡିଆ ବାଛନ୍ତୁ"</string> <string name="working_on_it" msgid="1373762827081252341">"ଏହା ଉପରେ କାମ କରୁଛି"</string> </resources> diff --git a/tools/photopickerV2/res/values-pa/strings.xml b/tools/photopickerV2/res/values-pa/strings.xml index 1534b8014..4d8fd418e 100644 --- a/tools/photopickerV2/res/values-pa/strings.xml +++ b/tools/photopickerV2/res/values-pa/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ਚਿੱਤਰ ਚੁਣੋ"</string> <string name="action_get_content" msgid="4319210475508093083">"ਕਾਰਵਾਈ \'ਸਮੱਗਰੀ ਪ੍ਰਾਪਤ ਕਰੋ\'"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ਚਿੱਤਰਾਂ ਨੂੰ ਕ੍ਰਮ ਵਿੱਚ ਦਿਖਾਓ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"ਇੱਕ ਤੋਂ ਵੱਧ ਚੋਣ ਨੂੰ ਆਗਿਆ ਦਿਓ"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"ਵੱਧੋ-ਵੱਧ ਮੀਡੀਆ ਆਈਟਮਾਂ"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ਇੱਕ ਤੋਂ ਵੱਧ ਮੁੱਲ ਵਾਲੀ ਵੈਧ ਗਿਣਤੀ ਦਾਖਲ ਕਰੋ"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"ਮੀਡੀਆ ਚੁਣੋ"</string> <string name="working_on_it" msgid="1373762827081252341">"ਇਸ \'ਤੇ ਕੰਮ ਚੱਲ ਰਿਹਾ ਹੈ"</string> </resources> diff --git a/tools/photopickerV2/res/values-pl/strings.xml b/tools/photopickerV2/res/values-pl/strings.xml index 0818f1828..da9ceb43a 100644 --- a/tools/photopickerV2/res/values-pl/strings.xml +++ b/tools/photopickerV2/res/values-pl/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Wybierz obrazy"</string> <string name="action_get_content" msgid="4319210475508093083">"Działanie Pobierz treści"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Wyświetlaj obrazy w kolejności"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Zezwalaj na wielokrotny wybór"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksymalna liczba elementów multimedialnych"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Podaj prawidłową liczbę większą niż 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Wybierz multimedia"</string> <string name="working_on_it" msgid="1373762827081252341">"Pracujemy nad tym"</string> </resources> diff --git a/tools/photopickerV2/res/values-pt-rBR/strings.xml b/tools/photopickerV2/res/values-pt-rBR/strings.xml index 742ca9c6a..dbfe902a8 100644 --- a/tools/photopickerV2/res/values-pt-rBR/strings.xml +++ b/tools/photopickerV2/res/values-pt-rBR/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Escolher imagens"</string> <string name="action_get_content" msgid="4319210475508093083">"Ação \"Receber conteúdo\""</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Mostrar imagens em ordem"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir várias seleções"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de itens de mídia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Informe uma contagem válida maior que 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Escolher mídia"</string> <string name="working_on_it" msgid="1373762827081252341">"Estamos trabalhando nisso"</string> </resources> diff --git a/tools/photopickerV2/res/values-pt-rPT/strings.xml b/tools/photopickerV2/res/values-pt-rPT/strings.xml index 509c3e74b..4ca39c34d 100644 --- a/tools/photopickerV2/res/values-pt-rPT/strings.xml +++ b/tools/photopickerV2/res/values-pt-rPT/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Escolher imagens"</string> <string name="action_get_content" msgid="4319210475508093083">"Ação Obter conteúdo"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Apresentar imagens por ordem"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir seleção múltipla"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máx. de itens multimédia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Introduza um número válido superior a 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Escolher multimédia"</string> <string name="working_on_it" msgid="1373762827081252341">"A tratar disso"</string> </resources> diff --git a/tools/photopickerV2/res/values-pt/strings.xml b/tools/photopickerV2/res/values-pt/strings.xml index 742ca9c6a..dbfe902a8 100644 --- a/tools/photopickerV2/res/values-pt/strings.xml +++ b/tools/photopickerV2/res/values-pt/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Escolher imagens"</string> <string name="action_get_content" msgid="4319210475508093083">"Ação \"Receber conteúdo\""</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Mostrar imagens em ordem"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir várias seleções"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de itens de mídia"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Informe uma contagem válida maior que 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Escolher mídia"</string> <string name="working_on_it" msgid="1373762827081252341">"Estamos trabalhando nisso"</string> </resources> diff --git a/tools/photopickerV2/res/values-ro/strings.xml b/tools/photopickerV2/res/values-ro/strings.xml index 88b392cd8..6346fa6e3 100644 --- a/tools/photopickerV2/res/values-ro/strings.xml +++ b/tools/photopickerV2/res/values-ro/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Alege imagini"</string> <string name="action_get_content" msgid="4319210475508093083">"Acțiune de obținere conținut"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Afișează imaginile în ordine"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Permite selecțiile multiple"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Numărul maxim de articole media"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Introdu un număr valid, mai mare ca 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Alege articole media"</string> <string name="working_on_it" msgid="1373762827081252341">"Se procesează"</string> </resources> diff --git a/tools/photopickerV2/res/values-ru/strings.xml b/tools/photopickerV2/res/values-ru/strings.xml index 8fa354199..c17dcf4f2 100644 --- a/tools/photopickerV2/res/values-ru/strings.xml +++ b/tools/photopickerV2/res/values-ru/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"Выбор фотографий (версия 2)"</string> <string name="pick_images" msgid="5326258471545526911">"Выбрать изображения"</string> <string name="action_get_content" msgid="4319210475508093083">"ACTION_GET_CONTENT"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Показать изображения по порядку"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Разрешить выбор нескольких объектов"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимальное число мультимедийных объектов"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Введите допустимое число больше единицы"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Выбрать мультимедиа"</string> <string name="working_on_it" msgid="1373762827081252341">"Обработка…"</string> </resources> diff --git a/tools/photopickerV2/res/values-si/strings.xml b/tools/photopickerV2/res/values-si/strings.xml index f76391a56..384e18703 100644 --- a/tools/photopickerV2/res/values-si/strings.xml +++ b/tools/photopickerV2/res/values-si/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"රූප තෝරන්න"</string> <string name="action_get_content" msgid="4319210475508093083">"අන්තර්ගතය ලබා ගන්න ක්රියාව"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"රූප පිළිවෙලට පෙන්වන්න"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"බහු තේරීමට ඉඩ දෙන්න"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"මාධ්ය අයිතම උපරිම සංඛ්යාව"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"එකකට වඩා වලංගු සංඛ්යාවක් ඇතුළු කරන්න"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"මාධ්ය තෝරන්න"</string> <string name="working_on_it" msgid="1373762827081252341">"ඒ පිළිබඳ කටයුතු සිදු කෙරෙයි"</string> </resources> diff --git a/tools/photopickerV2/res/values-sk/strings.xml b/tools/photopickerV2/res/values-sk/strings.xml index ce3e92c2c..f1f193cdc 100644 --- a/tools/photopickerV2/res/values-sk/strings.xml +++ b/tools/photopickerV2/res/values-sk/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Vybrať obrázky"</string> <string name="action_get_content" msgid="4319210475508093083">"Akcia Získať obsah"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Zobraziť obrázky v poradí"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Povoliť výber viacerých položiek"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximálny počet mediálnych položiek"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Zadajte platný počet položiek (viac ako jednu)"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Vybrať médiá"</string> <string name="working_on_it" msgid="1373762827081252341">"Pracujeme na tom"</string> </resources> diff --git a/tools/photopickerV2/res/values-sl/strings.xml b/tools/photopickerV2/res/values-sl/strings.xml index 0a34aedc5..02d86a731 100644 --- a/tools/photopickerV2/res/values-sl/strings.xml +++ b/tools/photopickerV2/res/values-sl/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Izbira slik"</string> <string name="action_get_content" msgid="4319210475508093083">"Dejanje »Pridobivanje vsebine«"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Prikaz slik v vrstnem redu"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Dovoli izbiro več možnosti"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Največje dovoljeno število predstavnostnih elementov"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Vnesite veljavno številko, večjo od ena"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Izbira predstavnosti"</string> <string name="working_on_it" msgid="1373762827081252341">"Ukvarjamo se s tem."</string> </resources> diff --git a/tools/photopickerV2/res/values-sq/strings.xml b/tools/photopickerV2/res/values-sq/strings.xml index d0a8f8aa6..073484783 100644 --- a/tools/photopickerV2/res/values-sq/strings.xml +++ b/tools/photopickerV2/res/values-sq/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Zgjidh imazhet"</string> <string name="action_get_content" msgid="4319210475508093083">"Veprimi: Merr përmbajtjen"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Shfaq imazhet sipas rendit"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Lejo disa zgjedhje"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Numri maksimal i artikujve të medias"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Fut një numër të vlefshëm më të madh se një"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Zgjidh median"</string> <string name="working_on_it" msgid="1373762827081252341">"Po punohet për të"</string> </resources> diff --git a/tools/photopickerV2/res/values-sr/strings.xml b/tools/photopickerV2/res/values-sr/strings.xml index 7256a4c7b..403fb8134 100644 --- a/tools/photopickerV2/res/values-sr/strings.xml +++ b/tools/photopickerV2/res/values-sr/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker верзије 2"</string> <string name="pick_images" msgid="5326258471545526911">"Изаберите слике"</string> <string name="action_get_content" msgid="4319210475508093083">"Радња преузимања садржаја"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Приказујте слике у наведеном редоследу"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Дозволите избор више ставки"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максималан број медијских елемената"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Унесите важећи број већи од један"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Изаберите медије"</string> <string name="working_on_it" msgid="1373762827081252341">"Радимо на томе"</string> </resources> diff --git a/tools/photopickerV2/res/values-sv/strings.xml b/tools/photopickerV2/res/values-sv/strings.xml index b8d208c57..de5504193 100644 --- a/tools/photopickerV2/res/values-sv/strings.xml +++ b/tools/photopickerV2/res/values-sv/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"Fotoväljare v2"</string> <string name="pick_images" msgid="5326258471545526911">"Välj bilder"</string> <string name="action_get_content" msgid="4319210475508093083">"Åtgärden hämta innehåll"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Visa bilder i rätt ordning"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Tillåt flera val"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximalt antal medieobjekt"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Ange ett giltigt antal som är större än detta"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Välj media"</string> <string name="working_on_it" msgid="1373762827081252341">"Jobbar på det"</string> </resources> diff --git a/tools/photopickerV2/res/values-sw/strings.xml b/tools/photopickerV2/res/values-sw/strings.xml index 63fb50e39..62c0eb606 100644 --- a/tools/photopickerV2/res/values-sw/strings.xml +++ b/tools/photopickerV2/res/values-sw/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Teua Picha"</string> <string name="action_get_content" msgid="4319210475508093083">"Kitendo cha Kupata Maudhui"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Onyesha Picha katika Mpangilio"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Ruhusu Uteuzi Mwingi"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Idadi ya juu zaidi ya vipengee vya maudhui"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Weka idadi sahihi inayozidi moja"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Teua Maudhui"</string> <string name="working_on_it" msgid="1373762827081252341">"Inashughulikiwa"</string> </resources> diff --git a/tools/photopickerV2/res/values-ta/strings.xml b/tools/photopickerV2/res/values-ta/strings.xml index c91120d72..dc8b36633 100644 --- a/tools/photopickerV2/res/values-ta/strings.xml +++ b/tools/photopickerV2/res/values-ta/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"ஃபோட்டோ தேர்வுக் கருவி V2"</string> <string name="pick_images" msgid="5326258471545526911">"படங்களைத் தேர்ந்தெடுங்கள்"</string> <string name="action_get_content" msgid="4319210475508093083">"உள்ளடக்கத்தைப் பெறுவதற்கான செயல்"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"படங்களை வரிசையாகக் காட்டு"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"பல தேர்வுகளை அனுமதி"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"மீடியா ஃபைல்களுக்கான அதிகபட்ச எண்ணிக்கை"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ஒன்றை விட அதிகமான சரியான எண்ணை டைப் செய்க"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"மீடியாவைத் தேர்ந்தெடுங்கள்"</string> <string name="working_on_it" msgid="1373762827081252341">"சிறிது நேரம் காத்திருங்கள்"</string> </resources> diff --git a/tools/photopickerV2/res/values-te/strings.xml b/tools/photopickerV2/res/values-te/strings.xml index a1e38c622..234a1ac1b 100644 --- a/tools/photopickerV2/res/values-te/strings.xml +++ b/tools/photopickerV2/res/values-te/strings.xml @@ -8,10 +8,18 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"ఇమేజ్లను ఎంచుకోండి"</string> <string name="action_get_content" msgid="4319210475508093083">"కంటెంట్ పొందడానికి సంబంధించిన చర్య"</string> + <string name="open_document" msgid="8593796561386540777">"డాక్యుమెంట్ను తెరవండి"</string> + <string name="open_document_tree" msgid="8979404185180480396">"డాక్యుమెంట్ ట్రీని తెరవండి"</string> <string name="display_images_in_order" msgid="7880116254468536174">"ఇమేజ్లను ఆర్డర్లో డిస్ప్లే చేయండి"</string> + <string name="show_images_only" msgid="6365019348435132030">"ఇమేజ్లను మాత్రమే చూపండి"</string> + <string name="show_videos_only" msgid="7302756380142587762">"వీడియోలను మాత్రమే చూడండి"</string> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <string name="select_launch_tab" msgid="1219436289162294907">"లాంచ్ ట్యాబ్ను ఎంచుకోండి"</string> <string name="allow_multiple_selection" msgid="3485101220559262266">"పలు ఎంపికలను అనుమతించండి"</string> + <string name="allow_custom_mime_type" msgid="770032039896650761">"అనుకూల MIME రకాన్ని అనుమతించండి"</string> <string name="max_number_of_media_items" msgid="2736386927806685966">"మీడియా ఫైల్స్ గరిష్ఠ సంఖ్య"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ఒకటి కంటే ఎక్కువ చెల్లుబాటు అయ్యే సంఖ్యను ఎంటర్ చేయండి"</string> + <string name="enter_valid_number" msgid="650407643348891234">"ఒకటి కంటే ఎక్కువ చెల్లుబాటు అయ్యే నంబర్ను ఎంటర్ చేయండి"</string> <string name="pick_media" msgid="5269447618857205416">"మీడియాను ఎంచుకోండి"</string> <string name="working_on_it" msgid="1373762827081252341">"ఆ పనిలోనే ఉన్నాను"</string> </resources> diff --git a/tools/photopickerV2/res/values-th/strings.xml b/tools/photopickerV2/res/values-th/strings.xml index 976d83441..cdfd3cf2e 100644 --- a/tools/photopickerV2/res/values-th/strings.xml +++ b/tools/photopickerV2/res/values-th/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"เลือกรูปภาพ"</string> <string name="action_get_content" msgid="4319210475508093083">"การดำเนินการรับเนื้อหา"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"แสดงรูปภาพตามลำดับ"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"อนุญาตให้เลือกหลายรายการ"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"จำนวนรายการสื่อสูงสุด"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ป้อนค่าที่ถูกต้องที่มากกว่า 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"เลือกสื่อ"</string> <string name="working_on_it" msgid="1373762827081252341">"กำลังดำเนินการ"</string> </resources> diff --git a/tools/photopickerV2/res/values-tl/strings.xml b/tools/photopickerV2/res/values-tl/strings.xml index 7b83b8b5c..5124a3187 100644 --- a/tools/photopickerV2/res/values-tl/strings.xml +++ b/tools/photopickerV2/res/values-tl/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Pumili ng Mga Larawan"</string> <string name="action_get_content" msgid="4319210475508093083">"Aksyong Pagkuha ng Content"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Ipakita ang Mga Larawan ayon sa Pagkakasunod-sunod"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Payagan ang Pagpili ng Marami"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Max na bilang ng media item"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Maglagay ng valid na bilang na mas malaki sa isa"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Pumili ng Media"</string> <string name="working_on_it" msgid="1373762827081252341">"Pinoproseso na"</string> </resources> diff --git a/tools/photopickerV2/res/values-tr/strings.xml b/tools/photopickerV2/res/values-tr/strings.xml index 60efde612..541992054 100644 --- a/tools/photopickerV2/res/values-tr/strings.xml +++ b/tools/photopickerV2/res/values-tr/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Resim Seçin"</string> <string name="action_get_content" msgid="4319210475508093083">"İçeriği Alma İşlemi"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Resimleri Sırayla Göster"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Birden Fazla Seçime İzin Verin"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimum medya öğesi sayısı"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Birden büyük geçerli bir sayı girin"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Medya Seçin"</string> <string name="working_on_it" msgid="1373762827081252341">"Üzerinde çalışıyorum"</string> </resources> diff --git a/tools/photopickerV2/res/values-uk/strings.xml b/tools/photopickerV2/res/values-uk/strings.xml index 99479aa1a..dc3eed505 100644 --- a/tools/photopickerV2/res/values-uk/strings.xml +++ b/tools/photopickerV2/res/values-uk/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Вибір зображень"</string> <string name="action_get_content" msgid="4319210475508093083">"Дія для отримання контенту"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Показувати зображення по порядку"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Дозволити множинний вибір"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимальна кількість медіафайлів"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Введіть дійсну кількість, що перевищує одиницю"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Вибір медіафайлів"</string> <string name="working_on_it" msgid="1373762827081252341">"Триває обробка"</string> </resources> diff --git a/tools/photopickerV2/res/values-ur/strings.xml b/tools/photopickerV2/res/values-ur/strings.xml index acdd08bcc..79750e03c 100644 --- a/tools/photopickerV2/res/values-ur/strings.xml +++ b/tools/photopickerV2/res/values-ur/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"تصاویر منتخب کریں"</string> <string name="action_get_content" msgid="4319210475508093083">"مواد حاصل کرنے کے لیے کارروائی"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"ترتیب میں تصاویر ڈسپلے کریں"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"متعدد انتخاب کی اجازت دیں"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"میڈیا آئٹمز کی زیادہ سے زیادہ تعداد"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"ایک سے زیادہ کی درست گنتی درج کریں"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"میڈیا منتخب کریں"</string> <string name="working_on_it" msgid="1373762827081252341">"اس پر کام ہو رہا ہے"</string> </resources> diff --git a/tools/photopickerV2/res/values-uz/strings.xml b/tools/photopickerV2/res/values-uz/strings.xml index a3f6d6a78..6e6175e04 100644 --- a/tools/photopickerV2/res/values-uz/strings.xml +++ b/tools/photopickerV2/res/values-uz/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Rasm tanlash"</string> <string name="action_get_content" msgid="4319210475508093083">"Amal kontent olish"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Rasmlarni tartiblash"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Multi-tanlashga ruxsat"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Media elementlar maksimal soni"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Birdan katta yaroqli sonni kiriting"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Media tanlash"</string> <string name="working_on_it" msgid="1373762827081252341">"Buning ustida ishlayapmiz"</string> </resources> diff --git a/tools/photopickerV2/res/values-vi/strings.xml b/tools/photopickerV2/res/values-vi/strings.xml index 054a198d3..2374d8d38 100644 --- a/tools/photopickerV2/res/values-vi/strings.xml +++ b/tools/photopickerV2/res/values-vi/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Chọn hình ảnh"</string> <string name="action_get_content" msgid="4319210475508093083">"ACTION_GET_CONTENT"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Hiển thị hình ảnh theo thứ tự"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Cho phép chọn nhiều mục"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Số mục nội dung nghe nhìn tối đa"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Nhập một số lượng hợp lệ, lớn hơn 1"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Chọn nội dung nghe nhìn"</string> <string name="working_on_it" msgid="1373762827081252341">"Đang xử lý"</string> </resources> diff --git a/tools/photopickerV2/res/values-zh-rCN/strings.xml b/tools/photopickerV2/res/values-zh-rCN/strings.xml index 22c7b7091..bbb0aac40 100644 --- a/tools/photopickerV2/res/values-zh-rCN/strings.xml +++ b/tools/photopickerV2/res/values-zh-rCN/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"选择图片"</string> <string name="action_get_content" msgid="4319210475508093083">"操作获取内容"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"按顺序显示图片"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"允许多选"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"媒体文件数上限"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"输入大于 1 的有效数字"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"选择媒体"</string> <string name="working_on_it" msgid="1373762827081252341">"正在处理中"</string> </resources> diff --git a/tools/photopickerV2/res/values-zh-rHK/strings.xml b/tools/photopickerV2/res/values-zh-rHK/strings.xml index b3f934f7d..362012004 100644 --- a/tools/photopickerV2/res/values-zh-rHK/strings.xml +++ b/tools/photopickerV2/res/values-zh-rHK/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"點選圖片"</string> <string name="action_get_content" msgid="4319210475508093083">"取得內容的動作"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"依序顯示圖片"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"允許選取多項"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"最大媒體項目數量"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"輸入大於一的有效計數"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"點選媒體"</string> <string name="working_on_it" msgid="1373762827081252341">"正在處理"</string> </resources> diff --git a/tools/photopickerV2/res/values-zh-rTW/strings.xml b/tools/photopickerV2/res/values-zh-rTW/strings.xml index 6fa8ead20..20d5b98cc 100644 --- a/tools/photopickerV2/res/values-zh-rTW/strings.xml +++ b/tools/photopickerV2/res/values-zh-rTW/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"相片挑選工具 V2"</string> <string name="pick_images" msgid="5326258471545526911">"選擇圖片"</string> <string name="action_get_content" msgid="4319210475508093083">"操作取得內容"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"依序顯示圖片"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"允許多選"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"媒體數量上限"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"輸入大於一的有效數字"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"選擇媒體"</string> <string name="working_on_it" msgid="1373762827081252341">"處理中"</string> </resources> diff --git a/tools/photopickerV2/res/values-zu/strings.xml b/tools/photopickerV2/res/values-zu/strings.xml index ecff25132..dcd226374 100644 --- a/tools/photopickerV2/res/values-zu/strings.xml +++ b/tools/photopickerV2/res/values-zu/strings.xml @@ -8,10 +8,25 @@ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string> <string name="pick_images" msgid="5326258471545526911">"Khetha Izithombe"</string> <string name="action_get_content" msgid="4319210475508093083">"Isenzo Sokuthola Okuqukethwe"</string> + <!-- no translation found for open_document (8593796561386540777) --> + <skip /> + <!-- no translation found for open_document_tree (8979404185180480396) --> + <skip /> <string name="display_images_in_order" msgid="7880116254468536174">"Veza Izithombe Ngokulandelana"</string> + <!-- no translation found for show_images_only (6365019348435132030) --> + <skip /> + <!-- no translation found for show_videos_only (7302756380142587762) --> + <skip /> + <!-- no translation found for enter_mime_type (6599304148898478294) --> + <skip /> + <!-- no translation found for select_launch_tab (1219436289162294907) --> + <skip /> <string name="allow_multiple_selection" msgid="3485101220559262266">"Vumela Ukukhetha Okuningi"</string> + <!-- no translation found for allow_custom_mime_type (770032039896650761) --> + <skip /> <string name="max_number_of_media_items" msgid="2736386927806685966">"Umkhawulo wesibalo wezinto zemidiya"</string> - <string name="enter_valid_number" msgid="5077320506805084248">"Faka isibalo esivumelekile esingaphezu kokukodwa"</string> + <!-- no translation found for enter_valid_number (650407643348891234) --> + <skip /> <string name="pick_media" msgid="5269447618857205416">"Khetha Imidiya"</string> <string name="working_on_it" msgid="1373762827081252341">"Izama ukukwenza"</string> </resources> diff --git a/tools/photopickerV2/res/values/strings.xml b/tools/photopickerV2/res/values/strings.xml index d3ed2da7d..57e7d4827 100644 --- a/tools/photopickerV2/res/values/strings.xml +++ b/tools/photopickerV2/res/values/strings.xml @@ -9,7 +9,9 @@ <string name="action_get_content">Action Get Content</string> <string name="open_document">Open Document</string> <string name="open_document_tree">Open Document Tree</string> - <string name="display_images_in_order">Display Images in Order</string> + <string name="create_document">Create Document</string> + <string name="create_file">Create File</string> + <string name="display_order_of_selection">Display Order of Selection</string> <string name="show_images_only"> Show Images Only</string> <string name="show_videos_only"> Show Videos Only</string> <string name="enter_mime_type">Enter Mime Type</string> @@ -20,4 +22,16 @@ <string name="enter_valid_number">Enter a valid number greater than one</string> <string name="pick_media">Pick Media</string> <string name="working_on_it">Working on it</string> + <string name="show_metadata">Show Meta Data for the media selected</string> + <string name="enable_preselection">Enable Pre-selection</string> + <string name="request_permissions">Request Permissions</string> + <string name="request_permissions_for">Request Permissions for:</string> + <string name="picker_choice_unsupported"> + Picker Choice feature is only available for devices with Android U and above. + \n\nPlease upgrade your device to use this feature.</string> + <string name="images">Images</string> + <string name="videos">Videos</string> + <string name="both_images_and_videos">Both Images and Videos</string> + <string name="show_latest_selection_only">Show Latest Selection Only</string> + </resources>
\ No newline at end of file diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt index 407677953..1e5ab8f3b 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt @@ -17,10 +17,11 @@ package com.android.providers.media.tools.photopickerv2.docsui import android.app.Activity import android.net.Uri -import android.widget.Toast +import android.os.Build import android.widget.VideoView import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -53,6 +54,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.viewmodel.compose.viewModel import com.android.providers.media.tools.photopickerv2.R import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent +import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent import com.android.providers.media.tools.photopickerv2.utils.TextFieldComponent import com.android.providers.media.tools.photopickerv2.utils.isImage @@ -63,6 +65,7 @@ import com.bumptech.glide.integration.compose.GlideImage /** * This is the screen for the DocsUI tab. */ +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalGlideComposeApi::class) @Composable fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { @@ -75,6 +78,7 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { var isActionGetContentSelected by remember { mutableStateOf(true) } var isOpenDocumentSelected by remember { mutableStateOf(false) } + var isCreateDocumentSelected by remember { mutableStateOf(false) } var allowCustomMimeType by remember { mutableStateOf(false) } var selectedMimeType by remember { mutableStateOf("") } @@ -83,6 +87,9 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { var showImagesOnly by remember { mutableStateOf(false) } var showVideosOnly by remember { mutableStateOf(false) } + // Meta Data Details + var showMetaData by remember { mutableStateOf(false) } + // Color of ACTION_GET_CONTENT and OPEN_DOCUMENT button val getContentColor = if (isActionGetContentSelected){ ButtonDefaults.buttonColors() @@ -92,7 +99,13 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { ButtonDefaults.buttonColors() } else ButtonDefaults.buttonColors(Color.Gray) - val openDocumentTreeColor = if (!isActionGetContentSelected && !isOpenDocumentSelected) { + val createDocumentColor = if (isCreateDocumentSelected) { + ButtonDefaults.buttonColors() + } else ButtonDefaults.buttonColors(Color.Gray) + + val openDocumentTreeColor = if (!isActionGetContentSelected && + !isOpenDocumentSelected && + !isCreateDocumentSelected) { ButtonDefaults.buttonColors() } else ButtonDefaults.buttonColors(Color.Gray) @@ -127,10 +140,12 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { fun resetFeatureComponents( isGetContentSelected: Boolean, isOpenDocumentIntentSelected: Boolean, + isCreateDocumentIntentSelected: Boolean, selectedButtonType: Int ) { isActionGetContentSelected = isGetContentSelected isOpenDocumentSelected = isOpenDocumentIntentSelected + isCreateDocumentSelected = isCreateDocumentIntentSelected selectedButton = selectedButtonType allowMultiple = false showImagesOnly = false @@ -167,6 +182,7 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { resetFeatureComponents( isGetContentSelected = true, isOpenDocumentIntentSelected = false, + isCreateDocumentIntentSelected = false, selectedButtonType = R.string.action_get_content ) }, @@ -180,11 +196,12 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { resetFeatureComponents( isGetContentSelected = false, isOpenDocumentIntentSelected = true, + isCreateDocumentIntentSelected = false, selectedButtonType = R.string.open_document ) }, modifier = Modifier.weight(1f), - colors = openDocumentColor, + colors = openDocumentColor ) } @@ -201,12 +218,27 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { resetFeatureComponents( isGetContentSelected = false, isOpenDocumentIntentSelected = false, + isCreateDocumentIntentSelected = false, selectedButtonType = R.string.open_document_tree ) }, modifier = Modifier.weight(1f), colors = openDocumentTreeColor ) + + ButtonComponent( + label = stringResource(R.string.create_document), + onClick = { + resetFeatureComponents( + isGetContentSelected = false, + isOpenDocumentIntentSelected = false, + isCreateDocumentIntentSelected = true, + selectedButtonType = R.string.create_document + ) + }, + modifier = Modifier.weight(1f), + colors = createDocumentColor + ) } if (isActionGetContentSelected || isOpenDocumentSelected){ @@ -286,60 +318,92 @@ fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) { // Pick Media Button ButtonComponent( - label = stringResource(R.string.pick_media), + label = if (!isCreateDocumentSelected) { + stringResource(R.string.pick_media) + } else { + stringResource(R.string.create_file) + }, onClick = { + + // Resetting the custom Mime Type Box when allowCustomMimeType is unselected if (!allowCustomMimeType){ customMimeTypeInput = "" } + /* TODO: (@adityasngh) please check the URI below and fix this intent. + // For CREATE_DOCUMENT intent + val initialUri = Uri.parse("content://some/initial/uri") + val errorMessage = docsUIViewModel.validateAndLaunchPicker( isActionGetContentSelected = isActionGetContentSelected, isOpenDocumentSelected = isOpenDocumentSelected, + isCreateDocumentSelected = isCreateDocumentSelected, allowMultiple = allowMultiple, selectedMimeType = selectedMimeType, allowCustomMimeType = allowCustomMimeType, customMimeTypeInput = customMimeTypeInput, + pickerInitialUri = initialUri, launcher = launcher::launch ) if (errorMessage != null) { Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() } + */ } ) Spacer(modifier = Modifier.height(16.dp)) Column { - resultMedia.forEach { uri -> - if (isImage(context, uri)) { - // To display image - GlideImage( - model = uri, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .fillMaxSize() - .padding(top = 8.dp) - ) - } else { - AndroidView( - // To display video - factory = { ctx -> - VideoView(ctx).apply { - setVideoURI(uri) - start() - } - }, - modifier = Modifier - .fillMaxWidth() - .height(600.dp) - .padding(top = 8.dp) - ) + if (isActionGetContentSelected || isOpenDocumentSelected){ + // Switch for showing meta data + SwitchComponent( + label = stringResource(R.string.show_metadata), + checked = showMetaData, + onCheckedChange = { showMetaData = it } + ) + } + + if (!isCreateDocumentSelected){ + resultMedia.forEach { uri -> + if (showMetaData) { + MetaDataDetails( + uri = uri, + contentResolver = context.contentResolver, + showMetaData = showMetaData, + inDocsUITab = true + ) + } + if (isImage(context, uri)) { + // To display image + GlideImage( + model = uri, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .fillMaxSize() + .padding(top = 8.dp) + ) + } else { + AndroidView( + // To display video + factory = { ctx -> + VideoView(ctx).apply { + setVideoURI(uri) + start() + } + }, + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .padding(top = 8.dp) + ) + } + Spacer(modifier = Modifier.height(20.dp)) + HorizontalDivider(thickness = 6.dp) + Spacer(modifier = Modifier.height(17.dp)) } - Spacer(modifier = Modifier.height(20.dp)) - HorizontalDivider(thickness = 6.dp) - Spacer(modifier = Modifier.height(17.dp)) } } } diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt index adc52e777..c27d08e76 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt @@ -19,6 +19,7 @@ import android.app.Application import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri +import android.provider.DocumentsContract import android.widget.Toast import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -42,30 +43,37 @@ class DocsUIViewModel( fun validateAndLaunchPicker( isActionGetContentSelected: Boolean, isOpenDocumentSelected: Boolean, + isCreateDocumentSelected: Boolean, allowMultiple: Boolean, selectedMimeType: String, allowCustomMimeType: Boolean, customMimeTypeInput: String, + pickerInitialUri: Uri, launcher: (Intent) -> Unit ): String? { - var finalMimeType = "" - if (allowCustomMimeType) finalMimeType = customMimeTypeInput - else if (selectedMimeType != "") finalMimeType = selectedMimeType - else finalMimeType = "*/*" - val intent = if (isActionGetContentSelected) { Intent(Intent.ACTION_GET_CONTENT).apply { - type = finalMimeType + if (allowCustomMimeType) type = customMimeTypeInput + else if (selectedMimeType != "") type = selectedMimeType + else type = "*/*" putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple) addCategory(Intent.CATEGORY_OPENABLE) } } else if (isOpenDocumentSelected) { Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - type = finalMimeType + if (allowCustomMimeType) type = customMimeTypeInput + else if (selectedMimeType != "") type = selectedMimeType + else type = "*/*" putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple) addCategory(Intent.CATEGORY_OPENABLE) } + } else if (isCreateDocumentSelected){ + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/pdf" // TODO: (@adityasngh) please review and make it generic. + putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) + } } else { Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) } diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt index d8a87138a..eb423e29c 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt @@ -15,7 +15,6 @@ */ package com.android.providers.media.tools.photopickerv2.navigation -import android.annotation.SuppressLint import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -42,7 +41,6 @@ import com.android.providers.media.tools.photopickerv2.utils.NavigationComponent * MainScreen sets up the Scaffold with a bottom navigation bar * and hosts the NavGraph for navigation between the tabs. */ -@SuppressLint @Composable fun MainScreen() { val navController = rememberNavController() @@ -80,7 +78,6 @@ fun MainScreen() { * DocsUI * PickerChoice */ -@SuppressLint @Composable fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) { NavHost( diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt index f72a110c3..d45a9370f 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -36,9 +35,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -58,6 +59,7 @@ import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent import com.android.providers.media.tools.photopickerv2.utils.DropdownList import com.android.providers.media.tools.photopickerv2.utils.ErrorMessage import com.android.providers.media.tools.photopickerv2.utils.LaunchLocation +import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails import com.android.providers.media.tools.photopickerv2.utils.PhotoPickerTitle import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent import com.android.providers.media.tools.photopickerv2.utils.TextFieldComponent @@ -76,6 +78,9 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) var isOrderSelectionEnabled by remember { mutableStateOf(false) } var allowMultiple by remember { mutableStateOf(false) } var isActionGetContentSelected by remember { mutableStateOf(false) } + var selectedLaunchTab by remember { mutableStateOf(LaunchLocation.PHOTOS_TAB.name) } + var accentColor by remember { mutableStateOf("#FF6200EE") } // default + var allowCustomMimeType by remember { mutableStateOf(false) } var selectedMimeType by remember { mutableStateOf("") } @@ -83,11 +88,10 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) var showImagesOnly by remember { mutableStateOf(false) } var showVideosOnly by remember { mutableStateOf(false) } - var selectedLaunchTab by remember { mutableStateOf(LaunchLocation.PHOTOS_TAB.name) } // We can only take string as an input, not an int using OutlinedTextField var maxSelectionInput by remember { mutableStateOf("10") } - var maxMediaItemsDisplayed by remember { mutableStateOf(10) } // default items + var maxMediaItemsDisplayed by remember { mutableIntStateOf(10) } // default items var selectionErrorMessage by remember { mutableStateOf("") } var maxSelectionLimitError by remember { mutableStateOf("") } @@ -95,8 +99,13 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) // The Pick Images intent is selected by default var selectedButton by remember { mutableStateOf<Int?>(R.string.pick_images) } + // Meta Data Details + var showMetaData by remember { mutableStateOf(false) } + + var isPreSelectionEnabled by remember { mutableStateOf(false) } + // Color of PickImages and ACTION_GET_CONTENT button - val getContentColor = if (isActionGetContentSelected){ + val getContentColor = if (isActionGetContentSelected) { ButtonDefaults.buttonColors() } else ButtonDefaults.buttonColors(Color.Gray) @@ -104,7 +113,6 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) ButtonDefaults.buttonColors() } else ButtonDefaults.buttonColors(Color.Gray) - // For handling the result of the photo picking activity val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() @@ -142,7 +150,9 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) allowMultiple = false showImagesOnly = false showVideosOnly = false + showMetaData = false selectedMimeType = "" + accentColor = "#FF6200EE" resetMedia(photoPickerViewModel) isOrderSelectionEnabled = false maxSelectionInput = "10" // resetting the max Selection limit to default @@ -150,6 +160,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) allowCustomMimeType = false customMimeTypeInput = "" selectedLaunchTab = LaunchLocation.PHOTOS_TAB.toString() + isPreSelectionEnabled = false } Column( @@ -182,7 +193,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) ) // ACTION_GET_CONTENT will only support "images/*" and "videos/*" - // in the Photo picker tab + // in the PhotoPicker tab ButtonComponent( label = stringResource(id = R.string.action_get_content), onClick = { @@ -197,16 +208,15 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) } if (!isActionGetContentSelected) { - // Display Images in Order + // Display Order of Selection SwitchComponent( - label = stringResource(id = R.string.display_images_in_order), + label = stringResource(id = R.string.display_order_of_selection), checked = isOrderSelectionEnabled, onCheckedChange = { isOrderSelectionEnabled = it } ) - Spacer(modifier = Modifier.height(8.dp)) } - if (!allowCustomMimeType || isActionGetContentSelected){ + if (!allowCustomMimeType || isActionGetContentSelected) { // SHOW ONLY IMAGES OR VIDEOS Row( modifier = Modifier @@ -214,7 +224,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) .padding(vertical = 5.dp), verticalAlignment = Alignment.CenterVertically ) { - Column (modifier = Modifier.weight(1f)){ + Column(modifier = Modifier.weight(1f)) { SwitchComponent( label = stringResource(R.string.show_images_only), checked = showImagesOnly, @@ -224,7 +234,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) showVideosOnly = false selectedMimeType = "image/*" } else if (!showImagesOnly && !showVideosOnly) { - selectedMimeType = "*/*" + selectedMimeType = "" } } ) @@ -232,7 +242,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) Spacer(modifier = Modifier.width(6.dp)) - Column (modifier = Modifier.weight(1f)){ + Column(modifier = Modifier.weight(1f)) { SwitchComponent( label = stringResource(R.string.show_videos_only), checked = showVideosOnly, @@ -242,7 +252,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) showImagesOnly = false selectedMimeType = "video/*" } else if (!showImagesOnly && !showVideosOnly) { - selectedMimeType = "*/*" + selectedMimeType = "" } } ) @@ -250,7 +260,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) } } - if (!isActionGetContentSelected){ + if (!isActionGetContentSelected) { // Allow Custom Mime Type SwitchComponent( label = stringResource(id = R.string.allow_custom_mime_type), @@ -260,7 +270,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) } ) - if (allowCustomMimeType){ + if (allowCustomMimeType) { TextFieldComponent( // Custom Mime Type Input value = customMimeTypeInput, @@ -276,13 +286,33 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) // Launch Tab DropdownList( label = stringResource(id = R.string.select_launch_tab), - options = LaunchLocation.values().map { it.name }, + options = LaunchLocation.entries.map { it.name }, selectedOption = selectedLaunchTab, onOptionSelected = { selectedLaunchTab = it }, enabled = true ) } + if (!isActionGetContentSelected){ + // Accent Color + TextFieldComponent( + value = accentColor, + onValueChange = { color -> + accentColor = color + }, + label = "Accent Color" + ) + } + + if (!isActionGetContentSelected) { + // Switch for enabling pre-selection + SwitchComponent( + label = stringResource(R.string.enable_preselection), + checked = isPreSelectionEnabled, + onCheckedChange = { isPreSelectionEnabled = it } + ) + } + // Multiple Selection SwitchComponent( label = stringResource(id = R.string.allow_multiple_selection), @@ -343,6 +373,8 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) customMimeTypeInput = customMimeTypeInput, isOrderSelectionEnabled = isOrderSelectionEnabled, selectedLaunchTab = LaunchLocation.valueOf(selectedLaunchTab), + accentColor = accentColor, + isPreSelectionEnabled = isPreSelectionEnabled, launcher = launcher::launch ) if (errorMessage != null) { @@ -361,8 +393,23 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) Spacer(modifier = Modifier.height(16.dp)) - Column{ + Column { + // Switch for showing meta data + SwitchComponent( + label = stringResource(R.string.show_metadata), + checked = showMetaData, + onCheckedChange = { showMetaData = it } + ) + resultMedia.forEach { uri -> + if (showMetaData) { + MetaDataDetails( + uri = uri, + contentResolver = context.contentResolver, + showMetaData = showMetaData, + inDocsUITab = false + ) + } if (isImage(context, uri)) { // To display image GlideImage( @@ -370,7 +417,7 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) contentDescription = null, modifier = Modifier .fillMaxWidth() - .fillMaxSize() + .height(600.dp) .padding(top = 8.dp) ) } else { @@ -384,12 +431,14 @@ fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) }, modifier = Modifier .fillMaxWidth() - .height(200.dp) + .height(600.dp) .padding(top = 8.dp) ) } + Spacer(modifier = Modifier.height(20.dp)) + HorizontalDivider(thickness = 6.dp) + Spacer(modifier = Modifier.height(17.dp)) } } } } - diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt index e72acb01b..897e14633 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt @@ -37,7 +37,6 @@ class PhotoPickerViewModel( application: Application, ) : AndroidViewModel(application) { - private val _selectedMedia = MutableStateFlow<List<Uri>>(emptyList()) val selectedMedia: StateFlow<List<Uri>> = _selectedMedia @@ -67,11 +66,13 @@ class PhotoPickerViewModel( customMimeTypeInput: String, isOrderSelectionEnabled: Boolean, selectedLaunchTab: LaunchLocation, + accentColor: String, + isPreSelectionEnabled: Boolean, launcher: (Intent) -> Unit ): String? { if (!isActionGetContentSelected && allowMultiple){ if (maxMediaItemsDisplayed <= 1) { - return "Enter a valid number greater than one" + return "Enter a valid count greater than one" } if (maxMediaItemsDisplayed > _pickImagesMaxSelectionLimit) { @@ -79,6 +80,16 @@ class PhotoPickerViewModel( } } + if (accentColor == "") { + return "Enter an accent color" + } + + val accentColorLong: Long = try { + android.graphics.Color.parseColor(accentColor).toLong() + } catch (e: IllegalArgumentException) { + android.graphics.Color.parseColor("#FF6200EE").toLong() // Default color + } + val intent = if (isActionGetContentSelected) { // ACTION_GET_CONTENT supports only images and videos in the PhotoPicker tab Intent(Intent.ACTION_GET_CONTENT).apply { @@ -105,16 +116,23 @@ class PhotoPickerViewModel( if (selectedLaunchTab == LaunchLocation.ALBUMS_TAB) 0 else 1 ) putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, isOrderSelectionEnabled) + putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, accentColorLong) + if (isPreSelectionEnabled){ + Intent(putParcelableArrayListExtra( + "android.provider.extra.PICKER_PRE_SELECTION_URIS", + ArrayList(_selectedMedia.value) + )) + } } } + try { launcher(intent) } catch (e: ActivityNotFoundException) { val errorMessage = - "No Activity found to handle Intent with type \"" + intent.getType() + "\"" + "No Activity found to handle Intent with type \"" + intent.type + "\"" Toast.makeText(getApplication(), errorMessage, Toast.LENGTH_SHORT).show() } return null } -} - +}
\ No newline at end of file diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt index 65fe38d2c..9363a5e57 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt @@ -15,47 +15,262 @@ */ package com.android.providers.media.tools.photopickerv2.pickerchoice +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED +import android.os.Build +import android.widget.VideoView import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.viewmodel.compose.viewModel import com.android.providers.media.tools.photopickerv2.R +import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent +import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails +import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent +import com.android.providers.media.tools.photopickerv2.utils.isImage +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage /** * This is the screen for the PickerChoice tab. */ +@OptIn(ExperimentalGlideComposeApi::class) @Composable -fun PickerChoiceScreen() { - Column ( - modifier = Modifier.fillMaxSize() - ){ +fun PickerChoiceScreen(pickerChoiceViewModel: PickerChoiceViewModel = viewModel()) { + // When VERSION.SDK_INT is lower than VERSION U, then PickerChoice will not work on the device + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // Error message when the device's version is lower than Version U Text( - text = stringResource(id = R.string.tab_pickerchoice), + text = stringResource(id = R.string.picker_choice_unsupported), fontWeight = FontWeight.Bold, - fontSize = 25.sp, - modifier = Modifier.padding(16.dp) + fontSize = 17.sp, + modifier = Modifier.padding(20.dp) + .paddingFromBaseline(40.dp), + color = Color.Red ) - Row(modifier = Modifier - .fillMaxWidth() - .padding(vertical = 100.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + } else { + val context = LocalContext.current + + var requestPermissionForImagesOnly by remember { mutableStateOf(false) } + var requestPermissionForVideosOnly by remember { mutableStateOf(false) } + var requestPermissionForBoth by remember { mutableStateOf(false) } + + var showMetaData by remember { mutableStateOf(false) } + + val showLatestSelectionOnly by pickerChoiceViewModel + .latestSelectionOnly.observeAsState(false) + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val allGranted = permissions.values.all { it } + val partialGranted = permissions[READ_MEDIA_VISUAL_USER_SELECTED] == true || + requestPermissionForImagesOnly || + requestPermissionForVideosOnly + if (allGranted || partialGranted) { + pickerChoiceViewModel.checkPermissions(context.contentResolver) + } else { + Toast.makeText(context, "Permissions not granted", Toast.LENGTH_SHORT).show() + } + } + + fun resetPermissions() { + requestPermissionForImagesOnly = false + requestPermissionForVideosOnly = false + requestPermissionForBoth = false + } + + Column( + modifier = Modifier.run { + padding(16.dp) + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + } ){ Text( - text = stringResource(id = R.string.working_on_it), - fontWeight = FontWeight.Medium, - fontSize = 40.sp, + text = stringResource(id = R.string.tab_pickerchoice), + fontWeight = FontWeight.Bold, + fontSize = 25.sp, + modifier = Modifier.padding(5.dp) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource(R.string.request_permissions_for), + fontWeight = FontWeight.Bold, + fontSize = 17.sp ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + // Request Permission for Only Images + SwitchComponent( + label = stringResource(id = R.string.images), + checked = requestPermissionForImagesOnly, + onCheckedChange = { + requestPermissionForImagesOnly = it + if (it) { + resetPermissions() + requestPermissionForImagesOnly = true + } + } + ) + } + + Spacer(modifier = Modifier.width(6.dp)) + + Column(modifier = Modifier.weight(1f)) { + // Request Permission for Only Videos + SwitchComponent( + label = stringResource(id = R.string.videos), + checked = requestPermissionForVideosOnly, + onCheckedChange = { + requestPermissionForVideosOnly = it + if (it) { + resetPermissions() + requestPermissionForVideosOnly = true + } + } + ) + } + } + + // Request Permission for Both Images and Videos + SwitchComponent( + label = stringResource(id = R.string.both_images_and_videos), + checked = requestPermissionForBoth, + onCheckedChange = { + requestPermissionForBoth = it + if (it) { + resetPermissions() + requestPermissionForBoth = true + } + } + ) + + // Switch to enable show latest selection only + SwitchComponent( + label = stringResource(id = R.string.show_latest_selection_only), + checked = showLatestSelectionOnly, + onCheckedChange = { + pickerChoiceViewModel.setLatestSelectionOnly(it) + } + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 15.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + ButtonComponent( + label = stringResource(id = R.string.request_permissions), + onClick = { + when { + requestPermissionForImagesOnly -> + pickerChoiceViewModel.requestAppPermissions(imagesOnly = true) + requestPermissionForVideosOnly -> + pickerChoiceViewModel.requestAppPermissions(videosOnly = true) + requestPermissionForBoth -> + pickerChoiceViewModel.requestAppPermissions() + } + permissionLauncher.launch( + pickerChoiceViewModel.permissionRequest.value ?: arrayOf()) + }, + enabled = requestPermissionForImagesOnly || + requestPermissionForVideosOnly || + requestPermissionForBoth, + modifier = Modifier.weight(1f) + ) + } + + // Switch for showing meta data + SwitchComponent( + label = stringResource(R.string.show_metadata), + checked = showMetaData, + onCheckedChange = { showMetaData = it } + ) + + val mediaList by pickerChoiceViewModel.media.observeAsState(emptyList()) + DisplayMedia(mediaList, showMetaData) + } + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun DisplayMedia(mediaList: List<PickerChoiceViewModel.Media>, showMetaData: Boolean) { + Column { + mediaList.forEach { media -> + if (showMetaData) { + MetaDataDetails( + uri = media.uri, + contentResolver = LocalContext.current.contentResolver, + showMetaData = showMetaData, + inDocsUITab = false + ) + } + if (isImage(LocalContext.current, media.uri)) { + // To display image + GlideImage( + model = media.uri, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .padding(top = 8.dp) + ) + } else { + AndroidView( + // To display video + factory = { ctx -> + VideoView(ctx).apply { + setVideoURI(media.uri) + start() + } + }, + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .padding(top = 8.dp) + ) + } + Spacer(modifier = Modifier.height(20.dp)) + HorizontalDivider(thickness = 6.dp) + Spacer(modifier = Modifier.height(17.dp)) } } -}
\ No newline at end of file +} diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt index 1bcfe28af..a81f87b66 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt @@ -15,12 +15,159 @@ */ package com.android.providers.media.tools.photopickerv2.pickerchoice -import androidx.lifecycle.ViewModel +import android.Manifest.permission.READ_MEDIA_IMAGES +import android.Manifest.permission.READ_MEDIA_VIDEO +import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED +import android.app.Application +import android.content.ContentResolver +import android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER +import android.content.ContentUris +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker.PERMISSION_GRANTED +import androidx.core.os.bundleOf +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * PickerChoiceViewModel is responsible for managing the state and logic - * of the PhotoPicker feature. + * of the PickerChoice feature. */ -class PickerChoiceViewModel() : ViewModel() { - // Working on it -}
\ No newline at end of file +class PickerChoiceViewModel(application: Application) : AndroidViewModel(application) { + + private val _permissionRequest = MutableLiveData<Array<String>>() + val permissionRequest: LiveData<Array<String>> = _permissionRequest + + private val _media = MutableLiveData<List<Media>>(emptyList()) + val media: LiveData<List<Media>> get() = _media + + private val _latestSelectionOnly = MutableLiveData(false) + val latestSelectionOnly: LiveData<Boolean> get() = _latestSelectionOnly + + fun setLatestSelectionOnly(enabled: Boolean) { + _latestSelectionOnly.value = enabled + } + + /** + * Requests the necessary permissions for accessing media on the device. + * + * This method sets the appropriate permissions to request based on the + * provided parameters and the Android version. + * + * @param imagesOnly a Boolean flag indicating if only image permissions should be requested. + * @param videosOnly a Boolean flag indicating if only video permissions should be requested. + */ + fun requestAppPermissions(imagesOnly: Boolean = false, videosOnly: Boolean = false) { + when { + imagesOnly -> { + _permissionRequest.value = arrayOf(READ_MEDIA_IMAGES) + } + videosOnly -> { + _permissionRequest.value = arrayOf(READ_MEDIA_VIDEO) + } + else -> { + _permissionRequest.value = arrayOf( + READ_MEDIA_IMAGES, + READ_MEDIA_VIDEO, + READ_MEDIA_VISUAL_USER_SELECTED + ) + } + } + } + + /** + * Checks the permissions for accessing media on the device. + * + * This method checks if the application has been granted the + * READ_MEDIA_VISUAL_USER_SELECTED permission. If the device is + * running Android 14 (UPSIDE_DOWN_CAKE) or higher and the permission + * is granted, it shows a toast indicating partial access. Otherwise, + * it shows a toast indicating access denied. + */ + fun checkPermissions(contentResolver: ContentResolver) { + val context = getApplication<Application>().applicationContext + when { + ContextCompat.checkSelfPermission(context, READ_MEDIA_VISUAL_USER_SELECTED) == + PERMISSION_GRANTED -> { + Toast.makeText(context, "Partial access on Android 14 or higher", + Toast.LENGTH_SHORT).show() + fetchMedia(contentResolver) + } + else -> { + Toast.makeText(context, "Access denied", Toast.LENGTH_SHORT).show() + } + } + } + + private fun fetchMedia(contentResolver: ContentResolver) { + viewModelScope.launch { + _media.value = getMedia(contentResolver) + } + } + + data class Media( + val uri: Uri, + val name: String, + val size: Long, + val mimeType: String, + ) + private suspend fun getMedia( + contentResolver: ContentResolver + ): List<Media> = withContext(Dispatchers.IO) { + val projection = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.SIZE, + MediaStore.MediaColumns.MIME_TYPE, + ) + + val collectionUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) + + val mediaList = mutableListOf<Media>() + + // TODO: BuildCompat.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 12 + // @riyaghai : Please add this dependency in Android.bp + val queryArgs = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + latestSelectionOnly.value == true + ) { + bundleOf( + QUERY_ARG_SQL_SORT_ORDER to "${MediaStore.MediaColumns.DATE_ADDED} DESC", + "android:query-arg-latest-selection-only" to true + ) + } else { + null + } + + contentResolver.query( + collectionUri, + projection, + queryArgs, + null + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + val displayNameColumn = cursor.getColumnIndexOrThrow( + MediaStore.MediaColumns.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) + val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE) + + while (cursor.moveToNext()) { + val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn)) + val name = cursor.getString(displayNameColumn) + val size = cursor.getLong(sizeColumn) + val mimeType = cursor.getString(mimeTypeColumn) + + val media = Media(uri, name, size, mimeType) + mediaList.add(media) + } + } + return@withContext mediaList + } +} diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt index 480210fe3..5821629c2 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt @@ -15,10 +15,15 @@ */ package com.android.providers.media.tools.photopickerv2.utils +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.provider.MediaStore import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -58,6 +63,9 @@ import androidx.compose.ui.window.PopupProperties import androidx.navigation.NavController import com.android.providers.media.tools.photopickerv2.R import com.android.providers.media.tools.photopickerv2.navigation.NavigationItem +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale /** * PhotoPickerTitle is a composable function that displays the title of the PhotoPicker app. @@ -158,6 +166,7 @@ fun ErrorMessage( * @param onClick the callback function to be called when the button component is clicked. * @param modifier the modifier to be applied to the button component. * @param colors the color of the button. + * @param enabled the enabled state of the button component. */ @Composable fun ButtonComponent( @@ -165,11 +174,13 @@ fun ButtonComponent( onClick: () -> Unit, modifier: Modifier = Modifier, colors: ButtonColors = ButtonDefaults.buttonColors(), + enabled: Boolean = true ) { Button( onClick = onClick, colors = colors, - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), + enabled = enabled ) { Text(label) } @@ -300,13 +311,95 @@ fun DropdownList( } } +@Composable +fun MetaDataDetails( + uri: Uri, + contentResolver: ContentResolver, + showMetaData: Boolean, + inDocsUITab: Boolean +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (showMetaData) { + val cursor: Cursor? = contentResolver.query( + uri, null, null, null, null + ) + cursor?.use { + // Metadata Details for PhotoPicker Tab and PickerChoice Tab + if (!inDocsUITab){ + if (it.moveToNext()) { + val mediaUri = it.getString(it.getColumnIndexOrThrow( + MediaStore.Images.Media.DATA)) + val displayName = it.getString(it.getColumnIndexOrThrow( + MediaStore.Images.Media.DISPLAY_NAME)) + val size = it.getLong(it.getColumnIndexOrThrow( + MediaStore.Images.Media.SIZE)) + val sizeInKB = size / 1000 + val dateTaken = it.getLong(it.getColumnIndexOrThrow( + MediaStore.Images.Media.DATE_TAKEN)) + + val duration = + it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media.DURATION)) + val durationInSec = duration / 1000 + val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val dateString = formatter.format(Date(dateTaken)) + + Column { + Text( + text = "Meta Data Details:", + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + Text(text = "URI: $mediaUri") + Text(text = "Display Name: $displayName") + Text(text = "Size: $sizeInKB KB") + Text(text = "Date Taken: $dateString") + Text(text = "Duration: $durationInSec s") + } + } + } else { + // Metadata Details for DocsUI Tab + if (it.moveToNext()){ + val documentID = it.getLong(it.getColumnIndexOrThrow( + MediaStore.Images.Media.DOCUMENT_ID)) + val mimeType = it.getString(it.getColumnIndexOrThrow( + MediaStore.Images.Media.MIME_TYPE)) + val displayName = + it.getString(it.getColumnIndexOrThrow( + MediaStore.Images.Media.DISPLAY_NAME)) + val size = it.getLong(it.getColumnIndexOrThrow( + MediaStore.Images.Media.SIZE)) + val sizeInKB = size / 1000 + Column { + Text( + text = "Meta Data Details:", + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + ) + + Text(text = "Document ID: $documentID") + Text(text = "Display Name: $displayName") + Text(text = "Size: $sizeInKB KB") + Text(text = "Mime Type: $mimeType") + } + } + } + } + } + } +} + enum class LaunchLocation { PHOTOS_TAB, ALBUMS_TAB; companion object { fun getListOfAvailableLocations(): List<String> { - return values().toList().map { it -> it.name } + return entries.map { it -> it.name } } } } diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt index 451ff6189..f2ba32868 100644 --- a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt +++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt @@ -21,6 +21,7 @@ import android.net.Uri import com.android.providers.media.tools.photopickerv2.docsui.DocsUIViewModel import com.android.providers.media.tools.photopickerv2.photopicker.PhotoPickerViewModel +// This function is to check if the type of URI is image /** * isImage checks if the provided URI points to an image file. * @@ -50,7 +51,4 @@ fun resetMedia(photoPickerViewModel: PhotoPickerViewModel) { */ fun resetMedia(docsUIViewModel: DocsUIViewModel) { docsUIViewModel.updateSelectedMediaList(emptyList()) -} - - - +}
\ No newline at end of file |