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()) {