Merge changes from topic "ExoPlayer_in_MP" into sc-mainline-prod
* changes:
Change Espresso tests to migrate to ExoPlayer
Add ExoPlayer support for video playback
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index af40b6b..21d25d7 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -75,9 +75,10 @@
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractFileExtension;
import static com.android.providers.media.util.FileUtils.extractFileName;
+import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
import static com.android.providers.media.util.FileUtils.extractRelativePath;
-import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
+import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
import static com.android.providers.media.util.FileUtils.extractVolumeName;
import static com.android.providers.media.util.FileUtils.extractVolumePath;
@@ -85,9 +86,10 @@
import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
import static com.android.providers.media.util.FileUtils.isCrossUserEnabled;
import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
import static com.android.providers.media.util.FileUtils.isDownload;
import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory;
-import static com.android.providers.media.util.FileUtils.isObbOrChildPath;
+import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
import static com.android.providers.media.util.FileUtils.sanitizePath;
import static com.android.providers.media.util.FileUtils.toFuseFile;
import static com.android.providers.media.util.Logging.LOGV;
@@ -325,7 +327,6 @@
private static final String DIRECTORY_MEDIA = "media";
private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
- private static final List<String> PRIVATE_SUBDIRECTORIES_ANDROID = Arrays.asList("data", "obb");
/**
* Hard-coded filename where the current value of
@@ -865,7 +866,17 @@
* deleted manually.
*/
private void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
- final String key = "created_default_folders_" + volume.getId();
+ final String volumeName = volume.getName();
+ String key;
+ if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
+ // For the primary volume, we use the ID, because we may be handling
+ // the primary volume for multiple users
+ key = "created_default_folders_" + volume.getId();
+ } else {
+ // For others, like public volumes, just use the name, because the id
+ // might not change when re-formatted
+ key = "created_default_folders_" + volumeName;
+ }
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
if (prefs.getInt(key, 0) == 0) {
@@ -2204,7 +2215,7 @@
}
// Get relative path for the contents of given directory.
- String relativePath = extractRelativePathForDirectory(path);
+ String relativePath = extractRelativePathWithDisplayName(path);
if (relativePath == null) {
// Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't
@@ -2478,8 +2489,8 @@
throws IllegalArgumentException {
// Try a simple check to see if the caller has full access to the given collections first
// before falling back to performing a query to probe for access.
- final String oldRelativePath = extractRelativePathForDirectory(oldPath);
- final String newRelativePath = extractRelativePathForDirectory(newPath);
+ final String oldRelativePath = extractRelativePathWithDisplayName(oldPath);
+ final String newRelativePath = extractRelativePathWithDisplayName(newPath);
boolean hasFullAccessToOldPath = false;
boolean hasFullAccessToNewPath = false;
for (String defaultDir : getIncludedDefaultDirectories()) {
@@ -3727,37 +3738,41 @@
}
/**
- * Check that values don't contain any external private path.
- * NOTE: The checks are gated on targetSDK S.
+ * For apps targetSdk >= S: Check that values does not contain any external private path.
+ * For all apps: Check that values does not contain any other app's external private paths.
*/
private void assertPrivatePathNotInValues(ContentValues values)
throws IllegalArgumentException {
- if (!CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
- Binder.getCallingUid())) {
- // For legacy apps, let the behaviour be as it is.
- return;
- }
-
ArrayList<String> relativePaths = new ArrayList<String>();
relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA)));
relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH));
- /**
- * Don't allow apps to insert/update database row to files in Android/data or
- * Android/obb dirs. These are app private directories and files in these private
- * directories can't be added to public media collection.
- */
+
for (final String relativePath : relativePaths) {
- if (relativePath == null) continue;
+ if (!isDataOrObbRelativePath(relativePath)) {
+ continue;
+ }
- final String[] relativePathSegments = relativePath.split("/", 3);
- final String primary =
- (relativePathSegments.length > 0) ? relativePathSegments[0] : null;
- final String secondary =
- (relativePathSegments.length > 1) ? relativePathSegments[1] : "";
+ /**
+ * Don't allow apps to insert/update database row to files in Android/data or
+ * Android/obb dirs. These are app private directories and files in these private
+ * directories can't be added to public media collection.
+ *
+ * Note: For backwards compatibility we allow apps with targetSdk < S to insert private
+ * files to MediaProvider
+ */
+ if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
+ Binder.getCallingUid())) {
+ throw new IllegalArgumentException(
+ "Inserting private file: " + relativePath + " is not allowed.");
+ }
- if (DIRECTORY_ANDROID_LOWER_CASE.equalsIgnoreCase(primary)
- && PRIVATE_SUBDIRECTORIES_ANDROID.contains(
- secondary.toLowerCase(Locale.ROOT))) {
+ /**
+ * Restrict all (legacy and non-legacy) apps from inserting paths in other
+ * app's private directories.
+ * Allow legacy apps to insert/update files in app private directories for backward
+ * compatibility but don't allow them to do so in other app's private directories.
+ */
+ if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) {
throw new IllegalArgumentException(
"Inserting private file: " + relativePath + " is not allowed.");
}
@@ -9171,25 +9186,28 @@
final LocalCallingIdentity token =
clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
try {
- // Files under the apps own private directory
- final String appSpecificDir = extractPathOwnerPackageName(path);
-
- if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
- return true;
- }
- // This is a private-package path; return true if accessible by the caller
- return isUidAllowedSpecialPrivatePathAccess(uid, path);
+ return isCallingIdentityAllowedAccessToDataOrObbPath(
+ extractRelativePathWithDisplayName(path));
} finally {
restoreLocalCallingIdentity(token);
}
}
+ private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) {
+ // Files under the apps own private directory
+ final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath);
+
+ if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
+ return true;
+ }
+ // This is a private-package relativePath; return true if accessible by the caller
+ return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath);
+ }
+
/**
* @return true iff the caller has installer privileges which gives write access to obb dirs.
- * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
- * package.
*/
- private boolean isCallingIdentityAllowedInstallerAccess(int uid) {
+ private boolean isCallingIdentityAllowedInstallerAccess() {
final boolean hasWrite = mCallingIdentity.get().
hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE);
@@ -9238,15 +9256,15 @@
return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
}
- private boolean isCallingIdentityDownloadProvider(int uid) {
- return uid == mDownloadsAuthorityAppId;
+ private boolean isCallingIdentityDownloadProvider() {
+ return getCallingUidOrSelf() == mDownloadsAuthorityAppId;
}
- private boolean isCallingIdentityExternalStorageProvider(int uid) {
- return uid == mExternalStorageAuthorityAppId;
+ private boolean isCallingIdentityExternalStorageProvider() {
+ return getCallingUidOrSelf() == mExternalStorageAuthorityAppId;
}
- private boolean isCallingIdentityMtp(int uid) {
+ private boolean isCallingIdentityMtp() {
return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP);
}
@@ -9260,30 +9278,25 @@
*
* Installer apps can only access private-app directories on Android/obb.
*
- * @param uid UID of the calling package
- * @param path the path of the file to access
+ * @param relativePath the relative path of the file to access
*/
- private boolean isUidAllowedSpecialPrivatePathAccess(int uid, String path) {
- final LocalCallingIdentity token =
- clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
- try {
- if (SdkLevel.isAtLeastS()) {
- return isMountModeAllowedPrivatePathAccess(uid, getCallingPackage(), path);
- } else {
- if (isCallingIdentityDownloadProvider(uid) ||
- isCallingIdentityExternalStorageProvider(uid) || isCallingIdentityMtp(
- uid)) {
- return true;
- }
- return (isObbOrChildPath(path) && isCallingIdentityAllowedInstallerAccess(uid));
+ private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) {
+ if (SdkLevel.isAtLeastS()) {
+ return isMountModeAllowedPrivatePathAccess(getCallingUidOrSelf(), getCallingPackage(),
+ relativePath);
+ } else {
+ if (isCallingIdentityDownloadProvider() ||
+ isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) {
+ return true;
}
- } finally {
- restoreLocalCallingIdentity(token);
+ return (isObbOrChildRelativePath(relativePath) &&
+ isCallingIdentityAllowedInstallerAccess());
}
}
@RequiresApi(Build.VERSION_CODES.S)
- private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName, String path) {
+ private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName,
+ String relativePath) {
// This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access
// mount modes.
final CallingIdentity token = clearCallingIdentity();
@@ -9294,7 +9307,7 @@
case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH:
return true;
case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER:
- return isObbOrChildPath(path);
+ return isObbOrChildRelativePath(relativePath);
}
} catch (Exception e) {
Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e);
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index 4b176dc..61907fd 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.app.Activity;
+import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -76,6 +77,8 @@
private static final int TAB_CHIP_TYPE_PHOTOS = 0;
private static final int TAB_CHIP_TYPE_ALBUMS = 1;
+ private static final float BOTTOM_SHEET_PEEK_HEIGHT_PERCENTAGE = 0.60f;
+
@IntDef(prefix = { "TAB_CHIP_TYPE" }, value = {
TAB_CHIP_TYPE_PHOTOS,
TAB_CHIP_TYPE_ALBUMS
@@ -317,9 +320,7 @@
private void initStateForBottomSheet() {
if (!mSelection.canSelectMultiple() && !isOrientationLandscape()) {
- final WindowManager windowManager = getSystemService(WindowManager.class);
- final Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds();
- final int peekHeight = (int) (displayBounds.height() * 0.60);
+ final int peekHeight = getBottomSheetPeekHeight(this);
mBottomSheetBehavior.setPeekHeight(peekHeight);
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
@@ -328,6 +329,12 @@
}
}
+ private static int getBottomSheetPeekHeight(Context context) {
+ final WindowManager windowManager = context.getSystemService(WindowManager.class);
+ final Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds();
+ return (int) (displayBounds.height() * BOTTOM_SHEET_PEEK_HEIGHT_PERCENTAGE);
+ }
+
private void restoreBottomSheetState() {
// BottomSheet is always EXPANDED for landscape
if (isOrientationLandscape()) {
diff --git a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
index 72560e8..b6ba48f 100644
--- a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
@@ -41,7 +41,7 @@
if (dateTaken == 0) {
mTitle.setText(R.string.recent);
} else {
- mTitle.setText(DateTimeUtils.getDateTimeString(itemView.getContext(), dateTaken));
+ mTitle.setText(DateTimeUtils.getDateTimeString(dateTaken));
}
}
}
diff --git a/src/com/android/providers/media/photopicker/util/DateTimeUtils.java b/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
index 8c07ff6..234067c 100644
--- a/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
+++ b/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
@@ -20,9 +20,11 @@
import static android.icu.text.RelativeDateTimeFormatter.Style.LONG;
import android.content.Context;
+import android.icu.text.DateFormat;
+import android.icu.text.DisplayContext;
import android.icu.text.RelativeDateTimeFormatter;
-import android.icu.text.RelativeDateTimeFormatter.Direction;
import android.icu.text.RelativeDateTimeFormatter.AbsoluteUnit;
+import android.icu.text.RelativeDateTimeFormatter.Direction;
import android.icu.util.ULocale;
import android.text.format.DateUtils;
@@ -41,6 +43,9 @@
*/
public class DateTimeUtils {
+ private static final String DATE_FORMAT_SKELETON = "EMMMd";
+ private static final String DATE_FORMAT_SKELETON_WITH_YEAR = "EMMMdy";
+
/**
* Formats a time according to the local conventions.
*
@@ -57,16 +62,16 @@
* since January 1, 1970 00:00:00.0 UTC.
* @return the formatted string
*/
- public static String getDateTimeString(Context context, long when) {
+ public static String getDateTimeString(long when) {
// Get the system time zone
final ZoneId zoneId = ZoneId.systemDefault();
final LocalDate nowDate = LocalDate.now(zoneId);
- return getDateTimeString(context, when, nowDate);
+ return getDateTimeString(when, nowDate);
}
@VisibleForTesting
- static String getDateTimeString(Context context, long when, LocalDate nowDate) {
+ static String getDateTimeString(long when, LocalDate nowDate) {
// Get the system time zone
final ZoneId zoneId = ZoneId.systemDefault();
final LocalDate whenDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(when),
@@ -80,17 +85,24 @@
} else if (dayDiff > 0 && dayDiff < 7) {
return whenDate.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault());
} else {
- int flags = DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE
- | DateUtils.FORMAT_ABBREV_ALL;
+ final String skeleton;
if (whenDate.getYear() == nowDate.getYear()) {
- flags |= DateUtils.FORMAT_NO_YEAR;
+ skeleton = DATE_FORMAT_SKELETON;
} else {
- flags |= DateUtils.FORMAT_SHOW_YEAR;
+ skeleton = DATE_FORMAT_SKELETON_WITH_YEAR;
}
- return DateUtils.formatDateTime(context, when, flags);
+
+ return getDateTimeString(when, skeleton, Locale.getDefault());
}
}
+ @VisibleForTesting
+ static String getDateTimeString(long when, String skeleton, Locale locale) {
+ final DateFormat format = DateFormat.getInstanceForSkeleton(skeleton, locale);
+ format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE);
+ return format.format(when);
+ }
+
/**
* It is borrowed from {@link DateUtils} since it is no official API yet.
*
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 8da9e97..8e7e75f 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -47,7 +47,6 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
-import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
@@ -88,7 +87,6 @@
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
-import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
@@ -967,20 +965,33 @@
+ "Android/(?:data|media|obb)/([^/]+)(/?.*)?");
/**
- * Regex that matches Android/obb or Android/data path.
+ * Regex that matches paths in all well-known package-specific relative directory
+ * path (as defined in {@link MediaColumns#RELATIVE_PATH})
+ * and which captures the package name as the first group.
*/
- public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
- "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
- + PROP_CROSS_USER_ROOT_PATTERN
- + "Android/(?:data|obb)/?$");
+ private static final Pattern PATTERN_OWNED_RELATIVE_PATH = Pattern.compile(
+ "(?i)^Android/(?:data|media|obb)/([^/]+)(/?.*)?");
/**
- * Regex that matches Android/obb paths.
+ * Regex that matches Android/obb or Android/data path.
*/
- public static final Pattern PATTERN_OBB_OR_CHILD_PATH = Pattern.compile(
+ private static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
"(?i)^/storage/[^/]+/(?:[0-9]+/)?"
+ PROP_CROSS_USER_ROOT_PATTERN
- + "Android/(?:obb)(/?.*)");
+ + "Android/(?:data|obb)(?:/.*)?$");
+
+ /**
+ * Regex that matches Android/obb or Android/data relative path (as defined in
+ * {@link MediaColumns#RELATIVE_PATH})
+ */
+ private static final Pattern PATTERN_DATA_OR_OBB_RELATIVE_PATH = Pattern.compile(
+ "(?i)^Android/(?:data|obb)(?:/.*)?$");
+
+ /**
+ * Regex that matches Android/obb {@link MediaColumns#RELATIVE_PATH}.
+ */
+ private static final Pattern PATTERN_OBB_OR_CHILD_RELATIVE_PATH = Pattern.compile(
+ "(?i)^Android/obb(?:/.*)?$");
private static final Pattern PATTERN_VISIBLE = Pattern.compile(
"(?i)^/storage/[^/]+(?:/[0-9]+)?$");
@@ -1115,14 +1126,13 @@
}
/**
- * Returns relative path for the directory.
+ * Returns relative path with display name.
*/
@VisibleForTesting
- public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) {
- if (directoryPath == null) return null;
+ public static @Nullable String extractRelativePathWithDisplayName(@Nullable String path) {
+ if (path == null) return null;
- if (directoryPath.equals("/storage/emulated") ||
- directoryPath.equals("/storage/emulated/")) {
+ if (path.equals("/storage/emulated") || path.equals("/storage/emulated/")) {
// This path is not reachable for MediaProvider.
return null;
}
@@ -1131,18 +1141,18 @@
// same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
// path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
// PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
- if (!directoryPath.endsWith("/")) {
+ if (!path.endsWith("/")) {
// Relative path for directory should end with "/".
- directoryPath += "/";
+ path += "/";
}
- final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath);
+ final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path);
if (matcher.find()) {
- if (matcher.end() == directoryPath.length()) {
+ if (matcher.end() == path.length()) {
// This is the top-level directory, so relative path is "/"
return "/";
}
- return directoryPath.substring(matcher.end());
+ return path.substring(matcher.end());
}
return null;
}
@@ -1152,9 +1162,17 @@
final Matcher m = PATTERN_OWNED_PATH.matcher(path);
if (m.matches()) {
return m.group(1);
- } else {
- return null;
}
+ return null;
+ }
+
+ public static @Nullable String extractOwnerPackageNameFromRelativePath(@Nullable String path) {
+ if (path == null) return null;
+ final Matcher m = PATTERN_OWNED_RELATIVE_PATH.matcher(path);
+ if (m.matches()) {
+ return m.group(1);
+ }
+ return null;
}
public static boolean isExternalMediaDirectory(@NonNull String path) {
@@ -1173,20 +1191,29 @@
}
/**
- * Returns true if relative path is Android/data or Android/obb path.
+ * Returns true if path is Android/data or Android/obb path.
*/
- public static boolean isDataOrObbPath(String path) {
+ public static boolean isDataOrObbPath(@Nullable String path) {
if (path == null) return false;
final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
return m.matches();
}
/**
+ * Returns true if relative path is Android/data or Android/obb path.
+ */
+ public static boolean isDataOrObbRelativePath(@Nullable String path) {
+ if (path == null) return false;
+ final Matcher m = PATTERN_DATA_OR_OBB_RELATIVE_PATH.matcher(path);
+ return m.matches();
+ }
+
+ /**
* Returns true if relative path is Android/obb path.
*/
- public static boolean isObbOrChildPath(String path) {
+ public static boolean isObbOrChildRelativePath(@Nullable String path) {
if (path == null) return false;
- final Matcher m = PATTERN_OBB_OR_CHILD_PATH.matcher(path);
+ final Matcher m = PATTERN_OBB_OR_CHILD_RELATIVE_PATH.matcher(path);
return m.matches();
}
@@ -1471,7 +1498,7 @@
// DCIM/Camera should always be visible regardless of .nomedia presence.
if (CAMERA_RELATIVE_PATH.equalsIgnoreCase(
- extractRelativePathForDirectory(dir.getAbsolutePath()))) {
+ extractRelativePathWithDisplayName(dir.getAbsolutePath()))) {
nomedia.delete();
return false;
}
diff --git a/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java b/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java
index b25e8df..3deb259 100644
--- a/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java
+++ b/tests/client/src/com/android/providers/media/client/DownloadProviderTest.java
@@ -16,10 +16,10 @@
package com.android.providers.media.client;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.createNewPublicVolume;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.deletePublicVolumes;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.executeShellCommand;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.pollForCondition;
+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.tests.utils.PublicVolumeSetupHelper.executeShellCommand;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.pollForCondition;
import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java b/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java
index f4eea1f..af17d50 100644
--- a/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java
+++ b/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java
@@ -19,10 +19,10 @@
import static android.provider.MediaStore.VOLUME_EXTERNAL;
import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.createNewPublicVolume;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.deletePublicVolumes;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.mountPublicVolume;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.unmountPublicVolume;
+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.tests.utils.PublicVolumeSetupHelper.mountPublicVolume;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.unmountPublicVolume;
import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java b/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
index def72f7..988dcf9 100644
--- a/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
+++ b/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
@@ -16,10 +16,10 @@
package com.android.providers.media.client;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.createNewPublicVolume;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.deletePublicVolumes;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.mountPublicVolume;
-import static com.android.providers.media.client.PublicVolumeSetupHelper.unmountPublicVolume;
+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.tests.utils.PublicVolumeSetupHelper.mountPublicVolume;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.unmountPublicVolume;
import static com.google.common.truth.Truth.assertThat;
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 26baf6e..8f66bf5 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -19,7 +19,7 @@
import static com.android.providers.media.scan.MediaScannerTest.stage;
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractRelativePath;
-import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
+import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
import static com.android.providers.media.util.FileUtils.isDownload;
import static com.android.providers.media.util.FileUtils.isDownloadDir;
@@ -1081,7 +1081,7 @@
"Foo.jpg",
"storage/Foo"
}) {
- assertEquals(null, FileUtils.extractRelativePathForDirectory(path));
+ assertEquals(null, FileUtils.extractRelativePathWithDisplayName(path));
}
}
@@ -1382,7 +1382,7 @@
private static void assertRelativePathForDirectory(String directoryPath, String relativePath) {
assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :")
- .that(extractRelativePathForDirectory(directoryPath))
+ .that(extractRelativePathWithDisplayName(directoryPath))
.isEqualTo(relativePath);
}
diff --git a/tests/src/com/android/providers/media/PublicVolumeTest.java b/tests/src/com/android/providers/media/PublicVolumeTest.java
new file mode 100644
index 0000000..503fab1
--- /dev/null
+++ b/tests/src/com/android/providers/media/PublicVolumeTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+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.tests.utils.PublicVolumeSetupHelper.partitionPublicVolume;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.util.FileUtils;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class PublicVolumeTest {
+ static final int POLL_DELAY_MS = 500;
+ static final int WAIT_FOR_DEFAULT_FOLDERS_MS = 30000;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ createNewPublicVolume();
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ deletePublicVolumes();
+ }
+
+ public boolean containsDefaultFolders(String rootPath) {
+ for (String dirName : FileUtils.DEFAULT_FOLDER_NAMES) {
+ final File defaultFolder = new File(rootPath, dirName);
+ if (!defaultFolder.exists()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean pollContainsDefaultFolders(String rootPath) {
+ // Default folders are created by MediaProvider after receiving a callback from
+ // the StorageManagerService that the volume has been mounted.
+ // Unfortunately, we don't have a reliable way to determine when this callback has
+ // happened, so poll here.
+ for (int i = 0; i < WAIT_FOR_DEFAULT_FOLDERS_MS / POLL_DELAY_MS; i++) {
+ if (containsDefaultFolders(rootPath)) {
+ return true;
+ }
+ try {
+ Thread.sleep(POLL_DELAY_MS);
+ } catch (InterruptedException e) {
+ }
+ }
+ return false;
+ }
+
+ @Test
+ public void testPublicVolumeDefaultFolders() throws Exception {
+ Context context = InstrumentationRegistry.getTargetContext();
+
+ // Reformat the volume, which should make sure we have default folders
+ partitionPublicVolume();
+
+ List<StorageVolume> volumes = context.
+ getSystemService(StorageManager.class).getStorageVolumes();
+ for (StorageVolume volume : volumes) {
+ // We only want to verify reliable public volumes (not OTG)
+ if (!volume.isPrimary() && volume.getPath().startsWith("/storage")) {
+ assertTrue(pollContainsDefaultFolders(volume.getPath()));
+ }
+ }
+
+ // We had a bug before where we didn't re-create public volumes when the same
+ // volume was re-formatted. Repartition it and try again.
+ partitionPublicVolume();
+
+ volumes = context.getSystemService(StorageManager.class).getStorageVolumes();
+ for (StorageVolume volume : volumes) {
+ // We only want to verify reliable public volumes (not OTG)
+ if (!volume.isPrimary() && volume.getPath().startsWith("/storage")) {
+ assertTrue(pollContainsDefaultFolders(volume.getPath()));
+ }
+ }
+ }
+}
+
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
new file mode 100644
index 0000000..47eec97
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.espresso;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.IdlingResource;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+
+public class BottomSheetIdlingResource implements IdlingResource {
+ private final BottomSheetBehavior<View> mBottomSheetBehavior;
+ private ResourceCallback mResourceCallback;
+
+ public BottomSheetIdlingResource(View bottomSheetView) {
+ mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
+ mBottomSheetBehavior.addBottomSheetCallback(new IdleListener());
+ }
+
+ @Override
+ public String getName() {
+ return BottomSheetIdlingResource.class.getName();
+ }
+
+ @Override
+ public boolean isIdleNow() {
+ int state = mBottomSheetBehavior.getState();
+ return state != BottomSheetBehavior.STATE_DRAGGING
+ && state != BottomSheetBehavior.STATE_SETTLING;
+ }
+
+ @Override
+ public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
+ mResourceCallback = resourceCallback;
+ }
+
+ private final class IdleListener extends BottomSheetBehavior.BottomSheetCallback {
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (mResourceCallback != null && isIdleNow()) {
+ mResourceCallback.onTransitionToIdle();
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetTestUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetTestUtils.java
new file mode 100644
index 0000000..7bd5f2b
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetTestUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.espresso;
+
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.providers.media.R;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+
+class BottomSheetTestUtils {
+ public static void assertBottomSheetState(Activity activity, int state) {
+ final BottomSheetBehavior<View> bottomSheetBehavior =
+ BottomSheetBehavior.from(activity.findViewById(R.id.bottom_sheet));
+ assertThat(bottomSheetBehavior.getState()).isEqualTo(state);
+ if (state == STATE_COLLAPSED) {
+ final int peekHeight =
+ getBottomSheetPeekHeight(PhotoPickerBaseTest.getIsolatedContext());
+ assertThat(bottomSheetBehavior.getPeekHeight()).isEqualTo(peekHeight);
+ }
+ }
+
+ private static int getBottomSheetPeekHeight(Context context) {
+ final WindowManager windowManager = context.getSystemService(WindowManager.class);
+ final Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds();
+ return (int) (displayBounds.height() * 0.60);
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java b/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java
index 569944e..f207436 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java
@@ -19,15 +19,22 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import android.view.View;
+
import androidx.test.espresso.Espresso;
+import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.action.GeneralLocation;
import androidx.test.espresso.action.GeneralSwipeAction;
import androidx.test.espresso.action.Press;
import androidx.test.espresso.action.Swipe;
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.espresso.matcher.ViewMatchers;
import com.android.providers.media.R;
+import org.hamcrest.Matcher;
+
public class CustomSwipeAction {
private static final int PREVIEW_VIEW_PAGER_ID = R.id.preview_viewPager;
@@ -57,4 +64,31 @@
onView(withId(PREVIEW_VIEW_PAGER_ID)).perform(customSwipeRight());
Espresso.onIdle();
}
+
+ /**
+ * A custom swipeDown method to avoid 90% visibility criteria on a view
+ */
+ public static ViewAction customSwipeDownPartialScreen() {
+ return withCustomConstraints(ViewActions.swipeDown(),
+ ViewMatchers.isDisplayingAtLeast(/* areaPercentage */ 60));
+ }
+
+ private static ViewAction withCustomConstraints(ViewAction action, Matcher<View> constraints) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return constraints;
+ }
+
+ @Override
+ public String getDescription() {
+ return action.getDescription();
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ action.perform(uiController, view);
+ }
+ };
+ }
}
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 cd995a8..161bc52 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
@@ -30,8 +30,12 @@
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.CustomSwipeAction.customSwipeDownPartialScreen;
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;
import static org.hamcrest.Matchers.allOf;
@@ -39,6 +43,8 @@
import android.app.Activity;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.action.ViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
@@ -69,6 +75,40 @@
}
@Test
+ public void testBottomSheetState() {
+ // Register bottom sheet idling resource so that we don't read bottom sheet state when
+ // in between changing states
+ registerBottomSheetStateIdlingResource();
+
+ // Single select PhotoPicker is launched in partial screen mode
+ onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
+ // Swipe up and check that the PhotoPicker is in full screen mode
+ onView(withId(DRAG_BAR_ID)).perform(ViewActions.swipeUp());
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
+
+ // Swipe down and check that the PhotoPicker is in partial screen mode
+ onView(withId(DRAG_BAR_ID)).perform(ViewActions.swipeDown());
+ mRule.getScenario().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());
+ assertThat(mRule.getScenario().getResult().getResultCode()).isEqualTo(
+ Activity.RESULT_CANCELED);
+ }
+
+ @Test
public void testToolbarLayout() {
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
@@ -116,4 +156,9 @@
.atPositionOnView(0, R.id.date_header_title))
.check(matches(withText(R.string.recent)));
}
+
+ private void registerBottomSheetStateIdlingResource() {
+ mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
+ new BottomSheetIdlingResource(activity.findViewById(R.id.bottom_sheet)))));
+ }
}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
index 741f707..e1c7ee5 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
@@ -42,7 +42,6 @@
import com.android.providers.media.R;
import com.android.providers.media.photopicker.util.DateTimeUtils;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -133,8 +132,8 @@
// Verify that first item is TODAY
onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
.atPositionOnView(0, dateHeaderTitleId))
- .check(matches(withText(DateTimeUtils.getDateTimeString(getTargetContext(),
- System.currentTimeMillis()))));
+ .check(matches(
+ withText(DateTimeUtils.getDateTimeString(System.currentTimeMillis()))));
final int photoItemPosition = 1;
// Verify first item is image and has no other icons other than thumbnail
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
index b32389f..d45290d 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
@@ -31,6 +31,7 @@
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.CustomSwipeAction.swipeLeftAndWait;
import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeRightAndWait;
import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.assertItemNotSelected;
@@ -38,16 +39,19 @@
import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.clickItem;
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;
import static org.hamcrest.Matchers.not;
+import android.app.Activity;
import android.view.View;
import androidx.lifecycle.ViewModelProvider;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.action.ViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import androidx.viewpager2.widget.ViewPager2;
@@ -74,6 +78,11 @@
@Test
public void testPreview_multiSelect_common() {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+ registerBottomSheetStateIdlingResource();
+ onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
// Select two items and Navigate to preview
clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
@@ -84,6 +93,9 @@
// No dragBar in preview
onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
assertMultiSelectPreviewCommonLayoutDisplayed();
onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(not(isDisplayed())));
@@ -97,6 +109,15 @@
// Shows dragBar after we are back to Photos tab
onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ mRule.getScenario().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());
+ assertThat(mRule.getScenario().getResult().getResultCode()).isEqualTo(
+ Activity.RESULT_CANCELED);
}
@Test
@@ -401,4 +422,9 @@
new ViewPager2IdlingResource(activity.findViewById(PREVIEW_VIEW_PAGER_ID)))));
Espresso.onIdle();
}
+
+ private void registerBottomSheetStateIdlingResource() {
+ mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
+ new BottomSheetIdlingResource(activity.findViewById(R.id.bottom_sheet)))));
+ }
}
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 70d8bed..8073cd2 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
@@ -32,8 +32,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.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;
@@ -68,6 +71,12 @@
public void testPreview_singleSelect_image() {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+ registerBottomSheetStateIdlingResource();
+ onView(withId(DRAG_BAR_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);
@@ -75,6 +84,9 @@
// No dragBar in preview
onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
// Verify image is previewed
assertSingleSelectCommonLayoutMatches();
@@ -89,6 +101,9 @@
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Shows dragBar after we are back to Photos tab
onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
}
@Test
@@ -237,6 +252,11 @@
Espresso.onIdle();
}
+ private void registerBottomSheetStateIdlingResource() {
+ mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
+ new BottomSheetIdlingResource(activity.findViewById(R.id.bottom_sheet)))));
+ }
+
private void assertBackgroundColorOnToolbarAndBottomBar(Activity activity, int colorResId) {
final Toolbar toolbar = activity.findViewById(R.id.toolbar);
final Drawable toolbarDrawable = toolbar.getBackground();
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
index 90c2cad..14f979c 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
@@ -23,11 +23,20 @@
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.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
+
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.action.ViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
+import static org.hamcrest.Matchers.not;
+
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
@@ -43,11 +52,28 @@
@Rule
public ActivityScenarioRule<PhotoPickerTestActivity> mRule =
- new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
+ new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@Test
public void testProfileButton_dialog() throws Exception {
+ // Register bottom sheet idling resource so that we don't read bottom sheet state when
+ // in between changing states
+ registerBottomSheetStateIdlingResource();
+
+ // Single select PhotoPicker is launched in half sheet mode
+ onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
final int profileButtonId = R.id.profile_button;
+ // Verify profile button is not displayed in partial screen
+ onView(withId(profileButtonId)).check(matches(not(isDisplayed())));
+
+ onView(withId(DRAG_BAR_ID)).perform(ViewActions.swipeUp());
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
// Verify profile button is displayed
onView(withId(profileButtonId)).check(matches(isDisplayed()));
// Check the text on the button. It should be "Switch to work"
@@ -59,4 +85,9 @@
onView(withText(R.string.picker_profile_work_paused_msg)).check(matches(isDisplayed()));
onView(withText(android.R.string.ok)).check(matches(isDisplayed())).perform(click());
}
+
+ private void registerBottomSheetStateIdlingResource() {
+ mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
+ new BottomSheetIdlingResource(activity.findViewById(R.id.bottom_sheet)))));
+ }
}
diff --git a/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java b/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
index e87c88b..e534cfd 100644
--- a/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
@@ -30,26 +30,21 @@
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
+import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class DateTimeUtilsTest {
- private Context mContext;
private static LocalDate FAKE_DATE =
LocalDate.of(2020 /* year */, 7 /* month */, 7 /* dayOfMonth */);
private static long FAKE_TIME =
FAKE_DATE.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
- @Before
- public void setUp() {
- mContext = InstrumentationRegistry.getTargetContext();
- }
-
@Test
public void testGetDateTimeString_today() throws Exception {
final long when = generateDateTimeMillis(FAKE_DATE);
- final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
assertThat(result).isEqualTo(DateTimeUtils.getTodayString());
}
@@ -59,7 +54,7 @@
final LocalDate whenDate = FAKE_DATE.minusDays(1);
final long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
assertThat(result).isEqualTo(DateTimeUtils.getYesterdayString());
}
@@ -69,7 +64,7 @@
final LocalDate whenDate = FAKE_DATE.minusDays(3);
final long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
assertThat(result).isEqualTo("Saturday");
}
@@ -79,7 +74,7 @@
final LocalDate whenDate = FAKE_DATE.minusMonths(1);
final long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
assertThat(result).isEqualTo("Sun, Jun 7");
}
@@ -89,11 +84,41 @@
final LocalDate whenDate = FAKE_DATE.minusYears(1);
long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(mContext, when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
assertThat(result).isEqualTo("Sun, Jul 7, 2019");
}
+ /**
+ * Test the capitalized issue in different languages b/208864827.
+ * E.g. For pt-BR
+ * Wrong format: ter, 16 de nov.
+ * Right format: Ter, 16 de nov.
+ */
+ @Test
+ public void testCapitalizedInDifferentLanguages() throws Exception {
+ final LocalDate whenDate = FAKE_DATE.minusMonths(1).minusDays(4);;
+ final long when = generateDateTimeMillis(whenDate);
+ final String skeleton = "EMMMd";
+
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("PT-BR")))
+ .isEqualTo("Qua., 3 de jun.");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("ET")))
+ .isEqualTo("K, 3. juuni");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("LV")))
+ .isEqualTo("Trešd., 3. jūn.");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("BE")))
+ .isEqualTo("Ср, 3 чэр");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("RU")))
+ .isEqualTo("Ср, 3 июн.");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("SQ")))
+ .isEqualTo("Mër, 3 qer");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("IT")))
+ .isEqualTo("Mer 3 giu");
+ assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("KK")))
+ .isEqualTo("3 мау., ср");
+ }
+
@Test
public void testIsSameDay_differentYear_false() throws Exception {
final LocalDate whenDate = FAKE_DATE.minusYears(1);
@@ -127,4 +152,3 @@
return when.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
}
-
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 398f431..f241be3 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -40,12 +40,17 @@
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractFileExtension;
import static com.android.providers.media.util.FileUtils.extractFileName;
+import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
+import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
import static com.android.providers.media.util.FileUtils.extractRelativePath;
import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
import static com.android.providers.media.util.FileUtils.extractVolumeName;
import static com.android.providers.media.util.FileUtils.extractVolumePath;
import static com.android.providers.media.util.FileUtils.fromFuseFile;
+import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory;
+import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
import static com.android.providers.media.util.FileUtils.toFuseFile;
import static com.android.providers.media.util.FileUtils.translateModeAccessToPosix;
import static com.android.providers.media.util.FileUtils.translateModePfdToPosix;
@@ -878,6 +883,95 @@
assertThat(FileUtils.isDirectoryDirty(null)).isFalse();
}
+ @Test
+ public void testExtractPathOwnerPackageName() {
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data/foo"))
+ .isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb/foo"))
+ .isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media/foo"))
+ .isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/data/foo"))
+ .isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/obb/foo"))
+ .isEqualTo("foo");
+ assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media/foo"))
+ .isEqualTo("foo");
+
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data")).isNull();
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb")).isNull();
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media")).isNull();
+ assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media")).isNull();
+ assertThat(extractPathOwnerPackageName("/storage/emulated/0/Pictures/foo")).isNull();
+ assertThat(extractPathOwnerPackageName("Android/data")).isNull();
+ assertThat(extractPathOwnerPackageName("Android/obb")).isNull();
+ }
+
+ @Test
+ public void testExtractOwnerPackageNameFromRelativePath() {
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/data/foo")).isEqualTo("foo");
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/obb/foo")).isEqualTo("foo");
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/media/foo")).isEqualTo("foo");
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/media/foo.com/files"))
+ .isEqualTo("foo.com");
+
+ assertThat(extractOwnerPackageNameFromRelativePath("/storage/emulated/0/Android/data/foo"))
+ .isNull();
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/data")).isNull();
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/obb")).isNull();
+ assertThat(extractOwnerPackageNameFromRelativePath("Android/media")).isNull();
+ assertThat(extractOwnerPackageNameFromRelativePath("Pictures/foo")).isNull();
+ }
+
+ @Test
+ public void testIsDataOrObbPath() {
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/data")).isTrue();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb")).isTrue();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data")).isTrue();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb")).isTrue();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/data/foo")).isTrue();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb/foo")).isTrue();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data/foo")).isTrue();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb/foo")).isTrue();
+
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/media/")).isFalse();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/media/")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Pictures/")).isFalse();
+ assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obbfoo")).isFalse();
+ assertThat(isDataOrObbPath("/storage/emulated/0/Android/datafoo")).isFalse();
+ assertThat(isDataOrObbPath("Android/")).isFalse();
+ assertThat(isDataOrObbPath("Android/media/")).isFalse();
+ }
+
+ @Test
+ public void testIsDataOrObbRelativePath() {
+ assertThat(isDataOrObbRelativePath("Android/data")).isTrue();
+ assertThat(isDataOrObbRelativePath("Android/obb")).isTrue();
+ assertThat(isDataOrObbRelativePath("Android/data/foo")).isTrue();
+ assertThat(isDataOrObbRelativePath("Android/obb/foo")).isTrue();
+
+ assertThat(isDataOrObbRelativePath("/storage/emulated/0/Android/data")).isFalse();
+ assertThat(isDataOrObbRelativePath("Android/")).isFalse();
+ assertThat(isDataOrObbRelativePath("Android/media/")).isFalse();
+ assertThat(isDataOrObbRelativePath("Pictures/")).isFalse();
+ }
+
+ @Test
+ public void testIsObbOrChildRelativePath() {
+ assertThat(isObbOrChildRelativePath("Android/obb")).isTrue();
+ assertThat(isObbOrChildRelativePath("Android/obb/")).isTrue();
+ assertThat(isObbOrChildRelativePath("Android/obb/foo.com")).isTrue();
+
+ assertThat(isObbOrChildRelativePath("/storage/emulated/0/Android/obb")).isFalse();
+ assertThat(isObbOrChildRelativePath("/storage/emulated/0/Android/")).isFalse();
+ assertThat(isObbOrChildRelativePath("Android/")).isFalse();
+ assertThat(isObbOrChildRelativePath("Android/media/")).isFalse();
+ assertThat(isObbOrChildRelativePath("Pictures/")).isFalse();
+ assertThat(isObbOrChildRelativePath("Android/obbfoo")).isFalse();
+ assertThat(isObbOrChildRelativePath("Android/data")).isFalse();
+ }
+
private File getNewDirInDownload(String name) {
File file = new File(mTestDownloadDir, name);
assertTrue(file.mkdir());
diff --git a/tests/client/src/com/android/providers/media/client/PublicVolumeSetupHelper.java b/tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java
similarity index 89%
rename from tests/client/src/com/android/providers/media/client/PublicVolumeSetupHelper.java
rename to tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java
index 73bcf41..777abd8 100644
--- a/tests/client/src/com/android/providers/media/client/PublicVolumeSetupHelper.java
+++ b/tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.providers.media.client;
+package com.android.providers.media.tests.utils;
import android.app.UiAutomation;
import android.os.Environment;
@@ -37,16 +37,29 @@
/**
* Helper methods for public volume setup.
*/
-class PublicVolumeSetupHelper {
+public class PublicVolumeSetupHelper {
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(2);
private static final long POLLING_SLEEP_MILLIS = 100;
private static final String TAG = "TestUtils";
private static boolean usingExistingPublicVolume = false;
/**
+ * (Re-)partitions an already created pulic volume
+ */
+ public static void partitionPublicVolume() throws Exception {
+ pollForCondition(() -> partitionDisk(), "Timed out while waiting for"
+ + " disk partitioning");
+ // Poll twice to avoid using previous mount status
+ pollForCondition(() -> isPublicVolumeMounted(), "Timed out while waiting for"
+ + " the public volume to mount");
+ pollForCondition(() -> isExternalStorageStateMounted(), "Timed out while"
+ + " waiting for ExternalStorageState to be MEDIA_MOUNTED");
+ }
+
+ /**
* Creates a new virtual public volume and returns the volume's name.
*/
- static void createNewPublicVolume() throws Exception {
+ public static void createNewPublicVolume() throws Exception {
// Skip public volume setup if we can use already available public volume on the device.
if (getCurrentPublicVolumeString() != null && isPublicVolumeMounted()) {
usingExistingPublicVolume = true;
@@ -54,13 +67,8 @@
}
executeShellCommand("sm set-force-adoptable on");
executeShellCommand("sm set-virtual-disk true");
- pollForCondition(() -> partitionDisk(), "Timed out while waiting for"
- + " disk partitioning");
- // Poll twice to avoid using previous mount status
- pollForCondition(() -> isPublicVolumeMounted(), "Timed out while waiting for"
- + " the public volume to mount");
- pollForCondition(() -> isExternalStorageStateMounted(), "Timed out while"
- + " waiting for ExternalStorageState to be MEDIA_MOUNTED");
+
+ partitionPublicVolume();
}
private static boolean isExternalStorageStateMounted() {
@@ -112,7 +120,7 @@
/**
* @return the currently mounted public volume string, if any.
*/
- private static String getCurrentPublicVolumeString() {
+ static String getCurrentPublicVolumeString() {
final String[] allPublicVolumeDetails;
try {
allPublicVolumeDetails = executeShellCommand("sm list-volumes public")
@@ -134,15 +142,15 @@
return null;
}
- static void mountPublicVolume() throws Exception {
+ public static void mountPublicVolume() throws Exception {
executeShellCommand("sm mount " + getPublicVolumeString());
}
- static void unmountPublicVolume() throws Exception {
+ public static void unmountPublicVolume() throws Exception {
executeShellCommand("sm unmount " + getPublicVolumeString());
}
- static void deletePublicVolumes() throws Exception {
+ public static void deletePublicVolumes() throws Exception {
if (!usingExistingPublicVolume) {
executeShellCommand("sm set-virtual-disk false");
// Wait for the public volume to disappear.
@@ -181,7 +189,7 @@
}
}
- static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
+ public static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
throws Exception {
for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
if (condition.get()) {