Snap for 11164065 from b9a788afdc593c4364b7d9b0d328593bded50c5e to mainline-resolv-release
Change-Id: I0ab1d579deac82d6f256f1b75a6981c6073862aa
diff --git a/Android.bp b/Android.bp
index 785c8ee..1e9cea7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -21,6 +21,7 @@
"modules-utils-uieventlogger-interface",
"glide-prebuilt",
"glide-integration-recyclerview-prebuilt",
+ "glide-integration-webpdecoder-prebuilt",
"glide-gifdecoder-prebuilt",
"glide-disklrucache-prebuilt",
"glide-annotation-and-compiler-prebuilt",
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 5f4b64b..8e8eb1d 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -19,6 +19,15 @@
"name": "CtsScopedStorageDeviceOnlyTest[com.google.android.mediaprovider.apex]"
},
{
+ "name": "CtsScopedStorageBypassDatabaseOperationsTest[com.google.android.mediaprovider.apex]"
+ },
+ {
+ "name": "CtsScopedStorageGeneralTest[com.google.android.mediaprovider.apex]"
+ },
+ {
+ "name": "CtsScopedStorageRedactUriTest[com.google.android.mediaprovider.apex]"
+ },
+ {
"name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
}
],
@@ -66,6 +75,15 @@
"name": "CtsScopedStorageDeviceOnlyTest"
},
{
+ "name": "CtsScopedStorageBypassDatabaseOperationsTest"
+ },
+ {
+ "name": "CtsScopedStorageGeneralTest"
+ },
+ {
+ "name": "CtsScopedStorageRedactUriTest"
+ },
+ {
"name": "fuse_node_test"
}
],
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index 34825d1..cddfcfa 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -150,6 +150,7 @@
field public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
field public static final String EXTRA_OUTPUT = "output";
+ field @FlaggedApi("com.android.providers.media.flags.pick_ordered_images") public static final String EXTRA_PICK_IMAGES_IN_ORDER = "android.provider.extra.PICK_IMAGES_IN_ORDER";
field public static final String EXTRA_PICK_IMAGES_MAX = "android.provider.extra.PICK_IMAGES_MAX";
field public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
field public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index 4fc71c9..5e610a8 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -88,8 +88,8 @@
public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
/**
- * Number associated with a media item indicating what generation or batch the media item
- * was synced into the media collection.
+ * Non-negative number associated with a media item indicating what generation or batch the
+ * media item was synced into the media collection.
* <p>
* Providers should associate a monotonically increasing sync generation number to each
* media item which is expected to increase for each atomic modification on the media item.
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index ce76d52..b1172af 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -20,6 +20,7 @@
import android.annotation.CurrentTimeMillisLong;
import android.annotation.CurrentTimeSecondsLong;
import android.annotation.DurationMillisLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -517,18 +518,18 @@
public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
/**
- * The name of the Intent-extra used to control the onCompletion behavior of a MovieView.
- * This is a boolean property that specifies whether or not to finish the MovieView activity
- * when the movie completes playing. The default value is true, which means to automatically
- * exit the movie player activity when the movie completes playing.
+ * The name of the Intent-extra used to control the onCompletion behavior of a MovieView. This
+ * is a boolean property that specifies whether or not to finish the MovieView activity when the
+ * movie completes playing. The default value is true, which means to automatically exit the
+ * movie player activity when the movie completes playing.
*/
- public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
+ public static final String EXTRA_FINISH_ON_COMPLETION =
+ "android.intent.extra.finishOnCompletion";
- /**
- * The name of the Intent action used to launch a camera in still image mode.
- */
+ /** The name of the Intent action used to launch a camera in still image mode. */
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
+ public static final String INTENT_ACTION_STILL_IMAGE_CAMERA =
+ "android.media.action.STILL_IMAGE_CAMERA";
/**
* Name under which an activity handling {@link #INTENT_ACTION_STILL_IMAGE_CAMERA} or
@@ -755,42 +756,39 @@
public final static String EXTRA_OUTPUT = "output";
/**
- * Activity Action: Allow the user to select images or videos provided by
- * system and return it. This is different than {@link Intent#ACTION_PICK}
- * and {@link Intent#ACTION_GET_CONTENT} in that
+ * Activity Action: Allow the user to select images or videos provided by system and return it.
+ * This is different than {@link Intent#ACTION_PICK} and {@link Intent#ACTION_GET_CONTENT} in
+ * that
+ *
* <ul>
- * <li> the data for this action is provided by the system
- * <li> this action is only used for picking images and videos
- * <li> caller gets read access to user picked items even without storage
- * permissions
+ * <li>the data for this action is provided by the system
+ * <li>this action is only used for picking images and videos
+ * <li>caller gets read access to user picked items even without storage permissions
* </ul>
- * <p>
- * Callers can optionally specify MIME type (such as {@code image/*} or
- * {@code video/*}), resulting in a range of content selection that the
- * caller is interested in. The optional MIME type can be requested with
- * {@link Intent#setType(String)}.
- * <p>
- * If the caller needs multiple returned items (or caller wants to allow
- * multiple selection), then it can specify
- * {@link MediaStore#EXTRA_PICK_IMAGES_MAX} to indicate this.
- * <p>
- * When the caller requests multiple selection, the value of
- * {@link MediaStore#EXTRA_PICK_IMAGES_MAX} must be a positive integer
- * greater than 1 and less than or equal to
- * {@link MediaStore#getPickImagesMaxLimit}, otherwise
- * {@link Activity#RESULT_CANCELED} is returned.
- * <p>
- * Callers may use {@link Intent#EXTRA_LOCAL_ONLY} to limit content
- * selection to local data.
- * <p>
- * Output: MediaStore content URI(s) of the item(s) that was picked.
- * Unlike other MediaStore URIs, these are referred to as 'picker' URIs and
- * expose a limited set of read-only operations. Specifically, picker URIs
- * can only be opened for read and queried for columns in {@link PickerMediaColumns}.
- * <p>
- * Before this API, apps could use {@link Intent#ACTION_GET_CONTENT}. However,
- * {@link #ACTION_PICK_IMAGES} is now the recommended option for images and videos,
- * since it offers a better user experience.
+ *
+ * <p>Callers can optionally specify MIME type (such as {@code image/*} or {@code video/*}),
+ * resulting in a range of content selection that the caller is interested in. The optional MIME
+ * type can be requested with {@link Intent#setType(String)}.
+ *
+ * <p>If the caller needs multiple returned items (or caller wants to allow multiple selection),
+ * then it can specify {@link MediaStore#EXTRA_PICK_IMAGES_MAX} to indicate this.
+ *
+ * <p>When the caller requests multiple selection, the value of {@link
+ * MediaStore#EXTRA_PICK_IMAGES_MAX} must be a positive integer greater than 1 and less than or
+ * equal to {@link MediaStore#getPickImagesMaxLimit}, otherwise {@link Activity#RESULT_CANCELED}
+ * is returned. Use {@link MediaStore#EXTRA_PICK_IMAGES_IN_ORDER} in multiple selection mode to
+ * allow the user to pick images in order.
+ *
+ * <p>Callers may use {@link Intent#EXTRA_LOCAL_ONLY} to limit content selection to local data.
+ *
+ * <p>Output: MediaStore content URI(s) of the item(s) that was picked. Unlike other MediaStore
+ * URIs, these are referred to as 'picker' URIs and expose a limited set of read-only
+ * operations. Specifically, picker URIs can only be opened for read and queried for columns in
+ * {@link PickerMediaColumns}.
+ *
+ * <p>Before this API, apps could use {@link Intent#ACTION_GET_CONTENT}. However, {@link
+ * #ACTION_PICK_IMAGES} is now the recommended option for images and videos, since it offers a
+ * better user experience.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
@@ -838,6 +836,20 @@
"android.provider.action.PICK_IMAGES_SETTINGS";
/**
+ * The name of an optional intent-extra used to allow ordered selection of items. Set this extra
+ * to true to allow the user to see the order of their selected items. The result returned to
+ * the caller will be the same as the user selected order. This extra is only allowed via the
+ * {@link MediaStore#ACTION_PICK_IMAGES}.
+ *
+ * <p>The value of this intent-extra should be a boolean. Default value is false.
+ *
+ * @see #ACTION_PICK_IMAGES
+ */
+ @FlaggedApi("com.android.providers.media.flags.pick_ordered_images")
+ public static final String EXTRA_PICK_IMAGES_IN_ORDER =
+ "android.provider.extra.PICK_IMAGES_IN_ORDER";
+
+ /**
* The name of an optional intent-extra used to allow multiple selection of
* items and constrain maximum number of items that can be returned by
* {@link MediaStore#ACTION_PICK_IMAGES}, action may still return nothing
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 1b88251..f261ae2 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -1984,7 +1984,9 @@
struct fuse_dirent* dirent_out = (struct fuse_dirent*)((char*)dirents_out + fro->size);
struct stat stats;
int err;
- std::string child_path = path + "/" + dirent_in->name;
+
+ std::string child_name(dirent_in->name, dirent_in->namelen);
+ std::string child_path = path + "/" + child_name;
in += sizeof(*dirent_in) + round_up(dirent_in->namelen, sizeof(uint64_t));
err = stat(child_path.c_str(), &stats);
@@ -1992,9 +1994,9 @@
((stats.st_mode & 0001) || ((stats.st_mode & 0010) && req->ctx.gid == stats.st_gid) ||
((stats.st_mode & 0100) && req->ctx.uid == stats.st_uid) ||
fuse->mp->isUidAllowedAccessToDataOrObbPath(req->ctx.uid, child_path) ||
- strcmp(dirent_in->name, ".nomedia") == 0)) {
+ child_name == ".nomedia")) {
*dirent_out = *dirent_in;
- strcpy(dirent_out->name, dirent_in->name);
+ strcpy(dirent_out->name, child_name.c_str());
fro->size += sizeof(*dirent_out) + round_up(dirent_out->namelen, sizeof(uint64_t));
}
}
@@ -2579,6 +2581,16 @@
}
}
+void FuseDaemon::SetupPublicVolumeLevelDbInstance(const std::string& volume_name) {
+ if (android::base::StartsWith(fuse->root->GetIoPath(), PRIMARY_VOLUME_PREFIX)) {
+ // Setup leveldb instance for both external primary and internal volume.
+ fuse->level_db_mutex.lock();
+ // Create level db instance for public volume
+ SetupLevelDbConnection(volume_name);
+ fuse->level_db_mutex.unlock();
+ }
+}
+
std::string deriveVolumeName(const std::string& path) {
std::string volume_name;
if (!android::base::StartsWith(path, STORAGE_PREFIX)) {
@@ -2586,8 +2598,10 @@
} else if (android::base::StartsWith(path, PRIMARY_VOLUME_PREFIX)) {
volume_name = VOLUME_EXTERNAL_PRIMARY;
} else {
- size_t size = sizeof(STORAGE_PREFIX) / sizeof(STORAGE_PREFIX[0]);
- volume_name = volume_name.substr(size);
+ // Return "C58E-1702" from the path like "/storage/C58E-1702/Download/1935694997673.png"
+ volume_name = path.substr(9, 9);
+ // Convert to lowercase
+ std::transform(volume_name.begin(), volume_name.end(), volume_name.begin(), ::tolower);
}
return volume_name;
}
@@ -2607,8 +2621,8 @@
}
}
-void FuseDaemon::InsertInLevelDb(const std::string& key, const std::string& value) {
- std::string volume_name = deriveVolumeName(key);
+void FuseDaemon::InsertInLevelDb(const std::string& volume_name, const std::string& key,
+ const std::string& value) {
if (!CheckLevelDbConnection(volume_name)) {
LOG(ERROR) << "InsertInLevelDb: Missing leveldb connection.";
return;
@@ -2757,7 +2771,7 @@
bool FuseDaemon::CheckLevelDbConnection(const std::string& instance_name) {
if (fuse->level_db_connection_map.find(instance_name) == fuse->level_db_connection_map.end()) {
- LOG(ERROR) << "Leveldb setup is missing for :" << instance_name;
+ LOG(ERROR) << "Leveldb setup is missing for: " << instance_name;
return false;
}
return true;
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
index a634812..a9eaf22 100644
--- a/jni/FuseDaemon.h
+++ b/jni/FuseDaemon.h
@@ -80,6 +80,11 @@
void SetupLevelDbInstances();
/**
+ * Setup leveldb instances for public volume.
+ */
+ void SetupPublicVolumeLevelDbInstance(const std::string& volume_name);
+
+ /**
* Creates a leveldb instance and sets up a connection.
*/
void SetupLevelDbConnection(const std::string& instance_name);
@@ -90,9 +95,10 @@
void DeleteFromLevelDb(const std::string& key);
/**
- * Inserts in leveldb instance of volume derived from path.
+ * Inserts in leveldb instance of provided volume.
*/
- void InsertInLevelDb(const std::string& key, const std::string& value);
+ void InsertInLevelDb(const std::string& volume_name, const std::string& key,
+ const std::string& value);
/**
* Reads file paths for given volume from leveldb for given range.
diff --git a/jni/com_android_providers_media_FuseDaemon.cpp b/jni/com_android_providers_media_FuseDaemon.cpp
index 97a6a6e..2b78645 100644
--- a/jni/com_android_providers_media_FuseDaemon.cpp
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -196,6 +196,18 @@
daemon->SetupLevelDbInstances();
}
+void com_android_providers_media_FuseDaemon_setup_public_volume_db_backup(JNIEnv* env, jobject self,
+ jlong java_daemon,
+ jstring volume_name) {
+ fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+ ScopedUtfChars utf_chars_volumeName(env, volume_name);
+ if (!utf_chars_volumeName.c_str()) {
+ LOG(WARNING) << "Couldn't initialise FUSE device id for " << volume_name;
+ return;
+ }
+ daemon->SetupPublicVolumeLevelDbInstance(utf_chars_volumeName.c_str());
+}
+
void com_android_providers_media_FuseDaemon_delete_db_backup(JNIEnv* env, jobject self,
jlong java_daemon, jstring java_path) {
fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
@@ -209,15 +221,19 @@
void com_android_providers_media_FuseDaemon_backup_volume_db_data(JNIEnv* env, jobject self,
jlong java_daemon,
- jstring java_path, jstring value) {
+ jstring volume_name,
+ jstring java_path,
+ jstring value) {
fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
ScopedUtfChars utf_chars_path(env, java_path);
ScopedUtfChars utf_chars_value(env, value);
+ ScopedUtfChars utf_chars_volumeName(env, volume_name);
if (!utf_chars_path.c_str()) {
LOG(WARNING) << "Couldn't initialise FUSE device id";
return;
}
- daemon->InsertInLevelDb(utf_chars_path.c_str(), utf_chars_value.c_str());
+ daemon->InsertInLevelDb(utf_chars_volumeName.c_str(), utf_chars_path.c_str(),
+ utf_chars_value.c_str());
}
bool com_android_providers_media_FuseDaemon_is_fuse_thread(JNIEnv* env, jclass clazz) {
@@ -328,9 +344,11 @@
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_initialize_device_id)},
{"native_setup_volume_db_backup", "(J)V",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_setup_volume_db_backup)},
+ {"native_setup_public_volume_db_backup", "(JLjava/lang/String;)V",
+ reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_setup_public_volume_db_backup)},
{"native_delete_db_backup", "(JLjava/lang/String;)V",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_delete_db_backup)},
- {"native_backup_volume_db_data", "(JLjava/lang/String;Ljava/lang/String;)V",
+ {"native_backup_volume_db_data", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_backup_volume_db_data)},
{"native_read_backed_up_file_paths",
"(JLjava/lang/String;Ljava/lang/String;I)[Ljava/lang/String;",
diff --git a/res/drawable/ic_artwork_camera.xml b/res/drawable/ic_artwork_camera.xml
index dc22c49..9c39e64 100644
--- a/res/drawable/ic_artwork_camera.xml
+++ b/res/drawable/ic_artwork_camera.xml
@@ -14,9 +14,11 @@
limitations under the License.
-->
+<!-- This vector draws the camera graphic that is displayed in the picker when the
+ device has no images/videos i.e. the picker is empty -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="120dp"
- android:height="80dp"
+ android:width="100dp"
+ android:height="66.67dp"
android:viewportWidth="120"
android:viewportHeight="80">
<path
diff --git a/res/drawable/picker_item_check.xml b/res/drawable/picker_item_check.xml
index fb0ef88..c73c699 100644
--- a/res/drawable/picker_item_check.xml
+++ b/res/drawable/picker_item_check.xml
@@ -1,31 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
+ <!-- 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
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_selected="true">
- <layer-list>
- <item android:gravity="center"
- android:width="18dp"
- android:height="18dp">
- <shape android:shape="oval">
- <solid android:color="@color/picker_background_color"/>
- </shape>
- </item>
- <item android:drawable="@drawable/ic_check_circle_filled"/>
- </layer-list>
- </item>
- <item android:drawable="@drawable/ic_radio_button_unchecked"/>
-</selector>
+<item android:state_selected="true">
+ <layer-list>
+ <item android:gravity="center"
+ android:width="18dp"
+ android:height="18dp">
+ <shape android:shape="oval">
+ <solid android:color="@color/picker_background_color"/>
+ </shape>
+ </item>
+ <item android:drawable="@drawable/ic_check_circle_filled"/>
+ </layer-list>
+</item>
+<item android:drawable="@drawable/ic_radio_button_unchecked"/>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/picker_item_order.xml b/res/drawable/picker_item_order.xml
new file mode 100644
index 0000000..ceeae40
--- /dev/null
+++ b/res/drawable/picker_item_order.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true">
+ <layer-list>
+ <item android:gravity="center"
+ android:width="18dp"
+ android:height="18dp">
+ <shape android:shape="oval">
+ <solid android:color="?attr/pickerSelectedColor"/>
+ </shape>
+ </item>
+ </layer-list>
+ </item>
+ <item android:drawable="@drawable/ic_radio_button_unchecked"/>
+</selector>
\ No newline at end of file
diff --git a/res/layout/fragment_picker_tab.xml b/res/layout/fragment_picker_tab.xml
index 90e0cc5..7dd6ea9 100644
--- a/res/layout/fragment_picker_tab.xml
+++ b/res/layout/fragment_picker_tab.xml
@@ -20,34 +20,44 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout
+ <!-- The nested scroll view holds the layout that is made visible when
+ the picker is empty. It has been wrapped in the scroll view to tackle
+ bugs where the "empty_text_view" gets rolled off the screen partially
+ or completely in small screen devices -->
+ <androidx.core.widget.NestedScrollView
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
- android:orientation="vertical"
android:visibility="gone">
- <ImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:scaleType="fitCenter"
- android:src="@drawable/ic_artwork_camera"
- android:contentDescription="@null"/>
-
- <TextView
- android:id="@+id/empty_text_view"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/picker_empty_text_margin"
- android:gravity="center_horizontal"
- android:text="@string/picker_photos_empty_message"
- android:textColor="?android:attr/textColorSecondary"
- android:textSize="@dimen/picker_empty_text_size"
- style="?android:attr/textAppearanceListItem"/>
+ android:orientation="vertical">
- </LinearLayout>
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_artwork_camera"
+ android:contentDescription="@null"/>
+
+ <TextView
+ android:id="@+id/empty_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/picker_empty_text_margin"
+ android:gravity="center_horizontal"
+ android:text="@string/picker_photos_empty_message"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/picker_empty_text_size"
+ style="?android:attr/textAppearanceListItem"/>
+
+ </LinearLayout>
+
+ </androidx.core.widget.NestedScrollView>
<com.android.providers.media.photopicker.ui.AutoFitRecyclerView
android:id="@+id/picker_tab_recyclerview"
diff --git a/res/layout/item_photo_grid.xml b/res/layout/item_photo_grid.xml
index f28305c..cd19343 100644
--- a/res/layout/item_photo_grid.xml
+++ b/res/layout/item_photo_grid.xml
@@ -108,4 +108,17 @@
android:layout_gravity="top|start"
android:scaleType="fitCenter"/>
+ <TextView
+ android:id="@+id/selected_order"
+ android:layout_height="@dimen/picker_item_check_size"
+ android:layout_width="@dimen/picker_item_check_size"
+ android:layout_marginStart="@dimen/picker_item_check_margin"
+ android:layout_marginTop="@dimen/picker_item_check_margin"
+ android:background="@drawable/picker_item_order"
+ android:layout_gravity="top|start"
+ android:gravity="center"
+ android:textSize="12dp"
+ android:textColor="?attr/pickerHighlightTextColor"
+ android:scaleType="fitCenter"/>
+
</FrameLayout>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index e9f0466..7a76c15 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Geen albums nie"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Bekyk geselekteerde"</string>
<string name="picker_photos" msgid="7415035516411087392">"Foto\'s"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Voorskou"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Skakel oor na werk"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index cf08f5c..2889dd6 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ምንም አልበሞች የሉም"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"የተመረጡትን አሳይ"</string>
<string name="picker_photos" msgid="7415035516411087392">"ፎቶዎች"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"አልበሞች"</string>
<string name="picker_preview" msgid="6257414886055861039">"ቅድመ-ዕይታ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ወደ የሥራ ቀይር"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 1cba3df..7d55f12 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ما مِن ألبومات"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"عرض ما تم اختياره"</string>
<string name="picker_photos" msgid="7415035516411087392">"الصور"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"الألبومات"</string>
<string name="picker_preview" msgid="6257414886055861039">"معاينة"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"التبديل إلى الملف الشخصي للعمل"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index ec19082..8785eba 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"কোনো এলবাম নাই"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ভিউ বাছনি কৰা হৈছে"</string>
<string name="picker_photos" msgid="7415035516411087392">"ফট’"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"এলবাম"</string>
<string name="picker_preview" msgid="6257414886055861039">"পূৰ্বদৰ্শন কৰক"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"কৰ্মস্থানৰ প্ৰ’ফাইললৈ সলনি কৰক"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 5fc20b0..af1f72b 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Albom yoxdur"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Seçilənə baxın"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotolar"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albomlar"</string>
<string name="picker_preview" msgid="6257414886055861039">"Önbaxış"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"İş profilinə keçirin"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 495ab4b..56f8648 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nema albuma"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Prikaži izabrano"</string>
<string name="picker_photos" msgid="7415035516411087392">"Slike"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pregled"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Pređi na poslovni profil"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index c20866c..6421809 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Няма альбомаў"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Праглядзець выбранае"</string>
<string name="picker_photos" msgid="7415035516411087392">"Фота"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Альбомы"</string>
<string name="picker_preview" msgid="6257414886055861039">"Перадпрагляд"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Пераключыцца на працоўны"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 356cc1a..a9c7f24 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Няма албуми"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Преглед на избраното"</string>
<string name="picker_photos" msgid="7415035516411087392">"Снимки"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Албуми"</string>
<string name="picker_preview" msgid="6257414886055861039">"Визуализация"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Превкл. към служ. пoтр. профил"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 0f60019..42c3be2 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"কোনও অ্যালবাম নেই"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"কোনগুলি বাছা হয়েছে দেখুন"</string>
<string name="picker_photos" msgid="7415035516411087392">"ফটো"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"অ্যালবাম"</string>
<string name="picker_preview" msgid="6257414886055861039">"প্রিভিউ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"অফিস প্রোফাইলে সুইচ করুন"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index f6da691..f9550a5 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nema albuma"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Prikaži odabrano"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotografije"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pregled"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Prebacite se na radni profil"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index ed89418..7f8822b 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No hi ha cap àlbum"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Mostra la selecció"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Àlbums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Previsualitza"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Canvia al perfil de treball"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 0b0773a..33790ca 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Žádná alba"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Zobrazit vybrané"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotky"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Alba"</string>
<string name="picker_preview" msgid="6257414886055861039">"Náhled"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Přepnout na pracovní profil"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index cea19ca..0bc8433 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Ingen album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Se valgte"</string>
<string name="picker_photos" msgid="7415035516411087392">"Billeder"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Forhåndsvisning"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Skift til arbejdsprofil"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 37a6bc6..82f878e 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Keine Alben"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Auswahl ansehen"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Alben"</string>
<string name="picker_preview" msgid="6257414886055861039">"Vorschau"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Zum Arbeitsprofil wechseln"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 2a44df5..0736242 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Δεν υπάρχουν άλμπουμ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Προβολή επιλεγμένων"</string>
<string name="picker_photos" msgid="7415035516411087392">"Φωτογραφίες"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Άλμπουμ"</string>
<string name="picker_preview" msgid="6257414886055861039">"Προεπισκόπηση"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Μετάβαση σε προφίλ εργασίας"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index a719437..996ef60 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index c6b7563..be91653 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index a719437..996ef60 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index a719437..996ef60 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index dbc133f..6dd8620 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 11b0976..f2b7c73 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No hay álbumes"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Ver seleccionados"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Álbumes"</string>
<string name="picker_preview" msgid="6257414886055861039">"Vista previa"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Cambiar al perfil de trabajo"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index d7f2b64..c23f8ad 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"No hay ningún álbum"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Ver seleccionado"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Álbumes"</string>
<string name="picker_preview" msgid="6257414886055861039">"Vista previa"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Cambiar a perfil de trabajo"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 4aa4017..433ea73 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Albumeid pole"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Kuva valitud"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotod"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumid"</string>
<string name="picker_preview" msgid="6257414886055861039">"Eelvaade"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Lülituge tööprofiilile"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 49fb67a..aa0f303 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Ez dago albumik"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Ikusi hautatutakoak"</string>
<string name="picker_photos" msgid="7415035516411087392">"Argazkiak"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumak"</string>
<string name="picker_preview" msgid="6257414886055861039">"Aurrebista"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Aldatu laneko profilera"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 6209611..9a6a0f9 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"آلبومی موجود نیست"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"مشاهده موارد انتخابشده"</string>
<string name="picker_photos" msgid="7415035516411087392">"عکسها"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"آلبومها"</string>
<string name="picker_preview" msgid="6257414886055861039">"پیشنما"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"رفتن به نمایه کاری"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 3f4101b..ea1db49 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Ei albumeita"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Katso valitut"</string>
<string name="picker_photos" msgid="7415035516411087392">"Kuvat"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumit"</string>
<string name="picker_preview" msgid="6257414886055861039">"Esikatselu"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Siirry työprofiiliin"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 1d46b00..eadca62 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Aucun album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Afficher la sélection"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Aperçu"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Passez au profil professionnel"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 1122665..792e3af 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Aucun album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Afficher la sélection"</string>
<string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Aperçu"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Passer au profil professionnel"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 5b90bca..04d94f8 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Non hai álbums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Ver selección"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Álbums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Vista previa"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Cambiar ao perfil de traballo"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index cbaf312..0544ced 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"કોઈ આલ્બમ નથી"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"પસંદ કરેલા ફોટા જુઓ"</string>
<string name="picker_photos" msgid="7415035516411087392">"ફોટા"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"આલ્બમ"</string>
<string name="picker_preview" msgid="6257414886055861039">"પ્રીવ્યૂ કરો"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ઑફિસની પ્રોફાઇલ પર સ્વિચ કરો"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 070b0f9..3eaa289 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"कोई एल्बम नहीं है"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"चुनी गई फ़ोटो या वीडियो देखें"</string>
<string name="picker_photos" msgid="7415035516411087392">"फ़ोटो"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"एल्बम"</string>
<string name="picker_preview" msgid="6257414886055861039">"झलक"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"वर्क प्रोफ़ाइल पर जाएं"</string>
@@ -69,7 +71,7 @@
<string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"निजी डेटा को ऑफ़िस के काम से जुड़े ऐप्लिकेशन से ऐक्सेस करने की अनुमति नहीं है"</string>
<string name="picker_profile_work_paused_title" msgid="382212880704235925">"वर्क ऐप्लिकेशन रोक दिए गए हैं"</string>
<string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"वर्क फ़ोटो देखने के लिए, ऑफ़िस के काम से जुड़े ऐप्लिकेशन चालू करें और दोबारा कोशिश करें"</string>
- <string name="picker_privacy_message" msgid="9132700451027116817">"इस ऐप्लिकेशन के पास आपकी उन फ़ोटो का ही ऐक्सेस होता है जिन्हें आपने चुना हो"</string>
+ <string name="picker_privacy_message" msgid="9132700451027116817">"इस ऐप्लिकेशन के पास आपकी उन फ़ोटो का ही ऐक्सेस होता है जिन्हें आपने चुना है"</string>
<string name="picker_header_permissions" msgid="675872774407768495">"वे फ़ोटो और वीडियो चुनें जिनका ऐक्सेस इस ऐप्लिकेशन को देना है"</string>
<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>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index bd32b54..9339e27 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nema albuma"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Prikaži odabrano"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotografije"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pregled"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Prijeđite na poslovni"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 0132ca5..7f62bf1 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nincsenek albumok"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Kijelöltek megnézése"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotók"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumok"</string>
<string name="picker_preview" msgid="6257414886055861039">"Előnézet"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Átváltás munkaprofilra"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index df9771b..b67062e 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Ալբոմներ չկան"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Դիտել ընտրվածը"</string>
<string name="picker_photos" msgid="7415035516411087392">"Լուսանկարներ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Ալբոմներ"</string>
<string name="picker_preview" msgid="6257414886055861039">"Նախադիտում"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Բացել աշխատանքային պրոֆիլը"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 5431783..3767083 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Tidak ada album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Lihat yang dipilih"</string>
<string name="picker_photos" msgid="7415035516411087392">"Foto"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pratinjau"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Beralih ke profil kerja"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 211f051..c492315 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Engin albúm"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Skoða valið"</string>
<string name="picker_photos" msgid="7415035516411087392">"Myndir"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albúm"</string>
<string name="picker_preview" msgid="6257414886055861039">"Forskoða"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Skipta yfir í vinnusnið"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index c03968a..73249cb 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nessun album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Visualizza selezione"</string>
<string name="picker_photos" msgid="7415035516411087392">"Foto"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Anteprima"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Passa al profilo di lavoro"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 4e76d6b..abbdb8f 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"אין אלבומים"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"הצגת הפריטים שנבחרו"</string>
<string name="picker_photos" msgid="7415035516411087392">"תמונות"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"אלבומים"</string>
<string name="picker_preview" msgid="6257414886055861039">"תצוגה מקדימה"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"לפרופיל העבודה"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 1524313..160c68f 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"アルバムはありません"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"選択した写真を見る"</string>
<string name="picker_photos" msgid="7415035516411087392">"写真"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"アルバム"</string>
<string name="picker_preview" msgid="6257414886055861039">"プレビュー"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"仕事用に切り替える"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index d6700f3..382d5a1 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ალბომები არ არის"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"არჩეულის ნახვა"</string>
<string name="picker_photos" msgid="7415035516411087392">"ფოტოები"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ალბომები"</string>
<string name="picker_preview" msgid="6257414886055861039">"გადახედვა"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"სამსახურზე გადართვა"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 263ff2d..e59be1e 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Альбомдар жоқ."</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Таңдалғанды көру"</string>
<string name="picker_photos" msgid="7415035516411087392">"Фотосуреттер"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Aльбомдар"</string>
<string name="picker_preview" msgid="6257414886055861039">"Алғы көрініс"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Жұмыс профиліне ауысу"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index a72a992..7468247 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"គ្មានអាល់ប៊ុមទេ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"មើលអ្វីដែលបានជ្រើសរើស"</string>
<string name="picker_photos" msgid="7415035516411087392">"រូបថត"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"អាល់ប៊ុម"</string>
<string name="picker_preview" msgid="6257414886055861039">"មើលសាកល្បង"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ប្ដូរទៅកម្រងព័ត៌មានការងារ"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 6baafe4..e0a5f07 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ಯಾವುದೇ ಆಲ್ಬಮ್ಗಳಿಲ್ಲ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ಆಯ್ಕೆಮಾಡಿರುವುದನ್ನು ವೀಕ್ಷಿಸಿ"</string>
<string name="picker_photos" msgid="7415035516411087392">"ಫೋಟೋಗಳು"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ಆಲ್ಬಮ್ಗಳು"</string>
<string name="picker_preview" msgid="6257414886055861039">"ಪೂರ್ವವೀಕ್ಷಣೆ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ಉದ್ಯೋಗ ಪ್ರೊಫೈಲ್ಗೆ ಬದಲಿಸಿ"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 60ac833..84b4241 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"앨범 없음"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"선택 항목 보기"</string>
<string name="picker_photos" msgid="7415035516411087392">"사진"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"앨범"</string>
<string name="picker_preview" msgid="6257414886055861039">"미리보기"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"직장 프로필로 전환"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 50f3f03..36cb921 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Альбомдор жок"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Тандалганды көрүү"</string>
<string name="picker_photos" msgid="7415035516411087392">"Сүрөттөр"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Альбомдор"</string>
<string name="picker_preview" msgid="6257414886055861039">"Алдын ала көрүү"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Жумуш профилине которулуу"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index f70953a..1806a7f 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ບໍ່ມີອະລະບ້ຳ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ເບິ່ງອັນທີ່ເລືອກໄວ້"</string>
<string name="picker_photos" msgid="7415035516411087392">"ຮູບພາບ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ອະລະບ້ຳ"</string>
<string name="picker_preview" msgid="6257414886055861039">"ຕົວຢ່າງ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ສະຫຼັບໄປໂປຣໄຟລ໌ວຽກ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index e8e06f0..a16f1f5 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nėra jokių albumų"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Žiūrėti pasirinktus"</string>
<string name="picker_photos" msgid="7415035516411087392">"Nuotraukos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumai"</string>
<string name="picker_preview" msgid="6257414886055861039">"Peržiūra"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Perjungti į darbo profilį"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 4331651..330f6cf 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nav albumu"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Skatīt atlasīto"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotoattēli"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
<string name="picker_preview" msgid="6257414886055861039">"Priekšskatījums"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Pārslēgties uz darba profilu"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 3fdc1c0..719221f 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Нема албуми"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Прикажи ги избраните"</string>
<string name="picker_photos" msgid="7415035516411087392">"Фотографии"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Албуми"</string>
<string name="picker_preview" msgid="6257414886055861039">"Преглед"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Префрлете се на работен профил"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index d02f7e7..b0e63c8 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ആൽബങ്ങളൊന്നുമില്ല"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"തിരഞ്ഞെടുത്തത് കാണുക"</string>
<string name="picker_photos" msgid="7415035516411087392">"ഫോട്ടോകൾ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ആൽബങ്ങൾ"</string>
<string name="picker_preview" msgid="6257414886055861039">"പ്രിവ്യു"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ഔദ്യോഗിക പ്രൊഫൈലിലേക്ക് മാറുക"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 38d6f15..540a629 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Цомог алга"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Сонгосныг харах"</string>
<string name="picker_photos" msgid="7415035516411087392">"Зураг"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Цомог"</string>
<string name="picker_preview" msgid="6257414886055861039">"Урьдчилан үзэх"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Ажлын профайл руу сэлгэх"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 3fb8729..0f0fcc4 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"कोणतेही अल्बम नाहीत"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"निवडलेले पहा"</string>
<string name="picker_photos" msgid="7415035516411087392">"फोटो"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"अल्बम"</string>
<string name="picker_preview" msgid="6257414886055861039">"पूर्वावलोकन"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ऑफिसवर स्विच करा"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index f96d6e4..5751c1a 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Tiada album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Lihat terpilih"</string>
<string name="picker_photos" msgid="7415035516411087392">"Foto"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pratonton"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Beralih kepada kerja"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 9a1013f..00d070a 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"အယ်လ်ဘမ်များ မရှိပါ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ပြသမှုကို ရွေးချယ်ထားသည်"</string>
<string name="picker_photos" msgid="7415035516411087392">"ဓာတ်ပုံများ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"အယ်လ်ဘမ်များ"</string>
<string name="picker_preview" msgid="6257414886055861039">"အစမ်းကြည့်ရှုခြင်း"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"အလုပ်သို့ ပြောင်းပါ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 901f7ed..c120204 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Ingen album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Vis valgte"</string>
<string name="picker_photos" msgid="7415035516411087392">"Bilder"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Forhåndsvisning"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Bytt til jobbprofilen"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index eed492e..d39dfc6 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"कुनै पनि एल्बम छैन"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"चयन गरिएका सामग्री हेर्नुहोस्"</string>
<string name="picker_photos" msgid="7415035516411087392">"फोटोहरू"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"एल्बमहरू"</string>
<string name="picker_preview" msgid="6257414886055861039">"प्रिभ्यू"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"कार्य प्रोफाइल प्रयोग गर्नुहोस्"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index de4fcc2..62caff6 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Geen albums"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Selectie bekijken"</string>
<string name="picker_photos" msgid="7415035516411087392">"Foto\'s"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
<string name="picker_preview" msgid="6257414886055861039">"Voorbeeld"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Overschakelen naar werkprofiel"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index afcb014..b6e37dc 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"କୌଣସି ଆଲବମ ନାହିଁ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ଚୟନିତଗୁଡ଼ିକୁ ଭ୍ୟୁ କରନ୍ତୁ"</string>
<string name="picker_photos" msgid="7415035516411087392">"ଫଟୋ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ଆଲବମ"</string>
<string name="picker_preview" msgid="6257414886055861039">"ପ୍ରିଭ୍ୟୁ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ୱାର୍କକୁ ସୁଇଚ କରନ୍ତୁ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index c17c1d0..950ad56 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ਕੋਈ ਐਲਬਮ ਨਹੀਂ"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ਚੁਣੀਆਂ ਗਈਆਂ ਆਈਟਮਾਂ ਦੇਖੋ"</string>
<string name="picker_photos" msgid="7415035516411087392">"ਫ਼ੋਟੋਆਂ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ਐਲਬਮਾਂ"</string>
<string name="picker_preview" msgid="6257414886055861039">"ਪੂਰਵ-ਝਲਕ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"ਕਾਰਜ ਪ੍ਰੋਫਾਈਲ \'ਤੇ ਸਵਿੱਚ ਕਰੋ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index cc3b1c5..d08e86b 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Brak albumów"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Wyświetl wybrane"</string>
<string name="picker_photos" msgid="7415035516411087392">"Zdjęcia"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumy"</string>
<string name="picker_preview" msgid="6257414886055861039">"Podgląd"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Włącz profil służbowy"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index bef70c1..feeeb74 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Sem álbuns"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Mostrar selecionados"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Álbuns"</string>
<string name="picker_preview" msgid="6257414886055861039">"Visualização"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Mudar para Trabalho"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a049d92..8a824c1 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nenhum álbum"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Ver selecionado(s)"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Álbuns"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pré-visualizar"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Mudar para trabalho"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index bef70c1..feeeb74 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Sem álbuns"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Mostrar selecionados"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Álbuns"</string>
<string name="picker_preview" msgid="6257414886055861039">"Visualização"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Mudar para Trabalho"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index d9d0df9..399078e 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Niciun album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Vezi elementele selectate"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotografii"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albume"</string>
<string name="picker_preview" msgid="6257414886055861039">"Previzualizare"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Comută la serviciu"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index b8ff7b3..edf4c87 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Альбомов нет."</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Посмотреть выбранное"</string>
<string name="picker_photos" msgid="7415035516411087392">"Фотографии"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Альбомы"</string>
<string name="picker_preview" msgid="6257414886055861039">"Предварительный просмотр"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Перейти в рабочий профиль"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index e972106..c41dad2 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ඇල්බම නැත"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"තෝරා ගත් දේවල් බලන්න"</string>
<string name="picker_photos" msgid="7415035516411087392">"ඡායාරූප"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ඇල්බම"</string>
<string name="picker_preview" msgid="6257414886055861039">"පෙරදසුන"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"කාර්යාලය වෙත මාරු වන්න"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 3a8f9fb..d20ec47 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Žiadne albumy"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Zobraziť vybrané"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotky"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumy"</string>
<string name="picker_preview" msgid="6257414886055861039">"Ukážka"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Prepnúť na pracovný profil"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 08cb6a1..a5bcb3c 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Ni albumov."</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Prikaži izbrano"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotografije"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
<string name="picker_preview" msgid="6257414886055861039">"Predogled"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Preklop na delovni profil"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 0b60fde..7681a67 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Nuk ka albume"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Shiko të zgjedhurat"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotografitë"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albumet"</string>
<string name="picker_preview" msgid="6257414886055861039">"Pamja paraprake"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Kalo te profili i punës"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 726459e..bc902e7 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Нема албума"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Прикажи изабранo"</string>
<string name="picker_photos" msgid="7415035516411087392">"Слике"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Албуми"</string>
<string name="picker_preview" msgid="6257414886055861039">"Преглед"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Пређи на пословни профил"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index d725331..d71c999 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Inga album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Visa valda"</string>
<string name="picker_photos" msgid="7415035516411087392">"Foton"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Förhandsgranska"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Byt till jobbprofilen"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 020b5a2..d220100 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Hakuna albamu"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Angalia ulizochagua"</string>
<string name="picker_photos" msgid="7415035516411087392">"Picha"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albamu"</string>
<string name="picker_preview" msgid="6257414886055861039">"Onyesho la kukagua"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Badili utumie wasifu wa kazini"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 309ba9d..fbb05c2 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ஆல்பங்கள் இல்லை"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"தேர்ந்தெடுத்ததைக் காட்டு"</string>
<string name="picker_photos" msgid="7415035516411087392">"படங்கள்"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ஆல்பங்கள்"</string>
<string name="picker_preview" msgid="6257414886055861039">"மாதிரிக்காட்சி"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"பணிச் சுயவிவரத்திற்கு மாறு"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 2621a48..5913fa6 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ఆల్బమ్లు ఏవీ లేవు"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ఎంచుకున్న వాటిని చూడండి"</string>
<string name="picker_photos" msgid="7415035516411087392">"ఫోటోలు"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"ఆల్బమ్లు"</string>
<string name="picker_preview" msgid="6257414886055861039">"ప్రివ్యూ"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"వర్క్ ప్రొఫైల్కు మార్చండి"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 538d732..faa59a6 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"ไม่มีอัลบั้ม"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"ดูรายการที่เลือก"</string>
<string name="picker_photos" msgid="7415035516411087392">"รูปภาพ"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"อัลบั้ม"</string>
<string name="picker_preview" msgid="6257414886055861039">"ตัวอย่าง"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"เปลี่ยนไปใช้โปรไฟล์งาน"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index ce60a92..6b0577d 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Walang album"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Tingnan ang napili"</string>
<string name="picker_photos" msgid="7415035516411087392">"Mga Larawan"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Mga Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Lumipat sa para sa trabaho"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index c959485..9c34218 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Albüm yok"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Seçilenleri görüntüle"</string>
<string name="picker_photos" msgid="7415035516411087392">"Fotoğraflar"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albümler"</string>
<string name="picker_preview" msgid="6257414886055861039">"Önizle"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"İş profiline geç"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 8ec4cfb..99039b9 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Немає альбомів"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Переглянути вибране"</string>
<string name="picker_photos" msgid="7415035516411087392">"Фото"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Альбоми"</string>
<string name="picker_preview" msgid="6257414886055861039">"Попередній перегляд"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Перейти в робочий профіль"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index dea4d10..6709e8a 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"کوئی البم نہیں"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"منتخب کردہ دیکھیں"</string>
<string name="picker_photos" msgid="7415035516411087392">"تصاویر"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"البمز"</string>
<string name="picker_preview" msgid="6257414886055861039">"پیش منظر"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"کام پر سوئچ کریں"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index b97c5df..719b153 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Albom kiritilmagan"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Belgilanganlarni ochish"</string>
<string name="picker_photos" msgid="7415035516411087392">"Suratlar"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Albomlar"</string>
<string name="picker_preview" msgid="6257414886055861039">"Razm solish"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Ish profiliga almashish"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index c0dd41d..4cac424 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Không có album nào"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Xem các mục được chọn"</string>
<string name="picker_photos" msgid="7415035516411087392">"Ảnh"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Album"</string>
<string name="picker_preview" msgid="6257414886055861039">"Xem trước"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Chuyển sang hồ sơ công việc"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 28c400c..a9be74e 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"无影集"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"查看所选内容"</string>
<string name="picker_photos" msgid="7415035516411087392">"照片"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"影集"</string>
<string name="picker_preview" msgid="6257414886055861039">"预览"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"切换到工作资料"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 870d569..a8b9e61 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"沒有相簿"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"查看所選項目"</string>
<string name="picker_photos" msgid="7415035516411087392">"相片"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"相簿"</string>
<string name="picker_preview" msgid="6257414886055861039">"預覽"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"切換至工作設定檔"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 4d2b5d6..13c77cc 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"沒有相簿"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"查看所選項目"</string>
<string name="picker_photos" msgid="7415035516411087392">"相片"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"相簿"</string>
<string name="picker_preview" msgid="6257414886055861039">"預覽"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"切換至工作資料夾"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index fd42bc1..0e53f4c 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -60,6 +60,8 @@
<string name="picker_albums_empty_message" msgid="8341079772950966815">"Awekho ama-albhamu"</string>
<string name="picker_view_selected" msgid="2266031384396143883">"Ukubuka kukhethiwe"</string>
<string name="picker_photos" msgid="7415035516411087392">"Izithombe"</string>
+ <!-- no translation found for picker_videos (2886971435439047097) -->
+ <skip />
<string name="picker_albums" msgid="4822511902115299142">"Ama-albhamu"</string>
<string name="picker_preview" msgid="6257414886055861039">"Hlola kuqala"</string>
<string name="picker_work_profile" msgid="2083221066869141576">"Shintshela kokmsebenzi"</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0651c0a..748e7c5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -146,9 +146,12 @@
<!-- PhotoPicker view selected action text. [CHAR LIMIT=17] -->
<string name="picker_view_selected">View selected</string>
- <!-- The text of the photos tab for PhotoPicker. [CHAR LIMIT=30] -->
+ <!-- The text of the photos tab in PhotoPicker for 'Image/' mime type. [CHAR LIMIT=30] -->
<string name="picker_photos">Photos</string>
+ <!-- The text of the photos tab in PhotoPicker for 'Video/' mime type. [CHAR LIMIT=30] -->
+ <string name="picker_videos">@string/root_videos</string>
+
<!-- The text of the albums tab for PhotoPicker. [CHAR LIMIT=30] -->
<string name="picker_albums">Albums</string>
@@ -257,6 +260,8 @@
<!-- A message for the Progress Dialog shown while preloading selected items before "closing" Photo Picker. [CHAR LIMIT=NONE] -->
<string name="preloading_progress_message"><xliff:g id="number_preloaded">%1$d</xliff:g> of <xliff:g id="number_total">%2$d</xliff:g> ready</string>
+ <string name="preloading_cancel_button">Cancel</string>
+
<!-- ========================= PHOTO PICKER CLOUD EDUCATION BANNERS ========================= -->
<!-- Title for the banner notifying the user that the cloud media is now available in the picker [CHAR LIMIT=NONE] -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 179edf8..2f9fd17 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -152,6 +152,11 @@
<item name="materialAlertDialogTitleTextStyle">@style/AlertDialogTitleStyle</item>
</style>
+ <style name="ProgressDialogCancelButtonStyle"
+ parent="@style/Widget.MaterialComponents.Button.TextButton">
+ <item name="android:textColor">?attr/colorOnSurface</item>
+ </style>
+
<style name="AlertDialogTitleStyle"
parent="@style/MaterialAlertDialog.MaterialComponents.Title.Text.CenterStacked">
<item name="android:textColor">?attr/colorOnSurface</item>
diff --git a/src/com/android/providers/media/ConfigStore.java b/src/com/android/providers/media/ConfigStore.java
index cd80866..828d620 100644
--- a/src/com/android/providers/media/ConfigStore.java
+++ b/src/com/android/providers/media/ConfigStore.java
@@ -56,6 +56,7 @@
boolean DEFAULT_USER_SELECT_FOR_APP = true;
boolean DEFAULT_STABILISE_VOLUME_INTERNAL = false;
boolean DEFAULT_STABILIZE_VOLUME_EXTERNAL = false;
+ boolean DEFAULT_STABILIZE_VOLUME_PUBLIC = false;
boolean DEFAULT_TRANSCODE_ENABLED = true;
boolean DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = false;
@@ -67,7 +68,7 @@
boolean DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED = true;
boolean DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST = true;
- boolean DEFAULT_PICKER_CHOICE_MANAGED_SELECTION_ENABLED = false;
+ boolean DEFAULT_PICKER_CHOICE_MANAGED_SELECTION_ENABLED = true;
/**
* @return if the Cloud-Media-in-Photo-Picker enabled (e.g. platform will recognize and
@@ -184,6 +185,13 @@
}
/**
+ * @return if stable URI are enabled for public volumes.
+ */
+ default boolean isStableUrisForPublicVolumeEnabled() {
+ return DEFAULT_STABILIZE_VOLUME_PUBLIC;
+ }
+
+ /**
* @return if transcoding is enabled.
*/
default boolean isTranscodeEnabled() {
@@ -254,6 +262,8 @@
public static final String KEY_STABILIZE_VOLUME_INTERNAL = "stabilize_volume_internal";
@VisibleForTesting
public static final String KEY_STABILIZE_VOLUME_EXTERNAL = "stabilize_volume_external";
+ @VisibleForTesting
+ public static final String KEY_STABILIZE_VOLUME_PUBLIC = "stabilize_volume_public";
private static final String KEY_TRANSCODE_ENABLED = "transcode_enabled";
private static final String KEY_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = "transcode_default";
@@ -400,6 +410,12 @@
}
@Override
+ public boolean isStableUrisForPublicVolumeEnabled() {
+ return getBooleanDeviceConfig(NAMESPACE_MEDIAPROVIDER, KEY_STABILIZE_VOLUME_PUBLIC,
+ DEFAULT_STABILIZE_VOLUME_PUBLIC);
+ }
+
+ @Override
public boolean isTranscodeEnabled() {
return getBooleanDeviceConfig(
KEY_TRANSCODE_ENABLED, DEFAULT_TRANSCODE_ENABLED);
diff --git a/src/com/android/providers/media/DatabaseBackupAndRecovery.java b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
index ec864cd..50ee804 100644
--- a/src/com/android/providers/media/DatabaseBackupAndRecovery.java
+++ b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
@@ -62,7 +62,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -82,12 +82,9 @@
"/data/media/" + UserHandle.myUserId() + "/.transforms/recovery/leveldb-ownership";
/**
- * Path which stores backup of external primary volume.
- * Lower file system path is used as upper file system does not support xattrs.
+ * Every LevelDB table name starts with this prefix.
*/
- private static final String EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH =
- "/data/media/" + UserHandle.myUserId()
- + "/.transforms/recovery/leveldb-external_primary";
+ private static final String LEVEL_DB_PREFIX = "leveldb-";
/**
* Frequency at which next value of owner id is backed up in the external storage.
@@ -128,7 +125,8 @@
MediaStore.Files.FileColumns._USER_ID,
MediaStore.Files.FileColumns.DATE_EXPIRES,
MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME,
- MediaStore.Files.FileColumns.GENERATION_MODIFIED
+ MediaStore.Files.FileColumns.GENERATION_MODIFIED,
+ MediaStore.Files.FileColumns.VOLUME_NAME
};
/**
@@ -153,8 +151,7 @@
private AtomicInteger mNextOwnerIdBackup;
private final ConfigStore mConfigStore;
private final VolumeCache mVolumeCache;
-
- private AtomicBoolean mIsBackupSetupComplete = new AtomicBoolean(false);
+ private Set<String> mSetupCompletePublicVolumes = ConcurrentHashMap.newKeySet();
private static Map<String, String> sOwnerIdRelationMap;
@@ -178,7 +175,11 @@
"persist.sys.fuse.backup.external_volume_backup",
/* defaultValue */ false);
default:
- return false;
+ // public volume
+ return isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+ && mConfigStore.isStableUrisForPublicVolumeEnabled()
+ || SystemProperties.getBoolean("persist.sys.fuse.backup.public_db_backup",
+ /* defaultValue */ false);
}
}
@@ -189,10 +190,9 @@
* volume on Media mount signal of EXTERNAL_PRIMARY.
*/
protected synchronized void setupVolumeDbBackupAndRecovery(String volumeName, File volumePath) {
- // We are setting up leveldb instance only for internal volume as of now. Since internal
- // volume does not have any fuse daemon thread, leveldb instance is created by fuse
- // daemon thread of EXTERNAL_PRIMARY.
- if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
+ // Since internal volume does not have any fuse daemon thread, leveldb instance
+ // for internal volume is created by fuse daemon thread of EXTERNAL_PRIMARY.
+ if (MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)) {
// Set backup only for external primary for now.
return;
}
@@ -202,7 +202,7 @@
return;
}
- if (mIsBackupSetupComplete.get()) {
+ if (mSetupCompletePublicVolumes.contains(volumeName)) {
// Return if setup is already done
return;
}
@@ -211,10 +211,19 @@
if (!new File(RECOVERY_DIRECTORY_PATH).exists()) {
new File(RECOVERY_DIRECTORY_PATH).mkdirs();
}
- FuseDaemon fuseDaemon = getFuseDaemonForFileWithWait(volumePath);
+ FuseDaemon fuseDaemon = getFuseDaemonForFileWithWait(new File(
+ DatabaseBackupAndRecovery.EXTERNAL_PRIMARY_ROOT_PATH));
Log.d(TAG, "Received db backup Fuse Daemon for: " + volumeName);
- fuseDaemon.setupVolumeDbBackup();
- mIsBackupSetupComplete.set(true);
+ if (isStableUrisEnabled(volumeName)) {
+ if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
+ // Setup internal and external volumes
+ fuseDaemon.setupVolumeDbBackup();
+ } else {
+ // Setup public volume
+ fuseDaemon.setupPublicVolumeDbBackup(volumeName);
+ }
+ mSetupCompletePublicVolumes.add(volumeName);
+ }
} catch (IOException e) {
Log.e(TAG, "Failure in setting up backup and recovery for volume: " + volumeName, e);
return;
@@ -231,7 +240,15 @@
new File(EXTERNAL_PRIMARY_ROOT_PATH));
Log.i(TAG, "Triggering database backup");
backupInternalDatabase(internalDatabaseHelper, signal);
- backupExternalDatabase(externalDatabaseHelper, signal);
+ backupExternalDatabase(externalDatabaseHelper, MediaStore.VOLUME_EXTERNAL_PRIMARY, signal);
+
+ for (MediaVolume mediaVolume : mVolumeCache.getExternalVolumes()) {
+ if (mediaVolume.isPublicVolume()) {
+ setupVolumeDbBackupAndRecovery(mediaVolume.getName(),
+ new File(EXTERNAL_PRIMARY_ROOT_PATH));
+ backupExternalDatabase(externalDatabaseHelper, mediaVolume.getName(), signal);
+ }
+ }
}
protected Optional<BackupIdRow> readDataFromBackup(String volumeName, String filePath) {
@@ -239,9 +256,9 @@
return Optional.empty();
}
- final String fuseDaemonFilePath = getFilePathForFuseRequests(filePath);
try {
- final String data = getFuseDaemonForPath(fuseDaemonFilePath).readBackedUpData(filePath);
+ final String data = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH)
+ .readBackedUpData(filePath);
if (data == null || data.isEmpty()) {
Log.w(TAG, "No backup found for path: " + filePath);
return Optional.empty();
@@ -261,7 +278,7 @@
return;
}
- if (!mIsBackupSetupComplete.get()) {
+ if (!mSetupCompletePublicVolumes.contains(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
return;
}
@@ -294,13 +311,13 @@
}
protected synchronized void backupExternalDatabase(DatabaseHelper externalDbHelper,
- CancellationSignal signal) {
- if (!isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+ String volumeName, CancellationSignal signal) {
+ if (!isStableUrisEnabled(volumeName)
|| externalDbHelper.isDatabaseRecovering()) {
return;
}
- if (!mIsBackupSetupComplete.get()) {
+ if (!mSetupCompletePublicVolumes.contains(volumeName)) {
return;
}
@@ -310,28 +327,24 @@
} catch (FileNotFoundException e) {
Log.e(TAG,
"Fuse Daemon not found for primary external storage, skipping backing up of "
- + "external database.",
+ + volumeName,
e);
return;
}
- // Read last backed up generation number
- Optional<Long> lastBackedUpGenNum = getXattrOfLongValue(
- EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY);
- long lastBackedGenerationNumber = lastBackedUpGenNum.isPresent()
- ? lastBackedUpGenNum.get() : 0;
- if (lastBackedGenerationNumber > 0) {
- Log.i(TAG, "Last backed up generation number is " + lastBackedGenerationNumber);
- }
+ final String backupPath = RECOVERY_DIRECTORY_PATH + "/" + LEVEL_DB_PREFIX + volumeName;
+ long lastBackedGenerationNumber = getLastBackedGenerationNumber(backupPath);
+
final String generationClause = MediaStore.Files.FileColumns.GENERATION_MODIFIED + " >= "
+ lastBackedGenerationNumber;
final String volumeClause = MediaStore.Files.FileColumns.VOLUME_NAME + " = '"
- + MediaStore.VOLUME_EXTERNAL_PRIMARY + "'";
+ + volumeName + "'";
final String selectionClause = generationClause + " AND " + volumeClause;
externalDbHelper.runWithTransaction((db) -> {
long maxGeneration = lastBackedGenerationNumber;
- Log.d(TAG, "Started to back up external database, maxGeneration:" + maxGeneration);
+ Log.d(TAG, "Started to back up " + volumeName
+ + ", maxGeneration:" + maxGeneration);
try (Cursor c = db.query(true, "files", QUERY_COLUMNS, selectionClause, null, null,
null, MediaStore.MediaColumns._ID + " ASC", null, signal)) {
while (c.moveToNext()) {
@@ -343,14 +356,14 @@
backupDataValues(fuseDaemon, c);
maxGeneration = Math.max(maxGeneration, c.getLong(9));
}
- setXattr(EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY,
+ setXattr(backupPath, LAST_BACKEDUP_GENERATION_XATTR_KEY,
String.valueOf(maxGeneration - 1));
Log.d(TAG, String.format(Locale.ROOT,
- "Backed up %d rows of external database to external storage on idle "
+ "Backed up %d rows of " + volumeName + " to external storage on idle "
+ "maintenance.",
c.getCount()));
} catch (Exception e) {
- Log.e(TAG, "Failure in backing up external database to external storage.", e);
+ Log.e(TAG, "Failure in backing up " + volumeName + " to external storage.", e);
return null;
}
return null;
@@ -367,10 +380,11 @@
final int userId = c.getInt(6);
final String dateExpires = c.getString(7);
final String ownerPackageName = c.getString(8);
+ final String volumeName = c.getString(10);
BackupIdRow backupIdRow = createBackupIdRow(fuseDaemon, id, mediaType,
isFavorite, isPending, isTrashed, userId, dateExpires,
ownerPackageName);
- fuseDaemon.backupVolumeDbData(data, BackupIdRow.serialize(backupIdRow));
+ fuseDaemon.backupVolumeDbData(volumeName, data, BackupIdRow.serialize(backupIdRow));
}
protected void deleteBackupForVolume(String volumeName) {
@@ -417,6 +431,19 @@
}
}
+ private long getLastBackedGenerationNumber(String backupPath) {
+ // Read last backed up generation number
+ Optional<Long> lastBackedUpGenNum = getXattrOfLongValue(
+ backupPath, LAST_BACKEDUP_GENERATION_XATTR_KEY);
+ long lastBackedGenerationNumber = lastBackedUpGenNum.isPresent()
+ ? lastBackedUpGenNum.get() : 0;
+ if (lastBackedGenerationNumber > 0) {
+ Log.i(TAG, "Last backed up generation number for " + backupPath + " is "
+ + lastBackedGenerationNumber);
+ }
+ return lastBackedGenerationNumber;
+ }
+
@NonNull
private FuseDaemon getFuseDaemonForPath(@NonNull String path)
throws FileNotFoundException {
@@ -438,32 +465,15 @@
}
try {
- FuseDaemon fuseDaemon = getFuseDaemonForPath(
- getFilePathForFuseRequests(insertedRow.getPath()));
+ FuseDaemon fuseDaemon = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH);
final BackupIdRow value = createBackupIdRow(fuseDaemon, insertedRow);
- fuseDaemon.backupVolumeDbData(insertedRow.getPath(), BackupIdRow.serialize(value));
+ fuseDaemon.backupVolumeDbData(insertedRow.getVolumeName(), insertedRow.getPath(),
+ BackupIdRow.serialize(value));
} catch (Exception e) {
Log.e(TAG, "Failure in backing up data to external storage", e);
}
}
- /**
- * Creates a fuse daemon file path for a given path.
- */
- protected static String getFilePathForFuseRequests(String filePath) {
- // For internal volume paths
- if (!filePath.startsWith("/storage")) {
- return EXTERNAL_PRIMARY_ROOT_PATH;
- }
-
- // For primary external and cloned app paths.
- if (filePath.equalsIgnoreCase("/storage") || filePath.startsWith("/storage/emulated")) {
- return EXTERNAL_PRIMARY_ROOT_PATH;
- }
-
- return filePath;
- }
-
private BackupIdRow createBackupIdRow(FuseDaemon fuseDaemon, FileRow insertedRow)
throws IOException {
return createBackupIdRow(fuseDaemon, insertedRow.getId(), insertedRow.getMediaType(),
@@ -592,7 +602,7 @@
}
try {
- getFuseDaemonForPath(getFilePathForFuseRequests(deletedFilePath)).deleteDbBackup(
+ getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH).deleteDbBackup(
deletedFilePath);
} catch (IOException e) {
Log.w(TAG, "Failure in deleting backup data for key: " + deletedFilePath, e);
@@ -602,7 +612,7 @@
protected boolean isBackupUpdateAllowed(DatabaseHelper databaseHelper, String volumeName) {
// Backup only if stable uris is enabled, db is not recovering and backup setup is complete.
return isStableUrisEnabled(volumeName) && !databaseHelper.isDatabaseRecovering()
- && mIsBackupSetupComplete.get();
+ && mSetupCompletePublicVolumes.contains(volumeName);
}
@@ -629,7 +639,8 @@
final String updatedFilePath = updatedRow.getPath();
try {
- getFuseDaemonForPath(getFilePathForFuseRequests(updatedFilePath)).backupVolumeDbData(
+ getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH).backupVolumeDbData(
+ updatedRow.getVolumeName(),
updatedFilePath,
BackupIdRow.serialize(BackupIdRow.newBuilder(updatedRow.getId()).setIsDirty(
true).build()));
@@ -805,6 +816,11 @@
}
protected void recoverData(SQLiteDatabase db, String volumeName) {
+ if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)
+ && !MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)) {
+ // todo: implement for public volume
+ return;
+ }
final long startTime = SystemClock.elapsedRealtime();
final String fuseFilePath = getFuseFilePathFromVolumeName(volumeName);
// Wait for external primary to be attached as we use same thread for internal volume.
@@ -862,8 +878,9 @@
volumeName));
if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
// Resetting generation number
- setXattr(EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY,
- String.valueOf(0));
+ setXattr(RECOVERY_DIRECTORY_PATH + "/" + LEVEL_DB_PREFIX
+ + MediaStore.VOLUME_EXTERNAL_PRIMARY,
+ LAST_BACKEDUP_GENERATION_XATTR_KEY, String.valueOf(0));
}
Log.i(TAG, String.format(Locale.ROOT, "Recovery time: %d ms", recoveryTime));
}
@@ -917,7 +934,7 @@
FuseDaemon fuseDaemon;
try {
- fuseDaemon = getFuseDaemonForPath(getFilePathForFuseRequests(oldRow.getPath()));
+ fuseDaemon = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH);
} catch (FileNotFoundException e) {
Log.e(TAG,
"Fuse Daemon not found for primary external storage, skipping update of "
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index d58972f..0fb7ff3 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -666,6 +666,10 @@
"%s database inconsistent: isLastUsedDatabaseSession:%b, "
+ "nextRowIdOptionalPresent:%b", mName, isLastUsedDatabaseSession,
nextRowIdFromXattrOptional.isPresent()));
+
+ // This could be a rollback, clear all media grants
+ clearMediaGrantsTable(db);
+
// TODO(b/222313219): Add an assert to ensure that next row id xattr is always
// present when DB session id matches across sequential open calls.
updateNextRowIdInDatabaseAndExternalStorage(db);
@@ -673,6 +677,15 @@
}
}
+ private void clearMediaGrantsTable(SQLiteDatabase db) {
+ mSchemaLock.writeLock().lock();
+ try {
+ updateAddMediaGrantsTable(db);
+ } finally {
+ mSchemaLock.writeLock().unlock();
+ }
+ }
+
@GuardedBy("sRecoveryLock")
private boolean isLastUsedDatabaseSession(SQLiteDatabase db) {
Optional<String> lastUsedSessionIdFromDatabasePathXattr = getXattr(db.getPath(),
diff --git a/src/com/android/providers/media/MediaGrants.java b/src/com/android/providers/media/MediaGrants.java
index 52f9a3e..d654ca0 100644
--- a/src/com/android/providers/media/MediaGrants.java
+++ b/src/com/android/providers/media/MediaGrants.java
@@ -25,6 +25,7 @@
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.MediaStore;
@@ -55,12 +56,12 @@
public static final String OWNER_PACKAGE_NAME_COLUMN =
MediaStore.MediaColumns.OWNER_PACKAGE_NAME;
+ private static final String CREATE_TEMPORARY_TABLE_QUERY = "CREATE TEMPORARY TABLE ";
private static final String MEDIA_GRANTS_AND_FILES_JOIN_TABLE_NAME = "media_grants LEFT JOIN "
+ "files ON media_grants.file_id = files._id";
private static final String WHERE_MEDIA_GRANTS_PACKAGE_NAME_IN =
"media_grants." + MediaGrants.OWNER_PACKAGE_NAME_COLUMN + " IN ";
- private static final String WHERE_MEDIA_GRANTS_FILE_ID_IN = MediaGrants.FILE_ID_COLUMN + " IN ";
private static final String WHERE_MEDIA_GRANTS_USER_ID =
"media_grants." + MediaGrants.PACKAGE_USER_ID_COLUMN + " = ? ";
@@ -80,6 +81,12 @@
private static final String WHERE_VOLUME_NAME_IN =
"files." + MediaStore.Files.FileColumns.VOLUME_NAME + " IN ";
+ private static final String TEMP_TABLE_NAME_FOR_DELETION =
+ "temp_table_for_media_grants_deletion";
+
+ private static final String TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME =
+ "temp_table_for_media_grants_deletion.file_id";
+
private static final String ARG_VALUE_FOR_FALSE = "0";
private static final int VISUAL_MEDIA_TYPE_COUNT = 2;
@@ -175,24 +182,31 @@
});
}
- int removeMediaGrantsForPackage(String[] packages, List<Uri> uris, int packageUserId) {
+ int removeMediaGrantsForPackage(@NonNull String[] packages, @NonNull List<Uri> uris,
+ int packageUserId) {
Objects.requireNonNull(packages);
+ Objects.requireNonNull(uris);
if (packages.length == 0) {
throw new IllegalArgumentException(
"Removing grants requires a non empty package name.");
}
- final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
- queryBuilder.setDistinct(true);
- queryBuilder.setTables(MEDIA_GRANTS_TABLE);
- String[] selectionArgs = buildSelectionArg(queryBuilder, QueryFilterBuilder.newInstance()
- .setPackageNameSelection(packages)
- .setUserIdSelection(packageUserId)
- .setUriSelection(uris)
- .build());
-
return mExternalDatabase.runWithTransaction(
(db) -> {
+ // create a temporary table to be used as a selection criteria for local ids.
+ createTempTableWithLocalIdsAsColumn(uris, db);
+
+ // Create query builder and add selection args.
+ final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+ queryBuilder.setDistinct(true);
+ queryBuilder.setTables(MEDIA_GRANTS_TABLE);
+ String[] selectionArgs = buildSelectionArg(queryBuilder,
+ QueryFilterBuilder.newInstance()
+ .setPackageNameSelection(packages)
+ .setUserIdSelection(packageUserId)
+ .setUriSelection(uris)
+ .build());
+ // execute query.
int grantsRemoved = queryBuilder.delete(db, null, selectionArgs);
Log.d(
TAG,
@@ -201,10 +215,55 @@
grantsRemoved,
String.valueOf(packageUserId),
Arrays.toString(packages)));
+ // Drop the temporary table.
+ deleteTempTableCreatedForLocalIdSelection(db);
return grantsRemoved;
});
}
+ private static void createTempTableWithLocalIdsAsColumn(@NonNull List<Uri> uris,
+ @NonNull SQLiteDatabase db) {
+
+ // create a temporary table and insert the ids from received uris.
+ db.execSQL(String.format(CREATE_TEMPORARY_TABLE_QUERY + "%s (%s INTEGER)",
+ TEMP_TABLE_NAME_FOR_DELETION, FILE_ID_COLUMN));
+
+ final SQLiteQueryBuilder queryBuilderTempTable = new SQLiteQueryBuilder();
+ queryBuilderTempTable.setTables(TEMP_TABLE_NAME_FOR_DELETION);
+
+ List<List<Uri>> listOfSelectionArgsForId = splitArrayList(uris,
+ /* number of ids per query */ 50);
+
+ StringBuilder sb = new StringBuilder();
+ List<Uri> selectionArgForIdSelection;
+ for (int itr = 0; itr < listOfSelectionArgsForId.size(); itr++) {
+ selectionArgForIdSelection = listOfSelectionArgsForId.get(itr);
+ if (itr == 0 || selectionArgForIdSelection.size() != listOfSelectionArgsForId.get(
+ itr - 1).size()) {
+ sb.setLength(0);
+ for (int i = 0; i < selectionArgForIdSelection.size() - 1; i++) {
+ sb.append("(?)").append(",");
+ }
+ sb.append("(?)");
+ }
+ db.execSQL("INSERT INTO " + TEMP_TABLE_NAME_FOR_DELETION + " VALUES " + sb.toString(),
+ selectionArgForIdSelection.stream().map(
+ ContentUris::parseId).collect(Collectors.toList()).stream().toArray());
+ }
+ }
+
+ private static <T> List<List<T>> splitArrayList(List<T> list, int chunkSize) {
+ List<List<T>> subLists = new ArrayList<>();
+ for (int i = 0; i < list.size(); i += chunkSize) {
+ subLists.add(list.subList(i, Math.min(i + chunkSize, list.size())));
+ }
+ return subLists;
+ }
+
+ private static void deleteTempTableCreatedForLocalIdSelection(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE " + TEMP_TABLE_NAME_FOR_DELETION);
+ }
+
/**
* Removes any existing media grants for the given package from the external database. This will
* not alter the files or file metadata themselves.
@@ -298,8 +357,8 @@
return isPickerUri(uri)
&& PickerUriResolver.unwrapProviderUri(uri)
- .getHost()
- .equals(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+ .getHost()
+ .equals(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
}
/**
@@ -326,14 +385,14 @@
// Append Where clause for Uris
if (queryFilter.mUris != null && !queryFilter.mUris.isEmpty()) {
// Append the where clause for local id selection to the query builder.
- qb.appendWhereStandalone(
- WHERE_MEDIA_GRANTS_FILE_ID_IN + buildPlaceholderForWhereClause(
- queryFilter.mUris.size()));
-
- // Add local ids to the selection args.
- selectArgs.addAll(queryFilter.mUris.stream().map(
- (Uri uri) -> String.valueOf(ContentUris.parseId(uri))).collect(
- Collectors.toList()));
+ // this query would look like this example query:
+ // WHERE EXISTS (SELECT 1 from temp_table_for_media_grants_deletion WHERE
+ // temp_table_for_media_grants_deletion.file_id = media_grants.file_id)
+ qb.appendWhereStandalone(String.format("EXISTS (SELECT %s from %s WHERE %s = %s)",
+ TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME,
+ TEMP_TABLE_NAME_FOR_DELETION,
+ TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME,
+ MediaGrants.MEDIA_GRANTS_TABLE + "." + MediaGrants.FILE_ID_COLUMN));
}
// Append where clause for userID.
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 7954565..ce1cfa8 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -978,7 +978,7 @@
if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
insertedRow.isPending())) {
- mPickerDataLayer.handleMediaEventNotification();
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
}
mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow);
@@ -1017,7 +1017,7 @@
oldRow.isPending(), newRow.isPending(),
oldRow.isFavorite(), newRow.isFavorite(),
oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
- mPickerDataLayer.handleMediaEventNotification();
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
}
mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow);
@@ -1077,7 +1077,7 @@
if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
deletedRow.getMediaType())) {
- mPickerDataLayer.handleMediaEventNotification();
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
}
mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow);
@@ -7096,7 +7096,7 @@
private Bundle getResultForNotifyCloudMediaChangedEvent(String arg) {
final boolean notifyCloudEventResult;
if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) {
- mPickerDataLayer.handleMediaEventNotification();
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ false);
notifyCloudEventResult = true;
} else {
notifyCloudEventResult = false;
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 34836af..84dc593 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -219,6 +219,18 @@
}
/**
+ * Sets up public volume's database backup to external storage to recover during a rollback.
+ */
+ public void setupPublicVolumeDbBackup(String volumeName) throws IOException {
+ synchronized (mLock) {
+ if (mPtr == 0) {
+ throw new IOException("FUSE daemon unavailable");
+ }
+ native_setup_public_volume_db_backup(mPtr, volumeName);
+ }
+ }
+
+ /**
* Deletes entry for given key from external storage.
*/
public void deleteDbBackup(String key) throws IOException {
@@ -231,14 +243,14 @@
}
/**
- * Backs up given key-value pair in external storage.
+ * Backs up given key-value pair in external storage for provided volume.
*/
- public void backupVolumeDbData(String key, String value) throws IOException {
+ public void backupVolumeDbData(String volumeName, String key, String value) throws IOException {
synchronized (mLock) {
if (mPtr == 0) {
throw new IOException("FUSE daemon unavailable");
}
- native_backup_volume_db_data(mPtr, key, value);
+ native_backup_volume_db_data(mPtr, volumeName, key, value);
}
}
@@ -333,8 +345,10 @@
private native FdAccessResult native_check_fd_access(long daemon, int fd, int uid);
private native void native_initialize_device_id(long daemon, String path);
private native void native_setup_volume_db_backup(long daemon);
+ private native void native_setup_public_volume_db_backup(long daemon, String volumeName);
private native void native_delete_db_backup(long daemon, String key);
- private native void native_backup_volume_db_data(long daemon, String key, String value);
+ private native void native_backup_volume_db_data(long daemon, String volumeName, String key,
+ String value);
private native String[] native_read_backed_up_file_paths(long daemon, String volumeName,
String lastReadValue, int limit);
private native String native_read_backed_up_data(long daemon, String key);
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index 2b33403..48ee7fb 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -130,7 +130,6 @@
private int mToolBarIconColor;
private int mToolbarHeight = 0;
- private boolean mIsAccessibilityEnabled;
private boolean mShouldLogCancelledResult = true;
@Override
@@ -186,10 +185,6 @@
mTabLayout = findViewById(R.id.tab_layout);
- final AccessibilityManager am = getSystemService(AccessibilityManager.class);
- mIsAccessibilityEnabled = am.isEnabled();
- am.addAccessibilityStateChangeListener(enabled -> mIsAccessibilityEnabled = enabled);
-
initBottomSheetBehavior();
// Save the fragment container layout so that we can adjust the padding based on preview or
@@ -489,7 +484,7 @@
}
private void initStateForBottomSheet() {
- if (!mIsAccessibilityEnabled && !mSelection.canSelectMultiple()
+ if (!isAccessibilityEnabled() && !mSelection.canSelectMultiple()
&& !isOrientationLandscape()) {
final int peekHeight = getBottomSheetPeekHeight(this);
mBottomSheetBehavior.setPeekHeight(peekHeight);
@@ -500,6 +495,15 @@
}
}
+ /**
+ * Warning: This method is visible for espresso tests, we are not customizing anything here.
+ * Allowing ourselves to control the accessibility state helps us mock it for these tests.
+ */
+ @VisibleForTesting
+ protected boolean isAccessibilityEnabled() {
+ return getSystemService(AccessibilityManager.class).isEnabled();
+ }
+
private static int getBottomSheetPeekHeight(Context context) {
final WindowManager windowManager = context.getSystemService(WindowManager.class);
final Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds();
@@ -544,6 +548,7 @@
if (shouldPreloadSelectedItems()) {
final var uris = PickerResult.getPickerUrisForItems(
mSelection.getSelectedItems());
+ mPickerViewModel.logPreloadingStarted(uris.size());
mPreloaderInstanceHolder.preloader =
SelectedMediaPreloader.preload(/* activity */ this, uris);
deSelectUnavailableMedia(mPreloaderInstanceHolder.preloader);
@@ -572,10 +577,13 @@
final Bundle extras = getIntent().getExtras();
final int uid = extras.getInt(Intent.EXTRA_UID);
final List<Uri> uris = getPickerUrisForItems(mSelection.getSelectedItemsWithoutGrants());
- ForegroundThread.getExecutor().execute(() -> {
- // Handle grants in another thread to not block the UI.
- grantMediaReadForPackage(getApplicationContext(), uid, uris);
- });
+ if (!uris.isEmpty()) {
+ ForegroundThread.getExecutor().execute(() -> {
+ // Handle grants in another thread to not block the UI.
+ grantMediaReadForPackage(getApplicationContext(), uid, uris);
+ mPickerViewModel.logPickerChoiceAddedGrantsCount(uris.size(), extras);
+ });
+ }
// Revoke READ_GRANT for items that were pre-granted but now in the current session user has
// deselected them.
@@ -587,6 +595,8 @@
// Handle grants in another thread to not block the UI.
MediaStore.revokeMediaReadForPackages(getApplicationContext(), uid,
urisForItemsWhoseGrantsNeedsToBeRevoked);
+ mPickerViewModel.logPickerChoiceRevokedGrantsCount(
+ urisForItemsWhoseGrantsNeedsToBeRevoked.size(), extras);
});
}
}
@@ -632,6 +642,7 @@
/* lifecycleOwner */ PhotoPickerActivity.this,
isFinished -> {
if (isFinished) {
+ mPickerViewModel.logPreloadingFinished();
setResultAndFinishSelfInternal();
}
});
@@ -655,9 +666,11 @@
DialogUtils.showDialog(this,
getResources().getString(R.string.dialog_error_title),
getResources().getString(R.string.dialog_error_message));
+ mPickerViewModel.logPreloadingFailed(unavailableMediaIndexes.size());
} else {
unavailableMediaIndexes.remove(
unavailableMediaIndexes.size() - 1);
+ mPickerViewModel.logPreloadingCancelled(unavailableMediaIndexes.size());
}
List<Item> selectedItems = mSelection.getSelectedItems();
for (var mediaIndex : unavailableMediaIndexes) {
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
index 575f950..49b3d4b 100644
--- a/src/com/android/providers/media/photopicker/PickerDataLayer.java
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -93,7 +93,7 @@
// {@link PickerSyncManager} to ensure that any request type is not blocked on other request
// types. It is advisable to use unique work requests because in case the number of queued
// requests grows, they should not block other work requests.
- private static final int WORK_MANAGER_THREAD_POOL_SIZE = 5;
+ private static final int WORK_MANAGER_THREAD_POOL_SIZE = 6;
@Nullable
private static volatile Executor sWorkManagerExecutor;
@@ -547,10 +547,11 @@
/**
* Handles notification about media events like inserts/updates/deletes received from cloud or
* local providers.
+ * @param localOnly - whether the media event is coming from the local provider
*/
- public void handleMediaEventNotification() {
+ public void handleMediaEventNotification(Boolean localOnly) {
try {
- mSyncManager.syncAllMediaProactively();
+ mSyncManager.syncMediaProactively(localOnly);
} catch (RuntimeException e) {
// Catch any unchecked exceptions so that critical paths in MP that call this method are
// not affected by Picker related issues.
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index 67909d5..e03b332 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -121,6 +121,7 @@
private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1;
private static final int SYNC_TYPE_MEDIA_FULL = 2;
private static final int SYNC_TYPE_MEDIA_RESET = 3;
+ private static final int SYNC_TYPE_MEDIA_FULL_WITH_RESET = 4;
public static final int PAGE_SIZE = 1000;
@NonNull
private static final Handler sBgThreadHandler = BackgroundThread.getHandler();
@@ -129,10 +130,12 @@
SYNC_TYPE_MEDIA_INCREMENTAL,
SYNC_TYPE_MEDIA_FULL,
SYNC_TYPE_MEDIA_RESET,
+ SYNC_TYPE_MEDIA_FULL_WITH_RESET,
})
@Retention(RetentionPolicy.SOURCE)
private @interface SyncType {}
+ private static final long DEFAULT_GENERATION = -1;
private final Context mContext;
private final ConfigStore mConfigStore;
private final PickerDbFacade mDbFacade;
@@ -702,14 +705,22 @@
// Can only happen when |authority| has been set to null and we need to clean up
disablePickerCloudMediaQueries(isLocal);
return resetAllMedia(authority, isLocal);
- case SYNC_TYPE_MEDIA_FULL:
- NonUiEventLogger.logPickerFullSyncStart(instanceId, MY_UID, authority);
+ case SYNC_TYPE_MEDIA_FULL_WITH_RESET:
disablePickerCloudMediaQueries(isLocal);
if (!resetAllMedia(authority, isLocal)) {
return false;
}
enablePickerCloudMediaQueries(authority, isLocal);
+ // Cache collection id with default generation id to prevent DB reset if full
+ // sync resumes the next time sync is triggered.
+ cacheMediaCollectionInfo(
+ authority, isLocal,
+ getDefaultGenerationCollectionInfo(params.latestMediaCollectionInfo));
+ // Fall through to run full sync
+ case SYNC_TYPE_MEDIA_FULL:
+ NonUiEventLogger.logPickerFullSyncStart(instanceId, MY_UID, authority);
+
final Bundle fullSyncQueryArgs = new Bundle();
if (enablePagedSync) {
fullSyncQueryArgs.putInt(EXTRA_PAGE_SIZE, params.mPageSize);
@@ -1169,7 +1180,7 @@
final String collectionId = mSyncPrefs.getString(
getPrefsKey(isLocal, MEDIA_COLLECTION_ID), /* default */ null);
final long generation = mSyncPrefs.getLong(
- getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), /* default */ -1);
+ getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), DEFAULT_GENERATION);
bundle.putString(MEDIA_COLLECTION_ID, collectionId);
bundle.putLong(LAST_MEDIA_SYNC_GENERATION, generation);
@@ -1191,6 +1202,14 @@
}
}
+ private Bundle getDefaultGenerationCollectionInfo(@NonNull Bundle latestCollectionInfo) {
+ final Bundle bundle = new Bundle();
+ final String collectionId = latestCollectionInfo.getString(MEDIA_COLLECTION_ID);
+ bundle.putString(MEDIA_COLLECTION_ID, collectionId);
+ bundle.putLong(LAST_MEDIA_SYNC_GENERATION, DEFAULT_GENERATION);
+ return bundle;
+ }
+
@NonNull
private SyncRequestParams getSyncRequestParams(@Nullable String authority,
boolean isLocal) throws RequestObsoleteException, UnableToAcquireLockException {
@@ -1246,6 +1265,8 @@
}
if (!Objects.equals(latestCollectionId, cachedCollectionId)) {
+ result = SyncRequestParams.forFullMediaWithReset(latestMediaCollectionInfo);
+ } else if (cachedGeneration == DEFAULT_GENERATION) {
result = SyncRequestParams.forFullMedia(latestMediaCollectionInfo);
} else if (cachedGeneration == latestGeneration) {
result = SyncRequestParams.forNone();
@@ -1674,7 +1695,12 @@
return SYNC_REQUEST_MEDIA_RESET;
}
- static SyncRequestParams forFullMedia(Bundle latestMediaCollectionInfo) {
+ static SyncRequestParams forFullMediaWithReset(@NonNull Bundle latestMediaCollectionInfo) {
+ return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL_WITH_RESET, /* generation */ 0,
+ latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE);
+ }
+
+ static SyncRequestParams forFullMedia(@NonNull Bundle latestMediaCollectionInfo) {
return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL, /* generation */ 0,
latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE);
}
@@ -1702,6 +1728,8 @@
return "MEDIA_FULL";
case SYNC_TYPE_MEDIA_RESET:
return "MEDIA_RESET";
+ case SYNC_TYPE_MEDIA_FULL_WITH_RESET:
+ return "MEDIA_FULL_WITH_RESET";
default:
return "Unknown";
}
diff --git a/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java b/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
index 8e3273a..deefc1b 100644
--- a/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
+++ b/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
@@ -31,6 +31,7 @@
import android.net.Uri;
import android.os.Looper;
import android.util.Log;
+import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -303,11 +304,17 @@
dialog.setCancelable(false);
dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
- // TODO(b/303013634): Add a Cancel string for this dialog and don't re-use an
- // existing string.
- context.getString(R.string.transcode_cancel), (dialog1, which) -> {
+ context.getString(R.string.preloading_cancel_button), (dialog1, which) -> {
mIsPreloadingCancelledLiveData.setValue(true);
});
+ dialog.create();
+
+ Button cancelButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (cancelButton != null) {
+ cancelButton.setTextAppearance(R.style.ProgressDialogCancelButtonStyle);
+ cancelButton.setAllCaps(false);
+ }
+
dialog.show();
return dialog;
@@ -355,4 +362,4 @@
}
return String.format("%.1f s", ms / 1000.0);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index cd51b9b..3fcdad9 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -33,6 +33,7 @@
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.database.MergeCursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
@@ -884,9 +885,13 @@
* {@code limit}. They can also be filtered with {@code query}.
*/
public Cursor queryMediaForUi(QueryFilter query) {
+ if (query.mIsLocalOnly && query.mLocalIdSelection != null
+ && !query.mLocalIdSelection.isEmpty()) {
+ return queryMediaForUiWithLocalIdSelection(query);
+ }
+
final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
final String[] selectionArgs = buildSelectionArgs(qb, query);
-
if (query.mIsLocalOnly) {
return queryMediaForUi(qb, selectionArgs, query.mLimit, /* isLocalOnly*/true,
TABLE_MEDIA, /* cloudProvider*/ null);
@@ -901,6 +906,36 @@
TABLE_MEDIA, cloudProvider);
}
+
+ private Cursor queryMediaForUiWithLocalIdSelection(QueryFilter query) {
+ // Since 'WHERE IN' clause has an upper limit of items that can be included in the sql
+ // statement and also there is an upper limit to the size of the sql statement.
+ // Splitting the query into multiple smaller ones.
+ // This query will now process 150 items in a batch.
+ List<List<Integer>> listOfSelectionArgsForLocalId = splitArrayList(
+ query.mLocalIdSelection,
+ /* number of ids per query */ 150);
+ List<Cursor> resultCursor = new ArrayList<>();
+
+ for (List<Integer> selectionArgForLocalIdSelection : listOfSelectionArgsForLocalId) {
+ final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
+ query.mLocalIdSelection = selectionArgForLocalIdSelection;
+ final String[] selectionArgs = buildSelectionArgs(qb, query);
+ resultCursor.add(queryMediaForUi(qb, selectionArgs, query.mLimit, true,
+ TABLE_MEDIA, /* cloud provider */null));
+ }
+
+ return new MergeCursor(resultCursor.toArray(new Cursor[resultCursor.size()]));
+ }
+
+ private static <T> List<List<T>> splitArrayList(List<T> list, int chunkSize) {
+ List<List<T>> subLists = new ArrayList<>();
+ for (int i = 0; i < list.size(); i += chunkSize) {
+ subLists.add(list.subList(i, Math.min(i + chunkSize, list.size())));
+ }
+ return subLists;
+ }
+
/**
* Returns sorted cloud or local media items from the picker db for a given album (either cloud
* or local).
diff --git a/src/com/android/providers/media/photopicker/data/Selection.java b/src/com/android/providers/media/photopicker/data/Selection.java
index 90bca93..d7667d3 100644
--- a/src/com/android/providers/media/photopicker/data/Selection.java
+++ b/src/com/android/providers/media/photopicker/data/Selection.java
@@ -33,6 +33,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -53,14 +54,15 @@
private final Map<Item, Integer> mCheckedItemIndexes = new HashMap<>();
// The list of selected items.
- private Map<Uri, Item> mSelectedItems = new HashMap<>();
-
+ private Map<Uri, Item> mSelectedItems = new LinkedHashMap<>();
+ private Map<Uri, MutableLiveData<Integer>> mSelectedItemsOrder = new HashMap<>();
private Map<String, Item> mItemGrantRevocationMap = new HashMap<>();
private MutableLiveData<Integer> mSelectedItemSize = new MutableLiveData<>();
// The list of selected items for preview. This needs to be saved separately so that if activity
// gets killed, we will still have deselected items for preview.
private List<Item> mSelectedItemsForPreview = new ArrayList<>();
+ private boolean mIsSelectionOrdered = false;
private boolean mSelectMultiple = false;
private int mMaxSelectionLimit = 1;
// This is set to false when max selection limit is reached.
@@ -99,7 +101,8 @@
* @return {@link #mSelectedItems} - A {@link List} of selected {@link Item}
*/
public List<Item> getSelectedItems() {
- return Collections.unmodifiableList(new ArrayList<>(mSelectedItems.values()));
+ ArrayList<Item> result = new ArrayList<>(mSelectedItems.values());
+ return Collections.unmodifiableList(result);
}
/**
@@ -158,6 +161,13 @@
return mSelectedItemSize;
}
+ /**
+ * @return {@link LiveData} of the item selection order.
+ */
+ public LiveData<Integer> getSelectedItemOrder(Item item) {
+ return mSelectedItemsOrder.get(item.getContentUri());
+ }
+
private int getTotalItemsCount() {
return mSelectedItems.size() - countOfPreGrantedItems() + mTotalNumberOfPreGrantedItems
- mItemGrantRevocationMap.size();
@@ -170,6 +180,10 @@
if (item.isPreGranted() && mItemGrantRevocationMap.containsKey(item.getId())) {
mItemGrantRevocationMap.remove(item.getId());
}
+ if (mIsSelectionOrdered) {
+ mSelectedItemsOrder.put(
+ item.getContentUri(), new MutableLiveData(getTotalItemsCount() + 1));
+ }
mSelectedItems.put(item.getContentUri(), item);
mSelectedItemSize.postValue(getTotalItemsCount());
updateSelectionAllowed();
@@ -186,8 +200,13 @@
* Clears {@link #mSelectedItems} and sets the selected item as given {@code item}
*/
public void setSelectedItem(Item item) {
+ mSelectedItemsOrder.clear();
mSelectedItems.clear();
mSelectedItems.put(item.getContentUri(), item);
+ if (mIsSelectionOrdered) {
+ mSelectedItemsOrder.put(
+ item.getContentUri(), new MutableLiveData(getTotalItemsCount()));
+ }
mSelectedItemSize.postValue(getTotalItemsCount());
updateSelectionAllowed();
}
@@ -204,6 +223,16 @@
// items.
mItemGrantRevocationMap.put(item.getId(), item);
}
+ if (mIsSelectionOrdered) {
+ MutableLiveData<Integer> removedItem = mSelectedItemsOrder.remove(item.getContentUri());
+ int removedItemOrder = removedItem.getValue().intValue();
+ mSelectedItemsOrder.values().stream()
+ .filter(order -> order.getValue().intValue() > removedItemOrder)
+ .forEach(
+ order -> {
+ order.setValue(order.getValue().intValue() - 1);
+ });
+ }
mSelectedItems.remove(item.getContentUri());
mSelectedItemSize.postValue(getTotalItemsCount());
updateSelectionAllowed();
@@ -222,6 +251,7 @@
* Clear all selected items and checked positions
*/
public void clearSelectedItems() {
+ mSelectedItemsOrder.clear();
mSelectedItems.clear();
mCheckedItemIndexes.clear();
mSelectedItemSize.postValue(getTotalItemsCount());
@@ -303,11 +333,15 @@
final Bundle extras = intent.getExtras();
final boolean isExtraPickImagesMaxSet =
extras != null && extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_MAX);
+ final boolean isExtraOrderedSelectionSet =
+ extras != null && extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER);
if (intent.getAction() != null
&& intent.getAction().equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) {
// If this is picking media for an app, enable multiselect.
mSelectMultiple = true;
+ // disable ordered selection.
+ mIsSelectionOrdered = false;
// Allow selections up to the limit.
// TODO(b/255301849): Update max limit after discussing with product team.
mMaxSelectionLimit = MediaStore.getPickImagesMaxLimit();
@@ -321,6 +355,11 @@
"EXTRA_PICK_IMAGES_MAX is not supported for " + "ACTION_GET_CONTENT");
}
+ if (isExtraOrderedSelectionSet) {
+ throw new IllegalArgumentException(
+ "EXTRA_PICK_IMAGES_IN_ORDER is not supported for ACTION_GET_CONTENT");
+ }
+
mSelectMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
if (mSelectMultiple) {
mMaxSelectionLimit = MediaStore.getPickImagesMaxLimit();
@@ -329,6 +368,10 @@
return;
}
+ if (isExtraOrderedSelectionSet) {
+ mIsSelectionOrdered = extras.getBoolean(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER);
+ }
+
// Check EXTRA_PICK_IMAGES_MAX value only if the flag is set.
if (isExtraPickImagesMaxSet) {
final int extraMax =
@@ -342,6 +385,7 @@
mSelectMultiple = true;
mMaxSelectionLimit = extraMax;
}
+
}
/**
@@ -351,6 +395,11 @@
return mSelectMultiple;
}
+ /** Return whether ordered selection is enabled or not. */
+ public boolean isSelectionOrdered() {
+ return mIsSelectionOrdered;
+ }
+
/**
* Return maximum limit of items that can be selected
*/
diff --git a/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java b/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java
index 54a53e2..b15d2ed 100644
--- a/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java
+++ b/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java
@@ -50,7 +50,13 @@
@UiEvent(doc = "Ended get media collection info in photo picker")
PHOTO_PICKER_GET_MEDIA_COLLECTION_INFO_END(1450),
@UiEvent(doc = "Ended get albums in photo picker")
- PHOTO_PICKER_GET_ALBUMS_END(1451);
+ PHOTO_PICKER_GET_ALBUMS_END(1451),
+ @UiEvent(doc = "Read grants added count.")
+ PHOTO_PICKER_GRANTS_ADDED_COUNT(1528),
+ @UiEvent(doc = "Read grants revoked count.")
+ PHOTO_PICKER_GRANTS_REVOKED_COUNT(1529),
+ @UiEvent(doc = "Total initial grants count.")
+ PHOTO_PICKER_INIT_GRANTS_COUNT(1530);
private final int mId;
@@ -221,4 +227,44 @@
LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_GET_ALBUMS_END, uid, authority,
instanceId, count);
}
+
+ /**
+ * Log metrics for count of grants added for a package.
+ * @param instanceId an identifier for the current session
+ * @param uid the uid of the MediaProvider logging this metric
+ * @param packageName the package name receiving the grant.
+ * @param count the number of items for which the grants have been added.
+ */
+ public static void logPickerChoiceGrantsAdditionCount(InstanceId instanceId, int uid,
+ String packageName, int count) {
+ LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_GRANTS_ADDED_COUNT, uid,
+ packageName, instanceId, count);
+ }
+
+ /**
+ * Log metrics for count of grants revoked for a package.
+ * @param instanceId an identifier for the current session
+ * @param uid the uid of the MediaProvider logging this metric
+ * @param packageName the package name for which the grants are being revoked.
+ * @param count the number of items for which the grants have been revoked.
+ */
+ public static void logPickerChoiceGrantsRemovedCount(InstanceId instanceId, int uid,
+ String packageName, int count) {
+ LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_GRANTS_REVOKED_COUNT, uid,
+ packageName, instanceId, count);
+ }
+
+ /**
+ * Log metrics for total count of grants previously added for the package.
+ * @param instanceId an identifier for the current session
+ * @param uid the uid of the MediaProvider logging this metric
+ * @param packageName the package name for which the grants are being initialized.
+ * @param count the number of items for which the grants have been initialized.
+ */
+ public static void logPickerChoiceInitGrantsCount(InstanceId instanceId, int uid,
+ String packageName, int count) {
+ LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_INIT_GRANTS_COUNT, uid,
+ packageName, instanceId, count);
+ }
+
}
diff --git a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
index 2f1cca4..e127e05 100644
--- a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
+++ b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
@@ -113,7 +113,23 @@
@UiEvent(doc = "Triggered create surface controller in photo picker")
PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_START(1452),
@UiEvent(doc = "Ended create surface controller in photo picker")
- PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_END(1453);
+ PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_END(1453),
+ @UiEvent(doc = "Started the selected media preloading in photo picker")
+ PHOTO_PICKER_PRELOADING_STARTED(1524),
+ @UiEvent(doc = "Finished the selected media preloading in photo picker")
+ PHOTO_PICKER_PRELOADING_FINISHED(1525),
+ @UiEvent(doc = "User cancelled the selected media preloading in photo picker")
+ PHOTO_PICKER_PRELOADING_CANCELLED(1526),
+ @UiEvent(doc = "Failed to preload some selected media items in photo picker")
+ PHOTO_PICKER_PRELOADING_FAILED(1527),
+ @UiEvent(doc = "The banner is added to display in the recycler view grids in photo picker")
+ PHOTO_PICKER_BANNER_ADDED(1539),
+ @UiEvent(doc = "The user clicks the dismiss button of the banner in photo picker")
+ PHOTO_PICKER_BANNER_DISMISSED(1540),
+ @UiEvent(doc = "The user clicks the action button of the banner in photo picker")
+ PHOTO_PICKER_BANNER_ACTION_BUTTON_CLICKED(1541),
+ @UiEvent(doc = "The user clicks on the remaining part of the banner in photo picker")
+ PHOTO_PICKER_BANNER_CLICKED(1542);
private final int mId;
@@ -365,8 +381,8 @@
* @param selectedItemCount the number of items selected for preview all
*/
public void logPreviewAllSelected(InstanceId instanceId, int selectedItemCount) {
- logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ALL_SELECTED,
- /* uid */ 0, /* packageName */ null, instanceId, selectedItemCount);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ALL_SELECTED, instanceId,
+ selectedItemCount);
}
/**
@@ -407,8 +423,8 @@
* @param backStackEntryCount the number of fragment entries currently in the back stack
*/
public void logBackGestureWithStackCount(InstanceId instanceId, int backStackEntryCount) {
- logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_BACK_GESTURE, /* uid */ 0,
- /* packageName */ null, instanceId, backStackEntryCount);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_BACK_GESTURE, instanceId,
+ backStackEntryCount);
}
/**
@@ -417,9 +433,8 @@
* @param backStackEntryCount the number of fragment entries currently in the back stack
*/
public void logActionBarHomeButtonClick(InstanceId instanceId, int backStackEntryCount) {
- logger.logWithInstanceIdAndPosition(
- PhotoPickerEvent.PHOTO_PICKER_ACTION_BAR_HOME_BUTTON_CLICK, /* uid */ 0,
- /* packageName */ null, instanceId, backStackEntryCount);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_ACTION_BAR_HOME_BUTTON_CLICK,
+ instanceId, backStackEntryCount);
}
/**
@@ -500,8 +515,8 @@
* @param position the position of the album in the recycler view
*/
public void logCloudAlbumOpened(InstanceId instanceId, int position) {
- logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_ALBUM_FROM_CLOUD_OPEN,
- /* uid */ 0, /* packageName */ null, instanceId, position);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_ALBUM_FROM_CLOUD_OPEN, instanceId,
+ position);
}
/**
@@ -510,8 +525,8 @@
* @param position the position of the album in the recycler view
*/
public void logSelectedMainGridItem(InstanceId instanceId, int position) {
- logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_MAIN_GRID,
- /* uid */ 0, /* packageName */ null, instanceId, position);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_MAIN_GRID,
+ instanceId, position);
}
/**
@@ -520,8 +535,8 @@
* @param position the position of the album in the recycler view
*/
public void logSelectedAlbumItem(InstanceId instanceId, int position) {
- logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_ALBUM,
- /* uid */ 0, /* packageName */ null, instanceId, position);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_ALBUM, instanceId,
+ position);
}
/**
@@ -530,8 +545,8 @@
* @param position the position of the album in the recycler view
*/
public void logSelectedCloudOnlyItem(InstanceId instanceId, int position) {
- logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_CLOUD_ONLY,
- /* uid */ 0, /* packageName */ null, instanceId, position);
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_CLOUD_ONLY,
+ instanceId, position);
}
/**
@@ -601,7 +616,86 @@
/* uid */ 0, authority, instanceId);
}
+ /**
+ * Log metrics to notify that the picker has started preloading the selected media items
+ * @param instanceId an identifier for the current picker session
+ * @param count the number of items to be preloaded
+ */
+ public void logPreloadingStarted(@NonNull InstanceId instanceId, int count) {
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_STARTED, instanceId,
+ count);
+ }
+
+ /**
+ * Log metrics to notify that the picker has finished preloading the selected media items
+ * @param instanceId an identifier for the current picker session
+ */
+ public void logPreloadingFinished(@NonNull InstanceId instanceId) {
+ logWithInstance(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_FINISHED, instanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user cancelled the selected media preloading
+ * @param instanceId an identifier for the current picker session
+ * @param count the number of items pending to preload
+ */
+ public void logPreloadingCancelled(@NonNull InstanceId instanceId, int count) {
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_CANCELLED, instanceId,
+ count);
+ }
+
+ /**
+ * Log metrics to notify that the selected media preloading failed for some items
+ * @param instanceId an identifier for the current picker session
+ * @param count the number of items pending / failed to preload
+ */
+ public void logPreloadingFailed(@NonNull InstanceId instanceId, int count) {
+ logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_FAILED, instanceId,
+ count);
+ }
+
+ /**
+ * Log metrics to notify that the banner is added to display in the recycler view grids
+ * @param instanceId an identifier for the current picker session
+ * @param bannerName the name of the banner added,
+ * refer {@link com.android.providers.media.photopicker.ui.TabAdapter.Banner}
+ */
+ public void logBannerAdded(@NonNull InstanceId instanceId, @NonNull String bannerName) {
+ logger.logWithInstanceId(PhotoPickerEvent.PHOTO_PICKER_BANNER_ADDED, /* uid= */ 0,
+ bannerName, instanceId);
+ }
+
+ /**
+ * Log metrics to notify that the banner is dismissed by the user
+ * @param instanceId an identifier for the current picker session
+ */
+ public void logBannerDismissed(@NonNull InstanceId instanceId) {
+ logWithInstance(PhotoPickerEvent.PHOTO_PICKER_BANNER_DISMISSED, instanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user clicked the banner action button
+ * @param instanceId an identifier for the current picker session
+ */
+ public void logBannerActionButtonClicked(@NonNull InstanceId instanceId) {
+ logWithInstance(PhotoPickerEvent.PHOTO_PICKER_BANNER_ACTION_BUTTON_CLICKED, instanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user clicked on the remaining part of the banner
+ * @param instanceId an identifier for the current picker session
+ */
+ public void logBannerClicked(@NonNull InstanceId instanceId) {
+ logWithInstance(PhotoPickerEvent.PHOTO_PICKER_BANNER_CLICKED, instanceId);
+ }
+
private void logWithInstance(@NonNull UiEventLogger.UiEventEnum event, InstanceId instance) {
logger.logWithInstanceId(event, /* uid */ 0, /* packageName */ null, instance);
}
+
+ private void logWithInstanceAndPosition(@NonNull UiEventLogger.UiEventEnum event,
+ @NonNull InstanceId instance, int position) {
+ logger.logWithInstanceIdAndPosition(event, /* uid= */ 0, /* packageName= */ null, instance,
+ position);
+ }
}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
index ab94d20..b086967 100644
--- a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
@@ -93,6 +93,7 @@
private static final int RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL = 12; // Time unit is hours.
private static final String PERIODIC_SYNC_WORK_NAME;
+ private static final String PROACTIVE_LOCAL_SYNC_WORK_NAME;
private static final String PROACTIVE_SYNC_WORK_NAME;
public static final String IMMEDIATE_LOCAL_SYNC_WORK_NAME;
private static final String IMMEDIATE_CLOUD_SYNC_WORK_NAME;
@@ -109,6 +110,7 @@
PERIODIC_ALBUM_RESET_WORK_NAME = "RESET_ALBUM_MEDIA_PERIODIC";
PERIODIC_SYNC_WORK_NAME = syncPeriodicPrefix + syncAllSuffix;
+ PROACTIVE_LOCAL_SYNC_WORK_NAME = syncProactivePrefix + syncLocalSuffix;
PROACTIVE_SYNC_WORK_NAME = syncProactivePrefix + syncAllSuffix;
IMMEDIATE_LOCAL_SYNC_WORK_NAME = syncImmediatePrefix + syncLocalSuffix;
IMMEDIATE_CLOUD_SYNC_WORK_NAME = syncImmediatePrefix + syncCloudSuffix;
@@ -231,16 +233,23 @@
/**
* Use this method for proactive syncs. The sync might take a while to start. Some device state
* conditions may apply before the sync can start like battery level etc.
+ *
+ * @param localOnly - whether the proactive sync should only sync with the local provider.
*/
- public void syncAllMediaProactively() {
- final Data inputData =
- new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
+ public void syncMediaProactively(Boolean localOnly) {
+
+ final int syncSource = localOnly ? SYNC_LOCAL_ONLY : SYNC_LOCAL_AND_CLOUD;
+ final String workName =
+ localOnly ? PROACTIVE_LOCAL_SYNC_WORK_NAME : PROACTIVE_SYNC_WORK_NAME;
+
+ final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
final OneTimeWorkRequest syncRequest = getOneTimeProactiveSyncRequest(inputData);
- // Don't wait for the sync operation to enqueue so that Picker sync enqueue requests in
+ // Don't wait for the sync operation to enqueue so that Picker sync enqueue
+ // requests in
// order to avoid adding latency to critical MP code paths.
- mWorkManager.enqueueUniqueWork(PROACTIVE_SYNC_WORK_NAME, ExistingWorkPolicy.KEEP,
- syncRequest);
+
+ mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, syncRequest);
}
/**
@@ -351,41 +360,40 @@
return new PeriodicWorkRequest.Builder(
ProactiveSyncWorker.class, SYNC_MEDIA_PERIODIC_WORK_INTERVAL, TimeUnit.HOURS)
.setInputData(inputData)
- .setConstraints(getProactiveSyncConstraints())
+ .setConstraints(getRequiresChargingAndIdleConstraints())
.build();
}
@NotNull
private PeriodicWorkRequest getPeriodicAlbumResetRequest(@NotNull Data inputData) {
- Constraints constraints =
- new Constraints.Builder()
- .setRequiresBatteryNotLow(true)
- .setRequiresDeviceIdle(true)
- .build();
-
return new PeriodicWorkRequest.Builder(
MediaResetWorker.class,
RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL,
TimeUnit.HOURS)
.setInputData(inputData)
- .setConstraints(constraints)
+ .setConstraints(getRequiresChargingAndIdleConstraints())
.addTag(SYNC_WORKER_TAG_IS_PERIODIC)
.build();
}
@NotNull
private OneTimeWorkRequest getOneTimeProactiveSyncRequest(@NotNull Data inputData) {
+ Constraints constraints = new Constraints.Builder()
+ .setRequiresBatteryNotLow(true)
+ .build();
+
return new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
.setInputData(inputData)
- .setConstraints(getProactiveSyncConstraints())
+ .setConstraints(constraints)
.build();
}
@NotNull
- private static Constraints getProactiveSyncConstraints() {
+ private static Constraints getRequiresChargingAndIdleConstraints() {
return new Constraints.Builder()
- .setRequiresBatteryNotLow(true)
+ .setRequiresCharging(true)
+ .setRequiresDeviceIdle(true)
.build();
}
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index 6c44f4d..12433fc 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -20,12 +20,9 @@
import android.content.Context;
import android.graphics.Bitmap;
-import android.graphics.ImageDecoder;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
-import android.util.Log;
import android.view.View;
import android.widget.ImageView;
@@ -59,6 +56,7 @@
RequestOptions.option(THUMBNAIL_REQUEST, /* enableThumbnail */ true);
private final Context mContext;
private final PreferredColorSpace mPreferredColorSpace;
+ private static final String PREVIEW_PREFIX = "preview_";
public ImageLoader(Context context) {
mContext = context;
@@ -122,13 +120,17 @@
loadWithGlide(
getGifRequestBuilder(loadable),
/* requestOptions */ null,
- getGlideSignature(loadable, /* prefix= */ null),
+ getGlideSignature(loadable, /* prefix= */ PREVIEW_PREFIX),
imageView);
return;
}
if (item.isAnimatedWebp()) {
- loadAnimatedWebpPreview(loadable, imageView);
+ loadWithGlide(
+ getDrawableRequestBuilder(loadable),
+ /* requestOptions */ null,
+ getGlideSignature(loadable, PREVIEW_PREFIX),
+ imageView);
return;
}
@@ -136,30 +138,7 @@
loadWithGlide(
getBitmapRequestBuilder(loadable),
/* requestOptions */ null,
- getGlideSignature(loadable, /* prefix= */ null),
- imageView);
- }
-
- 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);
- } catch (Exception e) {
- 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 ? loadable : drawable),
- /* requestOptions */ null,
- getGlideSignature(loadable, null),
+ getGlideSignature(loadable, /* prefix= */ PREVIEW_PREFIX),
imageView);
}
@@ -171,7 +150,7 @@
loadWithGlide(
getBitmapRequestBuilder(loadable),
new RequestOptions().frame(1000),
- getGlideSignature(loadable, "Preview"),
+ getGlideSignature(loadable, /* prefix= */ PREVIEW_PREFIX),
imageView);
}
diff --git a/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java b/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
index c6aa300..fef4ad3 100644
--- a/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
@@ -25,6 +25,8 @@
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.RecyclerView;
import com.android.providers.media.R;
@@ -35,6 +37,7 @@
* a video).
*/
class MediaItemGridViewHolder extends RecyclerView.ViewHolder {
+ private final LifecycleOwner mLifecycleOwner;
private final ImageLoader mImageLoader;
private final ImageView mIconThumb;
private final ImageView mIconGif;
@@ -43,14 +46,24 @@
private final TextView mVideoDuration;
private final View mOverlayGradient;
private final boolean mCanSelectMultiple;
+ private final boolean mShowOrderedSelectionLabel;
+ private final TextView mSelectedOrderText;
+ private LiveData<Integer> mSelectionOrder;
+ private final ImageView mCheckIcon;
private final View.OnHoverListener mOnMediaItemHoverListener;
private final PhotosTabAdapter.OnMediaItemClickListener mOnMediaItemClickListener;
- MediaItemGridViewHolder(@NonNull View itemView, @NonNull ImageLoader imageLoader,
+ MediaItemGridViewHolder(
+ @NonNull LifecycleOwner lifecycleOwner,
+ @NonNull View itemView,
+ @NonNull ImageLoader imageLoader,
@NonNull PhotosTabAdapter.OnMediaItemClickListener onMediaItemClickListener,
- View.OnHoverListener onMediaItemHoverListener, boolean canSelectMultiple) {
+ View.OnHoverListener onMediaItemHoverListener,
+ boolean canSelectMultiple,
+ boolean isOrderedSelection) {
super(itemView);
+ mLifecycleOwner = lifecycleOwner;
mIconThumb = itemView.findViewById(R.id.icon_thumbnail);
mIconGif = itemView.findViewById(R.id.icon_gif);
mIconMotionPhoto = itemView.findViewById(R.id.icon_motion_photo);
@@ -60,14 +73,19 @@
mImageLoader = imageLoader;
mOnMediaItemClickListener = onMediaItemClickListener;
mCanSelectMultiple = canSelectMultiple;
+ mShowOrderedSelectionLabel = isOrderedSelection;
mOnMediaItemHoverListener = onMediaItemHoverListener;
-
- itemView.findViewById(R.id.icon_check).setVisibility(mCanSelectMultiple ? VISIBLE : GONE);
+ mSelectedOrderText = itemView.findViewById(R.id.selected_order);
+ mCheckIcon = itemView.findViewById(R.id.icon_check);
+ mCheckIcon.setVisibility(
+ (mCanSelectMultiple && !mShowOrderedSelectionLabel) ? VISIBLE : GONE);
+ mSelectedOrderText.setVisibility(
+ (mCanSelectMultiple && mShowOrderedSelectionLabel) ? VISIBLE : GONE);
}
public void bind(@NonNull Item item, boolean isSelected) {
int position = getAbsoluteAdapterPosition();
- itemView.setOnClickListener(v -> mOnMediaItemClickListener.onItemClick(v, position));
+ itemView.setOnClickListener(v -> mOnMediaItemClickListener.onItemClick(v, position, this));
itemView.setOnLongClickListener(v ->
mOnMediaItemClickListener.onItemLongClick(v, position));
itemView.setOnHoverListener(mOnMediaItemHoverListener);
@@ -95,6 +113,7 @@
if (mCanSelectMultiple) {
itemView.setSelected(isSelected);
+ mSelectedOrderText.setText("");
// There is an issue b/223695510 about not selected in Accessibility mode. It only
// says selected state, but it doesn't say not selected state. Add the not selected
// only to avoid that it says selected twice.
@@ -103,6 +122,24 @@
}
}
+ /** Sets the LiveData selection order for the current grid item view. */
+ public void setSelectionOrder(LiveData<Integer> selectionOrder) {
+ if (selectionOrder == null) {
+ mSelectedOrderText.setText("");
+ if (mSelectionOrder != null) {
+ mSelectionOrder.removeObservers(mLifecycleOwner);
+ }
+ } else {
+ mSelectedOrderText.setText(selectionOrder.getValue().toString());
+ selectionOrder.observe(
+ mLifecycleOwner,
+ val -> {
+ mSelectedOrderText.setText(val.toString());
+ });
+ }
+ mSelectionOrder = selectionOrder;
+ }
+
@NonNull
private Context getContext() {
return itemView.getContext();
@@ -122,4 +159,12 @@
|| item.isVideo()
|| item.isMotionPhoto();
}
+
+ /** Release any non-reusable resources, as the view is being recycled. */
+ public void release() {
+ if (mSelectionOrder != null) {
+ mSelectionOrder.removeObservers(mLifecycleOwner);
+ mSelectionOrder = null;
+ }
+ }
}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
index 38d2fc9..1a4f4e7 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
@@ -45,7 +45,7 @@
public class PhotosTabAdapter extends TabAdapter {
private static final int RECENT_MINIMUM_COUNT = 12;
-
+ private final LifecycleOwner mLifecycleOwner;
private final boolean mShowRecentSection;
private final OnMediaItemClickListener mOnMediaItemClickListener;
private final Selection mSelection;
@@ -75,6 +75,7 @@
shouldShowAccountUpdatedBanner, shouldShowChooseAccountBanner,
onChooseAppBannerEventListener, onCloudMediaAvailableBannerEventListener,
onAccountUpdatedBannerEventListener, onChooseAccountBannerEventListener);
+ mLifecycleOwner = lifecycleOwner;
mShowRecentSection = showRecentSection;
mSelection = selection;
mOnMediaItemClickListener = onMediaItemClickListener;
@@ -95,11 +96,13 @@
final View view = getView(viewGroup, R.layout.item_photo_grid);
final MediaItemGridViewHolder viewHolder =
new MediaItemGridViewHolder(
+ mLifecycleOwner,
view,
mImageLoader,
mOnMediaItemClickListener,
mOnMediaItemHoverListener,
- mSelection.canSelectMultiple());
+ mSelection.canSelectMultiple(),
+ mSelection.isSelectionOrdered());
mPreloadSizeProvider.setView(viewHolder.getThumbnailImageView());
return viewHolder;
}
@@ -119,12 +122,15 @@
final boolean isSelected = mSelection.canSelectMultiple()
&& mSelection.isItemSelected(item);
+
if (isSelected) {
mSelection.addCheckedItemIndex(item, position);
}
mediaItemVH.bind(item, isSelected);
-
+ if (isSelected && mSelection.isSelectionOrdered()) {
+ mediaItemVH.setSelectionOrder(mSelection.getSelectedItemOrder(item));
+ }
// We also need to set Item as a tag so that OnClick/OnLongClickListeners can then
// retrieve it.
mediaItemVH.itemView.setTag(item);
@@ -207,7 +213,7 @@
}
interface OnMediaItemClickListener {
- void onItemClick(@NonNull View view, int position);
+ void onItemClick(@NonNull View view, int position, MediaItemGridViewHolder viewHolder);
boolean onItemLongClick(@NonNull View view, int position);
}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index f329a44..0917b55 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -153,8 +153,8 @@
showRecentSection,
mSelection,
mImageLoader,
- mOnMediaItemClickListener, /* lifecycleOwner */
- this,
+ mOnMediaItemClickListener,
+ this, /* lifecycleOwner */
mPickerViewModel.getCloudMediaProviderAppTitleLiveData(),
mPickerViewModel.getCloudMediaAccountNameLiveData(),
showChooseAppBanner,
@@ -231,14 +231,19 @@
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);
+ 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;
+ // Cancel pending glide load requests on recycling, to prevent a large
+ // backlog of requests building up in the event of large scrolls.
+ cancelGlideLoadForViewHolder(vh);
+ vh.release();
+ }
}
});
mRecyclerView.setItemViewCacheSize(10);
@@ -414,7 +419,8 @@
private final PhotosTabAdapter.OnMediaItemClickListener mOnMediaItemClickListener =
new PhotosTabAdapter.OnMediaItemClickListener() {
@Override
- public void onItemClick(@NonNull View view, int position) {
+ public void onItemClick(
+ @NonNull View view, int position, MediaItemGridViewHolder viewHolder) {
if (mSelection.canSelectMultiple()) {
final boolean isSelectedBefore =
@@ -423,6 +429,9 @@
Item item = (Item) view.getTag();
if (isSelectedBefore) {
+ if (mSelection.isSelectionOrdered()) {
+ viewHolder.setSelectionOrder(null);
+ }
mSelection.removeSelectedItem((Item) view.getTag());
mSelection.removeCheckedItemIndex((Item) view.getTag());
} else {
@@ -432,14 +441,19 @@
final CharSequence quantityText =
StringUtils.getICUFormatString(
getResources(), maxCount, R.string.select_up_to);
- final String itemCountString = NumberFormat
- .getInstance(Locale.getDefault()).format(maxCount);
- final CharSequence message = TextUtils.expandTemplate(quantityText,
- itemCountString);
+ final String itemCountString =
+ NumberFormat.getInstance(Locale.getDefault())
+ .format(maxCount);
+ final CharSequence message =
+ TextUtils.expandTemplate(quantityText, itemCountString);
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
return;
} else {
mSelection.addSelectedItem(item);
+ if (mSelection.isSelectionOrdered()) {
+ viewHolder.setSelectionOrder(
+ mSelection.getSelectedItemOrder(item));
+ }
mPickerViewModel.logMediaItemSelected(item, mCategory, position);
}
}
@@ -478,7 +492,8 @@
try {
// Transition to PreviewFragment.
- PreviewFragment.show(requireActivity().getSupportFragmentManager(),
+ PreviewFragment.show(
+ requireActivity().getSupportFragmentManager(),
PreviewFragment.getArgsForPreviewOnLongPress());
} catch (RuntimeException e) {
Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
@@ -596,15 +611,10 @@
*
* @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());
- }
+ private void cancelGlideLoadForViewHolder(MediaItemGridViewHolder vh) {
+ // Attempt to clear the potential pending load out of glide's request
+ // manager.
+ mGlideRequestManager.clear(vh.getThumbnailImageView());
}
@Override
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
index 8e4f120..31209d1 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
@@ -243,7 +243,9 @@
/* context= */ getContext(),
/* size= */ selectedItemCount,
/* isUserSelectForApp= */ mPickerViewModel
- .isUserSelectForApp()));
+ .isUserSelectForApp(),
+ /* isManagedSelectionEnabled */
+ mPickerViewModel.isManagedSelectionEnabled()));
});
selectedCheckButton.setOnClickListener(
@@ -391,7 +393,11 @@
// TODO: There is a same method in TabFragment. To find a way to reuse it.
private static String generateAddButtonString(
- @NonNull Context context, int size, boolean isUserSelectForApp) {
+ @NonNull Context context, int size, boolean isUserSelectForApp,
+ boolean isManagedSelection) {
+ if (isManagedSelection && size == 0) {
+ return context.getString(R.string.picker_add_button_allow_none_option);
+ }
final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size);
final String template =
isUserSelectForApp
diff --git a/src/com/android/providers/media/photopicker/ui/TabAdapter.java b/src/com/android/providers/media/photopicker/ui/TabAdapter.java
index 3295941..86f2de7 100644
--- a/src/com/android/providers/media/photopicker/ui/TabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/TabAdapter.java
@@ -216,7 +216,7 @@
mBanner = banner;
mOnBannerEventListener = onBannerEventListener;
notifyItemInserted(/* position */ 0);
- mOnBannerEventListener.onBannerAdded();
+ mOnBannerEventListener.onBannerAdded(banner.name());
} else {
mBanner = banner;
mOnBannerEventListener = onBannerEventListener;
@@ -385,11 +385,9 @@
void onDismissButtonClick();
- default void onBannerClick() {
- onActionButtonClick();
- }
+ void onBannerClick();
- void onBannerAdded();
+ void onBannerAdded(@NonNull String name);
default boolean shouldShowActionButton() {
return true;
diff --git a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
index 2832dcd..8e070b5 100644
--- a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
@@ -15,6 +15,8 @@
*/
package com.android.providers.media.photopicker.ui;
+import static com.android.providers.media.util.MimeUtils.isVideoMimeType;
+
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -32,6 +34,7 @@
import androidx.viewpager2.widget.ViewPager2;
import com.android.providers.media.R;
+import com.android.providers.media.photopicker.util.MimeFilterUtils;
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
@@ -101,13 +104,19 @@
}
final TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
+
mTabLayoutMediator = new TabLayoutMediator(tabLayout, mViewPager, (tab, pos) -> {
if (pos == PHOTOS_TAB_POSITION) {
- tab.setText(R.string.picker_photos);
+ if (isOnlyVideoMimeTypeFilterAvailable()) {
+ tab.setText(R.string.picker_videos);
+ } else {
+ tab.setText(R.string.picker_photos);
+ }
} else if (pos == ALBUMS_TAB_POSITION) {
tab.setText(R.string.picker_albums);
}
});
+
mTabLayoutMediator.attach();
// TabLayout only supports colorDrawable in xml. And if we set the color in the drawable by
// setSelectedTabIndicator method, it doesn't apply the color. So, we set color in xml and
@@ -116,6 +125,22 @@
tabLayout.addOnTabSelectedListener(mOnTabSelectedListener);
}
+ private boolean isOnlyVideoMimeTypeFilterAvailable() {
+ String [] mimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(getActivity().getIntent());
+ boolean hasVideoMimeTypeFilterOnly = false;
+ if (mimeTypeFilters != null && mimeTypeFilters.length > 0) {
+ for (String mimeTypeFilter : mimeTypeFilters) {
+ if (isVideoMimeType(mimeTypeFilter)) {
+ hasVideoMimeTypeFilterOnly = true;
+ } else {
+ hasVideoMimeTypeFilterOnly = false;
+ break;
+ }
+ }
+ }
+ return hasVideoMimeTypeFilterOnly;
+ }
+
@Override
public void onDestroyView() {
mTabLayoutMediator.detach();
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index 9d0b107..46a410f 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -539,29 +539,28 @@
private abstract class OnBannerEventListener implements TabAdapter.OnBannerEventListener {
@Override
public void onActionButtonClick() {
+ mPickerViewModel.logBannerActionButtonClicked();
dismissBanner();
-
- final Intent accountChangeIntent =
- mPickerViewModel.getChooseCloudMediaAccountActivityIntent();
-
- try {
- if (accountChangeIntent != null) {
- requirePickerActivity().startActivity(accountChangeIntent);
- } else {
- requirePickerActivity().startSettingsActivity();
- }
- } catch (RuntimeException e) {
- Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
- }
+ launchCloudProviderSettings();
}
@Override
public void onDismissButtonClick() {
+ mPickerViewModel.logBannerDismissed();
dismissBanner();
}
@Override
- public void onBannerAdded() {
+ public void onBannerClick() {
+ mPickerViewModel.logBannerClicked();
+ dismissBanner();
+ launchCloudProviderSettings();
+ }
+
+ @Override
+ public void onBannerAdded(@NonNull String name) {
+ mPickerViewModel.logBannerAdded(name);
+
// Should scroll to the banner only if the first completely visible item is the one
// just below it. The possible adapter item positions of such an item are 0 and 1.
// During onViewCreated, before restoring the state, the first visible item position
@@ -581,6 +580,21 @@
}
abstract void dismissBanner();
+
+ private void launchCloudProviderSettings() {
+ final Intent accountChangeIntent =
+ mPickerViewModel.getChooseCloudMediaAccountActivityIntent();
+
+ try {
+ if (accountChangeIntent != null) {
+ requirePickerActivity().startActivity(accountChangeIntent);
+ } else {
+ requirePickerActivity().startSettingsActivity();
+ }
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+ }
+ }
}
protected final OnBannerEventListener mOnChooseAppBannerEventListener =
diff --git a/src/com/android/providers/media/photopicker/ui/settings/CloudMediaProviderAccount.java b/src/com/android/providers/media/photopicker/ui/settings/CloudMediaProviderAccount.java
deleted file mode 100644
index fc2d332..0000000
--- a/src/com/android/providers/media/photopicker/ui/settings/CloudMediaProviderAccount.java
+++ /dev/null
@@ -1,47 +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.ui.settings;
-
-import static java.util.Objects.requireNonNull;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/* POJO for encapsulating cloud provider authority and it's linked account name. */
-class CloudMediaProviderAccount {
- @NonNull
- private final String mCloudProviderAuthority;
- @Nullable
- private final String mCloudProviderAccountName;
-
- CloudMediaProviderAccount(
- @NonNull String cloudProviderAuthority,
- @Nullable String cloudProviderAccountName) {
- mCloudProviderAuthority = requireNonNull(cloudProviderAuthority);
- mCloudProviderAccountName = cloudProviderAccountName;
- }
-
- @NonNull
- String getCloudProviderAuthority() {
- return mCloudProviderAuthority;
- }
-
- @Nullable
- String getCloudProviderAccountName() {
- return mCloudProviderAccountName;
- }
-}
diff --git a/src/com/android/providers/media/photopicker/ui/settings/CloudProviderMediaCollectionInfo.java b/src/com/android/providers/media/photopicker/ui/settings/CloudProviderMediaCollectionInfo.java
new file mode 100644
index 0000000..7c6d546
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/settings/CloudProviderMediaCollectionInfo.java
@@ -0,0 +1,62 @@
+/*
+ * 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.ui.settings;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/* POJO for encapsulating the cloud provider authority and it's media collection info. */
+class CloudProviderMediaCollectionInfo {
+ @NonNull
+ private final String mAuthority;
+ @Nullable
+ private final String mAccountName;
+ @Nullable
+ private final Intent mAccountConfigurationIntent;
+
+ CloudProviderMediaCollectionInfo(@NonNull String authority) {
+ mAuthority = requireNonNull(authority);
+ mAccountName = null;
+ mAccountConfigurationIntent = null;
+ }
+
+ CloudProviderMediaCollectionInfo(@NonNull String authority, @Nullable String accountName,
+ @Nullable Intent accountConfigurationIntent) {
+ mAuthority = requireNonNull(authority);
+ mAccountName = accountName;
+ mAccountConfigurationIntent = accountConfigurationIntent;
+ }
+
+ @NonNull
+ String getAuthority() {
+ return mAuthority;
+ }
+
+ @Nullable
+ String getAccountName() {
+ return mAccountName;
+ }
+
+ @Nullable
+ Intent getAccountConfigurationIntent() {
+ return mAccountConfigurationIntent;
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
index c8d1cc7..f08bd75 100644
--- a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
@@ -21,6 +21,7 @@
import static java.util.Objects.requireNonNull;
import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
@@ -67,7 +68,7 @@
public void onResume() {
super.onResume();
- mSettingsCloudMediaViewModel.loadAccountNameAsync();
+ mSettingsCloudMediaViewModel.loadMediaCollectionInfoAsync();
}
@UiThread
@@ -93,7 +94,7 @@
super.addPreferencesFromResource(R.xml.pref_screen_picker_settings);
mSettingsCloudMediaViewModel.loadData(getConfigStore());
- observeAccountNameChanges();
+ observeMediaCollectionInfoChanges();
refreshUI();
}
@@ -111,23 +112,34 @@
updateSelectedRadioButton();
}
- private void observeAccountNameChanges() {
- mSettingsCloudMediaViewModel.getCurrentProviderAccount()
- .observe(this, accountDetails -> {
- // Only update current account name on the UI if cloud provider linked to the
- // account name matches the current provider.
- if (accountDetails != null
- && accountDetails.getCloudProviderAuthority()
- .equals(mSettingsCloudMediaViewModel.getSelectedProviderAuthority())) {
- final Preference selectedPref = findPreference(
- mSettingsCloudMediaViewModel.getSelectedPreferenceKey());
- // TODO(b/262002538): {@code selectedPref} could be null if the selected
- // cloud provider is not in the allowed list. This is not something a
- // typical user will encounter.
- if (selectedPref != null) {
- selectedPref.setSummary(accountDetails.getCloudProviderAccountName());
- }
+ private void observeMediaCollectionInfoChanges() {
+ mSettingsCloudMediaViewModel.getCurrentProviderMediaCollectionInfo().observe(this,
+ providerMediaCollectionInfo -> {
+ // Only update the UI preference if the cloud provider linked to the media
+ // collection info matches the current provider.
+ if (providerMediaCollectionInfo == null
+ || !TextUtils.equals(providerMediaCollectionInfo.getAuthority(),
+ mSettingsCloudMediaViewModel.getSelectedProviderAuthority())) {
+ return;
}
+
+ final SelectorWithWidgetPreference selectedPref =
+ findPreference(mSettingsCloudMediaViewModel.getSelectedPreferenceKey());
+
+ // TODO(b/262002538): {@code selectedPref} could be null if the selected
+ // cloud provider is not in the allowed list. This is not something a
+ // typical user will encounter.
+ if (selectedPref == null) {
+ return;
+ }
+
+ selectedPref.setSummary(providerMediaCollectionInfo.getAccountName());
+
+ final Intent accountConfigurationIntent =
+ providerMediaCollectionInfo.getAccountConfigurationIntent();
+ selectedPref.setExtraWidgetOnClickListener(
+ accountConfigurationIntent == null ? null : v ->
+ requireActivity().startActivity(accountConfigurationIntent));
});
}
@@ -137,19 +149,19 @@
mSettingsCloudMediaViewModel.getSelectedPreferenceKey();
for (CloudMediaProviderOption providerOption
: mSettingsCloudMediaViewModel.getProviderOptions()) {
- final Preference pref = findPreference(providerOption.getKey());
- if (pref instanceof SelectorWithWidgetPreference) {
- final SelectorWithWidgetPreference providerPref =
- (SelectorWithWidgetPreference) pref;
+ final SelectorWithWidgetPreference preference = findPreference(providerOption.getKey());
+ if (preference == null) {
+ continue;
+ }
- final boolean newSelectionState =
- TextUtils.equals(providerPref.getKey(), selectedPreferenceKey);
- providerPref.setChecked(newSelectionState);
+ final boolean isSelected = TextUtils.equals(preference.getKey(), selectedPreferenceKey);
+ preference.setChecked(isSelected);
- providerPref.setSummary(null);
- if (newSelectionState) {
- mSettingsCloudMediaViewModel.loadAccountNameAsync();
- }
+ preference.setSummary(null);
+ preference.setExtraWidgetOnClickListener(null);
+
+ if (isSelected) {
+ mSettingsCloudMediaViewModel.loadMediaCollectionInfoAsync();
}
}
}
diff --git a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java
index ca9be63..e316970 100644
--- a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java
+++ b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java
@@ -20,17 +20,21 @@
import static com.android.providers.media.photopicker.util.CloudProviderUtils.fetchProviderAuthority;
import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders;
-import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaAccountName;
+import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaCollectionInfo;
import static com.android.providers.media.photopicker.util.CloudProviderUtils.persistSelectedProvider;
import static java.util.Objects.requireNonNull;
import android.content.ContentProviderClient;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
+import android.os.Bundle;
import android.os.Looper;
import android.os.UserHandle;
+import android.provider.CloudMediaProviderContract;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -56,12 +60,13 @@
public class SettingsCloudMediaViewModel extends ViewModel {
static final String NONE_PREF_KEY = "none";
private static final String TAG = "SettingsFragVM";
- private static final long GET_ACCOUNT_NAME_TIMEOUT_IN_MILLIS = 10000L;
+ private static final long GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS = 10000L;
@NonNull
private final Context mContext;
@NonNull
- private final MutableLiveData<CloudMediaProviderAccount> mCurrentProviderAccount;
+ private final MutableLiveData<CloudProviderMediaCollectionInfo>
+ mCurrentProviderMediaCollectionInfo;
@NonNull
private final List<CloudMediaProviderOption> mProviderOptions;
@NonNull
@@ -78,7 +83,7 @@
mUserId = requireNonNull(userId);
mProviderOptions = new ArrayList<>();
mSelectedProviderAuthority = null;
- mCurrentProviderAccount = new MutableLiveData<CloudMediaProviderAccount>();
+ mCurrentProviderMediaCollectionInfo = new MutableLiveData<>();
}
@NonNull
@@ -92,11 +97,11 @@
}
@NonNull
- LiveData<CloudMediaProviderAccount> getCurrentProviderAccount() {
- return mCurrentProviderAccount;
+ LiveData<CloudProviderMediaCollectionInfo> getCurrentProviderMediaCollectionInfo() {
+ return mCurrentProviderMediaCollectionInfo;
}
- @Nullable
+ @NonNull
String getSelectedPreferenceKey() {
return getPreferenceKey(mSelectedProviderAuthority);
}
@@ -141,7 +146,7 @@
? null : preferenceKey;
}
- @Nullable
+ @NonNull
private String getPreferenceKey(@Nullable String providerAuthority) {
return providerAuthority == null
? SettingsCloudMediaViewModel.NONE_PREF_KEY : providerAuthority;
@@ -172,39 +177,50 @@
}
@UiThread
- void loadAccountNameAsync() {
+ void loadMediaCollectionInfoAsync() {
if (!Looper.getMainLooper().isCurrentThread()) {
- // This method should only be run from the UI thread so that fetch account name
+ // This method should only be run from the UI thread so that fetch media collection info
// requests are executed serially.
- Log.d(TAG, "loadAccountNameAsync method needs to be called from the UI thread");
+ Log.w(TAG, "loadMediaCollectionInfoAsync method needs to be called from the UI thread");
return;
}
final String providerAuthority = getSelectedProviderAuthority();
// Foreground thread internally uses a queue to execute each request in a serialized manner.
ForegroundThread.getExecutor().execute(() -> {
- mCurrentProviderAccount.postValue(
- fetchAccountFromProvider(providerAuthority));
+ mCurrentProviderMediaCollectionInfo.postValue(
+ fetchMediaCollectionInfoFromProvider(providerAuthority));
});
}
@Nullable
- private CloudMediaProviderAccount fetchAccountFromProvider(
+ private CloudProviderMediaCollectionInfo fetchMediaCollectionInfoFromProvider(
@Nullable String currentProviderAuthority) {
+ // If the selected cloud provider preference is "None", the media collection info is not
+ // applicable.
if (currentProviderAuthority == null) {
- // If the selected cloud provider preference is "None", account name is not applicable.
return null;
- } else {
- try {
- final String accountName = getCloudMediaAccountName(
- mUserId.getContentResolver(mContext), currentProviderAuthority,
- GET_ACCOUNT_NAME_TIMEOUT_IN_MILLIS);
- return new CloudMediaProviderAccount(currentProviderAuthority, accountName);
- } catch (Exception e) {
- Log.w(TAG, "Failed to fetch account name from the cloud media provider.", e);
- return null;
- }
}
+
+ Bundle cloudMediaCollectionInfo = null;
+ try {
+ final ContentResolver currentUserContentResolver = mUserId.getContentResolver(mContext);
+ cloudMediaCollectionInfo = getCloudMediaCollectionInfo(currentUserContentResolver,
+ currentProviderAuthority, GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to fetch media collection info from the cloud media provider.", e);
+ }
+
+ if (cloudMediaCollectionInfo == null) {
+ return new CloudProviderMediaCollectionInfo(currentProviderAuthority);
+ }
+
+ final String accountName = cloudMediaCollectionInfo.getString(
+ CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME);
+ final Intent cloudProviderSettingsActivityIntent = cloudMediaCollectionInfo.getParcelable(
+ CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT);
+ return new CloudProviderMediaCollectionInfo(currentProviderAuthority, accountName,
+ cloudProviderSettingsActivityIntent);
}
@NonNull
diff --git a/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java b/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java
index a859ba3..57e6f75 100644
--- a/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java
+++ b/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java
@@ -18,7 +18,6 @@
import static android.provider.CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION;
import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
-import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME;
import static android.provider.MediaStore.EXTRA_ALBUM_AUTHORITY;
import static android.provider.MediaStore.EXTRA_ALBUM_ID;
import static android.provider.MediaStore.EXTRA_CLOUD_PROVIDER;
@@ -258,30 +257,6 @@
* @param resolver {@link ContentResolver} for the related user
* @param cloudMediaProviderAuthority authority {@link String} of the {@link CloudMediaProvider}
* @param timeout timeout in milliseconds for this query (<= 0 for timeout)
- * @return the current cloud media account name for the {@link CloudMediaProvider} with the
- * given {@code cloudMediaProviderAuthority}.
- */
- @Nullable
- public static String getCloudMediaAccountName(@NonNull ContentResolver resolver,
- @Nullable String cloudMediaProviderAuthority, @DurationMillisLong long timeout)
- throws ExecutionException, InterruptedException, TimeoutException {
- if (cloudMediaProviderAuthority == null) {
- return null;
- }
-
- CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
- final Bundle out = resolver.call(getMediaCollectionInfoUri(cloudMediaProviderAuthority),
- METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, /* extras */ null);
- return (out == null) ? null : out.getString(ACCOUNT_NAME);
- });
-
- return (timeout > 0) ? future.get(timeout, MILLISECONDS) : future.get();
- }
-
- /**
- * @param resolver {@link ContentResolver} for the related user
- * @param cloudMediaProviderAuthority authority {@link String} of the {@link CloudMediaProvider}
- * @param timeout timeout in milliseconds for this query (<= 0 for timeout)
* @return the current cloud media collection info for the {@link CloudMediaProvider} with the
* given {@code cloudMediaProviderAuthority}.
*/
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index 5b65155..f51d9cc 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -82,6 +82,7 @@
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.data.model.UserId;
+import com.android.providers.media.photopicker.metrics.NonUiEventLogger;
import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
import com.android.providers.media.photopicker.ui.ItemsAction;
import com.android.providers.media.photopicker.util.CategoryOrganiserUtils;
@@ -407,10 +408,12 @@
String[] mimeTypeFilters) {
if (isManagedSelectionEnabled() && selection.getPreGrantedItems() == null) {
DataLoaderThread.getHandler().postDelayed(() -> {
- selection.setPreGrantedItemSet(mItemsProvider.fetchReadGrantedItemsUrisForPackage(
- intentExtras.getInt(Intent.EXTRA_UID), mimeTypeFilters)
+ Set<String> preGrantedItems = mItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ intentExtras.getInt(Intent.EXTRA_UID), mimeTypeFilters)
.stream().map((Uri uri) -> String.valueOf(ContentUris.parseId(uri)))
- .collect(Collectors.toSet()));
+ .collect(Collectors.toSet());
+ selection.setPreGrantedItemSet(preGrantedItems);
+ logPickerChoiceInitGrantsCount(preGrantedItems.size(), intentExtras);
}, TOKEN, DELAY_MILLIS);
}
}
@@ -875,7 +878,9 @@
mPackageUid = intent.getExtras().getInt(Intent.EXTRA_UID);
}
// Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
- initBannerManager();
+ if (mBannerManager == null) {
+ initBannerManager();
+ }
}
private void initBannerManager() {
@@ -1174,6 +1179,103 @@
mLogger.logPickerCreateSurfaceControllerEnd(mInstanceId, authority);
}
+ /**
+ * Log metrics to notify that the selected media preloading started
+ * @param count the number of items to preload
+ */
+ public void logPreloadingStarted(int count) {
+ mLogger.logPreloadingStarted(mInstanceId, count);
+ }
+
+ /**
+ * Log metrics to notify that the selected media preloading finished
+ */
+ public void logPreloadingFinished() {
+ mLogger.logPreloadingFinished(mInstanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user cancelled the selected media preloading
+ * @param count the number of items pending to preload
+ */
+ public void logPreloadingCancelled(int count) {
+ mLogger.logPreloadingCancelled(mInstanceId, count);
+ }
+
+ /**
+ * Log metrics to notify that the selected media preloading failed for some items
+ * @param count the number of items pending / failed to preload
+ */
+ public void logPreloadingFailed(int count) {
+ mLogger.logPreloadingFailed(mInstanceId, count);
+ }
+
+ /**
+ * Logs metrics for count of grants initialised for a package.
+ */
+ public void logPickerChoiceInitGrantsCount(int numberOfGrants, Bundle intentExtras) {
+ NonUiEventLogger.logPickerChoiceInitGrantsCount(mInstanceId, android.os.Process.myUid(),
+ getPackageNameForUid(intentExtras), numberOfGrants);
+
+ }
+
+ /**
+ * Logs metrics for count of grants added for a package.
+ */
+ public void logPickerChoiceAddedGrantsCount(int numberOfGrants, Bundle intentExtras) {
+ NonUiEventLogger.logPickerChoiceGrantsAdditionCount(mInstanceId, android.os.Process.myUid(),
+ getPackageNameForUid(intentExtras), numberOfGrants);
+ }
+
+ /**
+ * Logs metrics for count of grants removed for a package.
+ */
+ public void logPickerChoiceRevokedGrantsCount(int numberOfGrants, Bundle intentExtras) {
+ NonUiEventLogger.logPickerChoiceGrantsRemovedCount(mInstanceId, android.os.Process.myUid(),
+ getPackageNameForUid(intentExtras), numberOfGrants);
+ }
+
+ /**
+ * Log metrics to notify that the banner is added to display in the recycler view grids
+ * @param bannerName the name of the banner added,
+ * refer {@link com.android.providers.media.photopicker.ui.TabAdapter.Banner}
+ */
+ public void logBannerAdded(@NonNull String bannerName) {
+ mLogger.logBannerAdded(mInstanceId, bannerName);
+ }
+
+ /**
+ * Log metrics to notify that the banner is dismissed by the user
+ */
+ public void logBannerDismissed() {
+ mLogger.logBannerDismissed(mInstanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user clicked the banner action button
+ */
+ public void logBannerActionButtonClicked() {
+ mLogger.logBannerActionButtonClicked(mInstanceId);
+ }
+
+ /**
+ * Log metrics to notify that the user clicked on the remaining part of the banner
+ */
+ public void logBannerClicked() {
+ mLogger.logBannerClicked(mInstanceId);
+ }
+
+ @NonNull
+ private String getPackageNameForUid(Bundle extras) {
+ final int uid = extras.getInt(Intent.EXTRA_UID);
+ final PackageManager pm = mAppContext.getPackageManager();
+ String[] packageNames = pm.getPackagesForUid(uid);
+ if (packageNames.length != 0) {
+ return packageNames[0];
+ }
+ return new String();
+ }
+
public InstanceId getInstanceId() {
return mInstanceId;
}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index ee78c19..7132276 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -55,6 +55,20 @@
</intent-filter>
</activity>
+ <!-- Intent Action "android.intent.action.MAIN"
+
+ This intent action is used to start the activity as a main entry point, does not expect
+ to receive data.
+
+ {@link androidx.test.core.app.ActivityScenario#launchActivityForResult(Class)} launches
+ the activity with the intent action {@link android.content.Intent#ACTION_MAIN}.
+ -->
+ <activity android:name="com.android.providers.media.photopicker.espresso.PhotoPickerAccessibilityDisabledTestActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ </intent-filter>
+ </activity>
+
<provider android:name="com.android.providers.media.photopicker.LocalProvider"
android:authorities="com.android.providers.media.photopicker.tests.local"
android:exported="false" />
diff --git a/tests/src/com/android/providers/media/ConfigStoreTest.java b/tests/src/com/android/providers/media/ConfigStoreTest.java
index 5f29d8f..10728b8 100644
--- a/tests/src/com/android/providers/media/ConfigStoreTest.java
+++ b/tests/src/com/android/providers/media/ConfigStoreTest.java
@@ -61,7 +61,7 @@
assertEquals(60000, mConfigStore.getTranscodeMaxDurationMs());
assertTrue(mConfigStore.isCloudMediaInPhotoPickerEnabled());
assertFalse(mConfigStore.isGetContentTakeOverEnabled());
- assertFalse(mConfigStore.isPickerChoiceManagedSelectionEnabled());
+ assertTrue(mConfigStore.isPickerChoiceManagedSelectionEnabled());
assertFalse(mConfigStore.isStableUrisForExternalVolumeEnabled());
assertFalse(mConfigStore.isStableUrisForInternalVolumeEnabled());
assertTrue(mConfigStore.isTranscodeEnabled());
diff --git a/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java b/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java
index 447a98f..6b45993 100644
--- a/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java
+++ b/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java
@@ -25,7 +25,6 @@
import static org.junit.Assert.assertTrue;
import android.content.Context;
-import android.os.UserHandle;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
@@ -78,16 +77,4 @@
assertThat(DatabaseBackupAndRecovery.getInvalidUsersList(xattrData, /* validUserIds */
Arrays.asList("0", "13"))).containsExactly("10", "11", "12");
}
-
- @Test
- public void testGetFilePathForFuseRequests() {
- assertThat(DatabaseBackupAndRecovery.getFilePathForFuseRequests("/storage")).isEqualTo(
- "/storage/emulated/" + UserHandle.myUserId());
- assertThat(DatabaseBackupAndRecovery.getFilePathForFuseRequests(
- "/storage/emulated/" + UserHandle.myUserId() + "/Android/media")).isEqualTo(
- "/storage/emulated/" + UserHandle.myUserId());
- assertThat(
- DatabaseBackupAndRecovery.getFilePathForFuseRequests("/storage/ABC-DEF")).isEqualTo(
- "/storage/ABC-DEF");
- }
}
diff --git a/tests/src/com/android/providers/media/MediaGrantsTest.java b/tests/src/com/android/providers/media/MediaGrantsTest.java
index 1f126fd..622c790 100644
--- a/tests/src/com/android/providers/media/MediaGrantsTest.java
+++ b/tests/src/com/android/providers/media/MediaGrantsTest.java
@@ -302,6 +302,27 @@
assertTrue(expectedFileIdsList2.contains(Long.valueOf(ContentUris.parseId(uri))));
}
}
+
+ @Test
+ public void testRemoveMediaGrantsForPackagesLargerDataSet() throws Exception {
+ List<Uri> inputFiles = new ArrayList<>();
+ for (int itr = 1; itr < 110; itr++) {
+ inputFiles.add(buildValidPickerUri(
+ insertFileInResolver(mIsolatedResolver, "test_file" + itr)));
+ }
+ mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, inputFiles, TEST_USER_ID);
+
+ String[] mimeTypes = {PNG_MIME_TYPE};
+ String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+
+ // The query used inside remove grants is batched by 50 ids, hence having a test like this
+ // would help ensure the batching worked perfectly.
+ mGrants.removeMediaGrantsForPackage(new String[]{TEST_OWNER_PACKAGE_NAME},
+ inputFiles.subList(0, 101), TEST_USER_ID);
+ List<Uri> fileUris3 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+ new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID, mimeTypes, volumes));
+ assertEquals(8, fileUris3.size());
+ }
@Test
public void testAddDuplicateMediaGrants() throws Exception {
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index b2d86ca..302a061 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -826,6 +826,41 @@
/**
* Tests {@link ItemsProvider#getLocalItemsForSelection(Category, List, String[],
* UserId, CancellationSignal)} to return only selected items from the media table for ids
+ * defined in the localId selection list.
+ */
+ @Test
+ public void testGetItemsImages_withLocalIdSelection_largeDataSet() throws Exception {
+ List<Uri> imageFilesUris = assertCreateNewImagesWithSameDateModifiedTimesAndReturnUri(200);
+ // Try to fetch all items via selection. 200 items, this will hit the split query and
+ // verify that it is working.
+ List<Integer> inputIdsAsIntegers = imageFilesUris.stream().map(ContentUris::parseId).map(
+ Long::intValue).collect(Collectors.toList());
+ try {
+ // get the item objects for the provided ids.
+ final Cursor res = mItemsProvider.getLocalItemsForSelection(Category.DEFAULT,
+ /* local id selection list */ inputIdsAsIntegers,
+ /* mimeType */ new String[]{"image/*"}, /* userId */ null,
+ /* cancellationSignal */ null);
+
+ // verify that the correct number of items are returned and that they have the correct
+ // ids.
+ assertThat(res).isNotNull();
+ assertThat(res.getCount()).isEqualTo(inputIdsAsIntegers.size());
+ res.moveToPosition(0);
+ while (res.moveToNext()) {
+ Item item = Item.fromCursor(res, UserId.CURRENT_USER);
+ assertTrue(inputIdsAsIntegers.contains(Integer.parseInt(item.getId())));
+ }
+ assertThatOnlyImages(res);
+ } finally {
+ // clean up.
+ deleteAllFilesNoThrow();
+ }
+ }
+
+ /**
+ * Tests {@link ItemsProvider#getLocalItemsForSelection(Category, List, String[],
+ * UserId, CancellationSignal)} to return only selected items from the media table for ids
* defined in the localId selection list. Here the list is empty so the parameter is ignored and
* the list is returned without any selection.
*/
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index e51678b..1d06576 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -20,7 +20,7 @@
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.photopicker.NotificationContentObserver.MEDIA;
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -44,8 +44,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.providers.media.PickerProviderMediaGenerator;
import com.android.providers.media.TestConfigStore;
@@ -152,7 +152,7 @@
mCloudSecondaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
mCloudFlakyMediaGenerator.setMediaCollectionId(COLLECTION_1);
- mContext = InstrumentationRegistry.getTargetContext();
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Delete db so it's recreated on next access and previous test state is cleared
final File dbPath = mContext.getDatabasePath(DB_NAME);
@@ -198,18 +198,22 @@
PickerSyncController controller =
PickerSyncController.initialize(mContext, mFacade, configStore, mLockManager);
- assertThat(controller.getCurrentCloudProviderInfo()).isEqualTo(CloudProviderInfo.EMPTY);
+ assertWithMessage(
+ "CloudProviderInfo should have been EMPTY when CloudMediaFeature is disabled.")
+ .that(controller.getCurrentCloudProviderInfo()).isEqualTo(CloudProviderInfo.EMPTY);
configStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
configStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
// Ensure the cloud provider is set to something. (The test package name here actually
// has multiple cloud providers in it, so just ensure something got set.)
- assertThat(controller.getCurrentCloudProviderInfo().authority).isNotNull();
+ assertWithMessage("Failed to set cloud provider on config change.")
+ .that(controller.getCurrentCloudProviderInfo().authority).isNotNull();
configStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
// Ensure the cloud provider is correctly nulled out when the config changes again.
- assertThat(controller.getCurrentCloudProviderInfo().authority).isNull();
+ assertWithMessage("Failed to nullify cloud provider on config change.")
+ .that(controller.getCurrentCloudProviderInfo().authority).isNull();
}
@Test
@@ -252,7 +256,9 @@
// The cursor should only contain the items from the local provider. (Even though we've
// added a total of 4 items to the linked providers.)
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage("Cursor should only contain the items from the local provider.")
+ .that(cr.getCount()).isEqualTo(2);
+
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -271,7 +277,9 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding two local only media.")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -282,7 +290,10 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after deleting one local-only "
+ + "media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
}
@@ -292,7 +303,10 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after resetting media without "
+ + "version bump.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
}
@@ -317,7 +331,10 @@
mController.syncAlbumMedia(ALBUM_ID_1, true);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of album medias in album albumId = "
+ + ALBUM_ID_1)
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -329,7 +346,10 @@
mController.syncAlbumMedia(ALBUM_ID_1, true);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of album medias in album albumId = "
+ + ALBUM_ID_1)
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -339,7 +359,10 @@
mController.syncAlbumMedia(ALBUM_ID_2, true);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album medias in album albumId = "
+ + ALBUM_ID_2)
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -350,7 +373,10 @@
assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, true);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album medias in album albumId = "
+ + ALBUM_ID_2)
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -376,7 +402,9 @@
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing all media")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -389,7 +417,9 @@
// 5. Set primary cloud provider once again
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after second sync of all media.")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -406,7 +436,9 @@
addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing all media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -420,7 +452,9 @@
mController.syncAllMediaFromLocalProvider(/* cancellationSignal=*/ null);
// Verify that the sync only synced local items
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(3);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after local sync")
+ .that(cr.getCount()).isEqualTo(3);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -448,7 +482,10 @@
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of album medias on queryAlbumMedia() after setting cloud "
+ + "providers and syncing cloud album media")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -463,7 +500,10 @@
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of album medias on queryAlbumMedia() after setting cloud "
+ + "providers and syncing cloud album media for the second time")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -498,7 +538,10 @@
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media on queryAlbumMedia() after syncing first "
+ + "album from cloud provider")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -515,7 +558,10 @@
// 4a. Sync the first album and query local albums
mController.syncAlbumMedia(ALBUM_ID_1, true);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media on queryAlbumMedia() after syncing first "
+ + "album from local provider")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -523,7 +569,10 @@
// 4b. Sync the second album
mController.syncAlbumMedia(ALBUM_ID_2, true);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media on queryAlbumMedia() after syncing second "
+ + "album from local provider")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
}
@@ -531,7 +580,10 @@
// 5. Sync and query cloud albums
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media on queryAlbumMedia() after syncing first "
+ + "album from cloud provider")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -554,7 +606,9 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing all media")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -569,7 +623,10 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after setting valid cloud version"
+ + " and syncing all media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -590,7 +647,10 @@
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media on queryAlbumMedia() after syncing album "
+ + "from cloud provider")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -606,7 +666,10 @@
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media on queryAlbumMedia() after cloud provider "
+ + "reset and syncing album from cloud provider")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -629,7 +692,9 @@
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing all media")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -639,7 +704,9 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after deleting local-only item.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -649,7 +716,9 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after re-adding local-only item.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -659,7 +728,9 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after deleting cloud+local item.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -674,69 +745,95 @@
@Test
public void testSetCloudProvider() {
//1. Get local provider assertion out of the way
- assertThat(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected local provider.")
+ .that(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
// Assert that no cloud provider set on facade
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Facade cloud provider should have been null.")
+ .that(mFacade.getCloudProvider()).isNull();
// 2. Can set cloud provider
- assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set cloud provider. ")
+ .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Unexpected cloud provider.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Assert that setting cloud provider clears facade cloud provider
// And after syncing, the latest provider is set on the facade
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+ .that(mFacade.getCloudProvider()).isNull();
mController.syncAllMedia();
- assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set latest provider on the facade post sync.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// 3. Can clear cloud provider
- assertThat(setCloudProviderAndSyncAllMedia(null)).isTrue();
- assertThat(mController.getCloudProvider()).isNull();
+ assertWithMessage("Failed to clear cloud provider.")
+ .that(setCloudProviderAndSyncAllMedia(null)).isTrue();
+ assertWithMessage("Cloud provider should have been null.")
+ .that(mController.getCloudProvider()).isNull();
// Assert that setting cloud provider clears facade cloud provider
// And after syncing, the latest provider is set on the facade
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+ .that(mFacade.getCloudProvider()).isNull();
mController.syncAllMedia();
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Facade Cloud provider should have been null post sync.")
+ .that(mFacade.getCloudProvider()).isNull();
// 4. Can set cloud proivder
- assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set cloud provider. ")
+ .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Unexpected cloud provider.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Assert that setting cloud provider clears facade cloud provider
// And after syncing, the latest provider is set on the facade
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+ .that(mFacade.getCloudProvider()).isNull();
mController.syncAllMedia();
- assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set latest provider on the facade post sync.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Invalid cloud provider is ignored
- assertThat(setCloudProviderAndSyncAllMedia(LOCAL_PROVIDER_AUTHORITY)).isFalse();
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Setting invalid cloud provider should have failed.")
+ .that(setCloudProviderAndSyncAllMedia(LOCAL_PROVIDER_AUTHORITY)).isFalse();
+ assertWithMessage("Unexpected cloud provider.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Assert that unsuccessfully setting cloud provider doesn't clear facade cloud provider
// And after syncing, nothing changes
- assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage(
+ "Unsuccessfully setting cloud provider should have failed to clear facade cloud "
+ + "provider.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
mController.syncAllMedia();
- assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected facade cloud provider post sync.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@Test
public void testEnableCloudQueriesAfterMPRestart() {
//1. Get local provider assertion out of the way
- assertThat(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected local provider.")
+ .that(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
// Assert that no cloud provider set on facade
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Facade cloud provider should have been null.")
+ .that(mFacade.getCloudProvider()).isNull();
// 2. Can set cloud provider
- assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set cloud provider.")
+ .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Unexpected cloud provider.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Assert that setting cloud provider clears facade cloud provider
// And after syncing, the latest provider is set on the facade
- assertThat(mFacade.getCloudProvider()).isNull();
+ assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+ .that(mFacade.getCloudProvider()).isNull();
mController.syncAllMedia();
- assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected facade cloud provider post sync.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// 3. Clear facade cloud provider to simulate MP restart.
mFacade.setCloudProvider(null);
@@ -744,7 +841,8 @@
// 4. Assert that latest provider is set in the facade after sync even when no sync was
// required.
mController.syncAllMedia();
- assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set latest provider in the facade after MP restart.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -762,7 +860,9 @@
new CloudProviderInfo(
FLAKY_CLOUD_PROVIDER_AUTHORITY, PACKAGE_NAME, Process.myUid());
- assertThat(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
+ assertWithMessage(
+ "Unexpected cloud provider in the list returned by getAvailableCloudProviders().")
+ .that(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
}
@Test
@@ -779,23 +879,34 @@
final PickerSyncController controller = PickerSyncController.initialize(
mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
final List<CloudProviderInfo> providers = controller.getAvailableCloudProviders();
- assertThat(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
+ assertWithMessage(
+ "Unexpected cloud provider in the list returned by getAvailableCloudProviders() "
+ + "when using allowList.")
+ .that(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
}
@Test
public void testNotifyPackageRemoval_NoDefaultCloudProviderPackage() {
mConfigStore.clearDefaultCloudProviderPackage();
- assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Failed to set cloud provider.")
+ .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Unexpected cloud provider.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Assert passing wrong package name doesn't clear the current cloud provider
mController.notifyPackageRemoval(PACKAGE_NAME + "invalid");
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage(
+ "Unexpected cloud provider, passing wrong package shouldn't have cleared the "
+ + "current cloud provider.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Assert passing the current cloud provider package name clears the current cloud provider
mController.notifyPackageRemoval(PACKAGE_NAME);
- assertThat(mController.getCloudProvider()).isNull();
+ assertWithMessage(
+ "Unexpected cloud provider, passing current package should have cleared the "
+ + "current cloud provider.")
+ .that(mController.getCloudProvider()).isNull();
// Assert that the cloud provider state was not UNSET after the last cloud provider removal
mConfigStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
@@ -803,7 +914,11 @@
mController = PickerSyncController.initialize(mContext, mFacade, mConfigStore,
mLockManager, LOCAL_PROVIDER_AUTHORITY);
- assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+ assertWithMessage(
+ "Unexpected cloud provider, cloud provider state got UNSET after the last cloud "
+ + "provider removal")
+ .that(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(
+ PACKAGE_NAME);
}
// TODO(b/278687585): Add test for PickerSyncController#notifyPackageRemoval with a different
@@ -812,33 +927,45 @@
@Test
public void testSelectDefaultCloudProvider_NoDefaultAuthority() {
PickerSyncController controller = createControllerWithDefaultProvider(null);
- assertThat(controller.getCloudProvider()).isNull();
+ assertWithMessage("Default provider was set to null.")
+ .that(controller.getCloudProvider()).isNull();
}
@Test
public void testSelectDefaultCloudProvider_defaultAuthoritySet() {
PickerSyncController controller = createControllerWithDefaultProvider(PACKAGE_NAME);
- assertThat(controller.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+ assertWithMessage("Default provider was set to " + PACKAGE_NAME)
+ .that(controller.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
}
@Test
public void testIsProviderAuthorityEnabled() {
- assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isFalse();
- assertThat(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
+ assertWithMessage("Expected " + LOCAL_PROVIDER_AUTHORITY + " to be enabled.")
+ .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Expected " + CLOUD_PRIMARY_PROVIDER_AUTHORITY + " to be disabled")
+ .that(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isFalse();
+ assertWithMessage("Expected " + CLOUD_SECONDARY_PROVIDER_AUTHORITY + " to be disabled")
+ .that(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
- assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
- assertThat(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
+ assertWithMessage("Expected " + LOCAL_PROVIDER_AUTHORITY + " to be enabled.")
+ .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Expected " + CLOUD_PRIMARY_PROVIDER_AUTHORITY + " to be enabled.")
+ .that(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+ assertWithMessage("Expected " + CLOUD_SECONDARY_PROVIDER_AUTHORITY + " to be disabled.")
+ .that(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
}
@Test
public void testIsProviderUidEnabled() {
- assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, Process.myUid()))
+ assertWithMessage("Expected " + LOCAL_PROVIDER_AUTHORITY + " uid = " + Process.myUid()
+ + " to be enabled.")
+ .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, Process.myUid()))
.isTrue();
- assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, 1000)).isFalse();
+ assertWithMessage(
+ "Expected " + LOCAL_PROVIDER_AUTHORITY + " uid = 1000" + " to be disabled.")
+ .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, 1000)).isFalse();
}
@Test
@@ -856,7 +983,8 @@
controller.syncAllMedia();
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of media after adding one local-only media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -873,14 +1001,16 @@
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(0);
+ assertWithMessage("Unexpected number of media after deleting and recreating the db.")
+ .that(cr.getCount()).isEqualTo(0);
}
controller.syncAllMedia();
// Fully synced db
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of media after fully syncing the recreated db.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -900,7 +1030,8 @@
controller.syncAllMedia();
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of media after adding one local-only media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -914,14 +1045,16 @@
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(0);
+ assertWithMessage("Unexpected number of media after upgrading the db version.")
+ .that(cr.getCount()).isEqualTo(0);
}
controller.syncAllMedia();
// Fully synced db
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of media after fully syncing the upgraded db.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -941,7 +1074,8 @@
controller.syncAllMedia();
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of media after adding one local-only media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -956,14 +1090,16 @@
// Initially empty db
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(0);
+ assertWithMessage("Unexpected number of media after downgrading the db version.")
+ .that(cr.getCount()).isEqualTo(0);
}
controller.syncAllMedia();
// Fully synced db
try (Cursor cr = queryMedia(facade)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage("Unexpected number of media after fully syncing the downgraded db.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
}
@@ -984,7 +1120,10 @@
// 5. Sync and verify media
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on syncing all media with correct "
+ + "extra_media_collection_id")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -997,7 +1136,10 @@
// 7. Sync and verify media after retry succeeded
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of media on syncing all media with incorrect "
+ + "extra_media_collection_id")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1018,7 +1160,7 @@
// 1. Set cloud provider
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
- // 2. Force the next 2 syncs (including retry) to have correct extra_media_collection_id
+ // 2. Force the next 2 syncs (including retry) to have correct extra_honored_args
mCloudPrimaryMediaGenerator.setNextCursorExtras(2, COLLECTION_1,
/* honoredSyncGeneration */ true, /* honoredAlbumId */ false, true);
@@ -1028,7 +1170,10 @@
// 4. Sync and verify media
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on syncing all media with correct "
+ + "extra_honored_args")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -1041,7 +1186,10 @@
// 6. Sync and verify media after retry succeeded
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(2);
+ assertWithMessage(
+ "Unexpected number of media on syncing all media with incorrect "
+ + "extra_honored_args")
+ .that(cr.getCount()).isEqualTo(2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1064,7 +1212,8 @@
// 4. Sync and verify media
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(/* expected= */ 2);
+ assertWithMessage("Unexpected number of media")
+ .that(cr.getCount()).isEqualTo(/* expected= */ 2);
assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1087,7 +1236,9 @@
// 4. Sync and verify album_media
mController.syncAlbumMedia(ALBUM_ID_1, false);
try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of album media from album with albumId = " + ALBUM_ID_1)
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@@ -1112,7 +1263,8 @@
mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
controller.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
- assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected cloud provider on db set up.")
+ .that(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
// Downgrade db version
dbHelperV1.close();
@@ -1122,7 +1274,8 @@
controller = PickerSyncController.initialize(
mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
- assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected cloud provider after db version downgrade.")
+ .that(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
@Test
@@ -1135,7 +1288,9 @@
PickerSyncController.initialize(
mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
- assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+ assertWithMessage("Unexpected cloud provider on testing the default NOT_SET state.")
+ .that(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(
+ PACKAGE_NAME);
// Set and test the UNSET state
mController.setCloudProvider(/* authority */ null);
@@ -1144,7 +1299,8 @@
PickerSyncController.initialize(
mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
- assertThat(mController.getCloudProvider()).isNull();
+ assertWithMessage("Unexpected cloud provider on setting and testing the NOT_SET state.")
+ .that(mController.getCloudProvider()).isNull();
// Set and test the SET state
mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
@@ -1153,14 +1309,19 @@
PickerSyncController.initialize(
mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
- assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Unexpected cloud provider on setting and testing the SET state.")
+ .that(mController.getCloudProvider()).isEqualTo(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
}
@Test
public void testAvailableCloudProviders_CloudFeatureDisabled() {
- assertThat(mController.getAvailableCloudProviders()).isNotEmpty();
+ assertWithMessage("Empty list returned by getAvailableCloudProviders().")
+ .that(mController.getAvailableCloudProviders()).isNotEmpty();
mConfigStore.disableCloudMediaFeature();
- assertThat(mController.getAvailableCloudProviders()).isEmpty();
+ assertWithMessage(
+ "Non-empty list returned by getAvailableCloudProviders() after disabling the "
+ + "cloud media feature.")
+ .that(mController.getAvailableCloudProviders()).isEmpty();
}
@Test
@@ -1181,7 +1342,9 @@
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(9);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 9 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(9);
assertCursor(cr, CLOUD_ID_9, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_8, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_7, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1212,7 +1375,9 @@
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(9);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 9 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(9);
assertCursor(cr, CLOUD_ID_9, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_8, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_7, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1236,14 +1401,17 @@
mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(1);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after deleting 8 out the 9 "
+ + "cloud-only media.")
+ .that(cr.getCount()).isEqualTo(1);
assertCursor(cr, CLOUD_ID_9, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
}
}
@Test
- public void testResumableSyncOperation() {
+ public void testResumableIncrementalSyncOperation() {
// First Page
addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
@@ -1253,10 +1421,14 @@
// Complete a full sync since it hasn't synced before.
setCloudProviderAndSyncAllMedia(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ mController.syncAllMedia();
+ mController.syncAllMedia();
try (Cursor cr = queryMedia()) {
// Should only have the first page since the sync is flaky
- assertThat(cr.getCount()).isEqualTo(5);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 5 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(5);
assertCursor(cr, CLOUD_ID_5, FLAKY_CLOUD_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_4, FLAKY_CLOUD_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_3, FLAKY_CLOUD_PROVIDER_AUTHORITY);
@@ -1283,7 +1455,10 @@
try (Cursor cr = queryMedia()) {
// Should have all pages now
- assertThat(cr.getCount()).isEqualTo(14);
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 9 cloud-only media, "
+ + "in addition to previously added 5 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(14);
assertCursor(cr, CLOUD_ID_14, FLAKY_CLOUD_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_13, FLAKY_CLOUD_PROVIDER_AUTHORITY);
assertCursor(cr, CLOUD_ID_12, FLAKY_CLOUD_PROVIDER_AUTHORITY);
@@ -1302,6 +1477,180 @@
}
@Test
+ public void testResumableFullSyncOperation() {
+ // First Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+ // Second Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+ // Third Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_12);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_13);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_14);
+
+ mController.setCloudProvider(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ try (Cursor cr = queryMedia()) {
+ // Db should be empty since we haven't synced yet.
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() before sync.")
+ .that(cr.getCount()).isEqualTo(0);
+ }
+
+ // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+ // should not be able to complete the sync.
+ mController.syncAllMedia();
+
+ try (Cursor cr = queryMedia()) {
+ // Assert that the sync is not complete.
+ assertWithMessage(
+ "Unexpected number of media on queryMedia().")
+ .that(cr.getCount()).isLessThan(14);
+ }
+
+ // Resume sync and complete it. It will take a few sync calls to complete the sync.
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+
+ try (Cursor cr = queryMedia()) {
+ // Should have all pages now
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 14 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(14);
+ assertCursor(cr, CLOUD_ID_14, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_13, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_12, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_11, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_10, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_9, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_8, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_7, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_6, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_5, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_4, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_3, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_2, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_1, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ }
+ }
+
+ @Test
+ public void testFullSyncWithCollectionIdChange() {
+ mController.setCloudProvider(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ mCloudFlakyMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+ // First Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+ // Second Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+ // Third Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+
+ // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+ // should not be able to complete the sync.
+ mController.syncAllMedia();
+
+ try (Cursor cr = queryMedia()) {
+ // Assert that the sync is not complete.
+ assertWithMessage(
+ "Unexpected number of media on queryMedia().")
+ .that(cr.getCount()).isLessThan(11);
+ }
+
+ // Reset data and change collection id.
+ mCloudFlakyMediaGenerator.resetAll();
+ mCloudFlakyMediaGenerator.setMediaCollectionId(COLLECTION_2);
+
+ // First Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_12);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_13);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_14);
+
+ // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests. It will take a few
+ // tries to complete the sync.
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+ mController.syncAllMedia();
+
+ try (Cursor cr = queryMedia()) {
+ // Db should be empty since we haven't synced yet.
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 3 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(3);
+ assertCursor(cr, CLOUD_ID_14, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_13, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_12, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+ }
+ }
+
+ @Test
+ public void testFullSyncWithCloudProviderChange() {
+ mController.setCloudProvider(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+
+ // First Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+ // Second Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+ // Third Page of data
+ addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+
+ // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+ // should not be able to complete the sync.
+ mController.syncAllMedia();
+
+ try (Cursor cr = queryMedia()) {
+ // Assert that the sync is not complete.
+ assertWithMessage(
+ "Unexpected number of media on queryMedia().")
+ .that(cr.getCount()).isLessThan(11);
+ }
+
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+ // First Page of data
+ addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_12);
+ addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_13);
+ addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_14);
+
+ mController.syncAllMedia();
+
+ try (Cursor cr = queryMedia()) {
+ // Db should be empty since we haven't synced yet.
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after adding 3 cloud-only media.")
+ .that(cr.getCount()).isEqualTo(3);
+ assertCursor(cr, CLOUD_ID_14, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_13, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_12, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+ }
+
+ @Test
public void testContentAddNotifications() throws Exception {
NotificationContentObserver observer = new NotificationContentObserver(null);
observer.register(mContext.getContentResolver());
@@ -1374,7 +1723,8 @@
contentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
/* notifyForDescendants */ false, refreshUiNotificationObserver);
- assertThat(refreshUiNotificationObserver.mNotificationReceived).isFalse();
+ assertWithMessage("Refresh ui notification should have not been received.")
+ .that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
mConfigStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
@@ -1383,14 +1733,18 @@
mController = PickerSyncController
.initialize(mContext, mFacade, mConfigStore, mLockManager);
TimeUnit.MILLISECONDS.sleep(100);
- assertThat(refreshUiNotificationObserver.mNotificationReceived).isTrue();
+ assertWithMessage(
+ "Failed to receive refresh ui notification on change in cloud provider.")
+ .that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
refreshUiNotificationObserver.mNotificationReceived = false;
// The SET_CLOUD_PROVIDER is called using a different cloud provider from before
mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
TimeUnit.MILLISECONDS.sleep(100);
- assertThat(refreshUiNotificationObserver.mNotificationReceived).isTrue();
+ assertWithMessage(
+ "Failed to receive refresh ui notification on change in cloud provider.")
+ .that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
refreshUiNotificationObserver.mNotificationReceived = false;
@@ -1398,12 +1752,18 @@
mController = PickerSyncController
.initialize(mContext, mFacade, mConfigStore, mLockManager);
TimeUnit.MILLISECONDS.sleep(100);
- assertThat(refreshUiNotificationObserver.mNotificationReceived).isFalse();
+ assertWithMessage(
+ "Refresh ui notification should have not been received when cloud provider "
+ + "remains unchanged.")
+ .that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
// The SET_CLOUD_PROVIDER is called using the same cloud provider as before
mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
TimeUnit.MILLISECONDS.sleep(100);
- assertThat(refreshUiNotificationObserver.mNotificationReceived).isFalse();
+ assertWithMessage(
+ "Refresh ui notification should have not been received when setCloudProvider "
+ + "is called using the same cloud provider as before.")
+ .that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
} finally {
contentResolver.unregisterContentObserver(refreshUiNotificationObserver);
}
@@ -1448,13 +1808,15 @@
private void assertEmptyCursorFromMediaQuery() {
try (Cursor cr = queryMedia()) {
- assertThat(cr.getCount()).isEqualTo(0);
+ assertWithMessage("Cursor should have been empty.")
+ .that(cr.getCount()).isEqualTo(0);
}
}
private void assertEmptyCursorFromAlbumMediaQuery(String albumId, boolean isLocal) {
try (Cursor cr = queryAlbumMedia(albumId, isLocal)) {
- assertThat(cr.getCount()).isEqualTo(0);
+ assertWithMessage("Cursor from queryAlbumMedia should have been empty.")
+ .that(cr.getCount()).isEqualTo(0);
}
}
@@ -1484,9 +1846,11 @@
private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
cursor.moveToNext();
- assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
+ assertWithMessage("Unexpected value of MediaColumns.ID in the cursor.")
+ .that(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
.isEqualTo(id);
- assertThat(cursor.getString(cursor.getColumnIndex( MediaColumns.AUTHORITY)))
+ assertWithMessage("Unexpected value of MediaColumns.AUTHORITY in the cursor.")
+ .that(cursor.getString(cursor.getColumnIndex(MediaColumns.AUTHORITY)))
.isEqualTo(expectedAuthority);
}
diff --git a/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
index f176325..745acc0 100644
--- a/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
@@ -34,7 +34,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -74,7 +73,6 @@
});
}
- @Ignore("Enable once b/269874157 is fixed")
@Test
public void testWhetherShouldUseSafetyProtectionResourcesWhenTOrAboveAndFeatureFlagOn() {
assumeTrue(SdkLevel.isAtLeastT());
diff --git a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
index bce370b..189f122 100644
--- a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
@@ -62,6 +62,22 @@
}
@Test
+ public void testAddSelectedItem_orderedSelection() {
+ try {
+ enableOrderedSelection();
+ final Item item1 = generateFakeImageItem("1");
+ final Item item2 = generateFakeImageItem("2");
+
+ mSelection.addSelectedItem(item1);
+ mSelection.addSelectedItem(item2);
+ assertThat(mSelection.getSelectedItemOrder(item1).getValue().intValue()).isEqualTo(1);
+ assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(2);
+ } finally {
+ disableOrderedSelection();
+ }
+ }
+
+ @Test
public void testDeleteSelectedItem() {
final String id = "1";
final Item item = generateFakeImageItem(id);
@@ -76,6 +92,56 @@
}
@Test
+ public void testDeleteSelectedItem_orderedSelection() {
+ try {
+ enableOrderedSelection();
+ final Item item1 = generateFakeImageItem("1");
+ final Item item2 = generateFakeImageItem("2");
+ final Item item3 = generateFakeImageItem("3");
+
+ mSelection.addSelectedItem(item1);
+ mSelection.addSelectedItem(item2);
+ mSelection.addSelectedItem(item3);
+
+ assertThat(mSelection.getSelectedItemOrder(item1).getValue().intValue()).isEqualTo(1);
+ assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(2);
+ assertThat(mSelection.getSelectedItemOrder(item3).getValue().intValue()).isEqualTo(3);
+
+ mSelection.removeSelectedItem(item1);
+
+ assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(1);
+ assertThat(mSelection.getSelectedItemOrder(item3).getValue().intValue()).isEqualTo(2);
+
+ mSelection.removeSelectedItem(item3);
+
+ assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(1);
+ } finally {
+ disableOrderedSelection();
+ }
+ }
+
+ @Test
+ public void testGetSelectedItems_orderedSelection() {
+ try {
+ enableOrderedSelection();
+ final Item item1 = generateFakeImageItem("1");
+ final Item item2 = generateFakeImageItem("2");
+ final Item item3 = generateFakeImageItem("3");
+
+ mSelection.addSelectedItem(item1);
+ mSelection.addSelectedItem(item2);
+ mSelection.addSelectedItem(item3);
+
+ List<Item> itemsSorted = mSelection.getSelectedItems();
+ assertThat(itemsSorted.get(0).getId()).isEqualTo("1");
+ assertThat(itemsSorted.get(1).getId()).isEqualTo("2");
+ assertThat(itemsSorted.get(2).getId()).isEqualTo("3");
+ } finally {
+ disableOrderedSelection();
+ }
+ }
+
+ @Test
public void testClearSelectedItem() {
final String id = "1";
final Item item = generateFakeImageItem(id);
@@ -164,6 +230,39 @@
}
@Test
+ public void testParseValuesFromIntent_orderedSelection() {
+ final Intent intent = new Intent();
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+
+ mSelection.parseSelectionValuesFromIntent(intent);
+
+ assertThat(mSelection.isSelectionOrdered()).isTrue();
+ }
+
+ @Test
+ public void testParseValuesFromIntent_InvalidOrderedSelectionGetContent_throwsException() {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+
+ try {
+ mSelection.parseSelectionValuesFromIntent(intent);
+ fail("Ordered selection not allowed for GET_CONTENT");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testParseValuesFromIntent_OrderedSelectionDisabledInPermissionMode() {
+ final Intent intent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+
+ mSelection.parseSelectionValuesFromIntent(intent);
+
+ assertThat(mSelection.isSelectionOrdered()).isFalse();
+ }
+
+ @Test
public void testParseValuesFromIntent_allowMultipleNotSupported() {
final Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
@@ -276,4 +375,16 @@
return generateJpegItem(id, dateTakenMs, /* generationModified */ 1L);
}
+
+ private void enableOrderedSelection() {
+ final Intent intent = new Intent();
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+ mSelection.parseSelectionValuesFromIntent(intent);
+ }
+
+ private void disableOrderedSelection() {
+ final Intent intent = new Intent();
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, false);
+ mSelection.parseSelectionValuesFromIntent(intent);
+ }
}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
index 693b445..a36531f 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
@@ -99,7 +99,8 @@
* given {@link ActivityScenarioRule}.
* @param scenario
*/
- public static BottomSheetIdlingResource register(ActivityScenario scenario) {
+ public static <T extends PhotoPickerTestActivity> BottomSheetIdlingResource register(
+ ActivityScenario<T> scenario) {
final BottomSheetIdlingResource[] idlingResources = new BottomSheetIdlingResource[1];
scenario.onActivity(
(activity -> {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
new file mode 100644
index 0000000..3e17e2f
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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.espresso;
+
+import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isNotSelected;
+import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
+import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.customSwipeDownPartialScreen;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeLeftAndWait;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeRightAndWait;
+import static com.android.providers.media.photopicker.espresso.OrientationUtils.setLandscapeOrientation;
+import static com.android.providers.media.photopicker.espresso.OrientationUtils.setPortraitOrientation;
+import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
+import static com.android.providers.media.photopicker.espresso.RecyclerViewMatcher.withRecyclerView;
+import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.longClickItem;
+
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.not;
+
+import android.app.Activity;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * {@link DisabledAccessibilityTest} tests the
+ * {@link com.android.providers.media.photopicker.PhotoPickerActivity} behaviors that require it to
+ * launch in partial screen.
+ */
+@RunOnlyOnPostsubmit
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class DisabledAccessibilityTest extends PhotoPickerBaseTest {
+
+ private ActivityScenario<PhotoPickerAccessibilityDisabledTestActivity> mScenario;
+
+ /**
+ * Note - {@link ActivityScenario#launchActivityForResult(Class)} launches the activity with the
+ * intent action {@link android.content.Intent#ACTION_MAIN}.
+ */
+ @Before
+ public void launchActivity() {
+ mScenario = ActivityScenario.launchActivityForResult(
+ PhotoPickerAccessibilityDisabledTestActivity.class);
+ }
+
+ @After
+ public void closeActivity() {
+ if (mScenario != null) {
+ mScenario.close();
+ }
+ }
+
+ @Test
+ @Ignore("b/313489524")
+ // TODO(b/313489524): Fix flaky orientation change in the photo picker espresso tests
+ public void testBottomSheetState() {
+ // Bottom sheet assertions are different based on the orientation
+ setPortraitOrientation(mScenario);
+
+ // Register bottom sheet idling resource so that we don't read bottom sheet state when
+ // in between changing states
+ final BottomSheetIdlingResource bottomSheetIdlingResource =
+ BottomSheetIdlingResource.register(mScenario);
+
+ try {
+ // Single select PhotoPicker is launched in partial screen mode
+ bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+ onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+ mScenario.onActivity(
+ activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
+ // Swipe up and check that the PhotoPicker is in full screen mode
+ bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+ onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
+ mScenario.onActivity(
+ activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
+
+ // Swipe down and check that the PhotoPicker is in partial screen mode
+ bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+ onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeDown());
+ mScenario.onActivity(
+ activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
+ // Swiping down on drag bar is not strong enough as closing the bottomsheet requires a
+ // stronger downward swipe using espresso.
+ // Simply swiping down on R.id.bottom_sheet throws an error from espresso, as the view
+ // is only 60% visible, but downward swipe is only successful on an element which is 90%
+ // visible.
+ onView(withId(R.id.bottom_sheet)).perform(customSwipeDownPartialScreen());
+ } finally {
+ IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+ }
+ assertThat(mScenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ @Ignore("b/313489524")
+ // TODO(b/313489524): Fix flaky orientation change in the photo picker espresso tests
+ public void testBottomSheetStateInLandscapeMode() {
+ // Bottom sheet assertions are different based on the orientation
+ setLandscapeOrientation(mScenario);
+
+ // Register bottom sheet idling resource so that we don't read bottom sheet state when
+ // in between changing states
+ final BottomSheetIdlingResource bottomSheetIdlingResource =
+ BottomSheetIdlingResource.register(mScenario);
+
+ try {
+ // Single select PhotoPicker is launched in full screen mode in Landscape orientation
+ mScenario.onActivity(
+ activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
+
+ // Swiping down on drag bar / privacy text is not strong enough as closing the
+ // bottomsheet requires a stronger downward swipe using espresso.
+ onView(withId(R.id.bottom_sheet)).perform(ViewActions.swipeDown());
+ } finally {
+ IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+ }
+ assertThat(mScenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void testTabSwiping() throws Exception {
+ // Bottom sheet assertions are different based on the orientation
+ setPortraitOrientation(mScenario);
+
+ onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
+
+ // If we want to swipe the viewPager2 of tabContainerFragment in Espresso tests, at least 90
+ // percent of the view's area is displayed to the user. Swipe up the bottom Sheet to make
+ // sure it is in full Screen mode.
+ // Register bottom sheet idling resource so that we don't read bottom sheet state when
+ // in between changing states
+ final BottomSheetIdlingResource bottomSheetIdlingResource =
+ BottomSheetIdlingResource.register(mScenario);
+
+ try {
+ // Single select PhotoPicker is launched in partial screen mode
+ bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+ mScenario.onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
+ // Swipe up and check that the PhotoPicker is in full screen mode.
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+ onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
+ bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+ mScenario.onActivity(
+ activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
+ } finally {
+ IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+ }
+
+ try (ViewPager2IdlingResource idlingResource =
+ ViewPager2IdlingResource.register(mScenario, TAB_VIEW_PAGER_ID)) {
+ // Swipe left, we should see albums tab
+ swipeLeftAndWait(TAB_VIEW_PAGER_ID);
+
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isNotSelected()));
+ // Verify Camera album is shown, we are in albums tab
+ onView(allOf(withText(R.string.picker_category_camera),
+ isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(
+ matches(isDisplayed()));
+
+ // Swipe right, we should see photos tab
+ swipeRightAndWait(TAB_VIEW_PAGER_ID);
+
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isNotSelected()));
+ // Verify first item is recent header, we are in photos tab
+ onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
+ .atPositionOnView(0, R.id.date_header_title))
+ .check(matches(withText(R.string.recent)));
+ }
+ }
+
+ @Test
+ public void testPreview_singleSelect_image() throws Exception {
+ // Bottom sheet assertions are different based on the orientation
+ setPortraitOrientation(mScenario);
+
+ onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+
+ final BottomSheetIdlingResource bottomSheetIdlingResource =
+ BottomSheetIdlingResource.register(mScenario);
+
+ try {
+ bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+ onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+ mScenario.onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
+ // Navigate to preview
+ longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+
+ UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(mScenario,
+ PhotoPickerUiEventLogger.PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+ _SPECIAL_FORMAT_NONE, JPEG_IMAGE_MIME_TYPE, IMAGE_1_POSITION);
+
+ try (ViewPager2IdlingResource idlingResource =
+ ViewPager2IdlingResource.register(mScenario, PREVIEW_VIEW_PAGER_ID)) {
+ // No dragBar in preview
+ bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+ onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+ // No privacy text in preview
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
+ mScenario.onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
+
+ // Verify image is previewed
+ PreviewFragmentAssertionUtils.assertSingleSelectCommonLayoutMatches();
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ // Verify the overflow menu is not shown for PICK_IMAGES intent
+ assertOverflowMenuNotShown();
+ }
+ // Navigate back to Photo grid
+ onView(withContentDescription("Navigate up")).perform(click());
+
+ onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+ onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+
+ bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+ // Shows dragBar and privacy text after we are back to Photos tab
+ mScenario.onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+ } finally {
+ IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
index e248e27..1905838 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
@@ -22,19 +22,21 @@
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
-import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.core.app.ActivityScenario;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
import com.android.providers.media.library.RunOnlyOnPostsubmit;
-import org.junit.Rule;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,14 +45,23 @@
public class MimeTypeFilterTest extends PhotoPickerBaseTest {
private static final String IMAGE_MIME_TYPE = "image/*";
+ private static final String VIDEO_MIME_TYPE = "video/*";
+ public ActivityScenario<PhotoPickerTestActivity> mScenario;
- @Rule
- public ActivityScenarioRule<PhotoPickerTestActivity> mRule = new ActivityScenarioRule<>(
- PhotoPickerBaseTest.getSingleSelectMimeTypeFilterIntent(IMAGE_MIME_TYPE));
+ @Before
+ public void launchActivity() {
+ mScenario =
+ ActivityScenario.launchActivityForResult(
+ PhotoPickerBaseTest.getSingleSelectMimeTypeFilterIntent(IMAGE_MIME_TYPE));
+ }
+
+ @After
+ public void closeActivity() {
+ mScenario.close();
+ }
@Test
public void testPhotosTabOnlyImageItems() {
-
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Two image items and one recent date header
@@ -92,4 +103,21 @@
onView(allOf(withId(itemCountId),
withParent(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(doesNotExist());
}
+
+ @Test
+ public void testPickerTabTitleText_forVariousMimeTypeFilters() {
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+
+ mScenario = ActivityScenario.launchActivityForResult(
+ PhotoPickerBaseTest.getSingleSelectMimeTypeFilterIntent(VIDEO_MIME_TYPE));
+ onView(allOf(withText(PICKER_VIDEOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+
+ mScenario = ActivityScenario.launchActivityForResult(
+ PhotoPickerBaseTest.getSingleSelectionIntent());
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+
+ }
}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
index 778c1ce..aed2d96 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
@@ -55,7 +55,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -63,8 +62,6 @@
@RunWith(AndroidJUnit4ClassRunner.class)
public class MultiSelectTest extends PhotoPickerBaseTest {
- private static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
-
private ActivityScenario<PhotoPickerTestActivity> mScenario;
@Before
@@ -80,11 +77,6 @@
}
@Test
- public void testMultiSelectDoesNotShowProfileButton() {
- assertProfileButtonNotShown();
- }
-
- @Test
public void testMultiselect_showDragBar() {
onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
}
@@ -256,7 +248,6 @@
}
@Test
- @Ignore("Enable after b/228574741 is fixed")
public void testMultiSelectTabSwiping() throws Exception {
onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
@@ -289,7 +280,6 @@
}
@Test
- @Ignore("Enable after b/222013536 is fixed")
public void testMultiSelectScrollDownToClose() {
final BottomSheetIdlingResource bottomSheetIdlingResource =
BottomSheetIdlingResource.register(mScenario);
@@ -302,15 +292,6 @@
assertBottomSheetState(activity, STATE_EXPANDED);
});
- // Shows dragBar and privacy text after we are back to Photos tab
- onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
- onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
- mScenario.onActivity(activity -> {
- assertBottomSheetState(activity, STATE_EXPANDED);
- });
-
- // Swiping down on drag bar or toolbar is not closing the bottom sheet as closing the
- // bottomsheet requires a stronger downward swipe.
onView(withId(R.id.bottom_sheet)).perform(ViewActions.swipeDown());
} finally {
IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
@@ -319,29 +300,4 @@
assertThat(mScenario.getResult().getResultCode()).isEqualTo(
Activity.RESULT_CANCELED);
}
-
-
- private void assertProfileButtonNotShown() {
- // Partial screen does not show profile button
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
- // Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .perform(click());
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
- final int cameraStringId = R.string.picker_category_camera;
- // Navigate to photos in Camera album
- onView(allOf(withText(cameraStringId),
- isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).perform(click());
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
- // Click back button
- onView(withContentDescription("Navigate up")).perform(click());
-
- // on clicking back button we are back to Album grid
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .check(matches(isSelected()));
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
- }
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java
index 9d0be47..2cc03f6 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java
@@ -28,16 +28,18 @@
import androidx.test.core.app.ActivityScenario;
class OrientationUtils {
- public static void setLandscapeOrientation(ActivityScenario<PhotoPickerTestActivity> scenario) {
+ public static <T extends PhotoPickerTestActivity> void setLandscapeOrientation(
+ ActivityScenario<T> scenario) {
changeOrientation(scenario, SCREEN_ORIENTATION_LANDSCAPE, ORIENTATION_LANDSCAPE);
}
- public static void setPortraitOrientation(ActivityScenario<PhotoPickerTestActivity> scenario) {
+ public static <T extends PhotoPickerTestActivity> void setPortraitOrientation(
+ ActivityScenario<T> scenario) {
changeOrientation(scenario, SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_PORTRAIT);
}
- private static void changeOrientation(
- ActivityScenario<PhotoPickerTestActivity> scenario,
+ private static <T extends PhotoPickerTestActivity> void changeOrientation(
+ ActivityScenario<T> scenario,
int screenOrientation,
int configOrientation) {
scenario.onActivity(
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerAccessibilityDisabledTestActivity.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerAccessibilityDisabledTestActivity.java
new file mode 100644
index 0000000..26d722e
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerAccessibilityDisabledTestActivity.java
@@ -0,0 +1,34 @@
+/*
+ * 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.espresso;
+
+/**
+ * In espresso tests, the default accessibility mode, evaluated by
+ * {@link android.view.accessibility.AccessibilityManager#isEnabled()}, is enabled.
+ *
+ * {@link PhotoPickerAccessibilityDisabledTestActivity} is used to cover the code that requires the
+ * accessibility to be disabled.
+ *
+ * This {@link android.app.Activity} is launched using the {@link android.content.Intent}
+ * {@link android.content.Intent#ACTION_MAIN}.
+ */
+public class PhotoPickerAccessibilityDisabledTestActivity extends PhotoPickerTestActivity {
+ @Override
+ protected boolean isAccessibilityEnabled() {
+ return false;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
index a7a5412..3d0bdc1 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
@@ -31,15 +31,10 @@
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
-import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.customSwipeDownPartialScreen;
-import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeLeftAndWait;
-import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeRightAndWait;
import static com.android.providers.media.photopicker.espresso.OrientationUtils.setLandscapeOrientation;
-import static com.android.providers.media.photopicker.espresso.OrientationUtils.setPortraitOrientation;
import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
import static com.android.providers.media.photopicker.espresso.RecyclerViewMatcher.withRecyclerView;
-import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
import static com.google.common.truth.Truth.assertThat;
@@ -71,8 +66,6 @@
@RunWith(AndroidJUnit4ClassRunner.class)
public class PhotoPickerActivityTest extends PhotoPickerBaseTest {
- private static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
-
public ActivityScenario<PhotoPickerTestActivity> mScenario;
@Before
@@ -96,7 +89,8 @@
onView(withId(R.id.fragment_container)).check(matches(isDisplayed()));
onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
- // Partial screen does not show profile button
+ // Assuming by default, the tests run without a managed user
+ // Single user mode does not show profile button
onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
onView(withId(android.R.id.empty)).check(matches(not(isDisplayed())));
@@ -108,75 +102,29 @@
}
@Test
- public void testDoesNotShowProfileButton_partialScreen() {
- assertProfileButtonNotShown();
- }
+ public void testProfileButtonHiddenInSingleUserMode() {
+ // Assuming that the test runs without a managed user
- @Test
- @Ignore("Enable after b/222013536 is fixed")
- public void testDoesNotShowProfileButton_fullScreen() {
- // Bottomsheet assertions are different for landscape mode
- setPortraitOrientation(mScenario);
-
- // Partial screen does not show profile button
+ // Single user mode does not show profile button in the main grid
onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
- BottomSheetTestUtils.swipeUp(mScenario);
+ onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
- assertProfileButtonNotShown();
+ // On clicking albums tab item, we should see albums tab
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .perform(click());
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isNotSelected()));
+
+ // Single user mode does not show profile button in the albums grid
+ onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
}
@Test
- @Ignore("Enable after b/222013536 is fixed")
- public void testBottomSheetState() {
- // Bottom sheet assertions are different for landscape mode
- setPortraitOrientation(mScenario);
-
- // Register bottom sheet idling resource so that we don't read bottom sheet state when
- // in between changing states
- final BottomSheetIdlingResource bottomSheetIdlingResource =
- BottomSheetIdlingResource.register(mScenario);
-
- try {
- // Single select PhotoPicker is launched in partial screen mode
- bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
- onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
- onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
- mScenario.onActivity(
- activity -> {
- assertBottomSheetState(activity, STATE_COLLAPSED);
- });
-
- // Swipe up and check that the PhotoPicker is in full screen mode
- bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
- onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
- mScenario.onActivity(
- activity -> {
- assertBottomSheetState(activity, STATE_EXPANDED);
- });
-
- // Swipe down and check that the PhotoPicker is in partial screen mode
- bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
- onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeDown());
- mScenario.onActivity(
- activity -> {
- assertBottomSheetState(activity, STATE_COLLAPSED);
- });
-
- // Swiping down on drag bar is not strong enough as closing the bottomsheet requires a
- // stronger downward swipe using espresso.
- // Simply swiping down on R.id.bottom_sheet throws an error from espresso, as the view
- // is only 60% visible, but downward swipe is only successful on an element which is 90%
- // visible.
- onView(withId(R.id.bottom_sheet)).perform(customSwipeDownPartialScreen());
- } finally {
- IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
- }
- assertThat(mScenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
- }
-
- @Test
- @Ignore("Enable after b/222013536 is fixed")
+ @Ignore("b/313489524")
+ // TODO(b/313489524): Fix flaky orientation change in the photo picker espresso tests
public void testBottomSheetStateInLandscapeMode() {
// Bottom sheet assertions are different for landscape mode
setLandscapeOrientation(mScenario);
@@ -254,69 +202,6 @@
}
@Test
- @Ignore("Enable after b/222013536 is fixed")
- public void testTabSwiping() throws Exception {
- onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
-
- // If we want to swipe the viewPager2 of tabContainerFragment in Espresso tests, at least 90
- // percent of the view's area is displayed to the user. Swipe up the bottom Sheet to make
- // sure it is in full Screen mode.
- // Register bottom sheet idling resource so that we don't read bottom sheet state when
- // in between changing states
- final BottomSheetIdlingResource bottomSheetIdlingResource =
- BottomSheetIdlingResource.register(mScenario);
-
- try {
-
- // When accessibility is enabled, we always launch the photo picker in full screen mode.
- // Accessibility is enabled in Espresso test, so we can't check the COLLAPSED state.
- // // Single select PhotoPicker is launched in partial screen mode
- // bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
- // mScenario.onActivity(activity -> {
- // assertBottomSheetState(activity, STATE_COLLAPSED);
- // });
-
- // Swipe up and check that the PhotoPicker is in full screen mode.
- // onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
- // onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
- bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
- mScenario.onActivity(
- activity -> {
- assertBottomSheetState(activity, STATE_EXPANDED);
- });
- } finally {
- IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
- }
-
- try (ViewPager2IdlingResource idlingResource =
- ViewPager2IdlingResource.register(mScenario, TAB_VIEW_PAGER_ID)) {
- // Swipe left, we should see albums tab
- swipeLeftAndWait(TAB_VIEW_PAGER_ID);
-
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .check(matches(isSelected()));
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .check(matches(isNotSelected()));
- // Verify Camera album is shown, we are in albums tab
- onView(allOf(withText(R.string.picker_category_camera),
- isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(
- matches(isDisplayed()));
-
- // Swipe right, we should see photos tab
- swipeRightAndWait(TAB_VIEW_PAGER_ID);
-
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .check(matches(isSelected()));
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .check(matches(isNotSelected()));
- // Verify first item is recent header, we are in photos tab
- onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
- .atPositionOnView(0, R.id.date_header_title))
- .check(matches(withText(R.string.recent)));
- }
- }
-
- @Test
public void testResetOnCloudProviderChange() throws InterruptedException {
// Enable cloud media feature for the activity through the test config store
mScenario.onActivity(
@@ -350,28 +235,4 @@
onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
}
-
- private void assertProfileButtonNotShown() {
- // Partial screen does not show profile button
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
- // Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .perform(click());
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
- final int cameraStringId = R.string.picker_category_camera;
- // Navigate to photos in Camera album
- onView(allOf(withText(cameraStringId),
- isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).perform(click());
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
- // Click back button
- onView(withContentDescription("Navigate up")).perform(click());
-
- // on clicking back button we are back to Album grid
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
- .check(matches(isSelected()));
- onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
- }
}
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 e254445..cc9626f 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -57,9 +57,11 @@
public class PhotoPickerBaseTest {
protected static final int PICKER_TAB_RECYCLERVIEW_ID = R.id.picker_tab_recyclerview;
+ protected static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
protected static final int TAB_LAYOUT_ID = R.id.tab_layout;
protected static final int PICKER_PHOTOS_STRING_ID = R.string.picker_photos;
protected static final int PICKER_ALBUMS_STRING_ID = R.string.picker_albums;
+ protected static final int PICKER_VIDEOS_STRING_ID = R.string.picker_videos;
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;
@@ -75,6 +77,8 @@
protected static final String JPEG_IMAGE_MIME_TYPE = "image/jpeg";
protected static final String MP4_VIDEO_MIME_TYPE = "video/mp4";
+ protected static final String MANAGED_SELECTION_ENABLED_EXTRA = "MANAGED_SELECTION_ENABLE";
+
protected static final int DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH
= R.dimen.preview_add_or_select_width;
@@ -123,6 +127,17 @@
sUserSelectImagesForAppIntent.putExtras(extras);
}
+ private static final Intent sPickerChoiceManagedSelectionIntent;
+ static {
+ sPickerChoiceManagedSelectionIntent = new Intent(
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+ sPickerChoiceManagedSelectionIntent.addCategory(
+ Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
+ Bundle extras = new Bundle();
+ extras.putInt(Intent.EXTRA_UID, Process.myUid());
+ extras.putBoolean(MANAGED_SELECTION_ENABLED_EXTRA, true);
+ sPickerChoiceManagedSelectionIntent.putExtras(extras);
+ }
private static final File IMAGE_1_FILE = new File(Environment.getExternalStorageDirectory(),
Environment.DIRECTORY_DCIM + "/Camera"
+ "/image_" + System.currentTimeMillis() + ".jpeg");
@@ -155,6 +170,9 @@
return sUserSelectImagesForAppIntent;
}
+ public static Intent getPickerChoiceManagedSelectionIntent() {
+ return sPickerChoiceManagedSelectionIntent;
+ }
public static Intent getMultiSelectionIntent(int max) {
final Intent intent = new Intent(sMultiSelectionIntent);
Bundle extras = new Bundle();
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 5bfedba..5026235 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
@@ -16,7 +16,7 @@
package com.android.providers.media.photopicker.espresso;
-import static com.android.providers.media.photopicker.espresso.PhotoPickerUserSelectActivityTest.MANAGED_SELECTION_ENABLED_EXTRA;
+import static com.android.providers.media.photopicker.espresso.PhotoPickerBaseTest.MANAGED_SELECTION_ENABLED_EXTRA;
import static org.mockito.Mockito.RETURNS_SMART_NULLS;
import static org.mockito.Mockito.mock;
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 a37a466..110fbb7 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
@@ -16,11 +16,13 @@
package com.android.providers.media.photopicker.espresso;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isNotSelected;
import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -39,6 +41,7 @@
import android.content.Intent;
import android.provider.MediaStore;
+import androidx.lifecycle.ViewModelProvider;
import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ActivityScenario;
import androidx.test.filters.SdkSuppress;
@@ -46,6 +49,8 @@
import com.android.providers.media.R;
import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.data.Selection;
+import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
import org.junit.After;
import org.junit.Test;
@@ -56,8 +61,6 @@
@RunWith(AndroidJUnit4ClassRunner.class)
public class PhotoPickerUserSelectActivityTest extends PhotoPickerBaseTest {
- public static final String MANAGED_SELECTION_ENABLED_EXTRA = "MANAGED_SELECTION_ENABLE";
-
public ActivityScenario<PhotoPickerTestActivity> mScenario;
@After
@@ -89,7 +92,7 @@
@Test
public void testActivityProfileButtonNotShown() {
launchValidActivity();
- // Partial screen does not show profile button
+ // User select mode does not show profile button
onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
// Navigate to Albums tab
@@ -174,6 +177,60 @@
}
@Test
+ public void testPreview_deselectAll_showAllowNone() throws Exception {
+ launchValidActivityWithManagedSelectionEnabled();
+ onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+
+ // Select first and second image
+ clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+ // Navigate to preview
+ onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
+
+ try (ViewPager2IdlingResource idlingResource =
+ ViewPager2IdlingResource.register(mScenario, PREVIEW_VIEW_PAGER_ID)) {
+ final int previewAddButtonId = R.id.preview_add_button;
+ final int previewSelectButtonId = R.id.preview_selected_check_button;
+ final String selectedString =
+ getTargetContext().getResources().getString(R.string.selected);
+
+ // Verify that, initially, we show "selected" check button
+ onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+ onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+ // Verify that the text in Add button matches "Allow (1)"
+ onView(withId(previewAddButtonId))
+ .check(matches(withText("Allow (1)")));
+
+ // Deselect item in preview
+ onView(withId(previewSelectButtonId)).perform(click());
+ onView(withId(previewSelectButtonId)).check(matches(isNotSelected()));
+ onView(withId(previewSelectButtonId)).check(matches(withText(R.string.deselected)));
+ // Verify that the text in Add button now changes to "Allow none"
+ onView(withId(previewAddButtonId))
+ .check(matches(withText("Allow none")));
+ // Verify that we have 0 items in selected items
+ mScenario.onActivity(activity -> {
+ Selection selection =
+ new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+ assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(0);
+ });
+
+ // Select the item again
+ onView(withId(previewSelectButtonId)).perform(click());
+ onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+ onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+ // Verify that the text in Add button now changes back to "Allow (1)"
+ onView(withId(previewAddButtonId))
+ .check(matches(withText("Allow (1)")));
+ // Verify that we have 1 item in selected items
+ mScenario.onActivity(activity -> {
+ Selection selection =
+ new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+ assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
+ });
+ }
+ }
+
+ @Test
public void testUserSelectCorrectHeaderTextIsShown() {
launchValidActivity();
onView(withText(R.string.picker_header_permissions)).check(matches(isDisplayed()));
@@ -188,9 +245,7 @@
/** 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);
+ mScenario = ActivityScenario.launchActivityForResult(
+ PhotoPickerBaseTest.getPickerChoiceManagedSelectionIntent());
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewFragmentAssertionUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewFragmentAssertionUtils.java
new file mode 100644
index 0000000..fefc641
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewFragmentAssertionUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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.espresso;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.not;
+
+import com.android.providers.media.R;
+
+class PreviewFragmentAssertionUtils {
+ private static final int PREVIEW_ADD_OR_SELECT_BUTTON_ID = R.id.preview_add_or_select_button;
+
+ static void assertSingleSelectCommonLayoutMatches() {
+ onView(withId(R.id.preview_viewPager)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
+ // Verify that the text in Add button
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
+
+ onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
index f5480f3..3de3575 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
@@ -29,13 +29,11 @@
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
import static com.android.providers.media.photopicker.espresso.OrientationUtils.setLandscapeOrientation;
import static com.android.providers.media.photopicker.espresso.OrientationUtils.setPortraitOrientation;
import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.longClickItem;
-import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.Matchers.allOf;
@@ -47,7 +45,6 @@
import android.view.View;
import androidx.appcompat.widget.Toolbar;
-import androidx.test.espresso.IdlingRegistry;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
@@ -68,74 +65,6 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@Test
- public void testPreview_singleSelect_image() throws Exception {
- onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
-
- // Bottomsheet assertions are different for landscape mode
- setPortraitOrientation(mRule.getScenario());
-
- final BottomSheetIdlingResource bottomSheetIdlingResource =
- BottomSheetIdlingResource.register(mRule.getScenario());
-
- try {
- // TODO(b/226318844): When accessibility is enabled, we always launch the photo picker
- // in full screen mode. Accessibility is enabled in Espresso test, we can't check the
- // COLLAPSED state.
-// bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-// onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
-// onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-// mRule.getScenario().onActivity(activity -> {
-// assertBottomSheetState(activity, STATE_COLLAPSED);
-// });
-
- // Navigate to preview
- longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
-
- UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
- mRule, PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
- _SPECIAL_FORMAT_NONE, JPEG_IMAGE_MIME_TYPE, IMAGE_1_POSITION);
-
- try (ViewPager2IdlingResource idlingResource =
- ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
- // No dragBar in preview
- bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
- onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
- // No privacy text in preview
- onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
- mRule.getScenario().onActivity(activity -> {
- assertBottomSheetState(activity, STATE_EXPANDED);
- });
-
- // Verify image is previewed
- assertSingleSelectCommonLayoutMatches();
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- // Verify the overflow menu is not shown for PICK_IMAGES intent
- assertOverflowMenuNotShown();
- }
- // Navigate back to Photo grid
- onView(withContentDescription("Navigate up")).perform(click());
-
- onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
- onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
- onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-
- // TODO(b/226318844): When accessibility is enabled, we always launch the photo picker
- // in full screen mode. Accessibility is enabled in Espresso test, we can't check the
- // COLLAPSED state.
-// bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-// // Shows dragBar and privacy text after we are back to Photos tab
-// mRule.getScenario().onActivity(activity -> {
-// assertBottomSheetState(activity, STATE_COLLAPSED);
-// });
- } finally {
- IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
- }
- }
-
- @Test
public void testPreview_singleSelect_video() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -148,7 +77,7 @@
try (ViewPager2IdlingResource idlingResource =
ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
- assertSingleSelectCommonLayoutMatches();
+ PreviewFragmentAssertionUtils.assertSingleSelectCommonLayoutMatches();
// Verify thumbnail view is displayed
onView(withId(R.id.preview_video_image)).check(matches(isDisplayed()));
// TODO (b/232792753): Assert video player visibility using custom IdlingResource
@@ -182,7 +111,7 @@
try (ViewPager2IdlingResource idlingResource =
ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
// Verify image is previewed
- assertSingleSelectCommonLayoutMatches();
+ PreviewFragmentAssertionUtils.assertSingleSelectCommonLayoutMatches();
onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
}
@@ -263,14 +192,4 @@
assertThat(bottomBarDrawable).isInstanceOf(ColorDrawable.class);
assertThat(((ColorDrawable) bottomBarDrawable).getColor()).isEqualTo(expectedColor);
}
-
- private void assertSingleSelectCommonLayoutMatches() {
- onView(withId(R.id.preview_viewPager)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
- // Verify that the text in Add button
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
-
- onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
- onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
- }
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
index 74f37b4..2567296 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
@@ -40,7 +40,6 @@
import com.android.providers.media.library.RunOnlyOnPostsubmit;
import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -158,7 +157,6 @@
}
@Test
- @Ignore("Enable after b/222013536 is fixed")
public void testPreview_singleSelect_nonAnimatedWebp() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java
index 06c9643..f088253 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java
@@ -18,6 +18,7 @@
import static org.mockito.Mockito.verify;
+import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import com.android.internal.logging.UiEventLogger;
@@ -45,8 +46,15 @@
static void verifyLogWithInstanceIdAndPosition(
ActivityScenarioRule<PhotoPickerTestActivity> rule, UiEventLogger.UiEventEnum event,
int uid, String packageName, int position) {
- rule.getScenario().onActivity(activity ->
- verify(activity.getLogger()).logWithInstanceIdAndPosition(
- event, uid, packageName, activity.getInstanceId(), position));
+ verifyLogWithInstanceIdAndPosition(rule.getScenario(), event, uid, packageName, position);
+ }
+
+ static <T extends PhotoPickerTestActivity> void verifyLogWithInstanceIdAndPosition(
+ ActivityScenario<T> scenario, UiEventLogger.UiEventEnum event,
+ int uid, String packageName, int position) {
+ scenario.onActivity(activity ->
+ verify(activity.getLogger())
+ .logWithInstanceIdAndPosition(
+ event, uid, packageName, activity.getInstanceId(), position));
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java b/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
index a7de9d6..27521ba 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
@@ -68,8 +68,8 @@
* @return {@link ViewPager2IdlingResource} that is registered to the activity related to the
* given {@link ActivityScenarioRule} and the resource ID of the ViewPager2.
*/
- public static ViewPager2IdlingResource register(
- ActivityScenario<PhotoPickerTestActivity> scenario, int viewPager2Id) {
+ public static <T extends PhotoPickerTestActivity> ViewPager2IdlingResource register(
+ ActivityScenario<T> scenario, int viewPager2Id) {
final ViewPager2IdlingResource[] idlingResources = new ViewPager2IdlingResource[1];
scenario.onActivity(
(activity -> {
diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
index 3cd2858..ae7221c 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
@@ -111,7 +111,8 @@
assertThat(periodicWorkRequest.getWorkSpec().expedited).isFalse();
assertThat(periodicWorkRequest.getWorkSpec().isPeriodic()).isTrue();
assertThat(periodicWorkRequest.getWorkSpec().id).isNotNull();
- assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
assertThat(periodicWorkRequest.getWorkSpec().input
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
.isEqualTo(SYNC_LOCAL_AND_CLOUD);
@@ -123,7 +124,7 @@
assertThat(periodicResetRequest.getWorkSpec().expedited).isFalse();
assertThat(periodicResetRequest.getWorkSpec().isPeriodic()).isTrue();
assertThat(periodicResetRequest.getWorkSpec().id).isNotNull();
- assertThat(periodicResetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
assertThat(periodicResetRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
assertThat(periodicResetRequest.getWorkSpec().input
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
@@ -162,7 +163,8 @@
assertThat(periodicWorkRequest.getWorkSpec().expedited).isFalse();
assertThat(periodicWorkRequest.getWorkSpec().isPeriodic()).isTrue();
assertThat(periodicWorkRequest.getWorkSpec().id).isNotNull();
- assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+ assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
assertThat(periodicWorkRequest.getWorkSpec().input
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
.isEqualTo(SYNC_LOCAL_AND_CLOUD);
@@ -174,7 +176,7 @@
assertThat(periodicResetRequest.getWorkSpec().expedited).isFalse();
assertThat(periodicResetRequest.getWorkSpec().isPeriodic()).isTrue();
assertThat(periodicResetRequest.getWorkSpec().id).isNotNull();
- assertThat(periodicResetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+ assertThat(periodicResetRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
assertThat(periodicResetRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
assertThat(periodicResetRequest.getWorkSpec().input
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
@@ -189,10 +191,32 @@
}
@Test
+ public void testAdhocProactiveSyncLocalOnly() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mPickerSyncManager.syncMediaProactively(/* localOnly */ true);
+ verify(mMockWorkManager, times(1))
+ .enqueueUniqueWork(anyString(),
+ any(),
+ mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(ProactiveSyncWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isFalse();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().id).isNotNull();
+ assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+ .isEqualTo(SYNC_LOCAL_ONLY);
+ }
+
+ @Test
public void testAdhocProactiveSync() {
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
- mPickerSyncManager.syncAllMediaProactively();
+ mPickerSyncManager.syncMediaProactively(/* localOnly */ false);
verify(mMockWorkManager, times(1))
.enqueueUniqueWork(anyString(),
any(),
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
index 113ecfc..4ca8dd9 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -63,6 +63,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
+import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.TestConfigStore;
@@ -70,6 +71,7 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.ItemsProvider;
import com.android.providers.media.photopicker.data.PaginationParameters;
+import com.android.providers.media.photopicker.data.Selection;
import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
@@ -84,6 +86,7 @@
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -119,6 +122,7 @@
when(mApplication.getApplicationContext()).thenReturn(sTargetContext);
mConfigStore = new TestConfigStore();
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(TEST_PACKAGE_NAME);
+ mConfigStore.enablePickerChoiceManagedSelectionEnabled();
getInstrumentation().runOnMainSync(() -> {
mPickerViewModel = new PickerViewModel(mApplication) {
@@ -209,8 +213,8 @@
LiveData<PickerViewModel.PaginatedItemsResult> testItems =
mPickerViewModel.getPaginatedItemsForAction(
- ACTION_VIEW_CREATED,
- new PaginationParameters());
+ ACTION_VIEW_CREATED,
+ new PaginationParameters());
DataLoaderThread.waitForIdle();
assertThat(testItems).isNotNull();
@@ -224,6 +228,45 @@
}
}
+ @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+ @Test
+ public void test_getRemainingPreGrantedItems_correctItemsLoaded() {
+ // Enable managed selection for this test.
+ Intent intent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+ intent.putExtra(Intent.EXTRA_UID, 0);
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ final int numberOfTestItems = 4;
+ final List<Item> expectedItems = generateFakeImageItemList(numberOfTestItems);
+ for (Item item : expectedItems) {
+ item.setPreGranted();
+ }
+ mItemsProvider.setItems(expectedItems);
+ List<String> preGrantedItems = List.of(expectedItems.get(0).getId(),
+ expectedItems.get(1).getId(),
+ expectedItems.get(2).getId());
+ Selection selection = mPickerViewModel.getSelection();
+ // Add 3 item ids is preGranted set.
+ selection.setPreGrantedItemSet(new HashSet<>(preGrantedItems));
+
+ // adding 1 item in selection item set.
+ selection.addSelectedItem(expectedItems.get(1));
+
+ // revoking grant for 1 id.
+ selection.removeSelectedItem(expectedItems.get(0));
+
+ // since only one item is added in selection set, the size should be one.
+ assertThat(selection.getSelectedItems().size()).isEqualTo(1);
+
+ // Since out of 3 one grant was removed, so there would be one item loaded when remaining
+ // grants are loaded.
+ mPickerViewModel.getRemainingPreGrantedItems();
+ DataLoaderThread.waitForIdle();
+
+ // Now the selection set should have 2 items.
+ assertThat(selection.getSelectedItems().size()).isEqualTo(2);
+ }
+
private static Item generateFakeImageItem(String id) {
final long dateTakenMs = System.currentTimeMillis()
+ Long.parseLong(id) * DateUtils.DAY_IN_MILLIS;
@@ -367,6 +410,60 @@
return c;
}
+ @Override
+ public Cursor getLocalItemsForSelection(Category category,
+ @NonNull List<Integer> localIdSelection,
+ @Nullable String[] mimeTypes,
+ @Nullable UserId userId,
+ @Nullable CancellationSignal cancellationSignal) throws IllegalArgumentException {
+ final String[] all_projection = new String[]{
+ ID,
+ // This field is unique to the cursor received by the pickerVIewModel.
+ // It is not a part of cloud provider contract.
+ ROW_ID,
+ DATE_TAKEN_MILLIS,
+ SYNC_GENERATION,
+ MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION,
+ SIZE_BYTES,
+ MEDIA_STORE_URI,
+ DURATION_MILLIS,
+ IS_FAVORITE,
+ WIDTH,
+ HEIGHT,
+ ORIENTATION,
+ DATA,
+ AUTHORITY,
+ };
+ final MatrixCursor c = new MatrixCursor(all_projection);
+
+ int itr = 1;
+ for (Item item : mItemList) {
+ if (localIdSelection.contains(Integer.parseInt(item.getId()))) {
+ c.addRow(new String[]{
+ item.getId(),
+ String.valueOf(itr),
+ String.valueOf(item.getDateTaken()),
+ String.valueOf(item.getGenerationModified()),
+ item.getMimeType(),
+ String.valueOf(item.getSpecialFormat()),
+ "1", // size_bytes
+ null, // media_store_uri
+ String.valueOf(item.getDuration()),
+ "0", // is_favorite
+ String.valueOf(800), // width
+ String.valueOf(500), // height
+ String.valueOf(0), // orientation
+ "/storage/emulated/0/foo",
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
+ });
+ itr++;
+ }
+ }
+ return c;
+
+ }
+
@Nullable
public Cursor getAllCategories(@Nullable String[] mimeType, @Nullable UserId userId,
@Nullable CancellationSignal cancellationSignal) {
diff --git a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
index 12b46ef..efba7ab 100644
--- a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
+++ b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
@@ -16,6 +16,10 @@
package com.android.providers.media.stableuris.job;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.createNewPublicVolume;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.deletePublicVolumes;
+import static com.android.providers.media.util.FileUtils.getVolumePath;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -34,6 +38,7 @@
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.MediaStore;
+import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SdkSuppress;
@@ -49,7 +54,6 @@
import java.io.File;
import java.io.FileOutputStream;
-import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
@@ -68,14 +72,18 @@
private static final String OWNERSHIP_BACKUP_NAME = "leveldb-ownership";
+ private static final String PUBLIC_VOLUME_BACKUP_NAME = "leveldb-";
+
private static boolean sInitialDeviceConfigValueForInternal = false;
private static boolean sInitialDeviceConfigValueForExternal = false;
+ private static boolean sInitialDeviceConfigValueForPublic = false;
+
private static final int IDLE_JOB_ID = -500;
@BeforeClass
- public static void setUpClass() {
+ public static void setUpClass() throws Exception {
adoptShellPermission();
// Read existing value of the flag
@@ -91,10 +99,20 @@
DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_EXTERNAL, Boolean.TRUE.toString(),
false);
+ sInitialDeviceConfigValueForPublic = Boolean.parseBoolean(
+ DeviceConfig.getProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+ ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_PUBLIC));
+ DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+ ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_PUBLIC, Boolean.TRUE.toString(),
+ false);
+
+ createNewPublicVolume();
}
@AfterClass
- public static void tearDownClass() throws IOException {
+ public static void tearDownClass() throws Exception {
+ deletePublicVolumes();
+
// Restore previous value of the flag
DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_INTERNAL,
@@ -102,6 +120,9 @@
DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_EXTERNAL,
String.valueOf(sInitialDeviceConfigValueForExternal), false);
+ DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+ ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_PUBLIC,
+ String.valueOf(sInitialDeviceConfigValueForPublic), false);
SystemClock.sleep(3000);
dropShellPermission();
}
@@ -192,6 +213,63 @@
}
@Test
+ public void testDataMigrationForPublicVolume() throws Exception {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final ContentResolver resolver = context.getContentResolver();
+ final Set<String> volNames = MediaStore.getExternalVolumeNames(context);
+
+ for (String volName : volNames) {
+ if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volName)
+ && !MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volName)) {
+ // public volume
+ Set<String> newFilePaths = new HashSet<String>();
+ Map<String, Long> pathToIdMap = new HashMap<>();
+ MediaStore.waitForIdle(resolver);
+
+ try {
+ for (int i = 0; i < 10; i++) {
+ File volPath = getVolumePath(context, volName);
+ final File dir = new File(volPath.getAbsoluteFile() + "/Download");
+ final File file = new File(dir, System.nanoTime() + ".png");
+
+ // Write 1 byte because 0 byte files are not valid in the db
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(1);
+ }
+
+ Uri uri = MediaStore.scanFile(resolver, file);
+ long id = ContentUris.parseId(uri);
+ newFilePaths.add(file.getAbsolutePath());
+ pathToIdMap.put(file.getAbsolutePath(), id);
+ }
+
+ assertFalse(newFilePaths.isEmpty());
+ MediaStore.waitForIdle(resolver);
+ // Creates backup
+ MediaStore.runIdleMaintenanceForStableUris(resolver);
+
+ verifyLevelDbPresence(resolver, PUBLIC_VOLUME_BACKUP_NAME + volName);
+ verifyLevelDbPresence(resolver, OWNERSHIP_BACKUP_NAME);
+ // Verify that all internal files are backed up
+ for (String filePath : newFilePaths) {
+ BackupIdRow backupIdRow = BackupIdRow.deserialize(
+ MediaStore.readBackup(resolver, volName, filePath));
+ assertNotNull(backupIdRow);
+ assertEquals(pathToIdMap.get(filePath).longValue(), backupIdRow.getId());
+ assertEquals(UserHandle.myUserId(), backupIdRow.getUserId());
+ assertEquals(context.getPackageName(),
+ MediaStore.getOwnerPackageName(resolver, backupIdRow.getOwnerPackageId()));
+ }
+ } finally {
+ for (String path : newFilePaths) {
+ new File(path).delete();
+ }
+ }
+ }
+ }
+ }
+
+ @Test
public void testJobScheduling() {
try {
final Context context = InstrumentationRegistry.getTargetContext();
diff --git a/tools/photopicker/res/layout/activity_main.xml b/tools/photopicker/res/layout/activity_main.xml
index 441cd0f..6348a4e 100644
--- a/tools/photopicker/res/layout/activity_main.xml
+++ b/tools/photopicker/res/layout/activity_main.xml
@@ -100,6 +100,13 @@
android:textSize="16sp" />
</LinearLayout>
+ <CheckBox
+ android:id="@+id/cbx_ordered_selection"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="ORDERED SELECTION"
+ android:textSize="16sp" />
+
<Button
android:id="@+id/launch_button"
android:layout_width="match_parent"
diff --git a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
index fc91077..1d549f3 100644
--- a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
+++ b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
@@ -63,6 +63,7 @@
private CheckBox mSetSelectionCountCheckBox;
private CheckBox mAllowMultipleCheckBox;
private CheckBox mGetContentCheckBox;
+ private CheckBox mOrderedSelectionCheckBox;
private EditText mMaxCountText;
private EditText mMimeTypeText;
@@ -77,6 +78,7 @@
mSetMimeTypeCheckBox = findViewById(R.id.cbx_set_mime_type);
mSetSelectionCountCheckBox = findViewById(R.id.cbx_set_selection_count);
mSetVideoOnlyCheckBox = findViewById(R.id.cbx_set_video_only);
+ mOrderedSelectionCheckBox = findViewById(R.id.cbx_ordered_selection);
mMaxCountText = findViewById(R.id.edittext_max_count);
mMimeTypeText = findViewById(R.id.edittext_mime_type);
mScrollView = findViewById(R.id.scrollview);
@@ -169,6 +171,10 @@
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
} else {
intent.putExtra(EXTRA_PICK_IMAGES_MAX, PICK_IMAGES_MAX_LIMIT);
+ // ordered selection is not allowed in get content.
+ if (mOrderedSelectionCheckBox.isChecked()) {
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+ }
}
}