Merge changes I73e06f6d,I39cf223d,I93417795
* changes:
Support transforms in file path
Add JNI methods to support multiple nodes with same name
Add FUSE node fields to support multiple nodes with same name
diff --git a/OWNERS b/OWNERS
index 9156e6b..53a4e42 100644
--- a/OWNERS
+++ b/OWNERS
@@ -2,5 +2,5 @@
maco@google.com
marcone@google.com
nandana@google.com
-shafik@google.com
zezeozue@google.com
+corinac@google.com
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index ce5f4d3..dfb989d 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -107,8 +107,8 @@
// Regex copied from FileUtils.java in MediaProvider, but without media directory.
const std::regex PATTERN_OWNED_PATH(
- "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
- std::regex_constants::icase);
+ "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/([^/]+)(/?.*)?",
+ std::regex_constants::icase);
/*
* In order to avoid double caching with fuse, call fadvise on the file handles
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index e6c51ea..9c8d778 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -137,7 +137,8 @@
int IsOpendirAllowed(const std::string& path, uid_t uid, bool forWrite);
/**
- * Determines if the given package name matches its uid.
+ * Determines if the given package name matches its uid
+ * or has special access to priv-app directories
*
* @param pkg the package name of the app
* @param uid UID of the app
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 10f4d77..717786d 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -54,6 +54,7 @@
import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
import static com.android.providers.media.util.DatabaseUtils.bindList;
+import static com.android.providers.media.util.FileUtils.DEFAULT_FOLDER_NAMES;
import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL;
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractFileName;
@@ -124,6 +125,7 @@
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.storage.StorageManager;
@@ -182,6 +184,7 @@
import com.android.providers.media.util.Metrics;
import com.android.providers.media.util.MimeUtils;
import com.android.providers.media.util.PermissionUtils;
+import com.android.providers.media.util.RedactingFileDescriptor;
import com.android.providers.media.util.SQLiteQueryBuilder;
import com.android.providers.media.util.XmpInterface;
@@ -303,6 +306,9 @@
@GuardedBy("sCacheLock")
private static final ArrayMap<File, String> sCachedVolumePathToId = new ArrayMap<>();
+ // WARNING/TODO: This will be replaced by signature APIs in S
+ private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
+
@GuardedBy("mShouldRedactThreadIds")
private final LongArray mShouldRedactThreadIds = new LongArray();
@@ -403,6 +409,8 @@
private StorageManager mStorageManager;
private AppOpsManager mAppOpsManager;
private PackageManager mPackageManager;
+ private int mExternalStorageAuthorityAppId;
+ private int mDownloadsAuthorityAppId;
private Size mThumbSize;
@@ -743,29 +751,6 @@
}
}
- private static final String[] sDefaultFolderNames = {
- Environment.DIRECTORY_MUSIC,
- Environment.DIRECTORY_PODCASTS,
- Environment.DIRECTORY_RINGTONES,
- Environment.DIRECTORY_ALARMS,
- Environment.DIRECTORY_NOTIFICATIONS,
- Environment.DIRECTORY_PICTURES,
- Environment.DIRECTORY_MOVIES,
- Environment.DIRECTORY_DOWNLOADS,
- Environment.DIRECTORY_DCIM,
- Environment.DIRECTORY_DOCUMENTS,
- Environment.DIRECTORY_AUDIOBOOKS,
- };
-
- private static boolean isDefaultDirectoryName(@Nullable String dirName) {
- for (String defaultDirName : sDefaultFolderNames) {
- if (defaultDirName.equals(dirName)) {
- return true;
- }
- }
- return false;
- }
-
/**
* Ensure that default folders are created on mounted primary storage
* devices. We only do this once per volume so we don't annoy the user if
@@ -790,7 +775,7 @@
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getContext());
if (prefs.getInt(key, 0) == 0) {
- for (String folderName : sDefaultFolderNames) {
+ for (String folderName : DEFAULT_FOLDER_NAMES) {
final File folder = new File(vol.getDirectory(), folderName);
if (!folder.exists()) {
folder.mkdirs();
@@ -937,6 +922,20 @@
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to start watching " + PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, e);
}
+
+ ProviderInfo provider = mPackageManager.resolveContentProvider(
+ DOWNLOADS_PROVIDER_AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+ if (provider != null) {
+ mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
+ }
+
+ provider = mPackageManager.resolveContentProvider(
+ MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+ if (provider != null) {
+ mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
+ }
return true;
}
@@ -2130,7 +2129,7 @@
} else if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) {
// Allow rename of files/folders other than default directories.
final String displayName = extractDisplayName(oldPath);
- for (String defaultFolder : sDefaultFolderNames) {
+ for (String defaultFolder : DEFAULT_FOLDER_NAMES) {
if (displayName.equals(defaultFolder)) {
Log.e(TAG, errorMessage + oldPath + " is a default folder."
+ " Renaming a default folder is not allowed.");
@@ -3105,7 +3104,11 @@
if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
// Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
// FileColumns.MEDIA_TYPE is already populated.
- } else if (path != null && shouldFileBeHidden(new File(path))) {
+ } else if (isFuseThread() && path != null && shouldFileBeHidden(new File(path))) {
+ // We should only mark MEDIA_TYPE as MEDIA_TYPE_NONE for Fuse Thread.
+ // MediaProvider#insert() returns the uri by appending the "rowId" to the given
+ // uri, hence to ensure the correct working of the returned uri, we shouldn't
+ // change the MEDIA_TYPE in insert operation and let scan change it for us.
values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
} else {
values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
@@ -6996,7 +6999,7 @@
if (isTopLevelDir) {
// We allow creating the default top level directories only, all other operations on
// top level directories are not allowed.
- if (forCreate && isDefaultDirectoryName(extractDisplayName(path))) {
+ if (forCreate && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
return 0;
}
Log.e(TAG,
@@ -7061,7 +7064,7 @@
final boolean isTopLevelDir =
relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
if (isTopLevelDir) {
- if (isDefaultDirectoryName(extractDisplayName(path))) {
+ if (FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
return 0;
} else {
Log.e(TAG,
@@ -7082,12 +7085,22 @@
final LocalCallingIdentity token =
clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
try {
- return isCallingIdentitySharedPackageName(packageName);
+ return isCallingIdentitySharedPackageName(packageName) ||
+ isCallingIdentityAllowedPrivAppAccess(uid);
} finally {
restoreLocalCallingIdentity(token);
}
}
+ /**
+ * External Storage Provider and Download Provider can access priv app directories.
+ *
+ * @param uid UID of the calling package
+ */
+ private boolean isCallingIdentityAllowedPrivAppAccess(int uid) {
+ return (uid == mExternalStorageAuthorityAppId) || (uid == mDownloadsAuthorityAppId);
+ }
+
private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
// System internals can work with all media
if (isCallingPackageSelf() || isCallingPackageShell()) {
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 489a9db..9311c0f 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -158,9 +158,9 @@
private static final int BATCH_SIZE = 32;
private static final Pattern PATTERN_VISIBLE = Pattern.compile(
- "(?i)^/storage/[^/]+(?:/[0-9]+)?(?:/Android/sandbox/([^/]+))?$");
+ "(?i)^/storage/[^/]+(?:/[0-9]+)?$");
private static final Pattern PATTERN_INVISIBLE = Pattern.compile(
- "(?i)^/storage/[^/]+(?:/[0-9]+)?(?:/Android/sandbox/([^/]+))?/"
+ "(?i)^/storage/[^/]+(?:/[0-9]+)?/"
+ "(?:(?:Android/(?:data|obb)$)|"
+ "(?:\\.transcode$)|"
+ "(?:(?:Movies|Music|Pictures)/.thumbnails$))");
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index dcb9e25..e816ef9 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -848,9 +848,9 @@
}
public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile(
- "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/.+");
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/.+");
public static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile(
- "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/?");
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/?");
public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile(
"(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$");
public static final Pattern PATTERN_PENDING_FILEPATH_FOR_SQL = Pattern.compile(
@@ -891,7 +891,7 @@
* and which captures the package name as the first group.
*/
public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
- "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/?.*)?");
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb)/([^/]+)(/?.*)?");
/**
* Regex that matches Android/obb or Android/data path.
@@ -899,12 +899,26 @@
public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
"(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/?$");
+ @VisibleForTesting
+ public static final String[] DEFAULT_FOLDER_NAMES = {
+ Environment.DIRECTORY_MUSIC,
+ Environment.DIRECTORY_PODCASTS,
+ Environment.DIRECTORY_RINGTONES,
+ Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_NOTIFICATIONS,
+ Environment.DIRECTORY_PICTURES,
+ Environment.DIRECTORY_MOVIES,
+ Environment.DIRECTORY_DOWNLOADS,
+ Environment.DIRECTORY_DCIM,
+ Environment.DIRECTORY_DOCUMENTS,
+ Environment.DIRECTORY_AUDIOBOOKS,
+ };
+
/**
- * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it
- * captures both top-level paths and sandboxed paths.
+ * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}
*/
private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
- "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)(Android/sandbox/([^/]+)/)?");
+ "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)");
/**
* Regex that matches paths under well-known storage paths.
@@ -912,6 +926,9 @@
private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
"(?i)^/storage/([^/]+)");
+ private static final String CAMERA_RELATIVE_PATH =
+ String.format("%s/%s/", Environment.DIRECTORY_DCIM, "Camera");
+
private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
}
@@ -1024,6 +1041,15 @@
return relativePathSegments.length > 0 ? relativePathSegments[0] : null;
}
+ public static boolean isDefaultDirectoryName(@Nullable String dirName) {
+ for (String defaultDirName : DEFAULT_FOLDER_NAMES) {
+ if (defaultDirName.equalsIgnoreCase(dirName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
* columns being modified by this operation.
@@ -1221,12 +1247,32 @@
}
final File nomedia = new File(dir, ".nomedia");
+
// check for .nomedia presence
- if (nomedia.exists()) {
- Logging.logPersistent("Observed non-standard " + nomedia);
- return true;
+ if (!nomedia.exists()) {
+ return false;
}
- return false;
+
+ // Handle top-level default directories. These directories should always be visible,
+ // regardless of .nomedia presence.
+ final String[] relativePath = sanitizePath(extractRelativePath(dir.getAbsolutePath()));
+ final boolean isTopLevelDir =
+ relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
+ if (isTopLevelDir && isDefaultDirectoryName(name)) {
+ nomedia.delete();
+ return false;
+ }
+
+ // DCIM/Camera should always be visible regardless of .nomedia presence.
+ if (CAMERA_RELATIVE_PATH.equalsIgnoreCase(
+ extractRelativePathForDirectory(dir.getAbsolutePath()))) {
+ nomedia.delete();
+ return false;
+ }
+
+ // .nomedia is present which makes this directory as hidden directory
+ Logging.logPersistent("Observed non-standard " + nomedia);
+ return true;
}
/**
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index b2b7825..210f901 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -336,17 +336,6 @@
{
final ContentValues values = new ContentValues();
values.put(FileColumns.DATA,
- "/storage/0000-0000/Android/sandbox/com.example2/Download/dir/foo.mp4");
- values.put(FileColumns.DATE_ADDED, System.currentTimeMillis());
- values.put(FileColumns.DATE_MODIFIED, System.currentTimeMillis());
- values.put(FileColumns.DISPLAY_NAME, "foo.mp4");
- values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_VIDEO);
- values.put(FileColumns.MIME_TYPE, "video/mp4");
- assertFalse(db.insert("files", FileColumns.DATA, values) == -1);
- }
- {
- final ContentValues values = new ContentValues();
- values.put(FileColumns.DATA,
"/storage/emulated/0/Download/foo");
values.put(FileColumns.DATE_ADDED, System.currentTimeMillis());
values.put(FileColumns.DATE_MODIFIED, System.currentTimeMillis());
@@ -396,18 +385,6 @@
c.getString(c.getColumnIndexOrThrow(FileColumns.OWNER_PACKAGE_NAME)));
assertEquals("1", c.getString(c.getColumnIndexOrThrow(FileColumns.IS_DOWNLOAD)));
}
- try (Cursor c = db.query("files", null, FileColumns.DISPLAY_NAME + "='foo.mp4'",
- null, null, null, null)) {
- assertEquals(1, c.getCount());
- assertTrue(c.moveToFirst());
- assertEquals("/storage/0000-0000/Android/sandbox/com.example2/Download/dir/foo.mp4",
- c.getString(c.getColumnIndexOrThrow(FileColumns.DATA)));
- assertEquals("video/mp4",
- c.getString(c.getColumnIndexOrThrow(FileColumns.MIME_TYPE)));
- assertEquals("com.example2",
- c.getString(c.getColumnIndexOrThrow(FileColumns.OWNER_PACKAGE_NAME)));
- assertEquals("1", c.getString(c.getColumnIndexOrThrow(FileColumns.IS_DOWNLOAD)));
- }
try (Cursor c = db.query("files", null,
FileColumns.DATA + "='/storage/emulated/0/Download/foo'",
null, null, null, null)) {
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 76faa16..30df5dc 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -434,8 +434,6 @@
getPathOwnerPackageName("/storage/emulated/0/Android/obb/com.example/foo.jpg"));
assertEquals("com.example",
getPathOwnerPackageName("/storage/emulated/0/Android/media/com.example/foo.jpg"));
- assertEquals("com.example",
- getPathOwnerPackageName("/storage/emulated/0/Android/sandbox/com.example/foo.jpg"));
}
@Test
@@ -832,36 +830,19 @@
assertTrue(isDownload("/storage/emulated/0/Download/test.pdf"));
assertTrue(isDownload("/storage/emulated/0/Download/dir/foo.mp4"));
assertTrue(isDownload("/storage/0000-0000/Download/foo.txt"));
- assertTrue(isDownload(
- "/storage/emulated/0/Android/sandbox/com.example/Download/colors.png"));
- assertTrue(isDownload(
- "/storage/emulated/0/Android/sandbox/shared-com.uid.shared/Download/colors.png"));
- assertTrue(isDownload(
- "/storage/0000-0000/Android/sandbox/com.example/Download/colors.png"));
- assertTrue(isDownload(
- "/storage/0000-0000/Android/sandbox/shared-com.uid.shared/Download/colors.png"));
-
assertFalse(isDownload("/storage/emulated/0/Pictures/colors.png"));
assertFalse(isDownload("/storage/emulated/0/Pictures/Download/colors.png"));
assertFalse(isDownload("/storage/emulated/0/Android/data/com.example/Download/foo.txt"));
- assertFalse(isDownload(
- "/storage/emulated/0/Android/sandbox/com.example/dir/Download/foo.txt"));
assertFalse(isDownload("/storage/emulated/0/Download"));
- assertFalse(isDownload("/storage/emulated/0/Android/sandbox/com.example/Download"));
- assertFalse(isDownload(
- "/storage/0000-0000/Android/sandbox/shared-com.uid.shared/Download"));
}
@Test
public void testIsDownloadDir() throws Exception {
assertTrue(isDownloadDir("/storage/emulated/0/Download"));
- assertTrue(isDownloadDir("/storage/emulated/0/Android/sandbox/com.example/Download"));
assertFalse(isDownloadDir("/storage/emulated/0/Download/colors.png"));
assertFalse(isDownloadDir("/storage/emulated/0/Download/dir/"));
- assertFalse(isDownloadDir(
- "/storage/emulated/0/Android/sandbox/com.example/Download/dir/foo.txt"));
}
@Test
@@ -922,7 +903,6 @@
for (String top : new String[] {
"/storage/emulated/0",
- "/storage/emulated/0/Android/sandbox/com.example",
}) {
values = computeDataValues(top + "/IMG1024.JPG");
assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index aa62a63..5a78d7c 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -45,6 +45,7 @@
import static org.mockito.Mockito.when;
import android.Manifest;
+import android.app.UiAutomation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -59,6 +60,7 @@
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
import android.util.Pair;
import androidx.test.InstrumentationRegistry;
@@ -68,20 +70,26 @@
import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
import com.android.providers.media.util.FileUtils;
+import com.google.common.io.ByteStreams;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
import java.util.Optional;
@RunWith(AndroidJUnit4.class)
public class ModernMediaScannerTest {
// TODO: scan directory-vs-files and confirm identical results
+ private static final String TAG = "ModernMediaScannerTest";
private File mDir;
private Context mIsolatedContext;
@@ -375,15 +383,11 @@
public void testShouldScanPathAndIsPathHidden() {
for (String prefix : new String[] {
"/storage/emulated/0",
- "/storage/emulated/0/Android/sandbox/com.example",
"/storage/0000-0000",
- "/storage/0000-0000/Android/sandbox/com.example",
}) {
assertShouldScanPathAndIsPathHidden(true, false, new File(prefix));
assertShouldScanPathAndIsPathHidden(true, false, new File(prefix + "/meow"));
assertShouldScanPathAndIsPathHidden(true, false, new File(prefix + "/Android/meow"));
- assertShouldScanPathAndIsPathHidden(true, false,
- new File(prefix + "/Android/sandbox/meow"));
assertShouldScanPathAndIsPathHidden(true, true, new File(prefix + "/.meow/dir"));
@@ -405,6 +409,41 @@
}
}
+ private void assertVisibleFolder(File dir) throws Exception {
+ final File nomediaFile = new File(dir, ".nomedia");
+
+ if (!nomediaFile.getParentFile().exists()) {
+ assertTrue(nomediaFile.getParentFile().mkdirs());
+ }
+ try {
+ if (!nomediaFile.exists()) {
+ executeShellCommand("touch " + nomediaFile.getAbsolutePath());
+ assertTrue(nomediaFile.exists());
+ }
+ assertShouldScanPathAndIsPathHidden(true, false, dir);
+ } finally {
+ executeShellCommand("rm " + nomediaFile.getAbsolutePath());
+ }
+ }
+
+ /**
+ * b/168830497: Test that default folders and Camera folder are always visible
+ */
+ @Test
+ public void testVisibleDefaultFolders() throws Exception {
+ final File root = new File("storage/emulated/0");
+
+ // Top level directories should always be visible
+ for (String dirName : FileUtils.DEFAULT_FOLDER_NAMES) {
+ final File defaultFolder = new File(root, dirName);
+ assertVisibleFolder(defaultFolder);
+ }
+
+ // DCIM/Camera should always be visible
+ final File cameraDir = new File(root, Environment.DIRECTORY_DCIM + "/" + "Camera");
+ assertVisibleFolder(cameraDir);
+ }
+
private static void assertShouldScanDirectory(File file) {
assertTrue(file.getAbsolutePath(), shouldScanDirectory(file));
}
@@ -417,16 +456,12 @@
public void testShouldScanDirectory() throws Exception {
for (String prefix : new String[] {
"/storage/emulated/0",
- "/storage/emulated/0/Android/sandbox/com.example",
"/storage/0000-0000",
- "/storage/0000-0000/Android/sandbox/com.example",
}) {
assertShouldScanDirectory(new File(prefix));
assertShouldScanDirectory(new File(prefix + "/meow"));
assertShouldScanDirectory(new File(prefix + "/Android"));
assertShouldScanDirectory(new File(prefix + "/Android/meow"));
- assertShouldScanDirectory(new File(prefix + "/Android/sandbox"));
- assertShouldScanDirectory(new File(prefix + "/Android/sandbox/meow"));
assertShouldScanDirectory(new File(prefix + "/.meow"));
assertShouldntScanDirectory(new File(prefix + "/Android/data"));
@@ -453,9 +488,7 @@
public void testIsDirectoryHidden() throws Exception {
for (String prefix : new String[] {
"/storage/emulated/0",
- "/storage/emulated/0/Android/sandbox/com.example",
"/storage/0000-0000",
- "/storage/0000-0000/Android/sandbox/com.example",
}) {
assertDirectoryNotHidden(new File(prefix));
assertDirectoryNotHidden(new File(prefix + "/meow"));
@@ -902,4 +935,30 @@
cursor.getInt(cursor.getColumnIndex(MediaColumns.HEIGHT)));
}
}
+
+
+ /**
+ * Executes a shell command.
+ */
+ public static String executeShellCommand(String command) throws IOException {
+ int attempt = 0;
+ while (attempt++ < 5) {
+ try {
+ return executeShellCommandInternal(command);
+ } catch (InterruptedIOException e) {
+ // Hmm, we had trouble executing the shell command; the best we
+ // can do is try again a few more times
+ Log.v(TAG, "Trouble executing " + command + "; trying again", e);
+ }
+ }
+ throw new IOException("Failed to execute " + command);
+ }
+
+ private static String executeShellCommandInternal(String cmd) throws IOException {
+ UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ try (FileInputStream output = new FileInputStream(
+ uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
+ return new String(ByteStreams.toByteArray(output));
+ }
+ }
}