Merge "Durable metadata for pending and trashed files." into rvc-dev
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 4f9528e..e49052d 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -734,10 +734,32 @@
final ContentValues values = new ContentValues();
while (c.moveToNext()) {
values.clear();
+
+ // Start by deriving all values from migrated data column,
+ // then overwrite with other migrated columns
+ final String data = c.getString(c.getColumnIndex(MediaColumns.DATA));
+ values.put(MediaColumns.DATA, data);
+ FileUtils.computeValuesFromData(values);
for (String column : sMigrateColumns) {
DatabaseUtils.copyFromCursorToContentValues(column, c, values);
}
+ // When migrating pending or trashed files, we might need to
+ // rename them on disk to match new schema
+ FileUtils.computeDataFromValues(values,
+ new File(FileUtils.extractVolumePath(data)));
+ final String recomputedData = values.getAsString(MediaColumns.DATA);
+ if (!Objects.equals(data, recomputedData)) {
+ try {
+ Os.rename(data, recomputedData);
+ } catch (ErrnoException e) {
+ // We only have one shot to migrate data, so log and
+ // keep marching forward
+ Log.w(TAG, "Failed to rename " + values + "; continuing");
+ FileUtils.computeValuesFromData(values);
+ }
+ }
+
if (db.insert("files", null, values) == -1) {
// We only have one shot to migrate data, so log and
// keep marching forward
@@ -752,7 +774,7 @@
// We have to guard ourselves against any weird behavior of the
// legacy provider by trying to catch everything
db.execSQL("ROLLBACK TO before_migrate");
- Log.w(TAG, "Failed migration from legacy provider: " + e);
+ Log.wtf(TAG, "Failed migration from legacy provider", e);
mMigrationListener.onFinished(client, mVolumeName);
}
}
@@ -1119,7 +1141,7 @@
final long id = c.getLong(0);
final String data = c.getString(1);
values.put(FileColumns.DATA, data);
- FileUtils.computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
values.remove(FileColumns.DATA);
if (!values.isEmpty()) {
db.update("files", values, "_id=" + id, null);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index d48d8b9..77497f4 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -52,7 +52,6 @@
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.computeDataValues;
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractFileName;
import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
@@ -62,7 +61,6 @@
import static com.android.providers.media.util.FileUtils.extractVolumeName;
import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
import static com.android.providers.media.util.FileUtils.isDownload;
-import static com.android.providers.media.util.FileUtils.sanitizeDisplayName;
import static com.android.providers.media.util.FileUtils.sanitizePath;
import static com.android.providers.media.util.Logging.LOGV;
import static com.android.providers.media.util.Logging.TAG;
@@ -1377,7 +1375,7 @@
computeAudioLocalizedValues(values);
computeAudioKeyValues(values);
}
- computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
return values;
}
@@ -2105,14 +2103,7 @@
// Force values when raw path provided
if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
- final String data = values.getAsString(MediaColumns.DATA);
-
- if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
- values.put(MediaColumns.DISPLAY_NAME, extractDisplayName(data));
- }
- if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
- values.put(MediaColumns.MIME_TYPE, MimeUtils.resolveMimeType(new File(data)));
- }
+ FileUtils.computeValuesFromData(values);
}
// Extract the MIME type from the display name if we couldn't resolve it from the raw path
if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
@@ -2170,42 +2161,42 @@
}
}
+ // Use default directories when missing
+ if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) {
+ if (defaultSecondary != null) {
+ values.put(MediaColumns.RELATIVE_PATH,
+ defaultPrimary + '/' + defaultSecondary + '/');
+ } else {
+ values.put(MediaColumns.RELATIVE_PATH,
+ defaultPrimary + '/');
+ }
+ }
+
// Generate path when undefined
if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
- if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) {
- if (defaultPrimary != null) {
- if (defaultSecondary != null) {
- values.put(MediaColumns.RELATIVE_PATH,
- defaultPrimary + '/' + defaultSecondary + '/');
- } else {
- values.put(MediaColumns.RELATIVE_PATH,
- defaultPrimary + '/');
- }
- }
- }
-
- final String[] relativePath = sanitizePath(
- values.getAsString(MediaColumns.RELATIVE_PATH));
- final String displayName = sanitizeDisplayName(
- values.getAsString(MediaColumns.DISPLAY_NAME));
-
- // Create result file
- File res;
+ File volumePath;
try {
- res = getVolumePath(resolvedVolumeName);
+ volumePath = getVolumePath(resolvedVolumeName);
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(e);
}
- res = FileUtils.buildPath(res, relativePath);
+
+ FileUtils.sanitizeValues(values);
+ FileUtils.computeDataFromValues(values, volumePath);
+
+ // Create result file
+ File res = new File(values.getAsString(MediaColumns.DATA));
try {
if (makeUnique) {
- res = FileUtils.buildUniqueFile(res, mimeType, displayName);
+ res = FileUtils.buildUniqueFile(res.getParentFile(),
+ mimeType, res.getName());
} else {
- res = FileUtils.buildNonUniqueFile(res, mimeType, displayName);
+ res = FileUtils.buildNonUniqueFile(res.getParentFile(),
+ mimeType, res.getName());
}
} catch (FileNotFoundException e) {
throw new IllegalStateException(
- "Failed to build unique file: " + res + " " + displayName + " " + mimeType);
+ "Failed to build unique file: " + res + " " + values);
}
// Require that content lives under well-defined directories to help
@@ -2215,7 +2206,8 @@
boolean validPath = res.getAbsolutePath().equals(currentPath);
// Next, consider allowing based on allowed primary directory
- final String primary = relativePath[0];
+ final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
+ final String primary = (relativePath.length > 0) ? relativePath[0] : null;
if (!validPath) {
validPath = allowedPrimary.contains(primary);
}
@@ -2275,8 +2267,13 @@
case AUDIO_ALBUMART:
case VIDEO_THUMBNAILS:
case IMAGES_THUMBNAILS:
- values.remove(MediaColumns.DISPLAY_NAME);
- values.remove(MediaColumns.MIME_TYPE);
+ final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class)
+ .keySet();
+ for (String key : new ArraySet<>(values.keySet())) {
+ if (!valid.contains(key)) {
+ values.remove(key);
+ }
+ }
break;
}
@@ -2544,7 +2541,7 @@
// compute bucket_id and bucket_display_name for all files
String path = values.getAsString(MediaStore.MediaColumns.DATA);
- computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
String title = values.getAsString(MediaStore.MediaColumns.TITLE);
@@ -2939,7 +2936,7 @@
values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
// Playlist names are stored as display names, but leave
// values untouched if the caller is ModernMediaScanner
- if (Binder.getCallingUid() != android.os.Process.myUid()) {
+ if (!isCallingPackageSystem()) {
if (values.containsKey(Playlists.NAME)) {
values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
}
@@ -4597,7 +4594,7 @@
case AUDIO_PLAYLISTS_ID:
// Playlist names are stored as display names, but leave
// values untouched if the caller is ModernMediaScanner
- if (Binder.getCallingUid() != android.os.Process.myUid()) {
+ if (!isCallingPackageSystem()) {
if (initialValues.containsKey(Playlists.NAME)) {
initialValues.put(MediaColumns.DISPLAY_NAME,
initialValues.getAsString(Playlists.NAME));
@@ -4624,6 +4621,7 @@
case VIDEO_MEDIA_ID:
case IMAGES_MEDIA_ID:
case DOWNLOADS_ID:
+ case FILES_ID:
break;
default:
throw new IllegalArgumentException("Movement of " + uri
@@ -4737,7 +4735,7 @@
case IMAGES_MEDIA_ID:
case FILES_ID:
case DOWNLOADS_ID: {
- computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
break;
}
}
@@ -6764,6 +6762,9 @@
sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE);
+ sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING);
+ sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED);
+ sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
}
/**
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 4540d56..dae8cfb 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -684,8 +684,7 @@
*/
private static @Nullable ContentProviderOperation.Builder scanItem(long existingId, File file,
BasicFileAttributes attrs, String mimeType, String volumeName) {
- final String name = file.getName();
- if (name.startsWith(".")) {
+ if (isFileHidden(file)) {
if (LOGD) Log.d(TAG, "Ignoring hidden file: " + file);
return null;
}
@@ -695,7 +694,7 @@
}
int mediaType = MimeUtils.resolveMediaType(mimeType);
- if (mediaType == FileColumns.MEDIA_TYPE_IMAGE && isFileAlbumArt(name)) {
+ if (mediaType == FileColumns.MEDIA_TYPE_IMAGE && isFileAlbumArt(file)) {
mediaType = FileColumns.MEDIA_TYPE_NONE;
}
switch (mediaType) {
@@ -1260,7 +1259,7 @@
/**
* Test if any parents of given directory should be considered hidden.
*/
- static boolean isDirectoryHiddenRecursive(File dir) {
+ static boolean isDirectoryHiddenRecursive(@NonNull File dir) {
Trace.beginSection("isDirectoryHiddenRecursive");
try {
while (dir != null) {
@@ -1278,7 +1277,8 @@
/**
* Test if this given directory should be considered hidden.
*/
- static boolean isDirectoryHidden(File dir) {
+ @VisibleForTesting
+ static boolean isDirectoryHidden(@NonNull File dir) {
final File nomedia = new File(dir, ".nomedia");
// Handle well-known paths that should always be visible or invisible,
@@ -1307,16 +1307,36 @@
return false;
}
+ /**
+ * Test if this given file should be considered hidden.
+ */
@VisibleForTesting
- static boolean isFileAlbumArt(String name) {
- return PATTERN_ALBUM_ART.matcher(name).matches();
+ static boolean isFileHidden(@NonNull File file) {
+ final String name = file.getName();
+
+ // Handle well-known file names that are pending or trashed; they
+ // normally appear hidden, but we give them special treatment
+ if (FileUtils.PATTERN_EXPIRES_FILE.matcher(name).matches()) {
+ return false;
+ }
+
+ // Otherwise fall back to file name
+ if (name.startsWith(".")) {
+ return true;
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static boolean isFileAlbumArt(@NonNull File file) {
+ return PATTERN_ALBUM_ART.matcher(file.getName()).matches();
}
/**
* Test if this given {@link Uri} is a
* {@link android.provider.MediaStore.Audio.Playlists} item.
*/
- static boolean isPlaylist(Uri uri) {
+ static boolean isPlaylist(@NonNull Uri uri) {
final List<String> path = uri.getPathSegments();
return (path.size() == 4) && path.get(1).equals("audio") && path.get(2).equals("playlists");
}
@@ -1333,7 +1353,7 @@
return true;
}
- static void logTroubleScanning(File file, Exception e) {
+ static void logTroubleScanning(@NonNull File file, @NonNull Exception e) {
if (LOGW) Log.w(TAG, "Trouble scanning " + file + ": " + e);
}
}
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index a42233f..ef33b04 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -531,4 +531,16 @@
}
return sb.toString();
}
+
+ public static boolean getAsBoolean(@NonNull ContentValues values,
+ @NonNull String key, boolean def) {
+ final Integer value = values.getAsInteger(key);
+ return (value != null) ? (value != 0) : def;
+ }
+
+ public static long getAsLong(@NonNull ContentValues values,
+ @NonNull String key, long def) {
+ final Long value = values.getAsLong(key);
+ return (value != null) ? value : def;
+ }
}
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index c051bef..fcee8bd 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -16,6 +16,8 @@
package com.android.providers.media.util;
+import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
+import static com.android.providers.media.util.DatabaseUtils.getAsLong;
import static com.android.providers.media.util.Logging.TAG;
import android.content.ClipDescription;
@@ -25,9 +27,9 @@
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.MediaStore;
-import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -46,7 +48,6 @@
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
@@ -260,7 +261,7 @@
* Recursively delete all contents inside the given directory. Gracefully
* attempts to delete as much as possible in the face of any failures.
*
- * @deprecated if you're calling this from inside {@link MediaProvider}, you
+ * @deprecated if you're calling this from inside {@code MediaProvider}, you
* likely want to call {@link #forEach} with a separate
* invocation to invalidate FUSE entries.
*/
@@ -639,6 +640,30 @@
"(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/.+");
public static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile(
"(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/?");
+ public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile(
+ "(?i)^\\.(pending|trashed)-(\\d+)-(.+)$");
+
+ /**
+ * File prefix indicating that the file {@link MediaColumns#IS_PENDING}.
+ */
+ public static final String PREFIX_PENDING = "pending";
+
+ /**
+ * File prefix indicating that the file {@link MediaColumns#IS_TRASHED}.
+ */
+ public static final String PREFIX_TRASHED = "trashed";
+
+ /**
+ * Default duration that {@link MediaColumns#IS_PENDING} items should be
+ * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
+ */
+ public static final long DEFAULT_DURATION_PENDING = DateUtils.WEEK_IN_MILLIS;
+
+ /**
+ * Default duration that {@link MediaColumns#IS_TRASHED} items should be
+ * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
+ */
+ public static final long DEFAULT_DURATION_TRASHED = DateUtils.WEEK_IN_MILLIS;
public static boolean isDownload(@NonNull String path) {
return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
@@ -678,6 +703,16 @@
return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null;
}
+ public static @Nullable String extractVolumePath(@Nullable String data) {
+ if (data == null) return null;
+ final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
+ if (matcher.find()) {
+ return data.substring(0, matcher.end());
+ } else {
+ return null;
+ }
+ }
+
public static @Nullable String extractVolumeName(@Nullable String data) {
if (data == null) return null;
final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
@@ -750,12 +785,22 @@
return null;
}
- public static void computeDataValues(@NonNull ContentValues values) {
+ /**
+ * Compute several scattered {@link MediaColumns} values from
+ * {@link MediaColumns#DATA}. This method performs no enforcement of
+ * argument validity.
+ */
+ public static void computeValuesFromData(@NonNull ContentValues values) {
// Worst case we have to assume no bucket details
- values.remove(ImageColumns.BUCKET_ID);
- values.remove(ImageColumns.BUCKET_DISPLAY_NAME);
- values.remove(ImageColumns.VOLUME_NAME);
- values.remove(ImageColumns.RELATIVE_PATH);
+ values.remove(MediaColumns.VOLUME_NAME);
+ values.remove(MediaColumns.RELATIVE_PATH);
+ values.remove(MediaColumns.IS_DOWNLOAD);
+ values.remove(MediaColumns.IS_PENDING);
+ values.remove(MediaColumns.IS_TRASHED);
+ values.remove(MediaColumns.DATE_EXPIRES);
+ values.remove(MediaColumns.DISPLAY_NAME);
+ values.remove(MediaColumns.BUCKET_ID);
+ values.remove(MediaColumns.BUCKET_DISPLAY_NAME);
final String data = values.getAsString(MediaColumns.DATA);
if (TextUtils.isEmpty(data)) return;
@@ -763,21 +808,78 @@
final File file = new File(data);
final File fileLower = new File(data.toLowerCase(Locale.ROOT));
- values.put(ImageColumns.VOLUME_NAME, extractVolumeName(data));
- values.put(ImageColumns.RELATIVE_PATH, extractRelativePath(data));
- values.put(ImageColumns.DISPLAY_NAME, extractDisplayName(data));
+ values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
+ values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
+ values.put(MediaColumns.IS_DOWNLOAD, isDownload(data));
+
+ final String displayName = extractDisplayName(data);
+ final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
+ if (matcher.matches()) {
+ values.put(MediaColumns.IS_PENDING,
+ matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0);
+ values.put(MediaColumns.IS_TRASHED,
+ matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0);
+ values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2)));
+ values.put(MediaColumns.DISPLAY_NAME, matcher.group(3));
+ } else {
+ values.put(MediaColumns.IS_PENDING, 0);
+ values.put(MediaColumns.IS_TRASHED, 0);
+ values.putNull(MediaColumns.DATE_EXPIRES);
+ values.put(MediaColumns.DISPLAY_NAME, displayName);
+ }
// Buckets are the parent directory
final String parent = fileLower.getParent();
if (parent != null) {
- values.put(ImageColumns.BUCKET_ID, parent.hashCode());
+ values.put(MediaColumns.BUCKET_ID, parent.hashCode());
// The relative path for files in the top directory is "/"
- if (!"/".equals(values.getAsString(ImageColumns.RELATIVE_PATH))) {
- values.put(ImageColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName());
+ if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) {
+ values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName());
}
}
}
+ /**
+ * Compute {@link MediaColumns#DATA} from several scattered
+ * {@link MediaColumns} values. This method performs no enforcement of
+ * argument validity.
+ */
+ public static void computeDataFromValues(@NonNull ContentValues values,
+ @NonNull File volumePath) {
+ values.remove(MediaColumns.DATA);
+
+ final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
+ final String resolvedDisplayName;
+ if (getAsBoolean(values, MediaColumns.IS_PENDING, false)) {
+ final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
+ (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
+ resolvedDisplayName = String.format(".%s-%d-%s",
+ FileUtils.PREFIX_PENDING, dateExpires, displayName);
+ } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) {
+ final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
+ (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
+ resolvedDisplayName = String.format(".%s-%d-%s",
+ FileUtils.PREFIX_TRASHED, dateExpires, displayName);
+ } else {
+ resolvedDisplayName = displayName;
+ }
+
+ final File filePath = buildPath(volumePath,
+ values.getAsString(MediaColumns.RELATIVE_PATH), resolvedDisplayName);
+ values.put(MediaColumns.DATA, filePath.getAbsolutePath());
+ }
+
+ public static void sanitizeValues(@NonNull ContentValues values) {
+ final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
+ for (int i = 0; i < relativePath.length; i++) {
+ relativePath[i] = sanitizeDisplayName(relativePath[i]);
+ }
+ values.put(MediaColumns.RELATIVE_PATH,
+ String.join("/", relativePath) + "/");
+ values.put(MediaColumns.DISPLAY_NAME,
+ sanitizeDisplayName(values.getAsString(MediaColumns.DISPLAY_NAME)));
+ }
+
/** {@hide} **/
@Nullable
public static String getAbsoluteSanitizedPath(String path) {
diff --git a/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java b/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
index a7d7e98..79efb4c 100644
--- a/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
+++ b/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
@@ -20,8 +20,6 @@
import static android.provider.MediaStore.VOLUME_INTERNAL;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.ContentResolver;
@@ -113,38 +111,6 @@
mContentResolver.delete(ContentUris.withAppendedId(mExternalAudio, mBlue), null);
}
- /**
- * Verify that creating playlists using only {@link Playlists#NAME} defined
- * will flow into the {@link MediaColumns#DISPLAY_NAME}, both during initial
- * insert and subsequent updates.
- */
- @Test
- public void testName() throws Exception {
- final String name1 = "Playlist " + System.nanoTime();
- final String name2 = "Playlist " + System.nanoTime();
- assertNotEquals(name1, name2);
-
- mValues.clear();
- mValues.put(Playlists.NAME, name1);
- final Uri playlist = mContentResolver.insert(mExternalPlaylists, mValues);
- try (Cursor c = mContentResolver.query(playlist,
- new String[] { Playlists.NAME, MediaColumns.DISPLAY_NAME }, null, null)) {
- assertTrue(c.moveToFirst());
- assertTrue(c.getString(0).startsWith(name1));
- assertTrue(c.getString(1).startsWith(name1));
- }
-
- mValues.clear();
- mValues.put(Playlists.NAME, name2);
- mContentResolver.update(playlist, mValues, null);
- try (Cursor c = mContentResolver.query(playlist,
- new String[] { Playlists.NAME, MediaColumns.DISPLAY_NAME }, null, null)) {
- assertTrue(c.moveToFirst());
- assertTrue(c.getString(0).startsWith(name2));
- assertTrue(c.getString(1).startsWith(name2));
- }
- }
-
@Test
public void testAdd() throws Exception {
mValues.clear();
diff --git a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
index 89390d3..fa2de15 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -106,6 +106,7 @@
final ContentValues values = new ContentValues();
values.put(FileColumns.MEDIA_TYPE, mediaType);
values.put(MediaColumns.DATA, file.getAbsolutePath());
+ values.put(MediaColumns.DISPLAY_NAME, file.getName());
values.put(MediaColumns.MIME_TYPE, mimeType);
values.put(MediaColumns.VOLUME_NAME, mVolumeName);
values.put(MediaColumns.DATE_ADDED, String.valueOf(System.currentTimeMillis() / 1_000));
@@ -215,6 +216,9 @@
// Drop media type from the columns we check, since it's implicitly
// verified via the collection Uri
values.remove(FileColumns.MEDIA_TYPE);
+
+ // Drop raw path, since we may rename pending or trashed files
+ values.remove(FileColumns.DATA);
}
// Clear data on the modern provider so that the initial scan recovers
@@ -232,9 +236,9 @@
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
final Bundle extras = new Bundle();
extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
- MediaColumns.DATA + "=?");
+ MediaColumns.DISPLAY_NAME + "=?");
extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
- new String[] { legacyFile.getAbsolutePath() });
+ new String[] { legacyFile.getName() });
extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 304124f..f4fa45d 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -34,7 +34,6 @@
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
-import android.os.CancellationSignal;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.Images.ImageColumns;
@@ -51,8 +50,8 @@
import com.android.providers.media.util.FileUtils;
import com.android.providers.media.util.SQLiteQueryBuilder;
-import org.junit.After;
-import org.junit.Before;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -66,25 +65,28 @@
public class MediaProviderTest {
static final String TAG = "MediaProviderTest";
- @Before
- public void setUp() {
+ private static Context sIsolatedContext;
+ private static ContentResolver sIsolatedResolver;
+
+ @BeforeClass
+ public static void setUp() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+
+ final Context context = InstrumentationRegistry.getTargetContext();
+ sIsolatedContext = new IsolatedContext(context, "modern");
+ sIsolatedResolver = sIsolatedContext.getContentResolver();
}
- @After
- public void tearDown() {
+ @AfterClass
+ public static void tearDown() {
InstrumentationRegistry.getInstrumentation()
.getUiAutomation().dropShellPermissionIdentity();
}
@Test
public void testSchema() {
- final Context context = InstrumentationRegistry.getTargetContext();
- final Context isolatedContext = new IsolatedContext(context, "modern");
- final ContentResolver isolatedResolver = isolatedContext.getContentResolver();
-
for (String path : new String[] {
"images/media",
"images/media/1",
@@ -123,11 +125,11 @@
}) {
final Uri probe = MediaStore.AUTHORITY_URI.buildUpon()
.appendPath(MediaStore.VOLUME_EXTERNAL).appendEncodedPath(path).build();
- try (Cursor c = isolatedResolver.query(probe, null, null, null)) {
+ try (Cursor c = sIsolatedResolver.query(probe, null, null, null)) {
assertNotNull("probe", c);
}
try {
- isolatedResolver.getType(probe);
+ sIsolatedResolver.getType(probe);
} catch (IllegalStateException tolerated) {
}
}
@@ -135,11 +137,7 @@
@Test
public void testLocale() {
- final Context context = InstrumentationRegistry.getTargetContext();
- final Context isolatedContext = new IsolatedContext(context, "modern");
- final ContentResolver isolatedResolver = isolatedContext.getContentResolver();
-
- try (ContentProviderClient cpc = isolatedResolver
+ try (ContentProviderClient cpc = sIsolatedResolver
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
((MediaProvider) cpc.getLocalContentProvider())
.onLocaleChanged();
@@ -310,6 +308,78 @@
}
@Test
+ public void testBuildData_Pending_FromValues() throws Exception {
+ final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ final ContentValues forward = new ContentValues();
+ forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
+ forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG");
+ forward.put(MediaColumns.MIME_TYPE, "image/jpeg");
+ forward.put(MediaColumns.IS_PENDING, 1);
+ forward.put(MediaColumns.IS_TRASHED, 0);
+ forward.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+ ensureFileColumns(uri, forward);
+
+ // Requested filename remains intact, but raw path on disk is mutated to
+ // reflect that it's a pending item with a specific expiration time
+ assertEquals("IMG1024.JPG",
+ forward.getAsString(MediaColumns.DISPLAY_NAME));
+ assertEndsWith(".pending-1577836800-IMG1024.JPG",
+ forward.getAsString(MediaColumns.DATA));
+ }
+
+ @Test
+ public void testBuildData_Pending_FromData() throws Exception {
+ final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ final ContentValues reverse = new ContentValues();
+ reverse.put(MediaColumns.DATA,
+ "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG");
+ ensureFileColumns(uri, reverse);
+
+ assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
+ assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME));
+ assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE));
+ assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_PENDING));
+ assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED));
+ assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES));
+ }
+
+ @Test
+ public void testBuildData_Trashed_FromValues() throws Exception {
+ final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ final ContentValues forward = new ContentValues();
+ forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
+ forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG");
+ forward.put(MediaColumns.MIME_TYPE, "image/jpeg");
+ forward.put(MediaColumns.IS_PENDING, 0);
+ forward.put(MediaColumns.IS_TRASHED, 1);
+ forward.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+ ensureFileColumns(uri, forward);
+
+ // Requested filename remains intact, but raw path on disk is mutated to
+ // reflect that it's a trashed item with a specific expiration time
+ assertEquals("IMG1024.JPG",
+ forward.getAsString(MediaColumns.DISPLAY_NAME));
+ assertEndsWith(".trashed-1577836800-IMG1024.JPG",
+ forward.getAsString(MediaColumns.DATA));
+ }
+
+ @Test
+ public void testBuildData_Trashed_FromData() throws Exception {
+ final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ final ContentValues reverse = new ContentValues();
+ reverse.put(MediaColumns.DATA,
+ "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG");
+ ensureFileColumns(uri, reverse);
+
+ assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
+ assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME));
+ assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE));
+ assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_PENDING));
+ assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED));
+ assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES));
+ }
+
+ @Test
public void testGreylist() throws Exception {
assertFalse(isGreylistMatch(
"SELECT secret FROM other_table"));
@@ -615,7 +685,7 @@
private static ContentValues computeDataValues(String path) {
final ContentValues values = new ContentValues();
values.put(MediaColumns.DATA, path);
- FileUtils.computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
Log.v(TAG, "Computed values " + values);
return values;
}
@@ -653,7 +723,7 @@
return false;
}
- private static String buildFile(Uri uri, String relativePath, String displayName,
+ private String buildFile(Uri uri, String relativePath, String displayName,
String mimeType) {
final ContentValues values = new ContentValues();
if (relativePath != null) {
@@ -662,13 +732,22 @@
values.put(MediaColumns.DISPLAY_NAME, displayName);
values.put(MediaColumns.MIME_TYPE, mimeType);
try {
- new MediaProvider().ensureFileColumns(uri, values);
+ ensureFileColumns(uri, values);
} catch (VolumeArgumentException e) {
throw e.rethrowAsIllegalArgumentException();
}
return values.getAsString(MediaColumns.DATA);
}
+ private void ensureFileColumns(Uri uri, ContentValues values)
+ throws VolumeArgumentException {
+ try (ContentProviderClient cpc = sIsolatedResolver
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ ((MediaProvider) cpc.getLocalContentProvider())
+ .ensureFileColumns(uri, values);
+ }
+ }
+
private static void assertEndsWith(String expected, String actual) {
if (!actual.endsWith(expected)) {
fail("Expected ends with " + expected + " but found " + actual);
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index ff78be2..448ed2c 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -20,6 +20,7 @@
import static com.android.providers.media.scan.MediaScannerTest.stage;
import static com.android.providers.media.scan.ModernMediaScanner.isDirectoryHidden;
import static com.android.providers.media.scan.ModernMediaScanner.isFileAlbumArt;
+import static com.android.providers.media.scan.ModernMediaScanner.isFileHidden;
import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalDateTaken;
import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalMimeType;
import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalYear;
@@ -289,6 +290,18 @@
}
@Test
+ public void testIsFileHidden() throws Exception {
+ assertFalse(isFileHidden(
+ new File("/storage/emulated/0/DCIM/IMG1024.JPG")));
+ assertFalse(isFileHidden(
+ new File("/storage/emulated/0/DCIM/.pending-1577836800-IMG1024.JPG")));
+ assertFalse(isFileHidden(
+ new File("/storage/emulated/0/DCIM/.trashed-1577836800-IMG1024.JPG")));
+ assertTrue(isFileHidden(
+ new File("/storage/emulated/0/DCIM/.IMG1024.JPG")));
+ }
+
+ @Test
public void testIsZero() throws Exception {
assertFalse(ModernMediaScanner.isZero(""));
assertFalse(ModernMediaScanner.isZero("meow"));
@@ -587,8 +600,7 @@
"/storage/emulated/0/albumart1.jpg",
}) {
final File file = new File(path);
- final String name = file.getName();
- assertEquals(LegacyMediaScannerTest.isNonMediaFile(path), isFileAlbumArt(name));
+ assertEquals(LegacyMediaScannerTest.isNonMediaFile(path), isFileAlbumArt(file));
}
for (String path : new String[] {
@@ -596,8 +608,7 @@
"/storage/emulated/0/albumartlarge.jpg",
}) {
final File file = new File(path);
- final String name = file.getName();
- assertTrue(isFileAlbumArt(name));
+ assertTrue(isFileAlbumArt(file));
}
}
}
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index ec0296f..5ae7384 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -24,12 +24,19 @@
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.extractRelativePath;
+import static com.android.providers.media.util.FileUtils.extractVolumeName;
+import static com.android.providers.media.util.FileUtils.extractVolumePath;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.content.ContentValues;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -248,6 +255,15 @@
}
@Test
+ public void testBuildUniqueFile_increment_hidden() throws Exception {
+ assertNameEquals(".hidden.jpg",
+ FileUtils.buildUniqueFile(mTarget, "image/jpeg", ".hidden.jpg"));
+ new File(mTarget, ".hidden.jpg").createNewFile();
+ assertNameEquals(".hidden (1).jpg",
+ FileUtils.buildUniqueFile(mTarget, "image/jpeg", ".hidden.jpg"));
+ }
+
+ @Test
public void testBuildUniqueFile_mimeless() throws Exception {
assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "test.jpg"));
new File(mTarget, "test.jpg").createNewFile();
@@ -311,6 +327,37 @@
}
@Test
+ public void testExtractVolumePath() throws Exception {
+ assertEquals("/storage/emulated/0/",
+ extractVolumePath("/storage/emulated/0/foo.jpg"));
+ assertEquals("/storage/0000-0000/",
+ extractVolumePath("/storage/0000-0000/foo.jpg"));
+ }
+
+ @Test
+ public void testExtractVolumeName() throws Exception {
+ assertEquals(MediaStore.VOLUME_EXTERNAL_PRIMARY,
+ extractVolumeName("/storage/emulated/0/foo.jpg"));
+ assertEquals("0000-0000",
+ extractVolumeName("/storage/0000-0000/foo.jpg"));
+ }
+
+ @Test
+ public void testExtractRelativePath() throws Exception {
+ for (String prefix : new String[] {
+ "/storage/emulated/0/",
+ "/storage/0000-0000/"
+ }) {
+ assertEquals("/",
+ extractRelativePath(prefix + "foo.jpg"));
+ assertEquals("DCIM/",
+ extractRelativePath(prefix + "DCIM/foo.jpg"));
+ assertEquals("DCIM/My Vacation/",
+ extractRelativePath(prefix + "DCIM/My Vacation/foo.jpg"));
+ }
+ }
+
+ @Test
public void testExtractDisplayName() throws Exception {
for (String probe : new String[] {
"foo.bar.baz",
@@ -391,6 +438,24 @@
}
}
+ @Test
+ public void testSanitizeValues() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.RELATIVE_PATH, "path/in\0valid/data/");
+ values.put(MediaColumns.DISPLAY_NAME, "inva\0lid");
+ FileUtils.sanitizeValues(values);
+ assertEquals("path/in_valid/data/", values.get(MediaColumns.RELATIVE_PATH));
+ assertEquals("inva_lid", values.get(MediaColumns.DISPLAY_NAME));
+ }
+
+ @Test
+ public void testSanitizeValues_Root() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.RELATIVE_PATH, "/");
+ FileUtils.sanitizeValues(values);
+ assertEquals("/", values.get(MediaColumns.RELATIVE_PATH));
+ }
+
private static File touch(File dir, String name) throws IOException {
final File res = new File(dir, name);
res.createNewFile();
diff --git a/tests/src/com/android/providers/media/util/MimeUtilsTest.java b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
index ab2b9d7..2334d5c 100644
--- a/tests/src/com/android/providers/media/util/MimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
@@ -37,6 +37,8 @@
public void testResolveMimeType() throws Exception {
assertEquals("image/jpeg",
MimeUtils.resolveMimeType(new File("foo.jpg")));
+ assertEquals("image/jpeg",
+ MimeUtils.resolveMimeType(new File(".hidden.jpg")));
assertEquals(ClipDescription.MIMETYPE_UNKNOWN,
MimeUtils.resolveMimeType(new File("foo")));