Merge "Fixing the CursorWindow error log for cloud albums." into udc-mainline-prod
diff --git a/Android.bp b/Android.bp
index 412541a..785c8ee 100644
--- a/Android.bp
+++ b/Android.bp
@@ -20,6 +20,7 @@
"modules-utils-build",
"modules-utils-uieventlogger-interface",
"glide-prebuilt",
+ "glide-integration-recyclerview-prebuilt",
"glide-gifdecoder-prebuilt",
"glide-disklrucache-prebuilt",
"glide-annotation-and-compiler-prebuilt",
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index f10ca96..ce76d52 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -325,6 +325,20 @@
* {@hide}
*/
@VisibleForTesting
+ public static final String GET_RECOVERY_DATA = "get_recovery_data";
+
+ /**
+ * Only used for testing.
+ * {@hide}
+ */
+ @VisibleForTesting
+ public static final String REMOVE_RECOVERY_DATA = "remove_recovery_data";
+
+ /**
+ * Only used for testing.
+ * {@hide}
+ */
+ @VisibleForTesting
public static final String DELETE_BACKED_UP_FILE_PATHS = "delete_backed_up_file_paths";
/** {@hide} */
@@ -4782,6 +4796,25 @@
}
/**
+ * Only used for testing.
+ * {@hide}
+ */
+ @VisibleForTesting
+ public static String[] getRecoveryData(@NonNull ContentResolver resolver) {
+ Bundle bundle = resolver.call(AUTHORITY, GET_RECOVERY_DATA, null, null);
+ return bundle.getStringArray(GET_RECOVERY_DATA);
+ }
+
+ /**
+ * Only used for testing.
+ * {@hide}
+ */
+ @VisibleForTesting
+ public static void removeRecoveryData(@NonNull ContentResolver resolver) {
+ resolver.call(AUTHORITY, REMOVE_RECOVERY_DATA, null, null);
+ }
+
+ /**
* Block until any pending operations have finished, such as
* {@link #scanFile} or {@link #scanVolume} requests.
*
diff --git a/mediaproviderutils.sh b/mediaproviderutils.sh
index f25640d..f0b3dc6 100644
--- a/mediaproviderutils.sh
+++ b/mediaproviderutils.sh
@@ -1,5 +1,6 @@
# Shell utility functions for mediaprovider developers.
# sudo apt-get install rlwrap to have a more fully featured sqlite CLI
+# sudo apt-get install sqlitebrowser to navigate the database with a GUI
set -x # enable debugging
function add-media-grant () {
@@ -63,14 +64,9 @@
fi
}
-function sqlite3-pull () {
- adb root
- if [ -z "$1" ]
- then
- dir=$(pwd)
- else
- dir=$1
- fi
+function media-pull () {
+ adb root && adb wait-for-device
+ dir=$(get-dir $1)
package=$(get-package)
if [ -f "$dir/external.db" ]; then
@@ -86,10 +82,21 @@
sqlite3 $dir/external.db "drop trigger files_insert"
sqlite3 $dir/external.db "drop trigger files_update"
sqlite3 $dir/external.db "drop trigger files_delete"
-
- rlwrap sqlite3 $dir/external.db
}
+function sqlite3-pull () {
+ dir="$(get-dir $1)"
+ media-pull "$dir"
+ rlwrap sqlite3 "$dir"/external.db
+}
+
+function sqlitebrowser-pull () {
+ dir="$(get-dir "$1")"
+ media-pull "$dir"
+ sqlitebrowser "$dir"/external.db
+}
+
+
function sqlite3-push () {
adb root
if [ -z "$1" ]
@@ -145,6 +152,16 @@
adb shell sqlite3 $dir $clause
}
+function get-dir (){
+ if [ -z "$1" ]
+ then
+ dir=$(pwd)
+ else
+ dir=$1
+ fi
+ echo "$dir"
+}
+
function get-package() {
if [ -z "$(adb shell pm list package com.android.providers.media.module)" ]
then
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 707f517..e9f0466 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Voeg by (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Laat toe (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Laat geen toe nie"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Aflaaie"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Gunstelinge"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 70c3843..cf08f5c 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ንጥል}one{<xliff:g id="COUNT_1">^1</xliff:g> ንጥል}other{<xliff:g id="COUNT_1">^1</xliff:g> ንጥሎች}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) አክል"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ለ(<xliff:g id="COUNT">^1</xliff:g>) ፍቀድ"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ምንም አትፍቀድ"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ካሜራ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ውርዶች"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ተወዳጆች"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 56157d8..1cba3df 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{صورة واحدة (<xliff:g id="COUNT_0">^1</xliff:g>)}zero{<xliff:g id="COUNT_1">^1</xliff:g> صورة}two{صورتان (<xliff:g id="COUNT_1">^1</xliff:g>)}few{<xliff:g id="COUNT_1">^1</xliff:g> صور}many{<xliff:g id="COUNT_1">^1</xliff:g> صورة}other{<xliff:g id="COUNT_1">^1</xliff:g> صورة}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"إضافة (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"السماح (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"لم يتم اختيار أي صورة"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"الكاميرا"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"العناصر التي تم تنزيلها"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"العناصر المفضّلة"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 8f48c1a..ec19082 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> টা বস্তু}one{<xliff:g id="COUNT_1">^1</xliff:g> টা বস্তু}other{<xliff:g id="COUNT_1">^1</xliff:g> টা বস্তু}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g> টা) যোগ দিয়ক"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"অনুমতি দিয়ক (<xliff:g id="COUNT">^1</xliff:g> টা)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"এখনৰো অনুমতি নিদিব"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"কেমেৰা"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ডাউনল’ড"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"প্ৰিয়"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 9277b3b..5fc20b0 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}other{<xliff:g id="COUNT_1">^1</xliff:g> element}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Əlavə edin (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"İcazə verin (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Heç birinə icazə verməyin"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Endirmələr"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Sevimlilər"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index a9499ad..495ab4b 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> stavka}one{<xliff:g id="COUNT_1">^1</xliff:g> stavka}few{<xliff:g id="COUNT_1">^1</xliff:g> stavke}other{<xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dozvoli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ne dozvoli nijednu"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Preuzeto"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Omiljeno"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 75fff53..c20866c 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> элемент}one{<xliff:g id="COUNT_1">^1</xliff:g> элемент}few{<xliff:g id="COUNT_1">^1</xliff:g> элементы}many{<xliff:g id="COUNT_1">^1</xliff:g> элементаў}other{<xliff:g id="COUNT_1">^1</xliff:g> элемента}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Дадаць (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дазволіць (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Не дазваляць ніякія"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Спампоўкі"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Абранае"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index f67db6d..356cc1a 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> елемент}other{<xliff:g id="COUNT_1">^1</xliff:g> елемента}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Добавяне (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Разрешаване (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Забраняване на всички"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Изтегляния"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Любими"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index f4569a7..0f60019 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g>টি আইটেম}one{<xliff:g id="COUNT_1">^1</xliff:g>টি আইটেম}other{<xliff:g id="COUNT_1">^1</xliff:g>টি আইটেম}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>)টি যোগ করুন"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"অনুমতি দিন (<xliff:g id="COUNT">^1</xliff:g>টি)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"কারও অনুমতি নেই"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ক্যামেরা"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ডাউনলোড করা আইটেম"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"পছন্দসই আইটেম"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 480b0a6..f6da691 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> stavka}one{<xliff:g id="COUNT_1">^1</xliff:g> stavka}few{<xliff:g id="COUNT_1">^1</xliff:g> stavke}other{<xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dozvoli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nemoj dozvoliti ništa"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Preuzimanja"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Omiljeno"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 79279ed..ed89418 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}many{<xliff:g id="COUNT_1">^1</xliff:g> elements}other{<xliff:g id="COUNT_1">^1</xliff:g> elements}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Afegeix (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permet (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"No en permetis cap"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Càmera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Baixades"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Preferits"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 051c300..0b0773a 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> položka}few{<xliff:g id="COUNT_1">^1</xliff:g> položky}many{<xliff:g id="COUNT_1">^1</xliff:g> položky}other{<xliff:g id="COUNT_1">^1</xliff:g> položek}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Přidat (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Povolit (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nepovolit nic"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Fotoaparát"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Stažené soubory"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Oblíbené"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 9826ae5..cea19ca 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}one{<xliff:g id="COUNT_1">^1</xliff:g> element}other{<xliff:g id="COUNT_1">^1</xliff:g> elementer}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Tilføj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Tillad (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tillad ingen"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritter"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index e49ab53..37a6bc6 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> Element}other{<xliff:g id="COUNT_1">^1</xliff:g> Elemente}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Hinzufügen (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Erlauben (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Keines zulassen"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoriten"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 24717c4..2a44df5 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> στοιχείο}other{<xliff:g id="COUNT_1">^1</xliff:g> στοιχεία}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Προσθήκη (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Αποδοχή (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Δεν επιτρέπεται καμία"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Κάμερα"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Λήψεις"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Αγαπημένα"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 7ac8e5d..a719437 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favourites"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index a3f9cd3..c6b7563 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favorites"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 7ac8e5d..a719437 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favourites"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 7ac8e5d..a719437 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favourites"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 0060917..dbc133f 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favorites"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index adfd307..11b0976 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}many{<xliff:g id="COUNT_1">^1</xliff:g> elementos}other{<xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Agregar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"No permitir ninguna"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Cámara"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Descargas"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 5b89460..d7f2b64 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}many{<xliff:g id="COUNT_1">^1</xliff:g> elementos}other{<xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Añadir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"No permitir ninguna"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Cámara"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Descargas"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index c2b6e68..4aa4017 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> üksus}other{<xliff:g id="COUNT_1">^1</xliff:g> üksust}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Lisa (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Luba (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ära luba ühtki"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kaamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Allalaadimised"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Lemmikud"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 51d180a..49fb67a 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elementu}other{<xliff:g id="COUNT_1">^1</xliff:g> elementu}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Gehitu (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Eman baimena (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ez eman baimenik"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Deskargak"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Gogokoak"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index b059945..6209611 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> مورد}one{<xliff:g id="COUNT_1">^1</xliff:g> مورد}other{<xliff:g id="COUNT_1">^1</xliff:g> مورد}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"افزودن (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"اجازه دادن (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"مجاز کردن عدم بارگذاری"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"دوربین"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"بارگیریها"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"موارد دلخواه"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 3edc4bc..3f4101b 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> kohde}other{<xliff:g id="COUNT_1">^1</xliff:g> kohdetta}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Lisää (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Salli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Älä salli mitään"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Lataukset"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Suosikit"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 9b92d6a..1d46b00 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> élément}one{<xliff:g id="COUNT_1">^1</xliff:g> élément}many{<xliff:g id="COUNT_1">^1</xliff:g> éléments}other{<xliff:g id="COUNT_1">^1</xliff:g> éléments}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ajouter (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Autoriser (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ne rien autoriser"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Appareil photo"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Téléchargements"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoris"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 8d482cb..1122665 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> élément}one{<xliff:g id="COUNT_1">^1</xliff:g> élément}many{<xliff:g id="COUNT_1">^1</xliff:g> éléments}other{<xliff:g id="COUNT_1">^1</xliff:g> éléments}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ajouter (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Autoriser (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ne rien autoriser"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Appareil photo"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Téléchargements"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoris"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 38793c5..5b90bca 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}other{<xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Engadir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Non permitir ningunha"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Cámara"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Descargas"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index ddb547c..cbaf312 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> આઇટમ}one{<xliff:g id="COUNT_1">^1</xliff:g> આઇટમ}other{<xliff:g id="COUNT_1">^1</xliff:g> આઇટમ}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"ઉમેરો (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"મંજૂરી આપો (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"કોઈને મંજૂરી આપશો નહીં"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"કૅમેરા"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ડાઉનલોડ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"મનપસંદ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index a2f5528..070b0f9 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> आइटम}one{<xliff:g id="COUNT_1">^1</xliff:g> आइटम}other{<xliff:g id="COUNT_1">^1</xliff:g> आइटम}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) जोड़ें"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"अनुमति दें (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"कोई फ़ोटो न चुनें"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"कैमरा"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोड किए गए आइटम"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"पसंदीदा"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 9ce8a92..bd32b54 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> stavka}one{<xliff:g id="COUNT_1">^1</xliff:g> stavka}few{<xliff:g id="COUNT_1">^1</xliff:g> stavke}other{<xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dopusti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nemoj dopustiti prijenos"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Preuzimanja"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Omiljeno"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index f79bd96..0132ca5 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elem}other{<xliff:g id="COUNT_1">^1</xliff:g> elem}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Hozzáadás (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Engedélyezés (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Egy se legyen engedélyezve"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Letöltések"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Kedvencek"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 559bba5..df9771b 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> տարր}one{<xliff:g id="COUNT_1">^1</xliff:g> տարր}other{<xliff:g id="COUNT_1">^1</xliff:g> տարր}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ավելացնել (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Թույլատրել (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Արգելել բոլորը"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Տեսախցիկ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Ներբեռնումներ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Ընտրանի"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index efc0042..5431783 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> item}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Tambahkan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Izinkan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tidak ada"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Hasil download"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favorit"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index a6f1f44..211f051 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> atriði}one{<xliff:g id="COUNT_1">^1</xliff:g> atriði}other{<xliff:g id="COUNT_1">^1</xliff:g> atriði}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Bæta við (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Leyfa (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ekki leyfa neitt"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Myndavél"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Niðurhal"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Uppáhald"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 45a8f88..c03968a 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}many{<xliff:g id="COUNT_1">^1</xliff:g> elementi}other{<xliff:g id="COUNT_1">^1</xliff:g> elementi}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Aggiungi (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Consenti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Non consentire foto"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Fotocamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Download"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Preferiti"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 67b3268..4e76d6b 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{פריט אחד (<xliff:g id="COUNT_0">^1</xliff:g>)}one{<xliff:g id="COUNT_1">^1</xliff:g> פריטים}two{<xliff:g id="COUNT_1">^1</xliff:g> פריטים}other{<xliff:g id="COUNT_1">^1</xliff:g> פריטים}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"הוספה (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"אישור (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"לא להוסיף"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"מצלמה"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"הורדות"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"מועדפים"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 2080159..1524313 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 件のアイテム}other{<xliff:g id="COUNT_1">^1</xliff:g> 件のアイテム}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"追加(<xliff:g id="COUNT">^1</xliff:g> 件)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"許可(<xliff:g id="COUNT">^1</xliff:g> 件)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"すべて許可しない"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"カメラ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ダウンロード"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"お気に入り"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 7e6b8ce..d6700f3 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ერთეული}other{<xliff:g id="COUNT_1">^1</xliff:g> ერთეული}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"დამატება (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"დაშვება (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"არცერთის დაშვება"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"კამერა"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ჩამოტვირთვები"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"რჩეულები"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index ebde170..263ff2d 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> элемент}other{<xliff:g id="COUNT_1">^1</xliff:g> элемент}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Қосу (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Рұқсат (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ешқайсысына рұқсат етпеу"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Жүктеп алынғандар"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Таңдаулылар"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 740c09f..a72a992 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{ធាតុ <xliff:g id="COUNT_0">^1</xliff:g>}other{ធាតុ <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"បញ្ចូល (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"អនុញ្ញាត (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"អនុញ្ញាត \"គ្មាន\""</string>
<string name="picker_category_camera" msgid="4857367052026843664">"កាមេរ៉ា"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ការទាញយក"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"សំណព្វ"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index c3dee90..6baafe4 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ಐಟಂ}one{<xliff:g id="COUNT_1">^1</xliff:g> ಐಟಂಗಳು}other{<xliff:g id="COUNT_1">^1</xliff:g> ಐಟಂಗಳು}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"ಸೇರಿಸಿ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) ಅನುಮತಿಸಿ"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ಯಾವುದನ್ನೂ ಅನುಮತಿಸಬೇಡಿ"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ಕ್ಯಾಮರಾ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ಡೌನ್ಲೋಡ್ಗಳು"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ಮೆಚ್ಚಿನವುಗಳು"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 7344101..60ac833 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{항목 <xliff:g id="COUNT_0">^1</xliff:g>개}other{항목 <xliff:g id="COUNT_1">^1</xliff:g>개}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"추가(<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"허용(<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"허용 안 함"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"카메라"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"다운로드"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"즐겨찾기"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 91428e4..50f3f03 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> нерсе}other{<xliff:g id="COUNT_1">^1</xliff:g> нерсе}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Кошуу (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Уруксат берүү (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Уруксат берилбейт"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Жүктөлүп алынгандар"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Тандалмалар"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index bdac330..f70953a 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ລາຍການ}other{<xliff:g id="COUNT_1">^1</xliff:g> ລາຍການ}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"ເພີ່ມ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ອະນຸຍາດ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ບໍ່ອະນຸຍາດ"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ກ້ອງຖ່າຍຮູບ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ດາວໂຫຼດ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ລາຍການທີ່ມັກ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index d8b74a2..e8e06f0 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elementas}one{<xliff:g id="COUNT_1">^1</xliff:g> elementas}few{<xliff:g id="COUNT_1">^1</xliff:g> elementai}many{<xliff:g id="COUNT_1">^1</xliff:g> elemento}other{<xliff:g id="COUNT_1">^1</xliff:g> elementų}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Pridėti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Leisti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Niekas neleidžiama"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Vaizdo kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Atsisiuntimai"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Mėgstamiausi"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 353b431..4331651 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> vienums}zero{<xliff:g id="COUNT_1">^1</xliff:g> vienumu}one{<xliff:g id="COUNT_1">^1</xliff:g> vienums}other{<xliff:g id="COUNT_1">^1</xliff:g> vienumi}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Pievienot <xliff:g id="COUNT">^1</xliff:g>"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Atļaut (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Neatļaut nevienu"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Lejupielādes"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Izlase"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 165246c..3fdc1c0 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ставка}one{<xliff:g id="COUNT_1">^1</xliff:g> ставка}other{<xliff:g id="COUNT_1">^1</xliff:g> ставки}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Додај (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дозволи (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ниедна"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Преземања"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Омилени"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 0e06e43..d02f7e7 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ഇനം}other{<xliff:g id="COUNT_1">^1</xliff:g> ഇനങ്ങൾ}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) ചേർക്കുക"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) എണ്ണത്തെ അനുവദിക്കുക"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ഒന്നും അനുവദിക്കരുത്"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ക്യാമറ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ഡൗൺലോഡുകൾ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"പ്രിയപ്പെട്ടവ"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 743bf69..38d6f15 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> зүйл}other{<xliff:g id="COUNT_1">^1</xliff:g> зүйл}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Нэмэх (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Зөвшөөрөх (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Юуг ч бүү зөвшөөр"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камер"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Таталтууд"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Дуртай зүйлс"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index f6d0126..3fb8729 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> आयटम}other{<xliff:g id="COUNT_1">^1</xliff:g> आयटम}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) जोडा"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) ला अनुमती द्या"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"काहीही नाही याला अनुमती द्या"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"कॅमेरा"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोड"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"आवडते"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 04725d1..f96d6e4 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> item}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Tambah (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Benarkan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tiada yang dibenarkan"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Muat turun"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Kegemaran"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7ae3f6c..9a1013f 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{ဖိုင် <xliff:g id="COUNT_0">^1</xliff:g> ခု}other{ဖိုင် <xliff:g id="COUNT_1">^1</xliff:g> ခု}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) ခု ထည့်ရန်"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) ခု ခွင့်ပြုရန်"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"သုည ခွင့်ပြုရန်"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ကင်မရာ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ဒေါင်းလုဒ်များ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"စိတ်ကြိုက်များ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index ad68b4b..901f7ed 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}other{<xliff:g id="COUNT_1">^1</xliff:g> elementer}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Legg til (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Tillat (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tillat ingen"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Nedlastinger"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritter"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 2f15afa..eed492e 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> वटा वस्तु}other{<xliff:g id="COUNT_1">^1</xliff:g> वटा वस्तु}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"थप्नुहोस् (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"अनुमति दिनुहोस् (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"कुनै पनि फोटो प्रयोग गर्न नदिनुहोस्"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"क्यामेरा"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोडहरू"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"मन पर्ने कुराहरू"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 7c2726f..de4fcc2 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Toevoegen (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Toestaan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Geen toestaan"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favorieten"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 0c07414..afcb014 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g>ଟି ଆଇଟମ}other{<xliff:g id="COUNT_1">^1</xliff:g>ଟି ଆଇଟମ}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>)ଟି ଯୋଗ କରନ୍ତୁ"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ଅନୁମତି ଦିଅନ୍ତୁ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"କାହାରିକୁ ଅନୁମତି ଦିଅନ୍ତୁ ନାହିଁ"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"କେମେରା"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ଡାଉନଲୋଡଗୁଡ଼ିକ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ପସନ୍ଦଗୁଡ଼ିକ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index db4e165..c17c1d0 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ਆਈਟਮ}one{<xliff:g id="COUNT_1">^1</xliff:g> ਆਈਟਮ}other{<xliff:g id="COUNT_1">^1</xliff:g> ਆਈਟਮਾਂ}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) ਸ਼ਾਮਲ ਕਰੋ"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ਆਗਿਆ ਦਿਓ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ਕੋਈ ਵੀ ਆਗਿਆ ਨਾ ਦਿਓ"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"ਕੈਮਰਾ"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ਡਾਊਨਲੋਡ"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ਮਨਪਸੰਦ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 7d38cdc..cc3b1c5 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}few{<xliff:g id="COUNT_1">^1</xliff:g> elementy}many{<xliff:g id="COUNT_1">^1</xliff:g> elementów}other{<xliff:g id="COUNT_1">^1</xliff:g> elementu}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Zezwól (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nie zezwalaj na żadne"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Aparat"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Pobrane"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Ulubione"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index cb7b017..bef70c1 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}one{<xliff:g id="COUNT_1">^1</xliff:g> item}many{<xliff:g id="COUNT_1">^1</xliff:g> itens}other{<xliff:g id="COUNT_1">^1</xliff:g> itens}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adicionar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Não autorizar"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Câmera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a153742..a049d92 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}many{<xliff:g id="COUNT_1">^1</xliff:g> itens}other{<xliff:g id="COUNT_1">^1</xliff:g> itens}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adicionar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Não permitir nenhuma"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Câmara"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Transferências"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index cb7b017..bef70c1 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}one{<xliff:g id="COUNT_1">^1</xliff:g> item}many{<xliff:g id="COUNT_1">^1</xliff:g> itens}other{<xliff:g id="COUNT_1">^1</xliff:g> itens}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adicionar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Não autorizar"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Câmera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index ebde095..d9d0df9 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}few{<xliff:g id="COUNT_1">^1</xliff:g> elemente}other{<xliff:g id="COUNT_1">^1</xliff:g> de elemente}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adaugă (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permite (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nu permite nimic"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Cameră"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Descărcări"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Preferate"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index c996b2c..b8ff7b3 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> объект}one{<xliff:g id="COUNT_1">^1</xliff:g> объект}few{<xliff:g id="COUNT_1">^1</xliff:g> объекта}many{<xliff:g id="COUNT_1">^1</xliff:g> объектов}other{<xliff:g id="COUNT_1">^1</xliff:g> объекта}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Добавить (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Открыть доступ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Запретить доступ всем"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Скачанные"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Избранное"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 1211c73..e972106 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{අයිතම <xliff:g id="COUNT_0">^1</xliff:g>}one{අයිතම <xliff:g id="COUNT_1">^1</xliff:g>}other{අයිතම <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"එක් කරන්න (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ඉඩ දෙන්න (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"කිසිවකට ඉඩ නොදෙන්න"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"කැමරාව"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"බාගැනීම්"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ප්රියතමයන්"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index a359609..3a8f9fb 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> položka}few{<xliff:g id="COUNT_1">^1</xliff:g> položky}many{<xliff:g id="COUNT_1">^1</xliff:g> items}other{<xliff:g id="COUNT_1">^1</xliff:g> položiek}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Pridať (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Povoliť (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nepovoliť žiadne"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Stiahnuté"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Obľúbené"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index e49c7dd..08cb6a1 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}one{<xliff:g id="COUNT_1">^1</xliff:g> element}two{<xliff:g id="COUNT_1">^1</xliff:g> elementa}few{<xliff:g id="COUNT_1">^1</xliff:g> elementi}other{<xliff:g id="COUNT_1">^1</xliff:g> elementov}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dovoli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Dovoli brez izbire"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Fotoaparat"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Prenosi"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Priljubljeno"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 9e47b0e..0b60fde 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> artikull}other{<xliff:g id="COUNT_1">^1</xliff:g> artikuj}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Shto (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Lejo (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Mos lejo asnjë"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Shkarkimet"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Të preferuarat"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index a0cf7e2..726459e 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ставка}one{<xliff:g id="COUNT_1">^1</xliff:g> ставка}few{<xliff:g id="COUNT_1">^1</xliff:g> ставке}other{<xliff:g id="COUNT_1">^1</xliff:g> ставки}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Додај (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дозволи (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Не дозволи ниједну"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Преузето"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Омиљено"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index c2bcd79..d725331 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> objekt}other{<xliff:g id="COUNT_1">^1</xliff:g> objekt}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Lägg till (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Tillåt (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tillåt inga"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Nedladdningar"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoriter"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 1a59554..020b5a2 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{Kipengee <xliff:g id="COUNT_0">^1</xliff:g>}other{Vipengee <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Weka (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Ruhusu (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Usiruhusu yoyote"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Vipakuliwa"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Vipendwa"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index bd9e8a1..309ba9d 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ஆவணம்}other{<xliff:g id="COUNT_1">^1</xliff:g> ஆவணங்கள்}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) படங்களைச் சேர்"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"அனுமதி (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"எதையும் அனுமதிக்காதே"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"கேமரா"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"பதிவிறக்கங்கள்"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"பிடித்தவை"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 628443b..2621a48 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ఐటెమ్}other{<xliff:g id="COUNT_1">^1</xliff:g> ఐటెమ్లు}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"జోడించండి (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"అనుమతించండి (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"వేటినీ అనుమతించవద్దు"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"కెమెరా"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"డౌన్లోడ్లు"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"ఫేవరెట్స్"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 1d0c192..538d732 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> รายการ}other{<xliff:g id="COUNT_1">^1</xliff:g> รายการ}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"เพิ่ม (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"อนุญาต (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ไม่อนุญาต"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"กล้อง"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"การดาวน์โหลด"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"รายการโปรด"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 22c2151..ce60a92 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}one{<xliff:g id="COUNT_1">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> na item}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Magdagdag (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Payagan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Walang papayagan"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Mga Download"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Mga Paborito"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 12b3693..c959485 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> öğe}other{<xliff:g id="COUNT_1">^1</xliff:g> öğe}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ekle (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"İzin ver (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Hiçbirine izin verme"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"İndirilenler"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Favoriler"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 1eeee66..8ec4cfb 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> об’єкт}one{<xliff:g id="COUNT_1">^1</xliff:g> об’єкт}few{<xliff:g id="COUNT_1">^1</xliff:g> об’єкти}many{<xliff:g id="COUNT_1">^1</xliff:g> об’єктів}other{<xliff:g id="COUNT_1">^1</xliff:g> об’єкта}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Додати (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дозволити (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Не дозволяти жодної фотографії"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Завантаження"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Вибране"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index caa4195..dea4d10 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> آئٹم}other{<xliff:g id="COUNT_1">^1</xliff:g> آئٹمز}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) شامل کریں"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) کو اجازت دیں"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"کسی کو اجازت نہ دیں"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"کیمرا"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"ڈاؤن لوڈز"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"پسندیدہ"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index c496bd8..b97c5df 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ta narsa}other{<xliff:g id="COUNT_1">^1</xliff:g> ta narsa}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Kiritish (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Ruxsat (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Hech qanday ruxsat berilmasin"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Yuklanmalar"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Sevimlilar"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index d8afa9f..c0dd41d 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> mục}other{<xliff:g id="COUNT_1">^1</xliff:g> mục}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Thêm (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Cho phép (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Không cho phép"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Máy ảnh"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Tệp đã tải xuống"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Mục yêu thích"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 056a809..28c400c 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 个}other{<xliff:g id="COUNT_1">^1</xliff:g> 个}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"添加(<xliff:g id="COUNT">^1</xliff:g> 项)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"允许 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"全部不允许"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"相机"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"下载内容"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"收藏"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 55eeedf..870d569 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 個項目}other{<xliff:g id="COUNT_1">^1</xliff:g> 個項目}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"新增 (<xliff:g id="COUNT">^1</xliff:g> 個)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"允許 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"全部禁止"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"相機"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"下載"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"我的最愛"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 24756f6..4d2b5d6 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 個項目}other{<xliff:g id="COUNT_1">^1</xliff:g> 個項目}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"新增 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"允許 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"全部禁止"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"相機"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"下載的內容"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"收藏的內容"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 94fc4e2..fd42bc1 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -74,6 +74,7 @@
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{into <xliff:g id="COUNT_0">^1</xliff:g>}one{izinto <xliff:g id="COUNT_1">^1</xliff:g>}other{izinto <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Engeza (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Vumela (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+ <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ungavumeli lutho"</string>
<string name="picker_category_camera" msgid="4857367052026843664">"Ikhamera"</string>
<string name="picker_category_downloads" msgid="793866660287361900">"Okulandiwe"</string>
<string name="picker_category_favorites" msgid="7008495397818966088">"Izintandokazi"</string>
diff --git a/src/com/android/providers/media/AccessChecker.java b/src/com/android/providers/media/AccessChecker.java
index 986b444..adde626 100644
--- a/src/com/android/providers/media/AccessChecker.java
+++ b/src/com/android/providers/media/AccessChecker.java
@@ -363,16 +363,6 @@
return PACKAGE_USER_ID_COLUMN + "=" + callingIdentity.uid / MediaStore.PER_USER_RANGE;
}
- /**
- * Returns true if redaction is needed for openFile calls on picker uri by checking calling
- * package permission
- *
- * @param callingIdentity - the current caller
- */
- public static boolean isRedactionNeededForPickerUri(LocalCallingIdentity callingIdentity) {
- return callingIdentity.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
- }
-
@VisibleForTesting
static String getWhereForMediaTypeMatch(int mediaType) {
return bindSelection("media_type=?", mediaType);
diff --git a/src/com/android/providers/media/DatabaseBackupAndRecovery.java b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
index 5cfec37..ec864cd 100644
--- a/src/com/android/providers/media/DatabaseBackupAndRecovery.java
+++ b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
@@ -980,7 +980,9 @@
List<String> invalidUsers = getInvalidUsersList(xattrList, validUsers);
Log.i(TAG, "Invalid users list is " + invalidUsers);
for (String userIdToBeRemoved : invalidUsers) {
- removeRecoveryDataForUserId(Integer.parseInt(userIdToBeRemoved));
+ if (userIdToBeRemoved != null && !userIdToBeRemoved.trim().isEmpty()) {
+ removeRecoveryDataForUserId(Integer.parseInt(userIdToBeRemoved));
+ }
}
}
diff --git a/src/com/android/providers/media/LocalUriMatcher.java b/src/com/android/providers/media/LocalUriMatcher.java
index 9beebdb..888a619 100644
--- a/src/com/android/providers/media/LocalUriMatcher.java
+++ b/src/com/android/providers/media/LocalUriMatcher.java
@@ -72,12 +72,11 @@
static final int DOWNLOADS_ID = 801;
static final int PICKER = 900;
- public static final int PICKER_ID = 901;
+ static final int PICKER_ID = 901;
static final int PICKER_INTERNAL_MEDIA_ALL = 902;
static final int PICKER_INTERNAL_MEDIA_LOCAL = 903;
static final int PICKER_INTERNAL_ALBUMS_ALL = 904;
static final int PICKER_INTERNAL_ALBUMS_LOCAL = 905;
- public static final int PICKER_GET_CONTENT_ID = 906;
public static final int MEDIA_GRANTS = 1000;
@@ -122,11 +121,6 @@
// content://media/picker/<user-id>/<authority>/media/<media-id>
mPublic.addURI(auth, "picker/#/*/media/*", PICKER_ID);
- // content://media/picker_get_content/<user-id>/<media-id>
- mPublic.addURI(auth, "picker_get_content/#/#", PICKER_GET_CONTENT_ID);
- // content://media/picker_get_content/<user-id>/<authority>/media/<media-id>
- mPublic.addURI(auth, "picker_get_content/#/*/media/*", PICKER_GET_CONTENT_ID);
-
mPublic.addURI(auth, "cli", CLI);
mPublic.addURI(auth, "*/images/media", IMAGES_MEDIA);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 4fbb46b..7954565 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -61,7 +61,6 @@
import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
import static com.android.providers.media.AccessChecker.hasAccessToCollection;
import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
-import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID;
@@ -110,7 +109,6 @@
import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID;
import static com.android.providers.media.LocalUriMatcher.MEDIA_GRANTS;
import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER;
-import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID;
import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_ALL;
import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_LOCAL;
@@ -124,10 +122,8 @@
import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID;
import static com.android.providers.media.LocalUriMatcher.VOLUMES;
import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID;
-import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
-import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT;
import static com.android.providers.media.PickerUriResolver.getMediaUri;
-import static com.android.providers.media.photopicker.data.MediaGrantsProvider.EXTRA_MIME_TYPE_SELECTION;
+import static com.android.providers.media.photopicker.data.ItemsProvider.EXTRA_MIME_TYPE_SELECTION;
import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
import static com.android.providers.media.util.DatabaseUtils.bindList;
@@ -254,6 +250,7 @@
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.MediaStore.Video;
+import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
@@ -291,6 +288,7 @@
import com.android.providers.media.photopicker.data.ExternalDbFacade;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import com.android.providers.media.playlist.Playlist;
import com.android.providers.media.scan.MediaScanner;
import com.android.providers.media.scan.MediaScanner.ScanReason;
@@ -853,6 +851,23 @@
editor.commit();
}
}
+
+ boolean isDeviceInDemoMode = false;
+ try {
+ isDeviceInDemoMode = Settings.Global.getInt(
+ getContext().getContentResolver(), Settings.Global.DEVICE_DEMO_MODE)
+ > 0;
+ } catch (Settings.SettingNotFoundException e) {
+ Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e);
+ }
+
+ Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode);
+ // Only allow default system user 0 to update xattrs on /data/media/0 and
+ // only on retail demo devices
+ if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) {
+ mDatabaseBackupAndRecovery.removeRecoveryDataForUserId(
+ userToBeRemoved.getIdentifier());
+ }
break;
}
}
@@ -1310,16 +1325,16 @@
mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery);
mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache);
- mPickerDbFacade = new PickerDbFacade(context);
mMediaGrants = new MediaGrants(mExternalDatabase);
+ PickerSyncLockManager pickerSyncLockManager = new PickerSyncLockManager();
+ mPickerDbFacade = new PickerDbFacade(context, pickerSyncLockManager);
mPickerSyncController = PickerSyncController.initialize(context, mPickerDbFacade,
- mConfigStore);
+ mConfigStore, pickerSyncLockManager);
mPickerDataLayer = PickerDataLayer.create(context, mPickerDbFacade, mPickerSyncController,
mConfigStore);
- mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper,
- mUriMatcher);
+ mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper);
if (SdkLevel.isAtLeastS()) {
mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore);
@@ -1613,12 +1628,12 @@
// removing calling userId
userIds.remove(String.valueOf(sUserId));
- List<String> validUsers = mUserManager.getEnabledProfiles().stream()
+ List<String> validUserProfiles = mUserManager.getEnabledProfiles().stream()
.map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
Collectors.toList());
// removing all the valid/existing user, remaining userIds would be users who would have
// been removed
- userIds.removeAll(validUsers);
+ userIds.removeAll(validUserProfiles);
// Cleaning media files of users who have been removed
mExternalDatabase.runWithTransaction((db) -> {
@@ -1629,6 +1644,25 @@
});
return null ;
});
+
+ boolean isDeviceInDemoMode = false;
+ try {
+ isDeviceInDemoMode = Settings.Global.getInt(getContext().getContentResolver(),
+ Settings.Global.DEVICE_DEMO_MODE) > 0;
+ } catch (Settings.SettingNotFoundException e) {
+ Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e);
+ }
+
+ Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode);
+ // Only allow default system user 0 to update xattrs on /data/media/0 and only when
+ // device is in retail mode
+ if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) {
+ List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream()
+ .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
+ Collectors.toList());
+ Log.i(TAG, "Active user ids are:" + validUsers);
+ mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers);
+ }
}
private void pruneStalePackages(CancellationSignal signal) {
@@ -2322,14 +2356,13 @@
boolean result = false;
switch (segmentCount) {
case 1:
- // .../picker or .../picker_get_content
- if (lastSegment.equals(PICKER_SEGMENT) || lastSegment.equals(
- PICKER_GET_CONTENT_SEGMENT)) {
+ // .../picker
+ if (lastSegment.equals("picker")) {
result = file.exists() || file.mkdir();
}
break;
case 2:
- // .../picker/<user-id> or .../picker_get_content/<user-id>
+ // .../picker/<user-id>
try {
Integer.parseInt(lastSegment);
result = file.exists() || file.mkdir();
@@ -2339,24 +2372,21 @@
}
break;
case 3:
- // .../picker/<user-id>/<authority> or .../picker_get_content/<user-id>/<authority>
+ // .../picker/<user-id>/<authority>
result = preparePickerAuthorityPathSegment(file, lastSegment, uid);
break;
case 4:
- // .../picker/<user-id>/<authority>/media or
- // .../picker_get_content/<user-id>/<authority>/media
+ // .../picker/<user-id>/<authority>/media
if (lastSegment.equals("media")) {
result = file.exists() || file.mkdir();
}
break;
case 5:
- // .../picker/<user-id>/<authority>/media/<media-id.extension> or
- // .../picker_get_content/<user-id>/<authority>/media/<media-id.extension>
- final String pickerSegmentType = syntheticRelativePathSegments.get(0);
+ // .../picker/<user-id>/<authority>/media/<media-id.extension>
final String fileUserId = syntheticRelativePathSegments.get(1);
final String authority = syntheticRelativePathSegments.get(2);
- result = preparePickerMediaIdPathSegment(file, pickerSegmentType, authority,
- lastSegment, fileUserId, uid);
+ result = preparePickerMediaIdPathSegment(file, authority, lastSegment, fileUserId,
+ uid);
break;
}
@@ -2374,9 +2404,8 @@
new long[0]);
}
- // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic',
- // 'picker' or 'picker_get_content', '<user-id>', '<host>', 'media', '<fileName>']
- final String pickerSegmentType = segments[6];
+ // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic', 'picker', '<user-id>',
+ // '<host>', 'media', '<fileName>']
final String userId = segments[7];
final String fileName = segments[10];
final String host = segments[8];
@@ -2412,19 +2441,7 @@
try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
final String mimeType = MimeUtils.resolveMimeType(new File(path));
- // Picker segment indicates we need to force redact location metadata.
- // Picker_get_content indicates that we need to check A_M_L permission to decide if the
- // metadata needs to be redacted
- LocalCallingIdentity callingIdentityForOriginalUid = getCachedCallingIdentityForFuse(
- uid);
- final boolean isRedactionNeeded = pickerSegmentType.equalsIgnoreCase(PICKER_SEGMENT)
- || callingIdentityForOriginalUid == null
- || isRedactionNeededForPickerUri(callingIdentityForOriginalUid);
- Log.v(TAG, "Redaction needed for file open: " + isRedactionNeeded);
- long[] redactionRanges = new long[0];
- if (isRedactionNeeded) {
- redactionRanges = getRedactionRanges(fis, mimeType).redactionRanges;
- }
+ final long[] redactionRanges = getRedactionRanges(fis, mimeType).redactionRanges;
return new FileOpenResult(0 /* status */, uid, /* transformsUid */ 0,
/* nativeFd */ pfd.detachFd(), redactionRanges);
} catch (IOException e) {
@@ -2441,14 +2458,13 @@
return false;
}
- private boolean preparePickerMediaIdPathSegment(File file, String pickerSegmentType,
- String authority, String fileName, String userId, int uid) {
+ private boolean preparePickerMediaIdPathSegment(File file, String authority, String fileName,
+ String userId, int uid) {
final String mediaId = extractFileName(fileName);
- final String[] projection = new String[]{MediaStore.PickerMediaColumns.SIZE};
+ final String[] projection = new String[] { MediaStore.PickerMediaColumns.SIZE };
- final Uri uri = Uri.parse(
- "content://media/" + pickerSegmentType + "/" + userId + "/" + authority + "/media/"
- + mediaId);
+ final Uri uri = Uri.parse("content://media/picker/" + userId + "/" + authority + "/media/"
+ + mediaId);
try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid,
mCallingIdentity.get().getPackageName())) {
if (cursor != null && cursor.moveToFirst()) {
@@ -4025,7 +4041,6 @@
return Downloads.CONTENT_TYPE;
case PICKER_ID:
- case PICKER_GET_CONTENT_ID:
return mPickerUriResolver.getType(url, Binder.getCallingPid(),
Binder.getCallingUid());
}
@@ -6639,6 +6654,13 @@
case MediaStore.GET_BACKUP_FILES: {
return getResultForGetBackupFiles();
}
+ case MediaStore.GET_RECOVERY_DATA: {
+ return getResultForGetRecoveryData();
+ }
+ case MediaStore.REMOVE_RECOVERY_DATA: {
+ removeRecoveryData();
+ return new Bundle();
+ }
default:
throw new UnsupportedOperationException("Unsupported call: " + method);
}
@@ -7168,6 +7190,36 @@
return bundle;
}
+ @NotNull
+ private Bundle getResultForGetRecoveryData() {
+ getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+ "Permission missing to call GET_RECOVERY_DATA by "
+ + "uid:" + Binder.getCallingUid());
+
+ String[] xattrs = null;
+ try {
+ xattrs = Os.listxattr("/data/media/0");
+ } catch (ErrnoException e) {
+ Log.w(TAG, "Error in getting xattr list ", e);
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putStringArray(MediaStore.GET_RECOVERY_DATA, xattrs);
+ return bundle;
+ }
+
+ private void removeRecoveryData() {
+ getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+ "Permission missing to call REMOVE_RECOVERY_DATA by "
+ + "uid:" + Binder.getCallingUid());
+
+ List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream()
+ .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
+ Collectors.toList());
+ Log.i(TAG, "Active user ids are:" + validUsers);
+ mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers);
+ }
+
private String getSecurityExceptionMessage(String method) {
int callingUid = Binder.getCallingUid();
return String.format("%s not allowed. Calling app ID: %d, Calling UID %d. "
@@ -8589,7 +8641,7 @@
private boolean isPickerUri(Uri uri) {
final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden());
- return match == PICKER_ID || match == PICKER_GET_CONTENT_ID;
+ return match == PICKER_ID;
}
@Override
@@ -8616,20 +8668,9 @@
uri = safeUncanonicalize(uri);
if (isPickerUri(uri)) {
- int tid = Process.myTid();
- synchronized (mPendingOpenInfo) {
- mPendingOpenInfo.put(tid, new PendingOpenInfo(
- Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */
- false, /* transcodeReason */ 0));
- }
-
- try {
- return mPickerUriResolver.openFile(uri, mode, signal, mCallingIdentity.get());
- } finally {
- synchronized (mPendingOpenInfo) {
- mPendingOpenInfo.remove(tid);
- }
- }
+ final int callingPid = mCallingIdentity.get().pid;
+ final int callingUid = mCallingIdentity.get().uid;
+ return mPickerUriResolver.openFile(uri, mode, signal, callingPid, callingUid);
}
final boolean allowHidden = isCallingPackageAllowedHidden();
@@ -8762,21 +8803,10 @@
// This is needed for thumbnail resolution as it doesn't go through openFileCommon
if (isPickerUri(uri)) {
- int tid = Process.myTid();
- synchronized (mPendingOpenInfo) {
- mPendingOpenInfo.put(tid, new PendingOpenInfo(
- Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */
- false, /* transcodeReason */ 0));
- }
-
- try {
- return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal,
- mCallingIdentity.get());
- } finally {
- synchronized (mPendingOpenInfo) {
- mPendingOpenInfo.remove(tid);
- }
- }
+ final int callingPid = mCallingIdentity.get().pid;
+ final int callingUid = mCallingIdentity.get().uid;
+ return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal,
+ callingPid, callingUid);
}
// TODO: enforce that caller has access to this uri
diff --git a/src/com/android/providers/media/MediaProviderShellCommand.java b/src/com/android/providers/media/MediaProviderShellCommand.java
index ddeef00..3886160 100644
--- a/src/com/android/providers/media/MediaProviderShellCommand.java
+++ b/src/com/android/providers/media/MediaProviderShellCommand.java
@@ -34,6 +34,7 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.CloudProviderInfo;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
import java.io.OutputStream;
import java.io.PrintWriter;
@@ -187,7 +188,12 @@
// TODO(b/242550131): add PickerSyncController's API to make it possible to reset just one
// provider's library at a time (i.e. either CMP or local).
- mPickerSyncController.resetAllMedia();
+ try {
+ mPickerSyncController.resetAllMedia();
+ } catch (UnableToAcquireLockException e) {
+ pw.print("Could not reset all media" + e.getMessage());
+ return 1;
+ }
pw.println("Done.");
return 0;
diff --git a/src/com/android/providers/media/PickerUriResolver.java b/src/com/android/providers/media/PickerUriResolver.java
index 3a37c9a..b56b694 100644
--- a/src/com/android/providers/media/PickerUriResolver.java
+++ b/src/com/android/providers/media/PickerUriResolver.java
@@ -19,9 +19,6 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
-import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
-import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID;
-import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
import static com.android.providers.media.util.FileUtils.toFuseFile;
@@ -33,7 +30,6 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
-import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
@@ -62,9 +58,7 @@
public class PickerUriResolver {
private static final String TAG = "PickerUriResolver";
- public static final String PICKER_SEGMENT = "picker";
-
- public static final String PICKER_GET_CONTENT_SEGMENT = "picker_get_content";
+ private static final String PICKER_SEGMENT = "picker";
private static final String PICKER_INTERNAL_SEGMENT = "picker_internal";
/** A uri with prefix "content://media/picker" is considered as a picker uri */
public static final Uri PICKER_URI = MediaStore.AUTHORITY_URI.buildUpon().
@@ -90,28 +84,23 @@
private final PickerDbFacade mDbFacade;
private final Set<String> mAllValidProjectionColumns;
private final String[] mAllValidProjectionColumnsArray;
- private final LocalUriMatcher mLocalUriMatcher;
- PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper,
- LocalUriMatcher localUriMatcher) {
+ PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper) {
mContext = context;
mDbFacade = dbFacade;
mAllValidProjectionColumns = projectionHelper.getProjectionMap(
MediaStore.PickerMediaColumns.class).keySet();
mAllValidProjectionColumnsArray = mAllValidProjectionColumns.toArray(new String[0]);
- mLocalUriMatcher = localUriMatcher;
}
public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal,
- LocalCallingIdentity localCallingIdentity)
- throws FileNotFoundException {
+ int callingPid, int callingUid) throws FileNotFoundException {
if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
throw new SecurityException("PhotoPicker Uris can only be accessed to read."
+ " Uri: " + uri);
}
- checkPermissionForRequireOriginalQueryParam(uri, localCallingIdentity);
- checkUriPermission(uri, localCallingIdentity.pid, localCallingIdentity.uid);
+ checkUriPermission(uri, callingPid, callingUid);
final ContentResolver resolver;
try {
@@ -128,10 +117,9 @@
}
public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
- CancellationSignal signal, LocalCallingIdentity localCallingIdentity)
+ CancellationSignal signal, int callingPid, int callingUid)
throws FileNotFoundException {
- checkPermissionForRequireOriginalQueryParam(uri, localCallingIdentity);
- checkUriPermission(uri, localCallingIdentity.pid, localCallingIdentity.uid);
+ checkUriPermission(uri, callingPid, callingUid);
final ContentResolver resolver;
try {
@@ -222,8 +210,7 @@
+ CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER);
}
- private ParcelFileDescriptor openPickerFile(Uri uri)
- throws FileNotFoundException {
+ private ParcelFileDescriptor openPickerFile(Uri uri) throws FileNotFoundException {
final File file = getPickerFileFromUri(uri);
if (file == null) {
throw new FileNotFoundException("File not found for uri: " + uri);
@@ -248,38 +235,19 @@
@VisibleForTesting
Cursor queryPickerUri(Uri uri, String[] projection) {
- String pickerSegmentType = getPickerSegmentType(uri);
uri = unwrapProviderUri(uri);
- return mDbFacade.queryMediaIdForApps(pickerSegmentType, uri.getHost(),
- uri.getLastPathSegment(), projection);
+ return mDbFacade.queryMediaIdForApps(uri.getHost(), uri.getLastPathSegment(),
+ projection);
}
- private String getPickerSegmentType(Uri uri) {
- switch (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false)) {
- case PICKER_ID:
- return PICKER_SEGMENT;
- case PICKER_GET_CONTENT_ID:
- return PICKER_GET_CONTENT_SEGMENT;
- }
-
- return null;
- }
-
- /**
- * Creates a picker uri incorporating authority, user id and cloud provider.
- */
- public static Uri wrapProviderUri(Uri uri, String action, int userId) {
+ public static Uri wrapProviderUri(Uri uri, int userId) {
final List<String> segments = uri.getPathSegments();
if (segments.size() != 2) {
throw new IllegalArgumentException("Unexpected provider URI: " + uri);
}
Uri.Builder builder = initializeUriBuilder(MediaStore.AUTHORITY);
- if (action.equalsIgnoreCase(Intent.ACTION_GET_CONTENT)) {
- builder.appendPath(PICKER_GET_CONTENT_SEGMENT);
- } else {
- builder.appendPath(PICKER_SEGMENT);
- }
+ builder.appendPath(PICKER_SEGMENT);
builder.appendPath(String.valueOf(userId));
builder.appendPath(uri.getHost());
@@ -325,36 +293,10 @@
}
private void checkUriPermission(Uri uri, int pid, int uid) {
- // Clear query parameters to check for URI permissions, apps can add requireOriginal
- // query parameter to URI, URI grants will not be present in that case.
- Uri uriWithoutQueryParams = uri.buildUpon().clearQuery().build();
- if (!isSelf(uid) && mContext.checkUriPermission(uriWithoutQueryParams, pid, uid,
+ if (!isSelf(uid) && mContext.checkUriPermission(uri, pid, uid,
Intent.FLAG_GRANT_READ_URI_PERMISSION) != PERMISSION_GRANTED) {
throw new SecurityException("Calling uid ( " + uid + " ) does not have permission to " +
- "access picker uri: " + uriWithoutQueryParams);
- }
- }
-
- private void checkPermissionForRequireOriginalQueryParam(Uri uri,
- LocalCallingIdentity localCallingIdentity) {
- String value = uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL);
- if (value == null || value.isEmpty()) {
- return;
- }
-
- // Check if requireOriginal is set
- if (Integer.parseInt(value) == 1) {
- if (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false) == PICKER_ID) {
- throw new UnsupportedOperationException(
- "Require Original is not supported for Picker URI " + uri);
- }
-
- if (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false) == PICKER_GET_CONTENT_ID
- && isRedactionNeededForPickerUri(localCallingIdentity)) {
- throw new UnsupportedOperationException("Calling uid ( " + Binder.getCallingUid()
- + " ) does not have ACCESS_MEDIA_LOCATION permission for requesting "
- + "original file");
- }
+ "access picker uri: " + uri);
}
}
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index 4fe7556..2b33403 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -543,7 +543,7 @@
logPickerSelectionConfirmed(mSelection.getSelectedItems().size());
if (shouldPreloadSelectedItems()) {
final var uris = PickerResult.getPickerUrisForItems(
- getIntent().getAction(), mSelection.getSelectedItems());
+ mSelection.getSelectedItems());
mPreloaderInstanceHolder.preloader =
SelectedMediaPreloader.preload(/* activity */ this, uris);
deSelectUnavailableMedia(mPreloaderInstanceHolder.preloader);
@@ -571,8 +571,7 @@
// The permission controller will pass the requesting package's UID here
final Bundle extras = getIntent().getExtras();
final int uid = extras.getInt(Intent.EXTRA_UID);
- final List<Uri> uris = getPickerUrisForItems(getIntent().getAction(),
- mSelection.getSelectedItemsWithoutGrants());
+ final List<Uri> uris = getPickerUrisForItems(mSelection.getSelectedItemsWithoutGrants());
ForegroundThread.getExecutor().execute(() -> {
// Handle grants in another thread to not block the UI.
grantMediaReadForPackage(getApplicationContext(), uid, uris);
@@ -580,10 +579,9 @@
// Revoke READ_GRANT for items that were pre-granted but now in the current session user has
// deselected them.
- if (isUserSelectImagesForAppAction()
- && mPickerViewModel.getConfigStore().isPickerChoiceManagedSelectionEnabled()) {
+ if (mPickerViewModel.isManagedSelectionEnabled()) {
final List<Uri> urisForItemsWhoseGrantsNeedsToBeRevoked = getPickerUrisForItems(
- getIntent().getAction(), mSelection.getPreGrantedItemsToBeRevoked());
+ mSelection.getPreGrantedItemsToBeRevoked());
if (!urisForItemsWhoseGrantsNeedsToBeRevoked.isEmpty()) {
ForegroundThread.getExecutor().execute(() -> {
// Handle grants in another thread to not block the UI.
@@ -595,8 +593,9 @@
}
private void setResultForPickImagesOrGetContentAction() {
- final Intent resultData = getPickerResponseIntent(getIntent().getAction(),
- mSelection.canSelectMultiple(), mSelection.getSelectedItems());
+ final Intent resultData = getPickerResponseIntent(
+ mSelection.canSelectMultiple(),
+ mSelection.getSelectedItems());
setResult(RESULT_OK, resultData);
}
@@ -912,8 +911,7 @@
/**
* Reset to Photo Picker initial launch state (Photos grid tab) in the current profile mode.
*/
- @VisibleForTesting
- public void resetInCurrentProfile() {
+ private void resetInCurrentProfile() {
// Clear all the fragments in the FragmentManager
final FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.popBackStackImmediate(/* name */ null,
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index 52f85ff..67909d5 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -54,7 +54,6 @@
import android.util.ArraySet;
import android.util.Log;
-import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -66,8 +65,11 @@
import com.android.providers.media.photopicker.data.CloudProviderInfo;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.metrics.NonUiEventLogger;
+import com.android.providers.media.photopicker.sync.CloseableReentrantLock;
+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 java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -136,16 +138,9 @@
private final PickerDbFacade mDbFacade;
private final SharedPreferences mSyncPrefs;
private final SharedPreferences mUserPrefs;
+ private final PickerSyncLockManager mPickerSyncLockManager;
private final String mLocalProvider;
- private final Object mCloudSyncLock = new Object();
- private final Object mCloudAlbumSyncLock = new Object();
-
-
- // TODO(b/278562157): If there is a dependency on the sync process, always acquire the
- // {@link mCloudSyncLock} before {@link mCloudProviderLock} to avoid deadlock.
- private final Object mCloudProviderLock = new Object();
- @GuardedBy("mCloudProviderLock")
private CloudProviderInfo mCloudProviderInfo;
@Nullable
private static PickerSyncController sInstance;
@@ -161,8 +156,10 @@
*/
@NonNull
public static PickerSyncController initialize(@NonNull Context context,
- @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore) {
- return initialize(context, dbFacade, configStore, LOCAL_PICKER_PROVIDER_AUTHORITY);
+ @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull
+ PickerSyncLockManager pickerSyncLockManager) {
+ return initialize(context, dbFacade, configStore, pickerSyncLockManager,
+ LOCAL_PICKER_PROVIDER_AUTHORITY);
}
/**
@@ -180,8 +177,8 @@
@VisibleForTesting
public static PickerSyncController initialize(@NonNull Context context,
@NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore,
- @NonNull String localProvider) {
- sInstance = new PickerSyncController(context, dbFacade, configStore,
+ @NonNull PickerSyncLockManager pickerSyncLockManager, @NonNull String localProvider) {
+ sInstance = new PickerSyncController(context, dbFacade, configStore, pickerSyncLockManager,
localProvider);
return sInstance;
}
@@ -209,7 +206,8 @@
}
private PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade,
- @NonNull ConfigStore configStore, @NonNull String localProvider) {
+ @NonNull ConfigStore configStore, @NonNull PickerSyncLockManager pickerSyncLockManager,
+ @NonNull String localProvider) {
mContext = context;
mConfigStore = configStore;
mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME,
@@ -217,6 +215,7 @@
mUserPrefs = mContext.getSharedPreferences(PICKER_USER_PREFS_FILE_NAME,
Context.MODE_PRIVATE);
mDbFacade = dbFacade;
+ mPickerSyncLockManager = pickerSyncLockManager;
mLocalProvider = localProvider;
// Listen to the device config, and try to enable cloud features when the config changes.
@@ -224,8 +223,14 @@
initCloudProvider();
}
+ @NonNull
+ public PickerSyncLockManager getPickerSyncLockManager() {
+ return mPickerSyncLockManager;
+ }
+
private void initCloudProvider() {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (!mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
Log.d(TAG, "Cloud-Media-in-Photo-Picker feature is disabled during " + TAG
+ " construction.");
@@ -262,14 +267,6 @@
}
/**
- * Returns the sync lock object for Cloud albums.
- * @return the lock object for protecting synchronized code related to cloud albums.
- */
- public Object getCloudAlbumSyncLock() {
- return mCloudAlbumSyncLock;
- }
-
- /**
* Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances
*/
public void syncAllMedia() {
@@ -305,8 +302,9 @@
*/
public void syncAllMediaFromCloudProvider(@Nullable CancellationSignal cancellationSignal) {
- synchronized (mCloudSyncLock) {
- final String cloudProvider = getCloudProvider();
+ try (CloseableReentrantLock ignored =
+ mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) {
+ final String cloudProvider = getCloudProviderWithTimeout();
// Trigger a sync.
final InstanceId instanceId = NonUiEventLogger.generateInstanceId();
@@ -320,6 +318,8 @@
+ ". The cloud provider may have changed during the sync, or only a"
+ " partial sync was completed.");
}
+ } catch (UnableToAcquireLockException e) {
+ Log.e(TAG, "Could not sync with the cloud provider", e);
}
}
@@ -332,8 +332,13 @@
executeSyncAlbumReset(getLocalProvider(), isLocal, albumId);
syncAlbumMediaFromLocalProvider(albumId, /* cancellationSignal=*/ null);
} else {
- synchronized (mCloudAlbumSyncLock) {
- executeSyncAlbumReset(getCloudProvider(), isLocal, albumId);
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) {
+ executeSyncAlbumReset(getCloudProviderWithTimeout(), isLocal, albumId);
+ } catch (UnableToAcquireLockException e) {
+ Log.e(TAG, "Unable to reset cloud album media " + albumId, e);
+ // Continue to attempt cloud album sync. This may show deleted album media on
+ // the album view.
}
syncAlbumMediaFromCloudProvider(albumId, /*cancellationSignal=*/ null);
}
@@ -349,9 +354,12 @@
/** Syncs album media from the currently enabled cloud {@link CloudMediaProvider}. */
public void syncAlbumMediaFromCloudProvider(
@NonNull String albumId, @Nullable CancellationSignal cancellationSignal) {
- synchronized (mCloudAlbumSyncLock) {
- syncAlbumMediaFromProvider(getCloudProvider(), /* isLocal */ false, albumId,
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) {
+ syncAlbumMediaFromProvider(getCloudProviderWithTimeout(), /* isLocal */ false, albumId,
/* enablePagedSync= */ true, cancellationSignal);
+ } catch (UnableToAcquireLockException e) {
+ Log.e(TAG, "Unable to sync cloud album media " + albumId, e);
}
}
@@ -359,14 +367,20 @@
* Resets media library previously synced from the current {@link CloudMediaProvider} as well
* as the {@link #mLocalProvider local provider}.
*/
- public void resetAllMedia() {
+ public void resetAllMedia() throws UnableToAcquireLockException {
+ // No need to acquire cloud lock for local reset.
resetAllMedia(mLocalProvider, /* isLocal */ true);
- synchronized (mCloudSyncLock) {
+
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) {
+
+ // This does not fall in any sync path. Try to acquire the lock indefinitely.
resetAllMedia(getCloudProvider(), /* isLocal */ false);
}
}
- private boolean resetAllMedia(@Nullable String authority, boolean isLocal) {
+ private boolean resetAllMedia(@Nullable String authority, boolean isLocal)
+ throws UnableToAcquireLockException {
Trace.beginSection(traceSectionName("resetAllMedia", isLocal));
try {
executeSyncReset(authority, isLocal);
@@ -445,7 +459,8 @@
Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
}
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (Objects.equals(mCloudProviderInfo.authority, authority)) {
Log.w(TAG, "Cloud provider already set: " + authority);
return true;
@@ -454,12 +469,13 @@
final CloudProviderInfo newProviderInfo = getCloudProviderInfo(authority, ignoreAllowList);
if (authority == null || !newProviderInfo.isEmpty()) {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
// Disable cloud provider queries on the db until next sync
// This will temporarily *clear* the cloud provider on the db facade and prevent
// any queries from seeing cloud media until a sync where the cloud provider will be
// reset on the facade
- disablePickerCloudMediaQueries(/* isLocal */ false);
+ mDbFacade.setCloudProvider(null);
final String oldAuthority = mCloudProviderInfo.authority;
persistCloudProviderInfo(newProviderInfo, /* shouldUnset */ true);
@@ -485,7 +501,8 @@
*/
@NonNull
public CloudProviderInfo getCurrentCloudProviderInfo() {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
return mCloudProviderInfo;
}
}
@@ -496,19 +513,38 @@
* disabled by the user.
*/
private void setCurrentCloudProviderInfo(@NonNull CloudProviderInfo cloudProviderInfo) {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
mCloudProviderInfo = cloudProviderInfo;
}
}
/**
+ * This should not be used in picker sync paths because we should not wait on a lock
+ * indefinitely during the picker sync process.
+ * Use {@link this#getCloudProviderWithTimeout()} instead.
* @return {@link android.content.pm.ProviderInfo#authority authority} of the current
* {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider}
* integration is not enabled.
*/
@Nullable
public String getCloudProvider() {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+ return mCloudProviderInfo.authority;
+ }
+ }
+
+ /**
+ * @return {@link android.content.pm.ProviderInfo#authority authority} of the current
+ * {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider}
+ * integration is not enabled. This operation acquires a lock internally with a timeout.
+ * @throws UnableToAcquireLockException if the lock was not acquired within the given timeout.
+ */
+ @Nullable
+ public String getCloudProviderWithTimeout() throws UnableToAcquireLockException {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
return mCloudProviderInfo.authority;
}
}
@@ -526,7 +562,8 @@
return true;
}
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (!mCloudProviderInfo.isEmpty()
&& Objects.equals(mCloudProviderInfo.authority, authority)) {
return true;
@@ -541,7 +578,8 @@
return true;
}
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (!mCloudProviderInfo.isEmpty() && uid == mCloudProviderInfo.uid
&& Objects.equals(mCloudProviderInfo.authority, authority)) {
return true;
@@ -574,7 +612,8 @@
* Notifies about package removal
*/
public void notifyPackageRemoval(String packageName) {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (mCloudProviderInfo.matches(packageName)) {
Log.i(TAG, "Package " + packageName
+ " is the current cloud provider and got removed");
@@ -584,7 +623,8 @@
}
private void resetCloudProvider() {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
setCloudProvider(/* authority */ null);
/**
@@ -621,7 +661,7 @@
executeSyncAddAlbum(
authority, isLocal, albumId, queryArgs, instanceId, cancellationSignal);
}
- } catch (RuntimeException e) {
+ } catch (RuntimeException | UnableToAcquireLockException 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
@@ -717,13 +757,17 @@
Log.e(TAG, "Failed to sync all media because authority has changed: ", e);
} catch (IllegalStateException e) {
// If we're in an illegal state, reset and start a full sync again.
- resetAllMedia(authority, isLocal);
Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e);
- if (retryOnFailure) {
- return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false,
- enablePagedSync, instanceId, cancellationSignal);
+ try {
+ resetAllMedia(authority, isLocal);
+ if (retryOnFailure) {
+ return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false,
+ enablePagedSync, instanceId, cancellationSignal);
+ }
+ } catch (UnableToAcquireLockException ex) {
+ Log.e(TAG, "Could not reset media", e);
}
- } catch (RuntimeException e) {
+ } catch (RuntimeException | UnableToAcquireLockException e) {
// Retry the failed operation to see if it was an intermittent problem. If this fails,
// the database will be in a partial state until the sync resumes from this point
// on next run.
@@ -742,9 +786,10 @@
* Disable cloud media queries from Picker database. After disabling cloud media queries, when a
* media query will run on Picker database, only local media items will be returned.
*/
- private void disablePickerCloudMediaQueries(boolean isLocal) {
+ private void disablePickerCloudMediaQueries(boolean isLocal)
+ throws UnableToAcquireLockException {
if (!isLocal) {
- mDbFacade.setCloudProvider(null);
+ mDbFacade.setCloudProviderWithTimeout(null);
}
}
@@ -752,11 +797,13 @@
* Enable cloud media queries from Picker database. After enabling cloud media queries, when a
* media query will run on Picker database, both local and cloud media items will be returned.
*/
- private void enablePickerCloudMediaQueries(String authority, boolean isLocal) {
+ private void enablePickerCloudMediaQueries(String authority, boolean isLocal)
+ throws UnableToAcquireLockException {
if (!isLocal) {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (Objects.equals(mCloudProviderInfo.authority, authority)) {
- mDbFacade.setCloudProvider(authority);
+ mDbFacade.setCloudProviderWithTimeout(authority);
}
}
}
@@ -817,7 +864,7 @@
Bundle queryArgs,
InstanceId instanceId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException {
+ throws RequestObsoleteException, UnableToAcquireLockException {
final Uri uri = getMediaUri(authority);
final List<String> expectedHonoredArgs = new ArrayList<>();
if (isIncrementalSync) {
@@ -867,7 +914,7 @@
Bundle queryArgs,
InstanceId instanceId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException {
+ throws RequestObsoleteException, UnableToAcquireLockException {
final Uri uri = getMediaUri(authority);
Log.i(TAG, "Executing SyncAddAlbum. "
@@ -918,7 +965,7 @@
Bundle queryArgs,
InstanceId instanceId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException {
+ throws RequestObsoleteException, UnableToAcquireLockException {
final Uri uri = getDeletedMediaUri(authority);
Log.i(TAG, "Executing SyncRemove. isLocal: " + isLocal + ". authority: " + authority);
@@ -949,7 +996,8 @@
* Persist cloud provider info and send a sync request to the background thread.
*/
private void persistCloudProviderInfo(@NonNull CloudProviderInfo info, boolean shouldUnset) {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
setCurrentCloudProviderInfo(info);
final String authority = info.authority;
@@ -981,7 +1029,11 @@
Log.d(TAG, "Updated cloud provider to: " + authority);
- resetCachedMediaCollectionInfo(info.authority, /* isLocal */ false);
+ try {
+ resetCachedMediaCollectionInfo(info.authority, /* isLocal */ false);
+ } catch (UnableToAcquireLockException e) {
+ Log.wtf(TAG, "CLOUD_PROVIDER_LOCK is already held by this thread.");
+ }
sendPickerUiRefreshNotification();
}
@@ -1010,7 +1062,7 @@
* Commit the latest media collection info when a sync operation is completed.
*/
private boolean cacheMediaCollectionInfo(@Nullable String authority, boolean isLocal,
- @Nullable Bundle bundle) {
+ @Nullable Bundle bundle) throws UnableToAcquireLockException {
if (authority == null) {
Log.d(TAG, "Ignoring cache media info for null authority with bundle: " + bundle);
return true;
@@ -1023,7 +1075,8 @@
cacheMediaCollectionInfoInternal(isLocal, bundle);
return true;
} else {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
// Check if the media collection info belongs to the current cloud provider
// authority.
if (Objects.equals(authority, mCloudProviderInfo.authority)) {
@@ -1072,9 +1125,11 @@
* @param token The token to remember. A null value will clear the preference.
* @param resumeKey The operation's key in sync preferences.
*/
- private void rememberNextPageToken(@Nullable String token, String resumeKey) {
+ private void rememberNextPageToken(@Nullable String token, String resumeKey)
+ throws UnableToAcquireLockException {
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
final SharedPreferences.Editor editor = mSyncPrefs.edit();
if (token == null) {
Log.d(TAG, String.format("Clearing next page token for key: %s", resumeKey));
@@ -1096,13 +1151,15 @@
* @return The PageToken to resume from, or {@code null} if there is no operation to resume.
*/
@Nullable
- public String getPageTokenFromResumeKey(String resumeKey) {
- synchronized (mCloudProviderLock) {
+ private String getPageTokenFromResumeKey(String resumeKey) throws UnableToAcquireLockException {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
return mSyncPrefs.getString(resumeKey, /* defValue= */ null);
}
}
- private boolean resetCachedMediaCollectionInfo(@Nullable String authority, boolean isLocal) {
+ private boolean resetCachedMediaCollectionInfo(@Nullable String authority, boolean isLocal)
+ throws UnableToAcquireLockException {
return cacheMediaCollectionInfo(authority, isLocal, /* bundle */ null);
}
@@ -1136,12 +1193,13 @@
@NonNull
private SyncRequestParams getSyncRequestParams(@Nullable String authority,
- boolean isLocal) throws RequestObsoleteException {
+ boolean isLocal) throws RequestObsoleteException, UnableToAcquireLockException {
if (isLocal) {
return getSyncRequestParamsInternal(authority, isLocal);
} else {
// Ensure that we are fetching sync request params for the current cloud provider.
- synchronized (mCloudProviderLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (Objects.equals(mCloudProviderInfo.authority, authority)) {
return getSyncRequestParamsInternal(authority, isLocal);
} else {
@@ -1264,7 +1322,8 @@
@OperationType int op,
String authority,
Boolean isLocal,
- @Nullable CancellationSignal cancellationSignal) throws RequestObsoleteException {
+ @Nullable CancellationSignal cancellationSignal)
+ throws RequestObsoleteException, UnableToAcquireLockException {
return executePagedSync(
uri,
expectedMediaCollectionId,
@@ -1308,7 +1367,8 @@
String authority,
Boolean isLocal,
@Nullable String albumId,
- @Nullable CancellationSignal cancellationSignal) throws RequestObsoleteException {
+ @Nullable CancellationSignal cancellationSignal)
+ throws RequestObsoleteException, UnableToAcquireLockException {
Trace.beginSection(traceSectionName("executePagedSync"));
try {
@@ -1350,7 +1410,7 @@
if (!isLocal) {
// Ensure the cloud provider hasn't change out from underneath the
// running sync. If it has, we need to stop syncing.
- String currentCloudProvider = getCloudProvider();
+ String currentCloudProvider = getCloudProviderWithTimeout();
if (TextUtils.isEmpty(currentCloudProvider)
|| !currentCloudProvider.equals(authority)) {
diff --git a/src/com/android/providers/media/photopicker/data/ItemsProvider.java b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
index 1c6f2c0..6e58395 100644
--- a/src/com/android/providers/media/photopicker/data/ItemsProvider.java
+++ b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
@@ -19,17 +19,21 @@
import static android.content.ContentResolver.QUERY_ARG_LIMIT;
import static android.database.DatabaseUtils.dumpCursorToString;
import static android.provider.MediaStore.AUTHORITY;
+import static android.provider.MediaStore.MediaColumns.DATA;
+import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_DATE_TAKEN_BEFORE_MS;
import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_LOCAL_ID_SELECTION;
import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_ROW_ID;
import static com.android.providers.media.photopicker.util.CloudProviderUtils.sendInitPhotoPickerDataNotification;
+import static com.android.providers.media.util.FileUtils.getContentUriForPath;
import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.net.Uri;
@@ -75,6 +79,10 @@
private static final Uri URI_ALBUMS_ALL;
private static final Uri URI_ALBUMS_LOCAL;
+ private static final String MEDIA_GRANTS_URI_PATH = "content://media/media_grants";
+ public static final String EXTRA_MIME_TYPE_SELECTION = "media_grant_mime_type_selection";
+
+
static {
final Uri media = PICKER_INTERNAL_URI.buildUpon()
.appendPath(PickerUriResolver.MEDIA_PATH).build();
@@ -406,6 +414,36 @@
}
/**
+ * Fetches file Uris for items having {@link com.android.providers.media.MediaGrants} for the
+ * given package. Returns an empty list if no grants are present.
+ */
+ @NonNull
+ public List<Uri> fetchReadGrantedItemsUrisForPackage(int packageUid, String[] mimeTypes) {
+ 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_MIME_TYPE_SELECTION, mimeTypes);
+ List<Uri> filesUriList = new ArrayList<>();
+ try (Cursor c = client.query(Uri.parse(MEDIA_GRANTS_URI_PATH),
+ /* projection= */ null,
+ /* queryArgs= */ extras,
+ null)) {
+ while (c.moveToNext()) {
+ final String file_path = c.getString(c.getColumnIndexOrThrow(DATA));
+ final Integer file_id = c.getInt(c.getColumnIndexOrThrow(FILE_ID_COLUMN));
+ filesUriList.add(getContentUriForPath(
+ file_path).buildUpon().appendPath(String.valueOf(file_id)).build());
+ }
+ }
+ return filesUriList;
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
* Sends a data init notification to the MP process.
*/
public void initPhotoPickerData(@Nullable String albumId,
diff --git a/src/com/android/providers/media/photopicker/data/MediaGrantsProvider.java b/src/com/android/providers/media/photopicker/data/MediaGrantsProvider.java
deleted file mode 100644
index 67718d7..0000000
--- a/src/com/android/providers/media/photopicker/data/MediaGrantsProvider.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.data;
-
-import static android.provider.MediaStore.AUTHORITY;
-import static android.provider.MediaStore.MediaColumns.DATA;
-
-import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
-import static com.android.providers.media.util.FileUtils.getContentUriForPath;
-
-import android.annotation.NonNull;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.RemoteException;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Provider grants for items selected in previous sessions.
- */
-public final class MediaGrantsProvider {
- private static final String MEDIA_GRANTS_URI_PATH = "content://media/media_grants";
- public static final String EXTRA_MIME_TYPE_SELECTION = "media_grant_mime_type_selection";
-
- /**
- * Fetches file Uris for items having {@link com.android.providers.media.MediaGrants} for the
- * given package. Returns an empty list if no grants are present.
- *
- * @hide
- */
- @NonNull
- public static List<Uri> fetchReadGrantedItemsUrisForPackage(
- @NonNull Context context, int packageUid, String[] mimeTypes) {
- final ContentResolver resolver = context.getContentResolver();
- try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
- assert client != null;
- final Bundle extras = new Bundle();
- extras.putInt(Intent.EXTRA_UID, packageUid);
- extras.putStringArray(EXTRA_MIME_TYPE_SELECTION, mimeTypes);
- final Cursor c = client.query(Uri.parse(MEDIA_GRANTS_URI_PATH),
- /* projection= */ null,
- /* queryArgs= */ extras,
- null);
- List<Uri> filesUriList = new ArrayList<>(0);
- while (c.moveToNext()) {
- final String file_path = c.getString(c.getColumnIndexOrThrow(DATA));
- final Integer file_id = c.getInt(c.getColumnIndexOrThrow(FILE_ID_COLUMN));
- filesUriList.add(getContentUriForPath(
- file_path).buildUpon().appendPath(String.valueOf(file_id)).build());
- }
- c.close();
- return filesUriList;
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- }
-}
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index acd60ca..cd51b9b 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -47,10 +47,12 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.sync.CloseableReentrantLock;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import com.android.providers.media.photopicker.sync.SyncTrackerRegistry;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
import com.android.providers.media.util.MimeUtils;
import java.io.PrintWriter;
@@ -66,34 +68,32 @@
*/
public class PickerDbFacade {
private static final String VIDEO_MIME_TYPES = "video/%";
-
- // TODO(b/278562157): If there is a dependency on
- // {@link PickerSyncController#mCloudProviderLock}, always acquire
- // {@link PickerSyncController#mCloudProviderLock} before {@link mLock} to avoid deadlock.
- @NonNull
- private final Object mLock = new Object();
private final Context mContext;
private final SQLiteDatabase mDatabase;
+ private final PickerSyncLockManager mPickerSyncLockManager;
private final String mLocalProvider;
// This is the cloud provider the database is synced with. It can be set as null to disable
// cloud queries when database is not in sync with the current cloud provider.
@Nullable
private String mCloudProvider;
- public PickerDbFacade(Context context) {
- this(context, PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+ public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager) {
+ this(context, pickerSyncLockManager, PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
}
@VisibleForTesting
- public PickerDbFacade(Context context, String localProvider) {
- this(context, localProvider, new PickerDatabaseHelper(context));
+ public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager,
+ String localProvider) {
+ this(context, pickerSyncLockManager, localProvider, new PickerDatabaseHelper(context));
}
@VisibleForTesting
- public PickerDbFacade(Context context, String localProvider, PickerDatabaseHelper dbHelper) {
+ public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager,
+ String localProvider, PickerDatabaseHelper dbHelper) {
mContext = context;
mLocalProvider = localProvider;
mDatabase = dbHelper.getWritableDatabase();
+ mPickerSyncLockManager = pickerSyncLockManager;
}
private static final String TAG = "PickerDbFacade";
@@ -103,7 +103,10 @@
private static final int FAIL = -1;
private static final String TABLE_MEDIA = "media";
-
+ // Intentionally use /sdcard path so that the receiving app resolves it to it's per-user
+ // external storage path, e.g. /storage/emulated/<userid>. That way FUSE cross-user access is
+ // not required for picker paths sent across users
+ private static final String PICKER_PATH = "/sdcard/" + getPickerRelativePath();
private static final String TABLE_ALBUM_MEDIA = "album_media";
@VisibleForTesting
@@ -225,19 +228,39 @@
/**
* Sets the cloud provider to be returned after querying the picker db
* If null, cloud media will be excluded from all queries.
+ * This should not be used in picker sync paths because we should not wait on a lock
+ * indefinitely during the picker sync process.
+ * Use {@link this#setCloudProviderWithTimeout} instead.
*/
public void setCloudProvider(String authority) {
- synchronized (mLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
mCloudProvider = authority;
}
}
/**
- * Returns the cloud provider that will be returned after querying the picker db
+ * Sets the cloud provider to be returned after querying the picker db
+ * If null, cloud media will be excluded from all queries.
+ * This should be used in picker sync paths because we should not wait on a lock
+ * indefinitely during the picker sync process
+ */
+ public void setCloudProviderWithTimeout(String authority) throws UnableToAcquireLockException {
+ try (CloseableReentrantLock ignored =
+ mPickerSyncLockManager.tryLock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
+ mCloudProvider = authority;
+ }
+ }
+
+ /**
+ * Returns the cloud provider that will be returned after querying the picker db.
+ * This should not be used in picker sync paths because we should not wait on a lock
+ * indefinitely during the picker sync process.
*/
@VisibleForTesting
public String getCloudProvider() {
- synchronized (mLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
return mCloudProvider;
}
}
@@ -869,13 +892,10 @@
TABLE_MEDIA, /* cloudProvider*/ null);
}
- final String cloudProvider;
- synchronized (mLock) {
- // If the cloud sync is in progress or the cloud provider has changed but a sync has not
- // been completed and committed, {@link PickerDBFacade.mCloudProvider} will be
- // {@code null}.
- cloudProvider = mCloudProvider;
- }
+ // If the cloud sync is in progress or the cloud provider has changed but a sync has not
+ // been completed and committed, {@link PickerDBFacade.mCloudProvider} will be
+ // {@code null}.
+ final String cloudProvider = getCloudProvider();
return queryMediaForUi(qb, selectionArgs, query.mLimit, query.mIsLocalOnly,
TABLE_MEDIA, cloudProvider);
@@ -907,7 +927,7 @@
* Returns a {@link Cursor} containing picker db media rows with columns as {@code projection},
* a subset of {@link PickerMediaColumns}.
*/
- public Cursor queryMediaIdForApps(String pickerSegmentType, String authority, String mediaId,
+ public Cursor queryMediaIdForApps(String authority, String mediaId,
@NonNull String[] projection) {
final String[] selectionArgs = new String[] { mediaId };
final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
@@ -918,12 +938,13 @@
}
if (authority.equals(mLocalProvider)) {
- return queryMediaIdForAppsLocked(qb, projection, selectionArgs, pickerSegmentType);
+ return queryMediaIdForAppsLocked(qb, projection, selectionArgs);
}
- synchronized (mLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
if (authority.equals(mCloudProvider)) {
- return queryMediaIdForAppsLocked(qb, projection, selectionArgs, pickerSegmentType);
+ return queryMediaIdForAppsLocked(qb, projection, selectionArgs);
}
}
@@ -931,9 +952,8 @@
}
private Cursor queryMediaIdForAppsLocked(@NonNull SQLiteQueryBuilder qb,
- @NonNull String[] projection, @NonNull String[] selectionArgs,
- String pickerSegmentType) {
- return qb.query(mDatabase, getMediaStoreProjectionLocked(projection, pickerSegmentType),
+ @NonNull String[] projection, @NonNull String[] selectionArgs) {
+ return qb.query(mDatabase, getMediaStoreProjectionLocked(projection),
/* selection */ null, selectionArgs, /* groupBy */ null, /* having */ null,
/* orderBy */ null, /* limitStr */ null);
}
@@ -1048,7 +1068,8 @@
// Hold lock while checking the cloud provider and querying so that cursor extras containing
// the cloud provider is consistent with the cursor results and doesn't race with
// #setCloudProvider
- synchronized (mLock) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
if (mCloudProvider == null || !Objects.equals(mCloudProvider, authority)) {
// TODO(b/278086344): If cloud provider is null or has changed from what we received
// from the UI, skip all cloud items in the picker db.
@@ -1071,7 +1092,7 @@
private String[] getCloudMediaProjectionLocked() {
return new String[] {
getProjectionAuthorityLocked(),
- getProjectionDataLocked(MediaColumns.DATA, PickerUriResolver.PICKER_SEGMENT),
+ getProjectionDataLocked(MediaColumns.DATA),
getProjectionId(MediaColumns.ID),
// The id in the picker.db table represents the row id. This is used in UI pagination.
getProjectionSimple(KEY_ID, Item.ROW_ID),
@@ -1085,14 +1106,13 @@
};
}
- private String[] getMediaStoreProjectionLocked(String[] columns, String pickerSegmentType) {
+ private String[] getMediaStoreProjectionLocked(String[] columns) {
final String[] projection = new String[columns.length];
for (int i = 0; i < projection.length; i++) {
switch (columns[i]) {
case PickerMediaColumns.DATA:
- projection[i] = getProjectionDataLocked(PickerMediaColumns.DATA,
- pickerSegmentType);
+ projection[i] = getProjectionDataLocked(PickerMediaColumns.DATA);
break;
case PickerMediaColumns.DISPLAY_NAME:
projection[i] =
@@ -1147,13 +1167,13 @@
KEY_CLOUD_ID, mLocalProvider, mCloudProvider, MediaColumns.AUTHORITY);
}
- private String getProjectionDataLocked(String asColumn, String pickerSegmentType) {
+ private String getProjectionDataLocked(String asColumn) {
// _data format:
// /sdcard/.transforms/synthetic/picker/<user-id>/<authority>/media/<display-name>
// See PickerUriResolver#getMediaUri
final String authority = String.format("CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END",
KEY_CLOUD_ID, mLocalProvider, mCloudProvider);
- final String fullPath = "'" + getPickerPath(pickerSegmentType) + "/'"
+ final String fullPath = "'" + PICKER_PATH + "/'"
+ "||" + "'" + MediaStore.MY_USER_ID + "/'"
+ "||" + authority
+ "||" + "'/" + CloudMediaProviderContract.URI_PATH_MEDIA + "/'"
@@ -1161,13 +1181,6 @@
return String.format("%s AS %s", fullPath, asColumn);
}
- private String getPickerPath(String pickerSegmentType) {
- // Intentionally use /sdcard path so that the receiving app resolves it to its per-user
- // external storage path, e.g. /storage/emulated/<userid>. That way FUSE cross-user
- // access is not required for picker paths sent across users
- return "/sdcard/" + getPickerRelativePath(pickerSegmentType);
- }
-
private String getProjectionId(String asColumn) {
// We prefer cloud_id first and it only matters for cloud+local items. For those, the row
// will already be associated with a cloud authority, see #getProjectionAuthorityLocked.
diff --git a/src/com/android/providers/media/photopicker/data/PickerResult.java b/src/com/android/providers/media/photopicker/data/PickerResult.java
index 8108afa..7bea27c 100644
--- a/src/com/android/providers/media/photopicker/data/PickerResult.java
+++ b/src/com/android/providers/media/photopicker/data/PickerResult.java
@@ -40,10 +40,10 @@
* @return {@code Intent} which contains Uri that has been granted access on.
*/
@NonNull
- public static Intent getPickerResponseIntent(String action, boolean canSelectMultiple,
+ public static Intent getPickerResponseIntent(boolean canSelectMultiple,
@NonNull List<Item> selectedItems) {
// 1. Get Picker Uris corresponding to the selected items
- List<Uri> selectedUris = getPickerUrisForItems(action, selectedItems);
+ List<Uri> selectedUris = getPickerUrisForItems(selectedItems);
// 2. Grant read access to picker Uris and return
Intent intent = new Intent();
@@ -71,23 +71,22 @@
}
@VisibleForTesting
- static Uri getPickerUri(String action, Uri uri) {
+ static Uri getPickerUri(Uri uri) {
final String userInfo = uri.getUserInfo();
final String userId = userInfo == null ? UserId.CURRENT_USER.toString() : userInfo;
- return PickerUriResolver.wrapProviderUri(uri, action, Integer.parseInt(userId));
+ return PickerUriResolver.wrapProviderUri(uri, Integer.parseInt(userId));
}
/**
* Returns list of PhotoPicker Uris corresponding to each {@link Item}
*
- * @param action action name which opened PhotoPicker
* @param items list of Item for which we return uri list.
*/
@NonNull
- public static List<Uri> getPickerUrisForItems(String action, @NonNull List<Item> items) {
+ public static List<Uri> getPickerUrisForItems(@NonNull List<Item> items) {
List<Uri> uris = new ArrayList<>();
for (Item item : items) {
- uris.add(getPickerUri(action, item.getContentUri()));
+ uris.add(getPickerUri(item.getContentUri()));
}
return uris;
diff --git a/src/com/android/providers/media/photopicker/data/glide/GlideLoadable.java b/src/com/android/providers/media/photopicker/data/glide/GlideLoadable.java
new file mode 100644
index 0000000..7b536a4
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/glide/GlideLoadable.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.data.glide;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.photopicker.data.model.Category;
+import com.android.providers.media.photopicker.data.model.Item;
+
+import com.bumptech.glide.signature.ObjectKey;
+
+import java.util.Optional;
+
+/**
+ * A data class to coalesce {@link Item} and {@link Category} into a common loadable glide object,
+ * with the relevant data required for Glide loading.
+ */
+public class GlideLoadable {
+
+ private final Optional<String> mCacheKey;
+
+ @NonNull private final Uri mUri;
+
+ public GlideLoadable(@NonNull Uri uri) {
+ this(uri, /* cacheKey= */ null);
+ }
+
+ public GlideLoadable(@NonNull Uri uri, @Nullable String cacheKey) {
+ this.mUri = uri;
+ this.mCacheKey = Optional.ofNullable(cacheKey);
+ }
+
+ /**
+ * Get a signature string to represent this item in the Glide cache.
+ *
+ * @param prefix Optional prefix to prepend to this item's signature.
+ * @return A glide cache signature string.
+ */
+ @Nullable
+ public ObjectKey getLoadableSignature(@Nullable String prefix) {
+ return new ObjectKey(
+ Optional.ofNullable(prefix).orElse("") + mUri.toString() + mCacheKey.orElse(""));
+ }
+ ;
+
+ /**
+ * @return A {@link Uri} object to locate the media for this loadable.
+ */
+ public Uri getLoadableUri() {
+ return mUri;
+ }
+ ;
+}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java b/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java
index cc48670..8ef68d9 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java
@@ -17,7 +17,6 @@
package com.android.providers.media.photopicker.data.glide;
import android.content.Context;
-import android.net.Uri;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
@@ -34,6 +33,7 @@
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
- registry.prepend(Uri.class, InputStream.class, new PickerModelLoaderFactory(context));
+ registry.append(
+ GlideLoadable.class, InputStream.class, new PickerModelLoaderFactory(context));
}
}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
index 1f3bb4c..f381630 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
@@ -20,7 +20,6 @@
import android.content.Context;
import android.content.UriMatcher;
-import android.net.Uri;
import android.provider.CloudMediaProviderContract;
import com.bumptech.glide.load.Options;
@@ -29,10 +28,8 @@
import java.io.InputStream;
-/**
- * Custom {@link ModelLoader} to load thumbnails from cloud media provider.
- */
-public final class PickerModelLoader implements ModelLoader<Uri, InputStream> {
+/** Custom {@link ModelLoader} to load thumbnails from cloud media provider. */
+public final class PickerModelLoader implements ModelLoader<GlideLoadable, InputStream> {
private final Context mContext;
PickerModelLoader(Context context) {
@@ -40,21 +37,24 @@
}
@Override
- public LoadData<InputStream> buildLoadData(Uri model, int width, int height,
- Options options) {
+ public LoadData<InputStream> buildLoadData(
+ GlideLoadable model, int width, int height, Options options) {
final boolean isThumbRequest = Boolean.TRUE.equals(options.get(THUMBNAIL_REQUEST));
- return new LoadData<>(new ObjectKey(model),
+ return new LoadData<>(
+ new ObjectKey(model.getLoadableSignature(/* prefix= */ null)),
new PickerThumbnailFetcher(mContext, model, width, height, isThumbRequest));
}
@Override
- public boolean handles(Uri model) {
+ public boolean handles(GlideLoadable model) {
final int pickerId = 1;
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
- matcher.addURI(model.getAuthority(),
- CloudMediaProviderContract.URI_PATH_MEDIA + "/*", pickerId);
+ matcher.addURI(
+ model.getLoadableUri().getAuthority(),
+ CloudMediaProviderContract.URI_PATH_MEDIA + "/*",
+ pickerId);
// Matches picker URIs of the form content://<authority>/media
- return matcher.match(model) == pickerId;
+ return matcher.match(model.getLoadableUri()) == pickerId;
}
}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java
index 938ef88..d7086d4 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java
@@ -17,7 +17,6 @@
package com.android.providers.media.photopicker.data.glide;
import android.content.Context;
-import android.net.Uri;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
@@ -29,7 +28,7 @@
* Custom {@link ModelLoaderFactory} which provides a {@link ModelLoader} for loading thumbnails
* from cloud media provider.
*/
-public class PickerModelLoaderFactory implements ModelLoaderFactory<Uri, InputStream> {
+public class PickerModelLoaderFactory implements ModelLoaderFactory<GlideLoadable, InputStream> {
private final Context mContext;
@@ -38,7 +37,7 @@
}
@Override
- public ModelLoader<Uri, InputStream> build(MultiModelLoaderFactory unused) {
+ public ModelLoader<GlideLoadable, InputStream> build(MultiModelLoaderFactory unused) {
return new PickerModelLoader(mContext);
}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerPreloadModelProvider.java b/src/com/android/providers/media/photopicker/data/glide/PickerPreloadModelProvider.java
new file mode 100644
index 0000000..9e5b4dd
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerPreloadModelProvider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.data.glide;
+
+import static com.android.providers.media.photopicker.ui.ImageLoader.THUMBNAIL_REQUEST;
+
+import static com.bumptech.glide.load.resource.bitmap.Downsampler.PREFERRED_COLOR_SPACE;
+
+import android.content.Context;
+import android.provider.MediaStore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.ui.PhotosTabAdapter;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.ListPreloader.PreloadModelProvider;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.PreferredColorSpace;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.signature.ObjectKey;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Custom glide module to enable the loading of thumbnails from CloudMediaProvider. */
+public class PickerPreloadModelProvider implements PreloadModelProvider<GlideLoadable> {
+
+ private final Context mContext;
+ private final PreferredColorSpace mPreferredColorSpace;
+ private final PhotosTabAdapter mAdapter;
+
+ public PickerPreloadModelProvider(Context context, PhotosTabAdapter adapter) {
+ mContext = context;
+ mAdapter = adapter;
+
+ final boolean isScreenWideColorGamut =
+ mContext.getResources().getConfiguration().isScreenWideColorGamut();
+ mPreferredColorSpace =
+ isScreenWideColorGamut ? PreferredColorSpace.DISPLAY_P3 : PreferredColorSpace.SRGB;
+ }
+
+ /**
+ * Return a list of items that should be preloaded for the given RecyclerView adapter position.
+ *
+ * @param position the current position of the RecyclerView's adapter.
+ * @return A list of items to begin preloading.
+ */
+ @Override
+ @NonNull
+ public List<GlideLoadable> getPreloadItems(int position) {
+ if (mAdapter.isItemTypeMediaItem(position)) {
+ Object adapterItem = mAdapter.getAdapterItem(position);
+ if (adapterItem instanceof Item) {
+ Item item = (Item) adapterItem;
+ return Collections.singletonList(item.toGlideLoadable());
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * This should generate a load request identical to the load request generated by the
+ * RecyclerView itself. This ensures that there are not inadvertent cache misses because the
+ * preload succeeded, but the actual RecyclerView request didn't match what was in the cache
+ * already.
+ *
+ * @param loadable The {@link GlideLoadable} model for the thumbnail.
+ * @return An identical glide RequestBuilder to what the RecyclerView will generate when it
+ * attempts to load this item.
+ */
+ @Override
+ @Nullable
+ public RequestBuilder getPreloadRequestBuilder(GlideLoadable loadable) {
+ RequestOptions options =
+ RequestOptions.option(THUMBNAIL_REQUEST, true)
+ .set(PREFERRED_COLOR_SPACE, mPreferredColorSpace);
+ // TODO(b/224725723): Remove media store version from key once MP ids are
+ // stable.
+ ObjectKey signature =
+ loadable.getLoadableSignature(/* prefix= */ MediaStore.getVersion(mContext));
+
+ return Glide.with(mContext).asBitmap().apply(options).signature(signature).load(loadable);
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
index 0d85196..b4ed01a 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
@@ -20,43 +20,46 @@
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.graphics.Point;
-import android.net.Uri;
import android.os.Bundle;
+import android.os.CancellationSignal;
import android.provider.CloudMediaProviderContract;
+import android.provider.MediaStore;
import android.util.Log;
-import com.bumptech.glide.Glide;
+import androidx.annotation.Nullable;
+
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.data.DataFetcher;
-import com.bumptech.glide.load.data.ExifOrientationStream;
-
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
- * Custom {@link DataFetcher} to fetch a {@link InputStream} for a thumbnail from a cloud
- * media provider.
+ * Custom {@link DataFetcher} to fetch a {@link InputStream} for a thumbnail from a cloud media
+ * provider.
*/
public class PickerThumbnailFetcher implements DataFetcher<InputStream> {
private static final String TAG = "PickerThumbnailFetcher";
private final Context mContext;
- private final Uri mModel;
+ private final GlideLoadable mModel;
private final int mWidth;
private final int mHeight;
private final boolean mIsThumbRequest;
+ private final CancellationSignal mCancellationSignal;
+ @Nullable private AssetFileDescriptor mAssetFileDescriptor = null;
+ @Nullable private InputStream mInputStream = null;
- PickerThumbnailFetcher(Context context, Uri model, int width, int height,
- boolean isThumbRequest) {
+ PickerThumbnailFetcher(
+ Context context, GlideLoadable model, int width, int height, boolean isThumbRequest) {
mContext = context;
mModel = model;
mWidth = width;
mHeight = height;
mIsThumbRequest = isThumbRequest;
+ mCancellationSignal = new CancellationSignal();
}
@Override
@@ -70,57 +73,50 @@
opts.putBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, true);
}
- try (AssetFileDescriptor afd = contentResolver.openTypedAssetFileDescriptor(mModel,
- /* mimeType */ "image/*", opts, /* cancellationSignal */ null)) {
- if (afd == null) {
+ try {
+ // Do not close the afd or InputStream as it will close the input stream. The
+ // afd needs to be closed when cleanup is called, so save a reference so it can
+ // be closed when Glide is done with it.
+ mAssetFileDescriptor =
+ contentResolver.openTypedAssetFileDescriptor(
+ mModel.getLoadableUri(),
+ /* mimeType= */ "image/*",
+ opts,
+ /* cancellationSignal= */ mCancellationSignal);
+ if (mAssetFileDescriptor == null) {
final String err = "Failed to load data for " + mModel;
callback.onLoadFailed(new FileNotFoundException(err));
return;
}
-
- final InputStream inputStream;
- if (mIsThumbRequest) {
- inputStream = getOrientationInputStream(afd);
- } else {
- // We don't need to handle orientation for preview requests. Glide load takes care
- // of loading the image in the right orientation.
- inputStream = afd.createInputStream();
- }
- callback.onDataReady(inputStream);
+ mInputStream = mAssetFileDescriptor.createInputStream();
+ callback.onDataReady(mInputStream);
} catch (IOException e) {
callback.onLoadFailed(e);
}
}
- private InputStream getOrientationInputStream(AssetFileDescriptor afd) throws IOException {
- InputStream inputStream = afd.createInputStream();
-
- int orientation = -1;
- if (inputStream != null) {
- try {
- orientation = ImageHeaderParserUtils.getOrientation(
- Glide.get(mContext).getRegistry().getImageHeaderParsers(), inputStream,
- Glide.get(mContext).getArrayPool());
- } catch (IOException | NullPointerException ignored) {
- Log.d(TAG, "Unable to fetch orientation for " + mModel, ignored);
- }
- }
-
- if (orientation != -1) {
- inputStream = new ExifOrientationStream(inputStream, orientation);
- }
- return inputStream;
- }
-
+ /**
+ * Cleanup is called after Glide is done with this Fetcher instance, and it is now safe to close
+ * the remembered AssetFileDescriptor.
+ */
@Override
public void cleanup() {
- // Intentionally empty only because we're not opening an InputStream or another I/O
- // resource.
+ try {
+ if (mInputStream != null) {
+ mInputStream.close();
+ }
+
+ if (mAssetFileDescriptor != null) {
+ mAssetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ Log.d(TAG, "Unexpected error during thumbnail request cleanup.", e);
+ }
}
@Override
public void cancel() {
- // Intentionally empty.
+ mCancellationSignal.cancel();
}
@Override
@@ -130,6 +126,13 @@
@Override
public DataSource getDataSource() {
- return DataSource.LOCAL;
+ // If the authority belongs to MediaProvider, we can consider this a local load.
+ if (mModel.getLoadableUri().getAuthority().equals(MediaStore.AUTHORITY)) {
+ return DataSource.LOCAL;
+ } else {
+ // Otherwise, let's assume it's a Remote data source so that Glide will cache
+ // the raw return value rather than manipulated bytes.
+ return DataSource.REMOTE;
+ }
}
}
diff --git a/src/com/android/providers/media/photopicker/data/model/Category.java b/src/com/android/providers/media/photopicker/data/model/Category.java
index 487a39c..927a031 100644
--- a/src/com/android/providers/media/photopicker/data/model/Category.java
+++ b/src/com/android/providers/media/photopicker/data/model/Category.java
@@ -39,6 +39,7 @@
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.glide.GlideLoadable;
import java.util.List;
import java.util.Locale;
@@ -187,4 +188,13 @@
return albumId;
}
}
-}
\ No newline at end of file
+
+ /**
+ * Convert this category into a loadable object for Glide.
+ *
+ * @return {@link GlideLoadable} that represents the relevant loadable data for this item.
+ */
+ public GlideLoadable toGlideLoadable() {
+ return new GlideLoadable(getCoverUri());
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/data/model/Item.java b/src/com/android/providers/media/photopicker/data/model/Item.java
index a98f03c..83b0bdb 100644
--- a/src/com/android/providers/media/photopicker/data/model/Item.java
+++ b/src/com/android/providers/media/photopicker/data/model/Item.java
@@ -38,6 +38,7 @@
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.glide.GlideLoadable;
import com.android.providers.media.photopicker.util.DateTimeUtils;
import com.android.providers.media.util.MimeUtils;
@@ -252,4 +253,14 @@
@Override public int hashCode() {
return Objects.hash(mUri);
}
-}
\ No newline at end of file
+
+ /**
+ * Convert this item into a loadable object for Glide.
+ *
+ * @return {@link GlideLoadable} that represents the relevant loadable data for this item.
+ */
+ public GlideLoadable toGlideLoadable() {
+ return new GlideLoadable(mUri, String.valueOf(getGenerationModified()));
+ }
+
+}
diff --git a/src/com/android/providers/media/photopicker/sync/CloseableReentrantLock.java b/src/com/android/providers/media/photopicker/sync/CloseableReentrantLock.java
new file mode 100644
index 0000000..bcd54e4
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/CloseableReentrantLock.java
@@ -0,0 +1,92 @@
+/*
+ * 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 android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A Reentrant lock that implements AutoCloseable interface.
+ */
+public class CloseableReentrantLock extends ReentrantLock implements AutoCloseable {
+ private static final String TAG = CloseableReentrantLock.class.getSimpleName();
+ private final String mLockName;
+
+ public CloseableReentrantLock(@NonNull String lockName) {
+ super();
+ mLockName = lockName;
+ }
+
+ /**
+ * Try to acquire lock with a timeout after running some validations.
+ */
+ public CloseableReentrantLock lockWithTimeout(long timeout, TimeUnit unit)
+ throws UnableToAcquireLockException {
+ try {
+ final boolean success =
+ this.tryLock(timeout, unit);
+ if (!success) {
+ throw new UnableToAcquireLockException(
+ "Could not acquire the lock within timeout " + this);
+ }
+ Log.d(TAG, "Successfully acquired lock " + this);
+ return this;
+ } catch (InterruptedException e) {
+ throw new UnableToAcquireLockException(
+ "Interrupted while waiting for lock " + this, e);
+ }
+ }
+
+ @Override
+ public void close() {
+ unlock();
+ }
+
+ /**
+ * Attempt to release the lock and swallow IllegalMonitorStateException, if thrown.
+ */
+ @Override
+ public void lock() {
+ super.lock();
+ Log.d(TAG, "Successfully acquired lock " + this);
+ }
+
+ /**
+ * Attempt to release the lock and swallow IllegalMonitorStateException, if thrown.
+ */
+ @Override
+ public void unlock() {
+ try {
+ super.unlock();
+ Log.d(TAG, "Successfully released lock " + this);
+ } catch (IllegalMonitorStateException e) {
+ Log.e(TAG, "Tried to release a lock that is not held by this thread - " + this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + ". Lock Name = " + mLockName
+ + ". Threads that may be waiting to acquire this lock = " + getQueuedThreads();
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java b/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java
index 47c7455..1558b9f 100644
--- a/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java
+++ b/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java
@@ -43,6 +43,7 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.sync.PickerSyncManager.SyncResetType;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
/**
* This is a {@link Worker} class responsible for handling table reset operations in the picker
@@ -80,9 +81,11 @@
PickerSyncController controller;
PickerDbFacade dBFacade;
+ PickerSyncLockManager pickerSyncLockManager;
try {
controller = PickerSyncController.getInstanceOrThrow();
- dBFacade = new PickerDbFacade(mContext);
+ pickerSyncLockManager = controller.getPickerSyncLockManager();
+ dBFacade = new PickerDbFacade(mContext, pickerSyncLockManager);
} catch (IllegalStateException ex) {
Log.e(TAG, "Unable to obtain PickerSyncController", ex);
return ListenableWorker.Result.failure();
@@ -91,39 +94,42 @@
return ListenableWorker.Result.failure();
}
- if (getTags().contains(SYNC_WORKER_TAG_IS_PERIODIC)) {
- // If this worker is being run as part of periodic work, it needs to register
- // its own sync with the sync tracker.
- trackNewAlbumMediaSyncRequests(mSyncSource, getId());
-
- // Since this is a periodic worker, we'll use the cloud authority, if it exists.
- // Using the cloud authority will reset files for all providers. If the local
- // authority is used, it will limit the query to only files with a local_id, but
- // the cloud authority does not have such a limitation.
- // (This is not intuitive, it's just how it works.)
- mAuthority = controller.getCloudProvider();
- if (mAuthority == null) {
- mAuthority = controller.getLocalProvider();
- }
- // If the authority is still null, end the operation.
- if (mAuthority == null) {
- Log.e(TAG, "Unable to set authority for periodic worker");
- return ListenableWorker.Result.failure();
- }
- }
try {
+ if (getTags().contains(SYNC_WORKER_TAG_IS_PERIODIC)) {
+ // If this worker is being run as part of periodic work, it needs to register
+ // its own sync with the sync tracker.
+ trackNewAlbumMediaSyncRequests(mSyncSource, getId());
+
+ // Since this is a periodic worker, we'll use the cloud authority, if it exists.
+ // Using the cloud authority will reset files for all providers. If the local
+ // authority is used, it will limit the query to only files with a local_id, but
+ // the cloud authority does not have such a limitation.
+ // (This is not intuitive, it's just how it works.)
+ mAuthority = controller.getCloudProviderWithTimeout();
+ if (mAuthority == null) {
+ mAuthority = controller.getLocalProvider();
+ }
+ // If the authority is still null, end the operation.
+ if (mAuthority == null) {
+ Log.e(TAG, "Unable to set authority for periodic worker");
+ return ListenableWorker.Result.failure();
+ }
+ }
if (mSyncSource == SYNC_LOCAL_ONLY) {
return start(dBFacade);
} else {
// SyncSource is either CLOUD_ONLY or LOCAL_AND_CLOUD, either way we need the
// cloud lock.
- final Object cloudAlbumSyncLock = controller.getCloudAlbumSyncLock();
- synchronized (cloudAlbumSyncLock) {
+ try (CloseableReentrantLock ignored = pickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) {
return start(dBFacade);
}
}
+ } catch (UnableToAcquireLockException e) {
+ Log.e(TAG, "Could not acquire lock", e);
+ return ListenableWorker.Result.failure();
} finally {
markAlbumMediaSyncAsComplete(mSyncSource, getId());
}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncLockManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncLockManager.java
new file mode 100644
index 0000000..f01026f
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncLockManager.java
@@ -0,0 +1,137 @@
+/*
+ * 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 android.annotation.IntDef;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Manages Java locks acquired during the sync process to ensure that the cloud sync is thread safe.
+ */
+public class PickerSyncLockManager {
+ private static final String TAG = PickerSyncLockManager.class.getSimpleName();
+ private static final Integer LOCK_ACQUIRE_TIMEOUT_MINS = 4;
+ private static final TimeUnit LOCK_ACQUIRE_TIMEOUT_UNIT = TimeUnit.MINUTES;
+
+ @IntDef(value = {CLOUD_SYNC_LOCK, CLOUD_ALBUM_SYNC_LOCK, CLOUD_PROVIDER_LOCK, DB_CLOUD_LOCK})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LockType {}
+ public static final int CLOUD_SYNC_LOCK = 0;
+ public static final int CLOUD_ALBUM_SYNC_LOCK = 1;
+ public static final int CLOUD_PROVIDER_LOCK = 2;
+ public static final int DB_CLOUD_LOCK = 3;
+
+ private final CloseableReentrantLock mCloudSyncLock =
+ new CloseableReentrantLock("CLOUD_SYNC_LOCK");
+ private final CloseableReentrantLock mCloudAlbumSyncLock =
+ new CloseableReentrantLock("CLOUD_ALBUM_SYNC_LOCK");
+ private final CloseableReentrantLock mCloudProviderLock =
+ new CloseableReentrantLock("CLOUD_PROVIDER_LOCK");
+ private final CloseableReentrantLock mDbCloudLock =
+ new CloseableReentrantLock("DB_CLOUD_LOCK");
+
+ /**
+ * Try to acquire lock with a default timeout after running some validations.
+ */
+ public CloseableReentrantLock tryLock(@LockType int lockType)
+ throws UnableToAcquireLockException {
+ return tryLock(lockType, LOCK_ACQUIRE_TIMEOUT_MINS, LOCK_ACQUIRE_TIMEOUT_UNIT);
+ }
+
+ /**
+ * Try to acquire lock with the provided timeout after running some validations.
+ */
+ public CloseableReentrantLock tryLock(@LockType int lockType, long timeout, TimeUnit unit)
+ throws UnableToAcquireLockException {
+ return tryLock(getLock(lockType), timeout, unit);
+ }
+
+ /**
+ * Try to acquire the given lock with the provided timeout after running some validations.
+ */
+ @VisibleForTesting
+ public CloseableReentrantLock tryLock(@NonNull CloseableReentrantLock lock,
+ long timeout, TimeUnit unit) throws UnableToAcquireLockException {
+ Log.d(TAG, "Trying to acquire lock " + lock + " with timeout.");
+ validateLockOrder(lock);
+ return lock.lockWithTimeout(timeout, unit);
+ }
+
+ /**
+ * Try to acquire the lock after running some validations.
+ */
+ public CloseableReentrantLock lock(@LockType int lockType) {
+ final CloseableReentrantLock reentrantLock = getLock(lockType);
+ Log.d(TAG, "Trying to acquire lock " + reentrantLock);
+ validateLockOrder(reentrantLock);
+ reentrantLock.lock();
+ return reentrantLock;
+ }
+
+ /**
+ * Return the {@link CloseableReentrantLock} corresponding to the given {@link LockType}.
+ * Throws a {@link RuntimeException} if the lock is not recognized.
+ */
+ @VisibleForTesting
+ public CloseableReentrantLock getLock(@LockType int lockType) {
+ switch (lockType) {
+ case CLOUD_SYNC_LOCK:
+ return mCloudSyncLock;
+ case CLOUD_ALBUM_SYNC_LOCK:
+ return mCloudAlbumSyncLock;
+ case CLOUD_PROVIDER_LOCK:
+ return mCloudProviderLock;
+ case DB_CLOUD_LOCK:
+ return mDbCloudLock;
+ default:
+ throw new RuntimeException("Unrecognizable lock type " + lockType);
+ }
+ }
+
+ private void validateLockOrder(@NonNull ReentrantLock lockToBeAcquired) {
+ if (lockToBeAcquired.equals(mCloudSyncLock)) {
+ validateLockOrder(lockToBeAcquired, mCloudAlbumSyncLock);
+ validateLockOrder(lockToBeAcquired, mCloudProviderLock);
+ validateLockOrder(lockToBeAcquired, mDbCloudLock);
+ } else if (lockToBeAcquired.equals(mCloudAlbumSyncLock)) {
+ validateLockOrder(lockToBeAcquired, mCloudSyncLock);
+ validateLockOrder(lockToBeAcquired, mCloudProviderLock);
+ validateLockOrder(lockToBeAcquired, mDbCloudLock);
+ } else if (lockToBeAcquired.equals(mCloudProviderLock)) {
+ validateLockOrder(lockToBeAcquired, mDbCloudLock);
+ }
+ }
+
+ private void validateLockOrder(@NonNull ReentrantLock lockToBeAcquired,
+ @NonNull ReentrantLock lockThatShouldNotBeHeld) {
+ if (lockThatShouldNotBeHeld.isHeldByCurrentThread()) {
+ Log.e(TAG, String.format("Lock {%s} should not be held before acquiring lock {%s}"
+ + " This could lead to a deadlock.",
+ lockThatShouldNotBeHeld, lockToBeAcquired));
+ }
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index 08f25d9..6c44f4d 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -32,6 +32,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.providers.media.photopicker.data.glide.GlideLoadable;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
@@ -43,6 +44,8 @@
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.signature.ObjectKey;
+import java.util.Optional;
+
/**
* A class to assist with loading and managing the Images (i.e. thumbnails and preview) associated
* with item.
@@ -81,7 +84,7 @@
defaultIcon.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
- loadWithGlide(getBitmapRequestBuilder(category.getCoverUri()), THUMBNAIL_OPTION,
+ loadWithGlide(getBitmapRequestBuilder(category.toGlideLoadable()), THUMBNAIL_OPTION,
/* signature */ null, imageView);
} else {
imageView.setVisibility(View.INVISIBLE);
@@ -99,11 +102,12 @@
* @param imageView the imageView shows the thumbnail
*/
public void loadPhotoThumbnail(@NonNull Item item, @NonNull ImageView imageView) {
+ final GlideLoadable loadable = item.toGlideLoadable();
// Always show all thumbnails as bitmap images instead of drawables
// This is to ensure that we do not animate any thumbnail (for eg GIF)
// TODO(b/194285082): Use drawable instead of bitmap, as it saves memory.
- loadWithGlide(getBitmapRequestBuilder(item.getContentUri()), THUMBNAIL_OPTION,
- getGlideSignature(item, /* prefix */ ""), imageView);
+ loadWithGlide(getBitmapRequestBuilder(loadable), THUMBNAIL_OPTION,
+ getGlideSignature(loadable, /* prefix */ null), imageView);
}
/**
@@ -113,26 +117,34 @@
* @param imageView the imageView shows the image
*/
public void loadImagePreview(@NonNull Item item, @NonNull ImageView imageView) {
+ final GlideLoadable loadable = item.toGlideLoadable();
if (item.isGif()) {
- loadWithGlide(getGifRequestBuilder(item.getContentUri()), /* requestOptions */ null,
- getGlideSignature(item, /* prefix */ ""), imageView);
+ loadWithGlide(
+ getGifRequestBuilder(loadable),
+ /* requestOptions */ null,
+ getGlideSignature(loadable, /* prefix= */ null),
+ imageView);
return;
}
if (item.isAnimatedWebp()) {
- loadAnimatedWebpPreview(item, imageView);
+ loadAnimatedWebpPreview(loadable, imageView);
return;
}
// Preview as bitmap image for all other image types
- loadWithGlide(getBitmapRequestBuilder(item.getContentUri()), /* requestOptions */ null,
- getGlideSignature(item, /* prefix */ ""), imageView);
+ loadWithGlide(
+ getBitmapRequestBuilder(loadable),
+ /* requestOptions */ null,
+ getGlideSignature(loadable, /* prefix= */ null),
+ imageView);
}
- private void loadAnimatedWebpPreview(@NonNull Item item, @NonNull ImageView imageView) {
- final Uri uri = item.getContentUri();
- final ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(),
- uri);
+ private void loadAnimatedWebpPreview(
+ @NonNull GlideLoadable loadable, @NonNull ImageView imageView) {
+ final Uri uri = loadable.getLoadableUri();
+ final ImageDecoder.Source source =
+ ImageDecoder.createSource(mContext.getContentResolver(), uri);
Drawable drawable = null;
try {
drawable = ImageDecoder.decodeDrawable(source);
@@ -140,38 +152,47 @@
Log.d(TAG, "Failed to decode drawable for uri: " + uri, e);
}
- // If we failed to decode drawable for a source using ImageDecoder, then try using uri
- // directly. Glide will show static image for an animated webp. That is okay as we tried our
- // best to load animated webp but couldn't, and we anyway show the GIF badge in preview.
- loadWithGlide(getDrawableRequestBuilder(drawable == null ? uri : drawable),
- /* requestOptions */ null, getGlideSignature(item, /* prefix */ ""), imageView);
+ // If we failed to decode drawable for a source using ImageDecoder, then try
+ // using uri directly. Glide will show static image for an animated webp. That
+ // is okay as we tried our best to load animated webp but couldn't, and we
+ // anyway show the GIF badge in preview.
+ loadWithGlide(
+ getDrawableRequestBuilder(drawable == null ? loadable : drawable),
+ /* requestOptions */ null,
+ getGlideSignature(loadable, null),
+ imageView);
}
/**
* Loads the image from first frame of the given video item
*/
public void loadImageFromVideoForPreview(@NonNull Item item, @NonNull ImageView imageView) {
- loadWithGlide(getBitmapRequestBuilder(item.getContentUri()),
- new RequestOptions().frame(1000), getGlideSignature(item, "Preview"), imageView);
+ final GlideLoadable loadable = item.toGlideLoadable();
+ loadWithGlide(
+ getBitmapRequestBuilder(loadable),
+ new RequestOptions().frame(1000),
+ getGlideSignature(loadable, "Preview"),
+ imageView);
}
- private ObjectKey getGlideSignature(Item item, String prefix) {
- // TODO(b/224725723): Remove media store version from key once MP ids are stable.
- return new ObjectKey(
- MediaStore.getVersion(mContext) + prefix + item.getContentUri().toString() +
- item.getGenerationModified());
+ private ObjectKey getGlideSignature(GlideLoadable loadable, @Nullable String prefix) {
+ // TODO(b/224725723): Remove media store version from key once MP ids are
+ // stable.
+ return loadable.getLoadableSignature(
+ /* prefix= */ MediaStore.getVersion(mContext)
+ + Optional.ofNullable(prefix).orElse(""));
}
- private RequestBuilder<Bitmap> getBitmapRequestBuilder(Uri uri) {
+ private RequestBuilder<Bitmap> getBitmapRequestBuilder(GlideLoadable loadable) {
return Glide.with(mContext)
.asBitmap()
- .load(uri);
+ .load(loadable);
}
- private RequestBuilder<GifDrawable> getGifRequestBuilder(Uri uri) {
+ private RequestBuilder<GifDrawable> getGifRequestBuilder(GlideLoadable loadable) {
return Glide.with(mContext)
.asGif()
- .load(uri);
+ .load(loadable);
}
private RequestBuilder<Drawable> getDrawableRequestBuilder(Object model) {
diff --git a/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java b/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
index f8d3400..c6aa300 100644
--- a/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
@@ -108,6 +108,14 @@
return itemView.getContext();
}
+ /**
+ * Get the {@link ImageView} for the thumbnail image representing this MediaItem.
+ * @return the image view for the thumbnail.
+ */
+ public ImageView getThumbnailImageView() {
+ return mIconThumb;
+ }
+
private boolean showShowOverlayGradient(@NonNull Item item) {
return mCanSelectMultiple
|| item.isGifOrAnimatedWebp()
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
index e734a4a..38d2fc9 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
@@ -33,6 +33,8 @@
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.util.DateTimeUtils;
+import com.bumptech.glide.util.ViewPreloadSizeProvider;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -40,13 +42,14 @@
/**
* Adapts from model to something RecyclerView understands.
*/
-class PhotosTabAdapter extends TabAdapter {
+public class PhotosTabAdapter extends TabAdapter {
private static final int RECENT_MINIMUM_COUNT = 12;
private final boolean mShowRecentSection;
private final OnMediaItemClickListener mOnMediaItemClickListener;
private final Selection mSelection;
+ private final ViewPreloadSizeProvider mPreloadSizeProvider;
private final View.OnHoverListener mOnMediaItemHoverListener;
@@ -65,7 +68,8 @@
@NonNull OnBannerEventListener onCloudMediaAvailableBannerEventListener,
@NonNull OnBannerEventListener onAccountUpdatedBannerEventListener,
@NonNull OnBannerEventListener onChooseAccountBannerEventListener,
- @NonNull View.OnHoverListener onMediaItemHoverListener) {
+ @NonNull View.OnHoverListener onMediaItemHoverListener,
+ @NonNull ViewPreloadSizeProvider preloadSizeProvider) {
super(imageLoader, lifecycleOwner, cloudMediaProviderAppTitle, cloudMediaAccountName,
shouldShowChooseAppBanner, shouldShowCloudMediaAvailableBanner,
shouldShowAccountUpdatedBanner, shouldShowChooseAccountBanner,
@@ -75,6 +79,7 @@
mSelection = selection;
mOnMediaItemClickListener = onMediaItemClickListener;
mOnMediaItemHoverListener = onMediaItemHoverListener;
+ mPreloadSizeProvider = preloadSizeProvider;
}
@NonNull
@@ -88,8 +93,15 @@
@Override
RecyclerView.ViewHolder createMediaItemViewHolder(@NonNull ViewGroup viewGroup) {
final View view = getView(viewGroup, R.layout.item_photo_grid);
- return new MediaItemGridViewHolder(view, mImageLoader, mOnMediaItemClickListener,
- mOnMediaItemHoverListener, mSelection.canSelectMultiple());
+ final MediaItemGridViewHolder viewHolder =
+ new MediaItemGridViewHolder(
+ view,
+ mImageLoader,
+ mOnMediaItemClickListener,
+ mOnMediaItemHoverListener,
+ mSelection.canSelectMultiple());
+ mPreloadSizeProvider.setView(viewHolder.getThumbnailImageView());
+ return viewHolder;
}
@Override
@@ -124,7 +136,7 @@
}
@Override
- boolean isItemTypeMediaItem(int position) {
+ public boolean isItemTypeMediaItem(int position) {
return getAdapterItem(position) instanceof Item;
}
@@ -199,4 +211,4 @@
boolean onItemLongClick(@NonNull View view, int position);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index 0822fcb..f329a44 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -18,6 +18,7 @@
import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE;
import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS;
import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
+import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_MEDIA_ITEM;
import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_ALBUM_PHOTOS_TAB;
import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_PHOTOS_TAB;
@@ -46,6 +47,7 @@
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.PaginationParameters;
+import com.android.providers.media.photopicker.data.glide.PickerPreloadModelProvider;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.util.LayoutModeUtils;
@@ -53,6 +55,10 @@
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
import com.android.providers.media.util.StringUtils;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
+import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.snackbar.Snackbar;
import org.jetbrains.annotations.NotNull;
@@ -80,6 +86,10 @@
private boolean mIsCloudMediaInPhotoPickerEnabled;
private int mPageSize;
+ private PickerPreloadModelProvider mPreloadModelProvider;
+
+ @Nullable
+ private RequestManager mGlideRequestManager = null;
private ProgressBar mProgressBar;
private TextView mLoadingTextView;
@@ -136,14 +146,39 @@
}
mSelection.clearCheckedItemList();
- final PhotosTabAdapter adapter = new PhotosTabAdapter(showRecentSection, mSelection,
- mImageLoader, mOnMediaItemClickListener, /* lifecycleOwner */ this,
- mPickerViewModel.getCloudMediaProviderAppTitleLiveData(),
- mPickerViewModel.getCloudMediaAccountNameLiveData(), showChooseAppBanner,
- showCloudMediaAvailableBanner, showAccountUpdatedBanner, showChooseAccountBanner,
- mOnChooseAppBannerEventListener, mOnCloudMediaAvailableBannerEventListener,
- mOnAccountUpdatedBannerEventListener, mOnChooseAccountBannerEventListener,
- mOnMediaItemHoverListener);
+ ViewPreloadSizeProvider viewSizeProvider = new ViewPreloadSizeProvider();
+
+ final PhotosTabAdapter adapter =
+ new PhotosTabAdapter(
+ showRecentSection,
+ mSelection,
+ mImageLoader,
+ mOnMediaItemClickListener, /* lifecycleOwner */
+ this,
+ mPickerViewModel.getCloudMediaProviderAppTitleLiveData(),
+ mPickerViewModel.getCloudMediaAccountNameLiveData(),
+ showChooseAppBanner,
+ showCloudMediaAvailableBanner,
+ showAccountUpdatedBanner,
+ showChooseAccountBanner,
+ mOnChooseAppBannerEventListener,
+ mOnCloudMediaAvailableBannerEventListener,
+ mOnAccountUpdatedBannerEventListener,
+ mOnChooseAccountBannerEventListener,
+ mOnMediaItemHoverListener,
+ viewSizeProvider);
+
+ mPreloadModelProvider = new PickerPreloadModelProvider(getContext(), adapter);
+ mGlideRequestManager = Glide.with(this);
+
+ RecyclerViewPreloader<Item> preloader =
+ new RecyclerViewPreloader<>(
+ Glide.with(getContext()),
+ mPreloadModelProvider,
+ viewSizeProvider,
+ /* maxPreload= */ 8);
+ mRecyclerView.addOnScrollListener(preloader);
+
// initialise pre-granted items is necessary.
Intent activityIntent = requireActivity().getIntent();
@@ -195,6 +230,19 @@
setLayoutManager(context, adapter, GRID_COLUMN_COUNT);
mRecyclerView.setAdapter(adapter);
mRecyclerView.addItemDecoration(itemDecoration);
+
+ // Listen for views as they are being recycled and attempt to cancel any pending glide load
+ // requests to prevent a large backlog of requests building up in the event of really
+ // large scrolls.
+ mRecyclerView.addRecyclerListener(
+ new RecyclerView.RecyclerListener() {
+ @Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ cancelGlideLoadForViewHolder(holder);
+ }
+ });
+ mRecyclerView.setItemViewCacheSize(10);
+
if (mIsCloudMediaInPhotoPickerEnabled) {
setOnScrollListenerForRecyclerView();
}
@@ -543,9 +591,26 @@
}
}
+ /**
+ * Attempts to cancel any outstanding Glide requests for the given ViewHolder.
+ *
+ * @param holder The View holder in the RecyclerView to cancel requests for.
+ */
+ private void cancelGlideLoadForViewHolder(RecyclerView.ViewHolder holder) {
+
+ if (mGlideRequestManager != null && holder.getItemViewType() == ITEM_TYPE_MEDIA_ITEM) {
+ // This cast is safe as we've already checked the view type is
+ MediaItemGridViewHolder vh = (MediaItemGridViewHolder) holder;
+ // Attempt to clear the potential pending load out of glide's request
+ // manager.
+ mGlideRequestManager.clear(vh.getThumbnailImageView());
+ }
+ }
+
@Override
public void onDestroy() {
super.onDestroy();
mMainThreadHandler.removeCallbacksAndMessages(mHideProgressBarToken);
+ mGlideRequestManager = null;
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/providers/media/photopicker/ui/TabAdapter.java b/src/com/android/providers/media/photopicker/ui/TabAdapter.java
index 35db580..3295941 100644
--- a/src/com/android/providers/media/photopicker/ui/TabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/TabAdapter.java
@@ -47,15 +47,14 @@
/**
* Adapts from model to something RecyclerView understands.
*/
-@VisibleForTesting
public abstract class TabAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@VisibleForTesting
public static final int ITEM_TYPE_BANNER = 0;
// Date header sections for "Photos" tab
- static final int ITEM_TYPE_SECTION = 1;
+ public static final int ITEM_TYPE_SECTION = 1;
// Media items (a.k.a. Items) for "Photos" tab, Albums (a.k.a. Categories) for "Albums" tab
- private static final int ITEM_TYPE_MEDIA_ITEM = 2;
+ public static final int ITEM_TYPE_MEDIA_ITEM = 2;
@NonNull final ImageLoader mImageLoader;
@NonNull private final LiveData<String> mCloudMediaProviderAppTitle;
@@ -273,7 +272,7 @@
}
@NonNull
- final Object getAdapterItem(int position) {
+ public final Object getAdapterItem(int position) {
if (position < 0) {
throw new IllegalStateException("Get adapter item for negative position " + position);
}
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index 269edb7..9d0b107 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -83,6 +83,8 @@
private boolean mIsAccessibilityEnabled;
private Button mAddButton;
+
+ private Button mViewSelectedButton;
private View mBottomBar;
private Animation mSlideUpAnimation;
private Animation mSlideDownAnimation;
@@ -157,6 +159,7 @@
final boolean canSelectMultiple = mSelection.canSelectMultiple();
if (canSelectMultiple) {
mAddButton = activity.findViewById(R.id.button_add);
+ mViewSelectedButton = activity.findViewById(R.id.button_view_selected);
mAddButton.setOnClickListener(v -> {
try {
requirePickerActivity().setResultAndFinishSelf();
@@ -164,10 +167,8 @@
Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
}
});
-
- final Button viewSelectedButton = activity.findViewById(R.id.button_view_selected);
// Transition to PreviewFragment on clicking "View Selected".
- viewSelectedButton.setOnClickListener(v -> {
+ mViewSelectedButton.setOnClickListener(v -> {
// Load items for preview that are pre granted but not yet loaded for UI.
mPickerViewModel.getRemainingPreGrantedItems();
mSelection.prepareSelectedItemsForPreviewAll();
@@ -184,6 +185,8 @@
});
mBottomBar = activity.findViewById(R.id.picker_bottom_bar);
+ // consume the event so that it doesn't get passed through to the next view b/287661737
+ mBottomBar.setOnClickListener(v -> {});
mSlideUpAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_up);
mSlideDownAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_down);
@@ -250,19 +253,38 @@
return;
}
- if (selectedItemListSize == 0) {
- if (mBottomBar.getVisibility() == View.VISIBLE) {
- mBottomBar.setVisibility(View.GONE);
- mBottomBar.startAnimation(mSlideDownAnimation);
+ if (mPickerViewModel.isManagedSelectionEnabled()) {
+ animateAndShowBottomBar(context, selectedItemListSize);
+ if (selectedItemListSize == 0) {
+ mViewSelectedButton.setVisibility(View.GONE);
+ // Update the add button to show "Allow none".
+ mAddButton.setText(R.string.picker_add_button_allow_none_option);
}
} else {
- if (mBottomBar.getVisibility() == View.GONE) {
- mBottomBar.setVisibility(View.VISIBLE);
- mBottomBar.startAnimation(mSlideUpAnimation);
+ if (selectedItemListSize == 0) {
+ animateAndHideBottomBar();
+ } else {
+ animateAndShowBottomBar(context, selectedItemListSize);
}
- mAddButton.setText(generateAddButtonString(context, selectedItemListSize));
}
- mIsBottomBarVisible.setValue(selectedItemListSize > 0);
+ mIsBottomBarVisible.setValue(
+ mPickerViewModel.isManagedSelectionEnabled() || selectedItemListSize > 0);
+ }
+
+ private void animateAndShowBottomBar(Context context, int selectedItemListSize) {
+ if (mBottomBar.getVisibility() == View.GONE) {
+ mBottomBar.setVisibility(View.VISIBLE);
+ mBottomBar.startAnimation(mSlideUpAnimation);
+ }
+ mViewSelectedButton.setVisibility(View.VISIBLE);
+ mAddButton.setText(generateAddButtonString(context, selectedItemListSize));
+ }
+
+ private void animateAndHideBottomBar() {
+ if (mBottomBar.getVisibility() == View.VISIBLE) {
+ mBottomBar.setVisibility(View.GONE);
+ mBottomBar.startAnimation(mSlideDownAnimation);
+ }
}
private void setUpListenersForProfileButton() {
diff --git a/src/com/android/providers/media/photopicker/util/exceptions/UnableToAcquireLockException.java b/src/com/android/providers/media/photopicker/util/exceptions/UnableToAcquireLockException.java
new file mode 100644
index 0000000..fad0c01
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/exceptions/UnableToAcquireLockException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util.exceptions;
+
+/**
+ * Exception thrown when the current thread tries to acquire a lock but fails. The failure could be
+ * because of a timeout or thread interruption.
+ */
+public class UnableToAcquireLockException extends Exception {
+ public UnableToAcquireLockException(String message) {
+ super(message);
+ }
+
+ public UnableToAcquireLockException(String message, Exception e) {
+ super(message, e);
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index 1a004b2..5b65155 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -27,7 +27,6 @@
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.photopicker.DataLoaderThread.TOKEN;
import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
-import static com.android.providers.media.photopicker.data.MediaGrantsProvider.fetchReadGrantedItemsUrisForPackage;
import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST;
import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_GRID;
import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_DEFAULT;
@@ -165,6 +164,8 @@
// Note - Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
private boolean mIsUserSelectForApp;
+
+ private boolean mIsManagedSelectionEnabled;
private boolean mIsLocalOnly;
private boolean mIsAllCategoryItemsLoaded = false;
private boolean mIsNotificationForUpdateReceived = false;
@@ -180,6 +181,7 @@
mInstanceId = new InstanceIdSequence(INSTANCE_ID_MAX).newInstanceId();
mLogger = new PhotoPickerUiEventLogger();
mIsUserSelectForApp = false;
+ mIsManagedSelectionEnabled = false;
mIsLocalOnly = false;
initConfigStore();
@@ -299,6 +301,15 @@
}
/**
+ * @return {@code mIsManagedSelectionEnabled} if the picker is currently being used
+ * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action and flag
+ * pickerChoiceManagedSelection is enabled..
+ */
+ public boolean isManagedSelectionEnabled() {
+ return mIsManagedSelectionEnabled;
+ }
+
+ /**
* @return a {@link LiveData} that holds the value (once it's fetched) of the
* {@link android.content.ContentProvider#mAuthority authority} of the current
* {@link android.provider.CloudMediaProvider}.
@@ -394,10 +405,9 @@
*/
public void initialisePreGrantsIfNecessary(Selection selection, Bundle intentExtras,
String[] mimeTypeFilters) {
- if (getConfigStore().isPickerChoiceManagedSelectionEnabled() && isUserSelectForApp()
- && selection.getPreGrantedItems() == null) {
+ if (isManagedSelectionEnabled() && selection.getPreGrantedItems() == null) {
DataLoaderThread.getHandler().postDelayed(() -> {
- selection.setPreGrantedItemSet(fetchReadGrantedItemsUrisForPackage(mAppContext,
+ selection.setPreGrantedItemSet(mItemsProvider.fetchReadGrantedItemsUrisForPackage(
intentExtras.getInt(Intent.EXTRA_UID), mimeTypeFilters)
.stream().map((Uri uri) -> String.valueOf(ContentUris.parseId(uri)))
.collect(Collectors.toSet()));
@@ -526,8 +536,7 @@
Set<String> preGrantedItems = new HashSet<>(0);
Set<String> deSelectedPreGrantedItems = new HashSet<>(0);
- if (isUserSelectForApp() && getConfigStore().isPickerChoiceManagedSelectionEnabled()
- && mSelection.getPreGrantedItems() != null) {
+ if (isManagedSelectionEnabled() && mSelection.getPreGrantedItems() != null) {
preGrantedItems = mSelection.getPreGrantedItems();
deSelectedPreGrantedItems = new HashSet<>(
mSelection.getPreGrantedItemIdsToBeRevoked());
@@ -573,9 +582,7 @@
* issue by selectively loading those items and adding them to the selection list.</p>
*/
public void getRemainingPreGrantedItems() {
- if (isUserSelectForApp()
- && getConfigStore().isPickerChoiceManagedSelectionEnabled()
- && mSelection.getPreGrantedItems() != null) {
+ if (isManagedSelectionEnabled() && mSelection.getPreGrantedItems() != null) {
List<String> idsForItemsToBeFetched = new ArrayList<>(mSelection.getPreGrantedItems());
idsForItemsToBeFetched.removeAll(mSelection.getSelectedItemsIds());
idsForItemsToBeFetched.removeAll(mSelection.getPreGrantedItemIdsToBeRevoked());
@@ -847,6 +854,8 @@
mIsUserSelectForApp =
MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intent.getAction());
+ mIsManagedSelectionEnabled = mIsUserSelectForApp
+ && getConfigStore().isPickerChoiceManagedSelectionEnabled();
if (!SdkLevel.isAtLeastU() && mIsUserSelectForApp) {
throw new IllegalArgumentException("ACTION_USER_SELECT_IMAGES_FOR_APP is not enabled "
+ " for this OS version");
diff --git a/src/com/android/providers/media/util/SyntheticPathUtils.java b/src/com/android/providers/media/util/SyntheticPathUtils.java
index 6d73802..aa0db93 100644
--- a/src/com/android/providers/media/util/SyntheticPathUtils.java
+++ b/src/com/android/providers/media/util/SyntheticPathUtils.java
@@ -16,17 +16,14 @@
package com.android.providers.media.util;
-import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
-import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT;
import static com.android.providers.media.util.FileUtils.buildPath;
import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile;
import static com.android.providers.media.util.FileUtils.extractFileName;
+import androidx.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
-import androidx.annotation.VisibleForTesting;
-
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
@@ -40,6 +37,7 @@
private static final String TRANSFORMS_DIR = ".transforms";
private static final String SYNTHETIC_DIR = "synthetic";
private static final String REDACTED_DIR = "redacted";
+ private static final String PICKER_DIR = "picker";
public static final String REDACTED_URI_ID_PREFIX = "RUID";
public static final int REDACTED_URI_ID_SIZE = 36;
@@ -50,12 +48,8 @@
return buildPath(/* base */ null, TRANSFORMS_DIR, SYNTHETIC_DIR, REDACTED_DIR).getPath();
}
- /**
- * Returns picker synthetic path directory.
- */
- public static String getPickerRelativePath(String pickerSegmentType) {
- return buildPath(/* base */ null, TRANSFORMS_DIR, SYNTHETIC_DIR,
- pickerSegmentType).getPath();
+ public static String getPickerRelativePath() {
+ return buildPath(/* base */ null, TRANSFORMS_DIR, SYNTHETIC_DIR, PICKER_DIR).getPath();
}
public static boolean isRedactedPath(String path, int userId) {
@@ -72,13 +66,10 @@
}
public static boolean isPickerPath(String path, int userId) {
- final String pickerDir = buildPrimaryVolumeFile(userId, getPickerRelativePath(
- PICKER_SEGMENT)).getAbsolutePath();
- final String pickerGetContentDir = buildPrimaryVolumeFile(userId,
- getPickerRelativePath(PICKER_GET_CONTENT_SEGMENT)).getAbsolutePath();
+ final String pickerDir = buildPrimaryVolumeFile(userId, getPickerRelativePath())
+ .getAbsolutePath();
- return path != null && (startsWith(path, pickerDir) || startsWith(path,
- pickerGetContentDir));
+ return path != null && startsWith(path, pickerDir);
}
public static boolean isSyntheticPath(String path, int userId) {
diff --git a/tests/Android.bp b/tests/Android.bp
index 4dbaf16..e3b267c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -190,6 +190,7 @@
"glide-gifdecoder-prebuilt",
"glide-disklrucache-prebuilt",
"glide-annotation-and-compiler-prebuilt",
+ "glide-integration-recyclerview-prebuilt",
"androidx.fragment_fragment",
"androidx.vectordrawable_vectordrawable-animated",
"androidx.exifinterface_exifinterface",
diff --git a/tests/src/com/android/providers/media/AccessCheckerTest.java b/tests/src/com/android/providers/media/AccessCheckerTest.java
index ef6c963..49a870e 100644
--- a/tests/src/com/android/providers/media/AccessCheckerTest.java
+++ b/tests/src/com/android/providers/media/AccessCheckerTest.java
@@ -29,7 +29,6 @@
import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
import static com.android.providers.media.AccessChecker.hasAccessToCollection;
import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
-import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA;
import static com.android.providers.media.LocalUriMatcher.DOWNLOADS;
import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID;
@@ -46,9 +45,7 @@
import static com.google.common.truth.Truth.assertWithMessage;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
import android.os.Bundle;
import android.system.Os;
@@ -401,28 +398,6 @@
}
@Test
- public void testIsRedactionNeededForPickerUri_returnsFalse_withNoRedactPerms() {
- LocalCallingIdentity callingIdentityWithRedactionNotNeededPermission =
- LocalCallingIdentity.forTest(
- InstrumentationRegistry.getTargetContext(), Os.getuid(),
- ~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
-
- assertFalse("App with write perms should get non redacted data",
- isRedactionNeededForPickerUri(callingIdentityWithRedactionNotNeededPermission));
- }
-
- @Test
- public void testIsRedactionNeededForPickerUri_returnsTrue_withRedactPerms() {
- LocalCallingIdentity callingIdentityWithRedactionNeededPermission =
- LocalCallingIdentity.forTest(
- InstrumentationRegistry.getTargetContext(), Os.getuid(),
- LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
-
- assertTrue("App with no perms should get redacted data",
- isRedactionNeededForPickerUri(callingIdentityWithRedactionNeededPermission));
- }
-
- @Test
public void testGetWhereForConstrainedAccess_forWrite_hasLegacyWrite() {
LocalCallingIdentity hasLegacyWrite = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
diff --git a/tests/src/com/android/providers/media/IdleServiceTest.java b/tests/src/com/android/providers/media/IdleServiceTest.java
index 9dd2eb6..bd0e7bd 100644
--- a/tests/src/com/android/providers/media/IdleServiceTest.java
+++ b/tests/src/com/android/providers/media/IdleServiceTest.java
@@ -32,8 +32,11 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import android.Manifest;
+import android.app.Instrumentation;
import android.app.job.JobScheduler;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
@@ -45,14 +48,21 @@
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
+import android.os.NewUserRequest;
import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.MediaStore;
import android.text.format.DateUtils;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
import com.android.providers.media.scan.MediaScannerTest;
import com.android.providers.media.util.FileUtils;
@@ -68,7 +78,10 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.Locale;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
public class IdleServiceTest {
@@ -85,6 +98,8 @@
android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
android.Manifest.permission.READ_DEVICE_CONFIG,
Manifest.permission.INTERACT_ACROSS_USERS,
+ Manifest.permission.MANAGE_USERS,
+ Manifest.permission.WRITE_MEDIA_STORAGE,
android.Manifest.permission.DUMP);
mDir = new File(context.getExternalMediaDirs()[0], "test_" + System.nanoTime());
@@ -294,6 +309,119 @@
}
}
+ /**
+ * Idle maintenance run on non-demo devices should not remove xattr data stored for different
+ * users on /data/media/0. This is not done due to b/305658663.
+ */
+ @Test
+ @RunOnlyOnPostsubmit
+ @LargeTest
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void test_idle_maintenance_nonDemoDevice() throws IOException {
+ assumeTrue(UserManager.supportsMultipleUsers());
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(
+ Manifest.permission.INTERACT_ACROSS_USERS,
+ Manifest.permission.CREATE_USERS,
+ Manifest.permission.MANAGE_USERS,
+ Manifest.permission.WRITE_MEDIA_STORAGE,
+ android.Manifest.permission.DUMP);
+ SystemClock.sleep(3000);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ Context context = instrumentation.getContext();
+ UserManager userManager = context.getSystemService(UserManager.class);
+ Integer secondaryUser = -1;
+ boolean secondaryUserPresent = false;
+
+ try {
+ secondaryUser = createNewUser(userManager);
+ secondaryUserPresent = true;
+ startUser(secondaryUser);
+ SystemClock.sleep(3000);
+
+ // Verify presence of recovery data
+ String[] recoveryData = MediaStore.getRecoveryData(context.getContentResolver());
+ assertThat(getUserIdsForUsersFromRecoveryData(recoveryData)).containsAtLeastElementsIn(
+ Arrays.asList(UserHandle.SYSTEM.getIdentifier(), secondaryUser));
+
+ executeShellCommand(
+ "content call --uri content://media/external/file --method "
+ + "run_idle_maintenance --user "
+ + UserHandle.SYSTEM.getIdentifier());
+ executeShellCommand(
+ "content call --uri content://media/external/file --method "
+ + "run_idle_maintenance --user " + secondaryUser);
+
+ // Verify presence of recovery data even after running idle maintenance
+ recoveryData = MediaStore.getRecoveryData(context.getContentResolver());
+ assertThat(getUserIdsForUsersFromRecoveryData(recoveryData)).containsAtLeastElementsIn(
+ Arrays.asList(UserHandle.SYSTEM.getIdentifier(), secondaryUser));
+
+ // Remove secondary user
+ removeUser(secondaryUser);
+ secondaryUserPresent = false;
+ SystemClock.sleep(3000);
+
+ // Run idle maintenance for user 0
+ executeShellCommand(
+ "content call --uri content://media/external/file --method "
+ + "run_idle_maintenance --user "
+ + UserHandle.SYSTEM.getIdentifier());
+
+ // Verify presence of recovery data
+ recoveryData = MediaStore.getRecoveryData(context.getContentResolver());
+ assertThat(getUserIdsForUsersFromRecoveryData(recoveryData)).containsAtLeastElementsIn(
+ Arrays.asList(UserHandle.SYSTEM.getIdentifier(), secondaryUser));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (secondaryUserPresent) {
+ removeUser(secondaryUser);
+ }
+ MediaStore.removeRecoveryData(context.getContentResolver());
+ }
+ }
+
+ private Set<Integer> getUserIdsForUsersFromRecoveryData(String[] recoveryData) {
+ Set<Integer> userIdSet = new HashSet<>();
+ for (String data : recoveryData) {
+ if (data.startsWith("user.extdbnextrowid")) {
+ userIdSet.add(Integer.valueOf(data.substring("user.extdbnextrowid".length())));
+ } else if (data.startsWith("user.extdbsessionid")) {
+ userIdSet.add(Integer.valueOf(data.substring("user.extdbsessionid".length())));
+ }
+ }
+
+ return userIdSet;
+ }
+
+
+ private int createNewUser(UserManager userManager) {
+ final NewUserRequest newUserRequest = new NewUserRequest.Builder().setName(
+ "test_user" + System.currentTimeMillis()).setUserType(
+ UserManager.USER_TYPE_FULL_SECONDARY).build();
+ final UserHandle newUser = userManager.createUser(newUserRequest).getUser();
+ if (newUser == null) {
+ fail("Error while creating a new user");
+ }
+ return newUser.getIdentifier();
+ }
+
+ private void startUser(int userId) throws IOException {
+ Log.i(TAG, "Starting user " + userId);
+ String output = executeShellCommand("am start-user -w " + userId);
+ if (output.startsWith("Error")) {
+ fail(String.format("Failed to start user %d: %s", userId, output));
+ }
+ }
+
+ private void removeUser(int userId) throws IOException {
+ final String output = executeShellCommand("cmd package remove-user " + userId);
+ if (output.startsWith("Error")) {
+ fail("Error removing the user #" + userId + ": " + output);
+ }
+ }
+
private void assertExpiredItemIsExtended(ContentResolver resolver, Uri uri,
long lastExpiredDate) {
final String[] projection = new String[]{DATE_EXPIRES};
diff --git a/tests/src/com/android/providers/media/LocalUriMatcherTest.java b/tests/src/com/android/providers/media/LocalUriMatcherTest.java
index 01ce3ec..32721ed 100644
--- a/tests/src/com/android/providers/media/LocalUriMatcherTest.java
+++ b/tests/src/com/android/providers/media/LocalUriMatcherTest.java
@@ -43,15 +43,6 @@
LocalUriMatcher.PICKER_ID,
assembleTestUri(new String[] {"picker", "0", "anything", "media", "anything"}));
- assertMatchesPublic(
- LocalUriMatcher.PICKER_GET_CONTENT_ID,
- assembleTestUri(new String[]{"picker_get_content", Integer.toString(1),
- Integer.toString(1)}));
- assertMatchesPublic(
- LocalUriMatcher.PICKER_GET_CONTENT_ID,
- assembleTestUri(
- new String[]{"picker_get_content", "0", "anything", "media", "anything"}));
-
assertMatchesPublic(LocalUriMatcher.CLI, assembleTestUri(new String[] {"cli"}));
assertMatchesPublic(
@@ -213,15 +204,6 @@
LocalUriMatcher.PICKER_ID,
assembleTestUri(new String[] {"picker", "0", "anything", "media", "anything"}));
- assertMatchesHidden(
- LocalUriMatcher.PICKER_GET_CONTENT_ID,
- assembleTestUri(new String[]{"picker_get_content", Integer.toString(1),
- Integer.toString(1)}));
- assertMatchesHidden(
- LocalUriMatcher.PICKER_GET_CONTENT_ID,
- assembleTestUri(
- new String[]{"picker_get_content", "0", "anything", "media", "anything"}));
-
assertMatchesHidden(LocalUriMatcher.CLI, assembleTestUri(new String[] {"cli"}));
assertMatchesHidden(
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index e8024ee..fe13649 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -16,7 +16,6 @@
package com.android.providers.media;
-import static com.android.providers.media.photopicker.data.MediaGrantsProvider.fetchReadGrantedItemsUrisForPackage;
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;
@@ -74,6 +73,7 @@
import com.android.providers.media.MediaProvider.VolumeArgumentException;
import com.android.providers.media.MediaProvider.VolumeNotFoundException;
import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.ItemsProvider;
import com.android.providers.media.util.FileUtils;
import com.android.providers.media.util.FileUtilsTest;
import com.android.providers.media.util.SQLiteQueryBuilder;
@@ -107,6 +107,8 @@
static final String PERMISSIONLESS_APP = "com.android.providers.media.testapp.withoutperms";
private static Context sIsolatedContext;
+
+ private static ItemsProvider sItemsProvider;
private static Context sContext;
private static ContentResolver sIsolatedResolver;
@@ -116,6 +118,11 @@
.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
Manifest.permission.READ_DEVICE_CONFIG,
+ // Adding this to use getUserHandles() api of UserManagerService which
+ // requires either MANAGE_USERS or CREATE_USERS. Since shell does not have
+ // MANAGER_USERS permissions, using CREATE_USERS in test. This works with
+ // MANAGE_USERS permission for MediaProvider module.
+ Manifest.permission.CREATE_USERS,
Manifest.permission.INTERACT_ACROSS_USERS);
resetIsolatedContext();
@@ -362,8 +369,8 @@
try {
String[] mimeTypes = {"image/*"};
// Verify empty list with no grants.
- List<Uri> grantedUris = fetchReadGrantedItemsUrisForPackage(
- sIsolatedContext, android.os.Process.myUid(), mimeTypes);
+ List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ android.os.Process.myUid(), mimeTypes);
assertTrue(grantedUris.isEmpty());
// Grants the READ-GRANT for the testUris for the current package.
@@ -372,8 +379,8 @@
List.of(testUri));
// Assert that the grant was returned.
- List<Uri> grantedUris2 = fetchReadGrantedItemsUrisForPackage(
- sIsolatedContext, android.os.Process.myUid(), mimeTypes);
+ List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ android.os.Process.myUid(), mimeTypes);
assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris2.get(0)));
} finally {
dir.delete();
@@ -406,16 +413,16 @@
MediaStore.grantMediaReadForPackage(sIsolatedContext,
android.os.Process.myUid(),
List.of(testUri));
- List<Uri> grantedUris = fetchReadGrantedItemsUrisForPackage(
- sIsolatedContext, android.os.Process.myUid(), mimeTypes);
+ List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ android.os.Process.myUid(), mimeTypes);
assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris.get(0)));
// Revoked the grant that was provided to testUri and verify that now the current
// package has no grants.
MediaStore.revokeMediaReadForPackages(sIsolatedContext, android.os.Process.myUid(),
grantedUris);
- List<Uri> grantedUris2 = fetchReadGrantedItemsUrisForPackage(
- sIsolatedContext, android.os.Process.myUid(), mimeTypes);
+ List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ android.os.Process.myUid(), mimeTypes);
assertEquals(0, grantedUris2.size());
} finally {
dir.delete();
@@ -1831,5 +1838,6 @@
sContext = InstrumentationRegistry.getTargetContext();
sIsolatedContext = new IsolatedContext(sContext, "modern", /*asFuseThread*/ false);
sIsolatedResolver = sIsolatedContext.getContentResolver();
+ sItemsProvider = new ItemsProvider(sIsolatedContext);
}
}
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index b3d9f70..3d8c260 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -16,10 +16,8 @@
package com.android.providers.media;
-import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.provider.MediaStore.ACTION_PICK_IMAGES;
import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.InstrumentationRegistry.getTargetContext;
@@ -28,7 +26,6 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -56,6 +53,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -80,13 +78,10 @@
private static Uri sTestPickerUri;
private static String TEST_ID;
- private static Uri sMediaStoreUriInOtherContext;
-
private static class TestPickerUriResolver extends PickerUriResolver {
TestPickerUriResolver(Context context) {
- super(context, new PickerDbFacade(getTargetContext()),
- new ProjectionHelper(Column.class, ExportedSince.class),
- new LocalUriMatcher(MediaStore.AUTHORITY));
+ super(context, new PickerDbFacade(getTargetContext(), new PickerSyncLockManager()),
+ new ProjectionHelper(Column.class, ExportedSince.class));
}
@Override
@@ -124,16 +119,14 @@
Manifest.permission.INTERACT_ACROSS_USERS);
sCurrentContext = mock(Context.class);
when(sCurrentContext.getUser()).thenReturn(UserHandle.of(UserHandle.myUserId()));
- PackageManager packageManager = mock(PackageManager.class);
- when(sCurrentContext.getPackageManager()).thenReturn(packageManager);
- when(packageManager.getPackagesForUid(anyInt())).thenReturn(
- new String[]{getContext().getPackageName()});
final Context otherUserContext = createOtherUserContext(TEST_USER);
sTestPickerUriResolver = new TestPickerUriResolver(sCurrentContext);
- sMediaStoreUriInOtherContext = createTestFileInContext(otherUserContext);
- TEST_ID = sMediaStoreUriInOtherContext.getLastPathSegment();
+ final Uri mediaStoreUriInOtherContext = createTestFileInContext(otherUserContext);
+ TEST_ID = mediaStoreUriInOtherContext.getLastPathSegment();
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(mediaStoreUriInOtherContext),
+ TEST_USER);
}
@AfterClass
@@ -143,37 +136,24 @@
@Test
public void wrapProviderUriValid() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final String providerSuffix = "authority/media/media_id";
final Uri providerUriUserImplicit = Uri.parse("content://" + providerSuffix);
final Uri providerUriUser0 = Uri.parse("content://0@" + providerSuffix);
final Uri mediaUriUser0 = Uri.parse("content://media/picker/0/" + providerSuffix);
- final Uri mediaUriUser0PickerGetContent = Uri.parse(
- "content://media/picker_get_content/0/" + providerSuffix);
final Uri providerUriUser10 = Uri.parse("content://10@" + providerSuffix);
final Uri mediaUriUser10 = Uri.parse("content://media/picker/10/" + providerSuffix);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit,
- ACTION_PICK_IMAGES, 0))
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit, 0))
.isEqualTo(mediaUriUser0);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit,
- ACTION_GET_CONTENT, 0))
- .isEqualTo(mediaUriUser0PickerGetContent);
- assertThat(
- PickerUriResolver.wrapProviderUri(providerUriUser0, ACTION_PICK_IMAGES,
- 0)).isEqualTo(mediaUriUser0);
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUser0, 0)).isEqualTo(mediaUriUser0);
assertThat(PickerUriResolver.unwrapProviderUri(mediaUriUser0)).isEqualTo(providerUriUser0);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit,
- ACTION_PICK_IMAGES, 10))
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit, 10))
.isEqualTo(mediaUriUser10);
- assertThat(
- PickerUriResolver.wrapProviderUri(providerUriUser10, ACTION_PICK_IMAGES,
- 10))
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUser10, 10))
.isEqualTo(mediaUriUser10);
assertThat(PickerUriResolver.unwrapProviderUri(mediaUriUser10))
.isEqualTo(providerUriUser10);
@@ -181,8 +161,6 @@
@Test
public void wrapProviderUriInvalid() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final String providerSuffixLong = "authority/media/media_id/another_media_id";
final String providerSuffixShort = "authority/media";
@@ -193,22 +171,18 @@
final Uri mediaUriUserShort = Uri.parse("content://media/picker/0/" + providerSuffixShort);
assertThrows(IllegalArgumentException.class,
- () -> PickerUriResolver.wrapProviderUri(providerUriUserLong, ACTION_PICK_IMAGES,
- 0));
+ () -> PickerUriResolver.wrapProviderUri(providerUriUserLong, 0));
assertThrows(IllegalArgumentException.class,
() -> PickerUriResolver.unwrapProviderUri(mediaUriUserLong));
assertThrows(IllegalArgumentException.class,
() -> PickerUriResolver.unwrapProviderUri(mediaUriUserShort));
assertThrows(IllegalArgumentException.class,
- () -> PickerUriResolver.wrapProviderUri(providerUriUserShort, ACTION_PICK_IMAGES,
- 0));
+ () -> PickerUriResolver.wrapProviderUri(providerUriUserShort, 0));
}
@Test
public void testGetAlbumUri() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/album");
assertThat(PickerUriResolver.getAlbumUri(authority)).isEqualTo(uri);
@@ -216,8 +190,6 @@
@Test
public void testGetMediaUri() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/media");
assertThat(PickerUriResolver.getMediaUri(authority)).isEqualTo(uri);
@@ -225,8 +197,6 @@
@Test
public void testGetDeletedMediaUri() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/deleted_media");
assertThat(PickerUriResolver.getDeletedMediaUri(authority)).isEqualTo(uri);
@@ -234,8 +204,6 @@
@Test
public void testCreateSurfaceControllerUri() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/surface_controller");
assertThat(PickerUriResolver.createSurfaceControllerUri(authority)).isEqualTo(uri);
@@ -243,13 +211,10 @@
@Test
public void testOpenFile_mode_w() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
try {
sTestPickerUriResolver.openFile(sTestPickerUri, "w", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ /* callingPid */ -1, /* callingUid */ -1);
fail("Write is not supported for Picker Uris. uri: " + sTestPickerUri);
} catch (SecurityException expected) {
// expected
@@ -260,13 +225,10 @@
@Test
public void testOpenFile_mode_rw() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
try {
sTestPickerUriResolver.openFile(sTestPickerUri, "rw", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ /* callingPid */ -1, /* callingUid */ -1);
fail("Read-Write is not supported for Picker Uris. uri: " + sTestPickerUri);
} catch (SecurityException expected) {
// expected
@@ -277,13 +239,10 @@
@Test
public void testOpenFile_mode_invalid() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
try {
sTestPickerUriResolver.openFile(sTestPickerUri, "foo", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ /* callingPid */ -1, /* callingUid */ -1);
fail("Invalid mode should not be supported for openFile. uri: " + sTestPickerUri);
} catch (IllegalArgumentException expected) {
// expected
@@ -293,8 +252,6 @@
@Test
public void testPickerUriResolver_permissionDenied() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ false);
testOpenFile_permissionDenied(sTestPickerUri);
@@ -305,15 +262,13 @@
@Test
public void testPermissionGrantedOnOtherUserUri() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
// This test requires the uri to be valid in 2 different users, but the permission is
// granted in one user only.
final int otherUserId = 50;
final Context otherUserContext = createOtherUserContext(otherUserId);
final Uri mediaStoreUserInAnotherValidUser = createTestFileInContext(otherUserContext);
final Uri grantedUri = getPickerUriForId(ContentUris.parseId(
- mediaStoreUserInAnotherValidUser), otherUserId, ACTION_PICK_IMAGES);
+ mediaStoreUserInAnotherValidUser), otherUserId);
updateReadUriPermission(grantedUri, /* grant */ true);
final Uri deniedUri = sTestPickerUri;
@@ -327,12 +282,9 @@
@Test
public void testPickerUriResolver_userInvalid() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final int invalidUserId = 40;
- final Uri inValidUserPickerUri = getPickerUriForId(/* id */ 1, invalidUserId,
- ACTION_PICK_IMAGES);
+ final Uri inValidUserPickerUri = getPickerUriForId(/* id */ 1, invalidUserId);
updateReadUriPermission(inValidUserPickerUri, /* grant */ true);
// This method is called on current context when pickerUriResolver wants to get the content
@@ -350,8 +302,6 @@
@Test
public void testPickerUriResolver_userValid() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
assertThat(PickerUriResolver.getUserId(sTestPickerUri)).isEqualTo(TEST_USER);
@@ -362,102 +312,7 @@
}
@Test
- public void testPickerUriResolver_pickerUri_fileOpenWithRequireOriginal() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
- // Grants given on original uri
- updateReadUriPermission(sTestPickerUri, /* grant */ true);
- sTestPickerUri = MediaStore.setRequireOriginal(sTestPickerUri);
-
- assertThat(PickerUriResolver.getUserId(sTestPickerUri)).isEqualTo(TEST_USER);
- try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
- "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0))) {
- fail("Require original should not be supported for picker uri:" + sTestPickerUri);
- } catch (UnsupportedOperationException expected) {
- // expected
- }
- try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
- "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0))) {
- fail("Require original should not be supported for picker uri:" + sTestPickerUri);
- } catch (UnsupportedOperationException expected) {
- // expected
- }
-
- try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
- "image/*", /* opts */ null, /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0))) {
- fail("Require original should not be supported for picker uri:" + sTestPickerUri);
- } catch (UnsupportedOperationException expected) {
- // expected
- }
- try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
- "image/*", /* opts */ null, /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0))) {
- fail("Require original should not be supported for picker uri:" + sTestPickerUri);
- } catch (UnsupportedOperationException expected) {
- // expected
- }
-
- testQuery(sTestPickerUri);
- testGetType(sTestPickerUri, "image/jpeg");
- }
-
- @Test
- public void testPickerUriResolver_pickerGetContentUri_fileOpenWithRequireOriginal()
- throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_GET_CONTENT);
- // Grants given on original uri
- updateReadUriPermission(sTestPickerUri, /* grant */ true);
- sTestPickerUri = MediaStore.setRequireOriginal(sTestPickerUri);
-
- assertThat(PickerUriResolver.getUserId(sTestPickerUri)).isEqualTo(TEST_USER);
- try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
- "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- ~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
- assertThat(pfd).isNotNull();
- }
- try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
- "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
- fail("Require original should not be supported when calling package does not have "
- + "required permission");
- } catch (UnsupportedOperationException expected) {
- // expected
- }
-
- try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
- "image/*", /* opts */ null, /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- ~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
- assertThat(afd).isNotNull();
- }
- try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
- "image/*", /* opts */ null, /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
- fail("Require original should not be supported when calling package does not have "
- + "required permission");
- } catch (UnsupportedOperationException expected) {
- // expected
- }
-
- testQuery(sTestPickerUri);
- testGetType(sTestPickerUri, "image/jpeg");
- }
-
- @Test
public void testQueryUnknownColumn() throws Exception {
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
- TEST_USER, ACTION_PICK_IMAGES);
final int myUid = Process.myUid();
final int myPid = Process.myPid();
final String myPackageName = getContext().getPackageName();
@@ -512,28 +367,25 @@
Intent.FLAG_GRANT_READ_URI_PERMISSION)).thenReturn(permission);
}
- private static Uri getPickerUriForId(long id, int user, String action) {
+ private static Uri getPickerUriForId(long id, int user) {
final Uri providerUri = PickerUriResolver
.getMediaUri(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
.buildUpon()
.appendPath(String.valueOf(id))
.build();
- return PickerUriResolver.wrapProviderUri(providerUri, action, user);
+ return PickerUriResolver.wrapProviderUri(providerUri, user);
}
private void testOpenFile(Uri uri) throws Exception {
try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0))) {
+ /* callingPid */ -1, /* callingUid */ -1)) {
assertThat(pfd).isNotNull();
}
}
private void testOpenTypedAssetFile(Uri uri) throws Exception {
try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(uri, "image/*",
- /* opts */ null, /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0))) {
+ /* opts */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1)) {
assertThat(afd).isNotNull();
}
}
@@ -557,9 +409,8 @@
private void testOpenFileInvalidUser(Uri uri) {
try {
- sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ sTestPickerUriResolver.openFile(uri, "r", /* signal */ null, /* callingPid */ -1,
+ /* callingUid */ -1);
fail("Invalid user specified in the picker uri: " + uri);
} catch (FileNotFoundException expected) {
// expected
@@ -570,9 +421,7 @@
private void testOpenTypedAssetFileInvalidUser(Uri uri) throws Exception {
try {
sTestPickerUriResolver.openTypedAssetFile(uri, "image/*", /* opts */ null,
- /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
fail("Invalid user specified in the picker uri: " + uri);
} catch (FileNotFoundException expected) {
// expected
@@ -600,9 +449,8 @@
private void testOpenFile_permissionDenied(Uri uri) throws Exception {
try {
- sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ sTestPickerUriResolver.openFile(uri, "r", /* signal */ null, /* callingPid */ -1,
+ /* callingUid */ -1);
fail("openFile should fail if the caller does not have permission grant on the picker"
+ " uri: " + uri);
} catch (SecurityException expected) {
@@ -615,9 +463,7 @@
private void testOpenTypedAssetFile_permissionDenied(Uri uri) throws Exception {
try {
sTestPickerUriResolver.openTypedAssetFile(uri, "image/*", /* opts */ null,
- /* signal */ null,
- LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
- 0));
+ /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
fail("openTypedAssetFile should fail if the caller does not have permission grant on"
+ " the picker uri: " + uri);
} catch (SecurityException expected) {
diff --git a/tests/src/com/android/providers/media/TestConfigStore.java b/tests/src/com/android/providers/media/TestConfigStore.java
index df7ef38..ca38ecc 100644
--- a/tests/src/com/android/providers/media/TestConfigStore.java
+++ b/tests/src/com/android/providers/media/TestConfigStore.java
@@ -35,6 +35,8 @@
*/
public class TestConfigStore implements ConfigStore {
private boolean mCloudMediaInPhotoPickerEnabled = false;
+
+ private boolean mPickerChoiceManagedSelectionEnabled = false;
private List<String> mAllowedCloudProviderPackages = Collections.emptyList();
private @Nullable String mDefaultCloudProviderPackage = null;
private List<Pair<Executor, Runnable>> mObservers = new ArrayList<>();
@@ -61,6 +63,13 @@
notifyObservers();
}
+ /**
+ * Enables pickerChoiceManagedSelection flag in the test config.
+ */
+ public void enablePickerChoiceManagedSelectionEnabled() {
+ mPickerChoiceManagedSelectionEnabled = true;
+ }
+
@Override
public @NonNull List<String> getAllowedCloudProviderPackages() {
return mAllowedCloudProviderPackages;
@@ -109,6 +118,11 @@
}
@Override
+ public boolean isPickerChoiceManagedSelectionEnabled() {
+ return mPickerChoiceManagedSelectionEnabled;
+ }
+
+ @Override
public void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener) {
Pair p = Pair.create(executor, listener);
mObservers.add(p);
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index 8395b1b..5f6f269 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -53,6 +53,7 @@
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import com.android.providers.media.photopicker.sync.PickerSyncManager;
import com.android.providers.media.util.ForegroundThread;
@@ -137,14 +138,16 @@
final File dbPath = mContext.getDatabasePath(DB_NAME);
dbPath.delete();
+ final PickerSyncLockManager lockManager = new PickerSyncLockManager();
+
mDbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
- mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
+ mFacade = new PickerDbFacade(mContext, lockManager, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
mConfigStore = new TestConfigStore();
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
mController = PickerSyncController.initialize(
- mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, mFacade, mConfigStore, lockManager, LOCAL_PROVIDER_AUTHORITY);
initializeTestWorkManager(mContext);
final WorkManager workManager = WorkManager.getInstance(mContext);
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 2462b32..e51678b 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -52,6 +52,8 @@
import com.android.providers.media.photopicker.data.CloudProviderInfo;
import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
import org.junit.After;
import org.junit.Before;
@@ -136,6 +138,7 @@
private TestConfigStore mConfigStore;
private PickerDbFacade mFacade;
private PickerSyncController mController;
+ private PickerSyncLockManager mLockManager;
@Before
public void setUp() {
@@ -155,14 +158,16 @@
final File dbPath = mContext.getDatabasePath(DB_NAME);
dbPath.delete();
+ mLockManager = new PickerSyncLockManager();
+
PickerDatabaseHelper dbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
- mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelper);
+ mFacade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY, dbHelper);
mConfigStore = new TestConfigStore();
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
mController = PickerSyncController.initialize(
- mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
// Set cloud provider to null to avoid trying to sync it during other tests
// that might be using an IsolatedContext
@@ -192,7 +197,7 @@
configStore.disableCloudMediaFeature();
PickerSyncController controller =
- PickerSyncController.initialize(mContext, mFacade, configStore);
+ PickerSyncController.initialize(mContext, mFacade, configStore, mLockManager);
assertThat(controller.getCurrentCloudProviderInfo()).isEqualTo(CloudProviderInfo.EMPTY);
configStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
configStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
@@ -208,7 +213,7 @@
}
@Test
- public void testSyncIsCancelledIfCloudProviderIsChanged() {
+ public void testSyncIsCancelledIfCloudProviderIsChanged() throws UnableToAcquireLockException {
PickerSyncController controller = spy(mController);
@@ -218,7 +223,7 @@
doReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
CLOUD_SECONDARY_PROVIDER_AUTHORITY)
.when(controller)
- .getCloudProvider();
+ .getCloudProviderWithTimeout();
// Add local only media, we expect these to be successfully sync'd from the local provider.
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
@@ -245,10 +250,9 @@
controller.syncAllMedia();
// The cursor should only contain the items from the local provider. (Even though we've
- // aded a total of 4 items to the linked providers.)
+ // added a total of 4 items to the linked providers.)
try (Cursor cr = queryMedia()) {
assertThat(cr.getCount()).isEqualTo(2);
-
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -773,7 +777,7 @@
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
final PickerSyncController controller = PickerSyncController.initialize(
- mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
final List<CloudProviderInfo> providers = controller.getAvailableCloudProviders();
assertThat(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
}
@@ -797,7 +801,7 @@
mConfigStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
mController = PickerSyncController.initialize(mContext, mFacade, mConfigStore,
- LOCAL_PROVIDER_AUTHORITY);
+ mLockManager, LOCAL_PROVIDER_AUTHORITY);
assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
}
@@ -843,10 +847,10 @@
final PickerDatabaseHelper dbHelper = new PickerDatabaseHelper(
mContext, DB_NAME, DB_VERSION_1);
- PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+ PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
dbHelper);
PickerSyncController controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.syncAllMedia();
@@ -863,9 +867,9 @@
final File dbPath = mContext.getDatabasePath(DB_NAME);
dbPath.delete();
- facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelper);
+ facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY, dbHelper);
controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
@@ -887,10 +891,10 @@
mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
PickerDatabaseHelper dbHelperV1 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
- PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+ PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
dbHelperV1);
PickerSyncController controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.syncAllMedia();
@@ -904,9 +908,9 @@
// Upgrade db version
dbHelperV1.close();
PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
- facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelperV2);
+ facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY, dbHelperV2);
controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
@@ -928,10 +932,10 @@
mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
- PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+ PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
dbHelperV2);
PickerSyncController controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.syncAllMedia();
@@ -945,10 +949,10 @@
// Downgrade db version
dbHelperV2.close();
PickerDatabaseHelper dbHelperV1 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
- facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+ facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
dbHelperV1);
controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
@@ -1102,10 +1106,10 @@
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
PickerDatabaseHelper dbHelperV1 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
- PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+ PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
dbHelperV1);
PickerSyncController controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
controller.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1113,10 +1117,10 @@
// Downgrade db version
dbHelperV1.close();
PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
- facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+ facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
dbHelperV2);
controller = PickerSyncController.initialize(
- mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -1129,7 +1133,7 @@
// Test the default NOT_SET state
mController =
PickerSyncController.initialize(
- mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
@@ -1138,7 +1142,7 @@
mController =
PickerSyncController.initialize(
- mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
assertThat(mController.getCloudProvider()).isNull();
@@ -1147,7 +1151,7 @@
mController =
PickerSyncController.initialize(
- mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
}
@@ -1376,7 +1380,8 @@
mConfigStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
// The cloud provider is changed on PickerSyncController construction
- mController = PickerSyncController.initialize(mContext, mFacade, mConfigStore);
+ mController = PickerSyncController
+ .initialize(mContext, mFacade, mConfigStore, mLockManager);
TimeUnit.MILLISECONDS.sleep(100);
assertThat(refreshUiNotificationObserver.mNotificationReceived).isTrue();
@@ -1390,7 +1395,8 @@
refreshUiNotificationObserver.mNotificationReceived = false;
// The cloud provider remains unchanged on PickerSyncController construction
- mController = PickerSyncController.initialize(mContext, mFacade, mConfigStore);
+ mController = PickerSyncController
+ .initialize(mContext, mFacade, mConfigStore, mLockManager);
TimeUnit.MILLISECONDS.sleep(100);
assertThat(refreshUiNotificationObserver.mNotificationReceived).isFalse();
@@ -1473,7 +1479,7 @@
}
return PickerSyncController.initialize(
- mockContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+ mockContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
}
private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
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 f6943a2..d7838af 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -21,6 +21,7 @@
import static com.android.providers.media.util.MimeUtils.getExtensionFromMimeType;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
@@ -40,8 +41,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.ProjectionHelper;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import com.android.providers.media.photopicker.sync.SyncTracker;
import com.android.providers.media.photopicker.sync.SyncTrackerRegistry;
@@ -105,7 +106,7 @@
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
dbPath.delete();
- mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER);
+ mFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), LOCAL_PROVIDER);
mFacade.setCloudProvider(CLOUD_PROVIDER);
mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class);
@@ -1350,38 +1351,20 @@
// Assert all projection columns
final String[] allProjection = mProjectionHelper.getProjectionMap(
PickerMediaColumns.class).keySet().toArray(new String[0]);
- try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_SEGMENT,
- LOCAL_PROVIDER, LOCAL_ID, allProjection)) {
- assertWithMessage(
- "Unexpected number of rows when asserting all projection columns with "
- + "PickerUriResolver as PICKER_SEGMENT on local provider.")
- .that(cr.getCount()).isEqualTo(1);
+ try (Cursor cr = mFacade.queryMediaIdForApps(LOCAL_PROVIDER, LOCAL_ID,
+ allProjection)) {
+ assertThat(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
- assertMediaStoreCursor(cr, LOCAL_ID, DATE_TAKEN_MS, PickerUriResolver.PICKER_SEGMENT);
- }
-
- try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_GET_CONTENT_SEGMENT,
- LOCAL_PROVIDER, LOCAL_ID, allProjection)) {
- assertWithMessage(
- "Unexpected number of rows when asserting all projection columns with "
- + "PickerUriResolver as PICKER_GET_CONTENT_SEGMENT on local provider.")
- .that(cr.getCount()).isEqualTo(1);
-
- cr.moveToFirst();
- assertMediaStoreCursor(cr, LOCAL_ID, DATE_TAKEN_MS,
- PickerUriResolver.PICKER_GET_CONTENT_SEGMENT);
+ assertMediaStoreCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
}
// Assert one projection column
final String[] oneProjection = new String[]{PickerMediaColumns.DATE_TAKEN};
- try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_SEGMENT,
- CLOUD_PROVIDER, CLOUD_ID, oneProjection)) {
- assertWithMessage(
- "Unexpected number of rows when asserting one projection column with cloud "
- + "provider.")
- .that(cr.getCount()).isEqualTo(1);
+ try (Cursor cr = mFacade.queryMediaIdForApps(CLOUD_PROVIDER, CLOUD_ID,
+ oneProjection)) {
+ assertThat(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
assertWithMessage(
@@ -1397,12 +1380,9 @@
invalidColumn
};
- try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_SEGMENT,
- CLOUD_PROVIDER, CLOUD_ID, invalidProjection)) {
- assertWithMessage(
- "Unexpected number of rows when asserting invalid projection column with "
- + "cloud provider.")
- .that(cr.getCount()).isEqualTo(1);
+ try (Cursor cr = mFacade.queryMediaIdForApps(CLOUD_PROVIDER, CLOUD_ID,
+ invalidProjection)) {
+ assertThat(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
assertWithMessage(
@@ -2263,8 +2243,8 @@
return mediaId + getExtensionFromMimeType(mimeType);
}
- private static String getData(String authority, String displayName, String pickerSegmentType) {
- return "/sdcard/.transforms/synthetic/" + pickerSegmentType + "/0/" + authority + "/media/"
+ private static String getData(String authority, String displayName) {
+ return "/sdcard/.transforms/synthetic/picker/0/" + authority + "/media/"
+ displayName;
}
@@ -2290,10 +2270,8 @@
private static void assertCloudMediaCursor(Cursor cursor, String id, String mimeType) {
final String displayName = getDisplayName(id, mimeType);
- final String localData = getData(LOCAL_PROVIDER, displayName,
- PickerUriResolver.PICKER_SEGMENT);
- final String cloudData = getData(CLOUD_PROVIDER, displayName,
- PickerUriResolver.PICKER_SEGMENT);
+ final String localData = getData(LOCAL_PROVIDER, displayName);
+ final String cloudData = getData(CLOUD_PROVIDER, displayName);
assertWithMessage("Unexpected value of MediaColumns.ID for the cloud media cursor.")
.that(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
@@ -2380,11 +2358,10 @@
}
}
- private static void assertMediaStoreCursor(Cursor cursor, String id, long dateTakenMs,
- String pickerSegmentType) {
+ private static void assertMediaStoreCursor(Cursor cursor, String id, long dateTakenMs) {
final String displayName = getDisplayName(id, MP4_VIDEO_MIME_TYPE);
- final String localData = getData(LOCAL_PROVIDER, displayName, pickerSegmentType);
- final String cloudData = getData(CLOUD_PROVIDER, displayName, pickerSegmentType);
+ final String localData = getData(LOCAL_PROVIDER, displayName);
+ final String cloudData = getData(CLOUD_PROVIDER, displayName);
assertWithMessage(
"Unexpected value for PickerMediaColumns.DISPLAY_NAME for the media store cursor.")
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
index 3fb365d..e4ded70 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
@@ -16,12 +16,9 @@
package com.android.providers.media.photopicker.data;
-import static android.content.Intent.ACTION_GET_CONTENT;
-import static android.provider.MediaStore.ACTION_PICK_IMAGES;
import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
import static com.google.common.truth.Truth.assertThat;
@@ -61,7 +58,7 @@
}
/**
- * Tests {@link PickerResult#getPickerResponseIntent(String, boolean, List)} with single item
+ * Tests {@link PickerResult#getPickerResponseIntent(boolean, List)} with single item
* @throws Exception
*/
@Test
@@ -69,10 +66,9 @@
List<Item> items = null;
try {
items = createItemSelection(1);
- final Uri expectedPickerUri = PickerResult.getPickerUri(ACTION_PICK_IMAGES,
- items.get(0).getContentUri());
+ final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri());
final Intent intent = PickerResult.getPickerResponseIntent(
- ACTION_PICK_IMAGES, /* canSelectMultiple */ false, items);
+ /* canSelectMultiple */ false, items);
final Uri result = intent.getData();
assertPickerUriFormat(result);
@@ -88,32 +84,8 @@
}
}
- @Test
- public void testGetResultSingleForActionGetContent() throws Exception {
- List<Item> items = null;
- try {
- items = createItemSelection(1);
- final Uri expectedPickerUri = PickerResult.getPickerUri(ACTION_GET_CONTENT,
- items.get(0).getContentUri());
- final Intent intent = PickerResult.getPickerResponseIntent(
- ACTION_GET_CONTENT, /* canSelectMultiple */ false, items);
-
- final Uri result = intent.getData();
- assertGetContentPickerUriFormat(result);
- assertThat(result).isEqualTo(expectedPickerUri);
-
- final ClipData clipData = intent.getClipData();
- assertThat(clipData).isNotNull();
- final int count = clipData.getItemCount();
- assertThat(count).isEqualTo(1);
- assertThat(clipData.getItemAt(0).getUri()).isEqualTo(expectedPickerUri);
- } finally {
- deleteFiles(items);
- }
- }
-
/**
- * Tests {@link PickerResult#getPickerResponseIntent(String, boolean, List)} with multiple items
+ * Tests {@link PickerResult#getPickerResponseIntent(boolean, List)} with multiple items
* @throws Exception
*/
@Test
@@ -123,12 +95,11 @@
final int itemCount = 3;
items = createItemSelection(itemCount);
List<Uri> expectedPickerUris = new ArrayList<>();
- for (Item item : items) {
- expectedPickerUris.add(PickerResult.getPickerUri(ACTION_PICK_IMAGES,
- item.getContentUri()));
+ for (Item item: items) {
+ expectedPickerUris.add(PickerResult.getPickerUri(item.getContentUri()));
}
- final Intent intent = PickerResult.getPickerResponseIntent(
- ACTION_PICK_IMAGES, /* canSelectMultiple */ true, items);
+ final Intent intent = PickerResult.getPickerResponseIntent(/* canSelectMultiple */ true,
+ items);
final ClipData clipData = intent.getClipData();
final int count = clipData.getItemCount();
@@ -143,36 +114,10 @@
}
}
- @Test
- public void testGetResultMultipleForActionGetContent() throws Exception {
- ArrayList<Item> items = null;
- try {
- final int itemCount = 3;
- items = createItemSelection(itemCount);
- List<Uri> expectedPickerUris = new ArrayList<>();
- for (Item item : items) {
- expectedPickerUris.add(PickerResult.getPickerUri(ACTION_GET_CONTENT,
- item.getContentUri()));
- }
- final Intent intent = PickerResult.getPickerResponseIntent(
- ACTION_GET_CONTENT, /* canSelectMultiple */ true, items);
-
- final ClipData clipData = intent.getClipData();
- final int count = clipData.getItemCount();
- assertThat(count).isEqualTo(itemCount);
- for (int i = 0; i < count; i++) {
- Uri uri = clipData.getItemAt(i).getUri();
- assertGetContentPickerUriFormat(uri);
- assertThat(uri).isEqualTo(expectedPickerUris.get(i));
- }
- } finally {
- deleteFiles(items);
- }
- }
-
/**
- * Tests {@link PickerResult#getPickerResponseIntent(String, boolean, List)} when the user
- * selected only one item in multi-select mode
+ * Tests {@link PickerResult#getPickerResponseIntent(boolean, List)} when the user selected
+ * only one item in multi-select mode
+ * @throws Exception
*/
@Test
public void testGetResultMultiple_onlyOneItemSelected() throws Exception {
@@ -180,10 +125,9 @@
try {
final int itemCount = 1;
items = createItemSelection(itemCount);
- final Uri expectedPickerUri = PickerResult.getPickerUri(ACTION_PICK_IMAGES,
- items.get(0).getContentUri());
- final Intent intent = PickerResult.getPickerResponseIntent(
- ACTION_PICK_IMAGES, /* canSelectMultiple */ true, items);
+ final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri());
+ final Intent intent = PickerResult.getPickerResponseIntent(/* canSelectMultiple */ true,
+ items);
final ClipData clipData = intent.getClipData();
final int count = clipData.getItemCount();
@@ -200,12 +144,6 @@
assertThat(uri.toString().startsWith(pickerUriPrefix)).isTrue();
}
- private void assertGetContentPickerUriFormat(Uri uri) {
- final String pickerNonRedactedUriPrefix = MediaStore.AUTHORITY_URI.buildUpon().appendPath(
- PICKER_GET_CONTENT_SEGMENT).build().toString();
- assertThat(uri.toString().startsWith(pickerNonRedactedUriPrefix)).isTrue();
- }
-
/**
* Returns a PhotoSelection on which the test app does not have access to.
*/
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
index 7be7b0c..e254445 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -29,6 +29,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
+import android.os.Process;
import android.provider.MediaStore;
import android.system.ErrnoException;
import android.system.Os;
@@ -55,10 +56,10 @@
import java.util.concurrent.TimeoutException;
public class PhotoPickerBaseTest {
- public static final int PICKER_TAB_RECYCLERVIEW_ID = R.id.picker_tab_recyclerview;
- public static final int TAB_LAYOUT_ID = R.id.tab_layout;
+ protected static final int PICKER_TAB_RECYCLERVIEW_ID = R.id.picker_tab_recyclerview;
+ protected static final int TAB_LAYOUT_ID = R.id.tab_layout;
protected static final int PICKER_PHOTOS_STRING_ID = R.string.picker_photos;
- public static final int PICKER_ALBUMS_STRING_ID = R.string.picker_albums;
+ protected static final int PICKER_ALBUMS_STRING_ID = R.string.picker_albums;
protected static final int PREVIEW_VIEW_PAGER_ID = R.id.preview_viewPager;
protected static final int ICON_CHECK_ID = R.id.icon_check;
protected static final int ICON_THUMBNAIL_ID = R.id.icon_thumbnail;
@@ -118,7 +119,7 @@
sUserSelectImagesForAppIntent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
sUserSelectImagesForAppIntent.addCategory(Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
Bundle extras = new Bundle();
- extras.putInt(Intent.EXTRA_UID, 1234);
+ extras.putInt(Intent.EXTRA_UID, Process.myUid());
sUserSelectImagesForAppIntent.putExtras(extras);
}
@@ -134,7 +135,7 @@
private static final long POLLING_SLEEP_MILLIS = 200;
private static IsolatedContext sIsolatedContext;
- static UserIdManager sUserIdManager;
+ private static UserIdManager sUserIdManager;
public static Intent getSingleSelectMimeTypeFilterIntent(String mimeTypeFilter) {
final Intent intent = new Intent(sSingleSelectIntent);
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
index 242ad58..5bfedba 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
@@ -16,9 +16,13 @@
package com.android.providers.media.photopicker.espresso;
+import static com.android.providers.media.photopicker.espresso.PhotoPickerUserSelectActivityTest.MANAGED_SELECTION_ENABLED_EXTRA;
+
import static org.mockito.Mockito.RETURNS_SMART_NULLS;
import static org.mockito.Mockito.mock;
+import androidx.annotation.NonNull;
+
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.UiEventLogger;
import com.android.providers.media.TestConfigStore;
@@ -32,18 +36,21 @@
private final UiEventLogger mLogger = mock(UiEventLogger.class, RETURNS_SMART_NULLS);
private InstanceId mInstanceId;
- private PickerViewModel mPickerViewModel;
-
@Override
+ @NonNull
protected PickerViewModel getOrCreateViewModel() {
- mPickerViewModel = super.getOrCreateViewModel();
- mPickerViewModel.setConfigStore(mConfigStore);
- mPickerViewModel.setItemsProvider(new ItemsProvider(
+ final PickerViewModel pickerViewModel = super.getOrCreateViewModel();
+ if (getIntent().getExtras() != null && getIntent().getExtras().getBoolean(
+ MANAGED_SELECTION_ENABLED_EXTRA)) {
+ mConfigStore.enablePickerChoiceManagedSelectionEnabled();
+ }
+ pickerViewModel.setConfigStore(mConfigStore);
+ pickerViewModel.setItemsProvider(new ItemsProvider(
PhotoPickerBaseTest.getIsolatedContext()));
- mPickerViewModel.setUserIdManager(PhotoPickerBaseTest.getMockUserIdManager());
- mPickerViewModel.setLogger(new PhotoPickerUiEventLogger(mLogger));
- mInstanceId = mPickerViewModel.getInstanceId();
- return mPickerViewModel;
+ pickerViewModel.setUserIdManager(PhotoPickerBaseTest.getMockUserIdManager());
+ pickerViewModel.setLogger(new PhotoPickerUiEventLogger(mLogger));
+ mInstanceId = pickerViewModel.getInstanceId();
+ return pickerViewModel;
}
TestConfigStore getConfigStore() {
@@ -57,12 +64,4 @@
InstanceId getInstanceId() {
return mInstanceId;
}
-
- void setItemsProvider(ItemsProvider itemsProvider) {
- mPickerViewModel.setItemsProvider(itemsProvider);
- }
-
- void initSyncForPhotosGrid() {
- mPickerViewModel.initPhotoPickerData();
- }
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
index a0df108..a37a466 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
@@ -56,6 +56,8 @@
@RunWith(AndroidJUnit4ClassRunner.class)
public class PhotoPickerUserSelectActivityTest extends PhotoPickerBaseTest {
+ public static final String MANAGED_SELECTION_ENABLED_EXTRA = "MANAGED_SELECTION_ENABLE";
+
public ActivityScenario<PhotoPickerTestActivity> mScenario;
@After
@@ -132,6 +134,34 @@
}
@Test
+ public void testAddButtonIsShowsAllowNone() {
+ launchValidActivityWithManagedSelectionEnabled();
+ final int bottomBarId = R.id.picker_bottom_bar;
+ final int viewSelectedId = R.id.button_view_selected;
+ final int addButtonId = R.id.button_add;
+
+ // Default view, no item selected.
+ onView(withId(bottomBarId)).check(matches(isDisplayed()));
+ onView(withId(viewSelectedId)).check(matches(not(isDisplayed())));
+ onView(withId(addButtonId)).check(matches(isDisplayed()));
+ // verify that 'Allow none' is displayed in this case.
+ onView(withId(addButtonId)).check(
+ matches(withText(R.string.picker_add_button_allow_none_option)));
+
+ clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+
+ onView(withId(bottomBarId)).check(matches(isDisplayed()));
+ onView(withId(viewSelectedId)).check(matches(isDisplayed()));
+
+ onView(withId(addButtonId)).check(matches(withText("Allow (1)")));
+ onView(withId(addButtonId)).check(matches(isDisplayed()));
+
+
+ onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
+ onView(withId(addButtonId)).check(matches(withText("Allow (1)")));
+ }
+
+ @Test
public void testNoCloudSettingsAndBanners() {
launchValidActivity();
@@ -155,4 +185,12 @@
ActivityScenario.launchActivityForResult(
PhotoPickerBaseTest.getUserSelectImagesForAppIntent());
}
+
+ /** Test helper to launch a valid test activity. */
+ private void launchValidActivityWithManagedSelectionEnabled() {
+ Intent intent = PhotoPickerBaseTest.getUserSelectImagesForAppIntent();
+ intent.putExtra(MANAGED_SELECTION_ENABLED_EXTRA, true);
+ mScenario =
+ ActivityScenario.launchActivityForResult(intent);
+ }
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java b/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java
index a9d3266..8db8149 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java
@@ -27,19 +27,15 @@
import static org.hamcrest.Matchers.allOf;
-import android.content.Context;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiSelector;
import android.text.format.DateUtils;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ActivityScenario;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.library.RunOnlyOnPostsubmit;
-import com.android.providers.media.photopicker.data.ItemsProvider;
-import com.android.providers.media.photopicker.data.model.UserId;
import org.junit.After;
import org.junit.Before;
@@ -84,29 +80,6 @@
PhotoPickerBaseTest.getSingleSelectionIntent());
}
- private void setItemsProviderWithDelayInActivity() {
- FakeItemsProvider itemsProvider = new FakeItemsProvider(getIsolatedContext());
- itemsProvider.delaySync();
- mScenario.onActivity(
- (activity -> {
- activity.setItemsProvider(itemsProvider);
- }));
- }
-
- private void assertProgressBarAndLoadingTextAppears() {
- final UiSelector progressBar = new UiSelector().resourceId(
- getIsolatedContext().getPackageName()
- + ":id/progress_bar");
- assertWithMessage("Waiting for progressBar to appear on photos grid").that(
- new UiObject(progressBar).waitForExists(DateUtils.SECOND_IN_MILLIS)).isTrue();
-
- final UiSelector loadingText = new UiSelector().resourceId(
- getIsolatedContext().getPackageName()
- + ":id/loading_text_view");
- assertWithMessage("Waiting for progressBar to appear on photos grid").that(
- new UiObject(loadingText).waitForExists(DateUtils.SECOND_IN_MILLIS)).isTrue();
- }
-
private void assertProgressBarAndLoadingTextDoesNotAppears() {
final UiSelector progressBar = new UiSelector().resourceId(
getIsolatedContext().getPackageName()
@@ -127,33 +100,4 @@
mScenario.close();
}
}
-
- public static class FakeItemsProvider extends ItemsProvider {
-
- private boolean mShouldDelaySync = false;
-
- public FakeItemsProvider(Context context) {
- super(context);
- }
-
- public void delaySync() {
- mShouldDelaySync = true;
- }
-
- @Override
- public void initPhotoPickerData(@Nullable String albumId,
- @Nullable String albumAuthority,
- boolean initLocalOnlyData,
- @Nullable UserId userId) {
- if (mShouldDelaySync) {
- try {
- Thread.sleep(DateUtils.SECOND_IN_MILLIS);
- } catch (Exception e) {
- // no-op
- }
- } else {
- super.initPhotoPickerData(albumId, albumAuthority, initLocalOnlyData, userId);
- }
- }
- }
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
index 2f1b5ab..5897164 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
@@ -34,7 +34,6 @@
import com.android.providers.media.R;
import com.android.providers.media.library.RunOnlyOnPostsubmit;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -126,12 +125,10 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testPreview_multiSelect_navigation() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select items
- clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
clickItem(PICKER_TAB_RECYCLERVIEW_ID, GIF_POSITION, ICON_THUMBNAIL_ID);
clickItem(PICKER_TAB_RECYCLERVIEW_ID, ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
clickItem(PICKER_TAB_RECYCLERVIEW_ID, MOTION_PHOTO_POSITION, ICON_THUMBNAIL_ID);
diff --git a/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java
index 62ee337..617028f 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java
@@ -92,7 +92,8 @@
SyncTrackerRegistry.setLocalAlbumSyncTracker(mMockLocalAlbumSyncTracker);
SyncTrackerRegistry.setCloudAlbumSyncTracker(mMockCloudAlbumSyncTracker);
- doReturn(new Object()).when(mMockPickerSyncController).getCloudAlbumSyncLock();
+ doReturn(new PickerSyncLockManager())
+ .when(mMockPickerSyncController).getPickerSyncLockManager();
doReturn(TEST_CLOUD_AUTHORITY).when(mMockPickerSyncController).getCloudProvider();
doReturn(TEST_LOCAL_AUTHORITY).when(mMockPickerSyncController).getLocalProvider();
@@ -102,7 +103,7 @@
File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
dbPath.delete();
- mDbFacade = new PickerDbFacade(mContext, TEST_LOCAL_AUTHORITY);
+ mDbFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), TEST_LOCAL_AUTHORITY);
mDbFacade.setCloudProvider(TEST_CLOUD_AUTHORITY);
initializeTestWorkManager(mContext);
diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncLockManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncLockManagerTest.java
new file mode 100644
index 0000000..de01adb
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncLockManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK;
+import static com.android.providers.media.photopicker.sync.PickerSyncLockManager.CLOUD_SYNC_LOCK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class PickerSyncLockManagerTest {
+ private PickerSyncLockManager mSyncLockManager;
+
+ @Before
+ public void setup() {
+ mSyncLockManager = new PickerSyncLockManager();
+ }
+
+ @Test
+ public void testLockIsCloseable() {
+ try (CloseableReentrantLock lock = mSyncLockManager.lock(CLOUD_SYNC_LOCK)) {
+ // Assert that the lock is help by the current thread.
+ assertThat(lock.isHeldByCurrentThread()).isTrue();
+ assertThat(lock.getHoldCount()).isEqualTo(1);
+
+ try (CloseableReentrantLock lockInLock = mSyncLockManager.lock(CLOUD_SYNC_LOCK)) {
+ // Assert that this is a reentrant lock and the thread was able to increment hold
+ // count.
+ assertThat(lock.isHeldByCurrentThread()).isTrue();
+ assertThat(lock).isEqualTo(lockInLock);
+ assertThat(lock.getHoldCount()).isEqualTo(2);
+ }
+
+ // Assert that the hold count has been decremented.
+ assertThat(lock.isHeldByCurrentThread()).isTrue();
+ assertThat(lock.getHoldCount()).isEqualTo(1);
+ assertThat(lock).isEqualTo(mSyncLockManager.getLock(CLOUD_SYNC_LOCK));
+ }
+
+ assertThat(mSyncLockManager.getLock(CLOUD_SYNC_LOCK).isHeldByCurrentThread()).isFalse();
+ }
+
+ @Test
+ public void testLockWithTimeoutIsCloseable() throws UnableToAcquireLockException {
+ try (CloseableReentrantLock lock = mSyncLockManager.tryLock(CLOUD_ALBUM_SYNC_LOCK)) {
+ // Assert that the lock is help by the current thread.
+ assertThat(lock.isHeldByCurrentThread()).isTrue();
+ assertThat(lock.getHoldCount()).isEqualTo(1);
+
+ try (CloseableReentrantLock lockInLock =
+ mSyncLockManager.tryLock(CLOUD_ALBUM_SYNC_LOCK)) {
+ // Assert that this is a reentrant lock and the thread was able to increment hold
+ // count.
+ assertThat(lock.isHeldByCurrentThread()).isTrue();
+ assertThat(lock).isEqualTo(lockInLock);
+ assertThat(lock.getHoldCount()).isEqualTo(2);
+ }
+
+ // Assert that the hold count has been decremented.
+ assertThat(lock.isHeldByCurrentThread()).isTrue();
+ assertThat(lock.getHoldCount()).isEqualTo(1);
+ assertThat(lock).isEqualTo(mSyncLockManager.getLock(CLOUD_ALBUM_SYNC_LOCK));
+ }
+
+ assertThat(mSyncLockManager.getLock(CLOUD_ALBUM_SYNC_LOCK).isHeldByCurrentThread())
+ .isFalse();
+ }
+
+ @Test
+ public void testLockTimeout() throws InterruptedException, TimeoutException {
+ CloseableReentrantLock lock = new CloseableReentrantLock("testLock");
+ try (CloseableReentrantLock ignored =
+ mSyncLockManager.tryLock(lock, 5, TimeUnit.MILLISECONDS)) {
+ // it is expected that the lock is held by the current thread within timeout.
+ } catch (UnableToAcquireLockException e) {
+ throw new AssertionError(
+ "Should be able to acquire the lock since no other thread holds it.", e);
+ }
+
+ HandlerThread thread = new HandlerThread("PickerSyncLockTestThread",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ acquireLock(handler, lock);
+
+ try (CloseableReentrantLock ignored =
+ mSyncLockManager.tryLock(lock, 5, TimeUnit.MILLISECONDS)) {
+ throw new AssertionError("The lock should not be acquired by this thread because "
+ + "it is already held by a different thread");
+ } catch (UnableToAcquireLockException e) {
+ // The expectation is that lock is not acquired within the timeout and
+ // UnableToAcquireLockException is thrown.
+ }
+
+ releaseLock(handler, lock);
+ thread.quitSafely();
+ }
+
+ private void acquireLock(Handler handler, CloseableReentrantLock lock)
+ throws InterruptedException, TimeoutException {
+ handler.post(() -> lock.lock());
+ waitForHandler(handler);
+ }
+
+ private void releaseLock(Handler handler, CloseableReentrantLock lock)
+ throws InterruptedException, TimeoutException {
+ handler.post(() -> lock.unlock());
+ waitForHandler(handler);
+ }
+
+ private void waitForHandler(Handler handler) throws InterruptedException, TimeoutException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ handler.post(() -> latch.countDown());
+ final boolean success = latch.await(30, TimeUnit.SECONDS);
+ if (!success) {
+ throw new TimeoutException("Could not wait for handler task to finish");
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java b/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java
index 6b2b399..169e86a 100644
--- a/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java
@@ -33,6 +33,8 @@
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.ui.PhotosTabAdapter.DateHeader;
+import com.bumptech.glide.util.ViewPreloadSizeProvider;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -191,7 +193,8 @@
mock(TabAdapter.OnBannerEventListener.class),
/* onChooseAccountBannerEventListener */
mock(TabAdapter.OnBannerEventListener.class),
- /* onHoverListener */ mock(View.OnHoverListener.class));
+ /* onHoverListener */ mock(View.OnHoverListener.class),
+ /* mPreloadSizeProvider */ mock(ViewPreloadSizeProvider.class));
}
private static Item generateFakeImageItem(String id) {
diff --git a/tests/src/com/android/providers/media/scan/DrmTest.java b/tests/src/com/android/providers/media/scan/DrmTest.java
index ea3562f..ccc9090 100644
--- a/tests/src/com/android/providers/media/scan/DrmTest.java
+++ b/tests/src/com/android/providers/media/scan/DrmTest.java
@@ -19,6 +19,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -48,7 +50,6 @@
import com.android.providers.media.util.FileUtils;
import org.junit.After;
-import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -99,7 +100,7 @@
@Test
public void testForwardLock_Audio() throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
+ assumeTrue(isForwardLockSupported());
doForwardLock("audio/mpeg", R.raw.test_audio, (values) -> {
assertEquals(1_045L, (long) values.getAsLong(FileColumns.DURATION));
assertEquals(FileColumns.MEDIA_TYPE_AUDIO,
@@ -109,7 +110,7 @@
@Test
public void testForwardLock_Video() throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
+ assumeTrue(isForwardLockSupported());
doForwardLock("video/mp4", R.raw.test_video, (values) -> {
assertEquals(40_000L, (long) values.getAsLong(FileColumns.DURATION));
assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
@@ -119,7 +120,7 @@
@Test
public void testForwardLock_Image() throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
+ assumeTrue(isForwardLockSupported());
doForwardLock("image/jpeg", R.raw.test_image, (values) -> {
// ExifInterface currently doesn't know how to scan DRM images, so
// the best we can do is verify the base test metadata
@@ -130,7 +131,7 @@
@Test
public void testForwardLock_Binary() throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
+ assumeTrue(isForwardLockSupported());
doForwardLock("application/octet-stream", R.raw.test_image, null);
}
@@ -140,7 +141,7 @@
*/
@Test
public void testForwardLock_130680734() throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
+ assumeTrue(isForwardLockSupported());
final ContentValues values = new ContentValues();
values.put(MediaColumns.DISPLAY_NAME, "temp" + System.nanoTime() + ".fl");
@@ -165,10 +166,16 @@
ContentUris.parseId(uri));
try (Cursor c = mResolver.query(filesUri, null, null, null)) {
assertTrue(c.moveToFirst());
+
+ final String mimeType = c.getString(c.getColumnIndex(FileColumns.MIME_TYPE));
+ // To be consistent with the logic in doForwardLock() below: if the devices does not
+ // handle .fl files we won't consider the test failing, but we also can't carry on here,
+ // thus let's do an AssumptionViolatedException.
+ assumeFalse(MIME_UNSUPPORTED.equals(mimeType));
+ assertEquals("video/mp4", mimeType);
+
assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
c.getInt(c.getColumnIndex(FileColumns.MEDIA_TYPE)));
- assertEquals("video/mp4",
- c.getString(c.getColumnIndex(FileColumns.MIME_TYPE)));
}
}
@@ -232,7 +239,7 @@
}
} else if (Objects.equals(MIME_UNSUPPORTED, actualMimeType)) {
// We don't scan unsupported items, so we can't check our custom
- // verifier, but we're still willing to consider this as passing
+ // verifier, but we're still willing to consider this as passing.
} else {
fail("Unexpected MIME type " + actualMimeType);
}
diff --git a/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java b/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
index 0c8c5df..b913114 100644
--- a/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
@@ -23,13 +23,9 @@
import static com.android.providers.media.util.SyntheticPathUtils.isPickerPath;
import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath;
import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath;
-
import static com.google.common.truth.Truth.assertThat;
import androidx.test.runner.AndroidJUnit4;
-
-import com.android.providers.media.PickerUriResolver;
-
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -50,10 +46,7 @@
@Test
public void testGetPickerRelativePath() throws Exception {
- assertThat(getPickerRelativePath(PickerUriResolver.PICKER_SEGMENT)).isEqualTo(
- ".transforms/synthetic/picker");
- assertThat(getPickerRelativePath(PickerUriResolver.PICKER_GET_CONTENT_SEGMENT)).isEqualTo(
- ".transforms/synthetic/picker_get_content");
+ assertThat(getPickerRelativePath()).isEqualTo(".transforms/synthetic/picker");
}
@Test