Merge "Support creating Playlist and Subtitle file" into rvc-dev
diff --git a/Android.bp b/Android.bp
index b7092bc..34acc32 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,6 +12,9 @@
libs: [
"unsupportedappusage",
"app-compat-annotations",
+ "framework-mediaprovider",
+ "framework_mediaprovider_annotation",
+ "framework-statsd-stubs-module_libs_api",
],
jni_libs: [
@@ -26,10 +29,6 @@
":mediaprovider-sources",
],
- // Rewrite our hidden API usage of MediaStore to avoid "Inlined method
- // resolution crossed dex file boundary" errors
- jarjar_rules: "jarjar-rules.txt",
-
optimize: {
proguard_flags_files: ["proguard.flags"],
},
@@ -38,7 +37,7 @@
"java_api_finder",
],
- sdk_version: "system_current",
+ sdk_version: "module_current",
certificate: "media",
privileged: true,
@@ -46,15 +45,12 @@
aaptflags: ["--custom-package com.android.providers.media"],
}
-// This is defined to give MediaProviderTests all the source it needs to
-// run its tests against
+// Used by MediaProvider and MediaProviderTests
filegroup {
name: "mediaprovider-sources",
srcs: [
"src/**/*.aidl",
"src/**/*.java",
- ":framework-mediaprovider-sources",
- ":framework-mediaprovider-annotation-sources",
":mediaprovider-database-sources",
":statslog-mediaprovider-java-gen",
],
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index f5747d5..7cb77f9 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -34,7 +34,7 @@
plugins: ["java_api_finder"],
hostdex: true, // for hiddenapi check
- visibility: ["//packages/providers/MediaProvider/apex:__subpackages__"],
+ visibility: ["//packages/providers/MediaProvider:__subpackages__"],
apex_available: [
"com.android.mediaprovider",
"test_com.android.mediaprovider",
diff --git a/jarjar-rules.txt b/jarjar-rules.txt
deleted file mode 100644
index a16c1ab..0000000
--- a/jarjar-rules.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-rule android.provider.Column* com.android.providers.media.framework.Column@1
-rule android.provider.MediaStore* com.android.providers.media.framework.MediaStore@1
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index 086f88c..a505ee1 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -94,8 +94,10 @@
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
final DatabaseHelper helper = getDatabaseForUri(uri);
- return helper.getReadableDatabase().query("files", projection, selection, selectionArgs,
- null, null, sortOrder);
+ return helper.runWithoutTransaction((db) -> {
+ return db.query("files", projection, selection, selectionArgs,
+ null, null, sortOrder);
+ });
}
@Override
@@ -114,7 +116,9 @@
}
final DatabaseHelper helper = getDatabaseForUri(uri);
- final long id = helper.getWritableDatabase().insert("files", null, values);
+ final long id = helper.runWithTransaction((db) -> {
+ return db.insert("files", null, values);
+ });
return ContentUris.withAppendedId(uri, id);
}
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 4f9528e..3913065 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -35,7 +35,6 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
-import android.os.Looper;
import android.os.SystemClock;
import android.os.Trace;
import android.provider.MediaStore;
@@ -75,7 +74,8 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
-import java.util.function.LongSupplier;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
@@ -110,8 +110,19 @@
final Set<String> mFilterVolumeNames = new ArraySet<>();
long mScanStartTime;
long mScanStopTime;
- /** Flag indicating if a schema change is in progress */
- boolean mSchemaChanging;
+
+ /**
+ * Lock used to guard against deadlocks in SQLite; the write lock is used to
+ * guard any schema changes, and the read lock is used for all other
+ * database operations.
+ * <p>
+ * As a concrete example: consider the case where the primary database
+ * connection is performing a schema change inside a transaction, while a
+ * secondary connection is waiting to begin a transaction. When the primary
+ * database connection changes the schema, it attempts to close all other
+ * database connections, which then deadlocks.
+ */
+ private final ReentrantReadWriteLock mSchemaLock = new ReentrantReadWriteLock();
public interface OnSchemaChangeListener {
public void onSchemaChange(@NonNull String volumeName, int versionFrom, int versionTo,
@@ -195,26 +206,33 @@
mFilterVolumeNames.addAll(filterVolumeNames);
}
- // We might be tempted to open a transaction and recreate views here,
- // but that would result in an obscure deadlock; instead we simply close
- // the entire database, letting the views be recreated the next time
- // it's opened.
- close();
+ // Recreate all views to apply this filter
+ final SQLiteDatabase db = super.getWritableDatabase();
+ mSchemaLock.writeLock().lock();
+ try {
+ db.beginTransaction();
+ createLatestViews(db, mInternal);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ mSchemaLock.writeLock().unlock();
+ }
}
@Override
public SQLiteDatabase getReadableDatabase() {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- Log.wtf(TAG, "Database operations must not happen on main thread", new Throwable());
- }
- return super.getReadableDatabase();
+ throw new UnsupportedOperationException("All database operations must be routed through"
+ + " runWithTransaction() or runWithoutTransaction() to avoid deadlocks");
}
@Override
public SQLiteDatabase getWritableDatabase() {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- Log.wtf(TAG, "Database operations must not happen on main thread", new Throwable());
- }
+ throw new UnsupportedOperationException("All database operations must be routed through"
+ + " runWithTransaction() or runWithoutTransaction() to avoid deadlocks");
+ }
+
+ @VisibleForTesting
+ SQLiteDatabase getWritableDatabaseForTest() {
return super.getWritableDatabase();
}
@@ -222,7 +240,8 @@
public void onConfigure(SQLiteDatabase db) {
Log.v(TAG, "onConfigure() for " + mName);
db.setCustomScalarFunction("_INSERT", (arg) -> {
- if (arg != null && mFilesListener != null && !mSchemaChanging) {
+ if (arg != null && mFilesListener != null
+ && !mSchemaLock.isWriteLockedByCurrentThread()) {
final String[] split = arg.split(":");
final String volumeName = split[0];
final long id = Long.parseLong(split[1]);
@@ -240,7 +259,8 @@
return null;
});
db.setCustomScalarFunction("_UPDATE", (arg) -> {
- if (arg != null && mFilesListener != null && !mSchemaChanging) {
+ if (arg != null && mFilesListener != null
+ && !mSchemaLock.isWriteLockedByCurrentThread()) {
final String[] split = arg.split(":");
final String volumeName = split[0];
final long oldId = Long.parseLong(split[1]);
@@ -265,7 +285,8 @@
return null;
});
db.setCustomScalarFunction("_DELETE", (arg) -> {
- if (arg != null && mFilesListener != null && !mSchemaChanging) {
+ if (arg != null && mFilesListener != null
+ && !mSchemaLock.isWriteLockedByCurrentThread()) {
final String[] split = arg.split(":");
final String volumeName = split[0];
final long id = Long.parseLong(split[1]);
@@ -286,7 +307,7 @@
return null;
});
db.setCustomScalarFunction("_GET_ID", (arg) -> {
- if (mIdGenerator != null && !mSchemaChanging) {
+ if (mIdGenerator != null && !mSchemaLock.isWriteLockedByCurrentThread()) {
Trace.beginSection("_GET_ID");
try {
return mIdGenerator.apply(arg);
@@ -299,43 +320,35 @@
}
@Override
- public void onOpen(SQLiteDatabase db) {
- // Always recreate latest views and triggers during open; they're
- // cheap and it's an easy way to ensure they're defined consistently
- createLatestViews(db, mInternal);
- createLatestTriggers(db, mInternal);
- }
-
- @Override
public void onCreate(final SQLiteDatabase db) {
Log.v(TAG, "onCreate() for " + mName);
- mSchemaChanging = true;
+ mSchemaLock.writeLock().lock();
try {
updateDatabase(db, 0, mVersion);
} finally {
- mSchemaChanging = false;
+ mSchemaLock.writeLock().unlock();
}
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
Log.v(TAG, "onUpgrade() for " + mName + " from " + oldV + " to " + newV);
- mSchemaChanging = true;
+ mSchemaLock.writeLock().lock();
try {
updateDatabase(db, oldV, newV);
} finally {
- mSchemaChanging = false;
+ mSchemaLock.writeLock().unlock();
}
}
@Override
public void onDowngrade(final SQLiteDatabase db, final int oldV, final int newV) {
Log.v(TAG, "onDowngrade() for " + mName + " from " + oldV + " to " + newV);
- mSchemaChanging = true;
+ mSchemaLock.writeLock().lock();
try {
downgradeDatabase(db, oldV, newV);
} finally {
- mSchemaChanging = false;
+ mSchemaLock.writeLock().unlock();
}
}
@@ -411,7 +424,8 @@
}
mTransactionState.set(new TransactionState());
- final SQLiteDatabase db = getWritableDatabase();
+ final SQLiteDatabase db = super.getWritableDatabase();
+ mSchemaLock.readLock().lock();
db.beginTransaction();
db.execSQL("UPDATE local_metadata SET generation=generation+1;");
}
@@ -423,7 +437,7 @@
}
state.successful = true;
- final SQLiteDatabase db = getWritableDatabase();
+ final SQLiteDatabase db = super.getWritableDatabase();
db.setTransactionSuccessful();
}
@@ -434,8 +448,9 @@
}
mTransactionState.remove();
- final SQLiteDatabase db = getWritableDatabase();
+ final SQLiteDatabase db = super.getWritableDatabase();
db.endTransaction();
+ mSchemaLock.readLock().unlock();
if (state.successful) {
// We carefully "phase" our two sets of work here to ensure that we
@@ -458,19 +473,23 @@
}
/**
- * Execute the given runnable inside a transaction. If the calling thread is
- * not already in an active transaction, this method will wrap the given
+ * Execute the given operation inside a transaction. If the calling thread
+ * is not already in an active transaction, this method will wrap the given
* runnable inside a new transaction.
*/
- public long runWithTransaction(@NonNull LongSupplier s) {
+ public @NonNull <T> T runWithTransaction(@NonNull Function<SQLiteDatabase, T> op) {
+ // We carefully acquire the database here so that any schema changes can
+ // be applied before acquiring the read lock below
+ final SQLiteDatabase db = super.getWritableDatabase();
+
if (mTransactionState.get() != null) {
// Already inside a transaction, so we can run directly
- return s.getAsLong();
+ return op.apply(db);
} else {
// Not inside a transaction, so we need to make one
beginTransaction();
try {
- final long res = s.getAsLong();
+ final T res = op.apply(db);
setTransactionSuccessful();
return res;
} finally {
@@ -479,6 +498,29 @@
}
}
+ /**
+ * Execute the given operation regardless of the calling thread being in an
+ * active transaction or not.
+ */
+ public @NonNull <T> T runWithoutTransaction(@NonNull Function<SQLiteDatabase, T> op) {
+ // We carefully acquire the database here so that any schema changes can
+ // be applied before acquiring the read lock below
+ final SQLiteDatabase db = super.getWritableDatabase();
+
+ if (mTransactionState.get() != null) {
+ // Already inside a transaction, so we can run directly
+ return op.apply(db);
+ } else {
+ // We still need to acquire a schema read lock
+ mSchemaLock.readLock().lock();
+ try {
+ return op.apply(db);
+ } finally {
+ mSchemaLock.readLock().unlock();
+ }
+ }
+ }
+
public void notifyInsert(@NonNull Uri uri) {
notifyChange(uri, ContentResolver.NOTIFY_INSERT);
}
@@ -734,10 +776,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 +816,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 +1183,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);
@@ -1309,6 +1373,11 @@
}
}
+ // Always recreate latest views and triggers during upgrade; they're
+ // cheap and it's an easy way to ensure they're defined consistently
+ createLatestViews(db, internal);
+ createLatestTriggers(db, internal);
+
getOrCreateUuid(db);
final long elapsedMillis = (SystemClock.elapsedRealtime() - startTime);
@@ -1361,8 +1430,8 @@
* {@link MediaColumns#GENERATION_ADDED} or
* {@link MediaColumns#GENERATION_MODIFIED}.
*/
- public long getGeneration() {
- return android.database.DatabaseUtils.longForQuery(getReadableDatabase(),
+ public static long getGeneration(@NonNull SQLiteDatabase db) {
+ return android.database.DatabaseUtils.longForQuery(db,
CURRENT_GENERATION_CLAUSE + ";", null);
}
@@ -1370,15 +1439,7 @@
* Return total number of items tracked inside this database. This includes
* only real media items, and does not include directories.
*/
- public long getItemCount() {
- return getItemCount(getReadableDatabase());
- }
-
- /**
- * Return total number of items tracked inside this database. This includes
- * only real media items, and does not include directories.
- */
- private long getItemCount(SQLiteDatabase db) {
+ public static long getItemCount(@NonNull SQLiteDatabase db) {
return android.database.DatabaseUtils.longForQuery(db,
"SELECT COUNT(_id) FROM files WHERE " + FileColumns.MIME_TYPE + " IS NOT NULL",
null);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 60c4b39..c843ce9 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;
@@ -127,7 +125,6 @@
import android.os.Trace;
import android.os.UserHandle;
import android.os.storage.StorageManager;
-import android.os.storage.StorageManager.StorageVolumeCallback;
import android.os.storage.StorageVolume;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
@@ -688,7 +685,7 @@
* devices. We only do this once per volume so we don't annoy the user if
* deleted manually.
*/
- private void ensureDefaultFolders(String volumeName, DatabaseHelper helper) {
+ private void ensureDefaultFolders(@NonNull String volumeName, @NonNull SQLiteDatabase db) {
try {
final File path = getVolumePath(volumeName);
final StorageVolume vol = mStorageManager.getStorageVolume(path);
@@ -711,7 +708,7 @@
final File folder = new File(vol.getDirectory(), folderName);
if (!folder.exists()) {
folder.mkdirs();
- insertDirectory(helper, folder.getAbsolutePath());
+ insertDirectory(db, folder.getAbsolutePath());
}
}
@@ -730,9 +727,8 @@
* {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on
* disk, then all thumbnails will be considered stable and will be deleted.
*/
- private void ensureThumbnailsValid(String volumeName, DatabaseHelper helper) {
- final String uuidFromDatabase = DatabaseHelper
- .getOrCreateUuid(helper.getReadableDatabase());
+ private void ensureThumbnailsValid(@NonNull String volumeName, @NonNull SQLiteDatabase db) {
+ final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
try {
for (File dir : getThumbnailDirectories(volumeName)) {
if (!dir.exists()) {
@@ -893,9 +889,6 @@
// Trim any stale log files before we emit new events below
Logging.trimPersistent();
- final DatabaseHelper helper = mExternalDatabase;
- final SQLiteDatabase db = helper.getReadableDatabase();
-
// Scan all volumes to resolve any staleness
for (String volumeName : getExternalVolumeNames()) {
// Possibly bail before digging into each volume
@@ -908,81 +901,100 @@
}
// Ensure that our thumbnails are valid
- ensureThumbnailsValid(volumeName, helper);
+ mExternalDatabase.runWithTransaction((db) -> {
+ ensureThumbnailsValid(volumeName, db);
+ return null;
+ });
}
// Delete any stale thumbnails
- final int staleThumbnails = pruneThumbnails(signal);
+ final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> {
+ return pruneThumbnails(db, signal);
+ });
Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails");
// Finished orphaning any content whose package no longer exists
- final ArraySet<String> unknownPackages = new ArraySet<>();
- try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" },
- null, null, null, null, null, null, signal)) {
- while (c.moveToNext()) {
- final String packageName = c.getString(0);
- if (TextUtils.isEmpty(packageName)) continue;
+ final int stalePackages = mExternalDatabase.runWithTransaction((db) -> {
+ final ArraySet<String> unknownPackages = new ArraySet<>();
+ try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" },
+ null, null, null, null, null, null, signal)) {
+ while (c.moveToNext()) {
+ final String packageName = c.getString(0);
+ if (TextUtils.isEmpty(packageName)) continue;
- if (!isPackageKnown(packageName)) {
- unknownPackages.add(packageName);
+ if (!isPackageKnown(packageName)) {
+ unknownPackages.add(packageName);
+ }
}
}
- }
-
- for (String packageName : unknownPackages) {
- onPackageOrphaned(packageName);
- }
- final int stalePackages = unknownPackages.size();
+ for (String packageName : unknownPackages) {
+ onPackageOrphaned(db, packageName);
+ }
+ return unknownPackages.size();
+ });
Log.d(TAG, "Pruned " + stalePackages + " unknown packages");
// Delete any expired content; we're paranoid about wildly changing
// clocks, so only delete items within the last week
final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000);
final long to = (System.currentTimeMillis() / 1000);
- final int expiredMedia;
- try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" },
- FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null,
- null, null, null, null, signal)) {
- while (c.moveToNext()) {
- final String volumeName = c.getString(0);
- final long id = c.getLong(1);
- delete(Files.getContentUri(volumeName, id), null, null);
+ final int expiredMedia = mExternalDatabase.runWithTransaction((db) -> {
+ try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" },
+ FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null,
+ null, null, null, null, signal)) {
+ while (c.moveToNext()) {
+ final String volumeName = c.getString(0);
+ final long id = c.getLong(1);
+ delete(Files.getContentUri(volumeName, id), null, null);
+ }
+ return c.getCount();
}
- expiredMedia = c.getCount();
- Log.d(TAG, "Deleted " + expiredMedia + " expired items on " + helper.mName);
- }
+ });
+ Log.d(TAG, "Deleted " + expiredMedia + " expired items");
// Forget any stale volumes
- final Set<String> recentVolumeNames = MediaStore.getRecentExternalVolumeNames(getContext());
- final Set<String> knownVolumeNames = new ArraySet<>();
- try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME },
- null, null, null, null, null, null, signal)) {
- while (c.moveToNext()) {
- knownVolumeNames.add(c.getString(0));
+ mExternalDatabase.runWithTransaction((db) -> {
+ final Set<String> recentVolumeNames = MediaStore
+ .getRecentExternalVolumeNames(getContext());
+ final Set<String> knownVolumeNames = new ArraySet<>();
+ try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME },
+ null, null, null, null, null, null, signal)) {
+ while (c.moveToNext()) {
+ knownVolumeNames.add(c.getString(0));
+ }
}
- }
- final Set<String> staleVolumeNames = new ArraySet<>();
- staleVolumeNames.addAll(knownVolumeNames);
- staleVolumeNames.removeAll(recentVolumeNames);
- for (String staleVolumeName : staleVolumeNames) {
- final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?",
- new String[] { staleVolumeName });
- Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName);
- }
+ final Set<String> staleVolumeNames = new ArraySet<>();
+ staleVolumeNames.addAll(knownVolumeNames);
+ staleVolumeNames.removeAll(recentVolumeNames);
+ for (String staleVolumeName : staleVolumeNames) {
+ final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?",
+ new String[] { staleVolumeName });
+ Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName);
+ }
+ return null;
+ });
synchronized (mDirectoryCache) {
mDirectoryCache.clear();
}
+ final long itemCount = mExternalDatabase.runWithTransaction((db) -> {
+ return DatabaseHelper.getItemCount(db);
+ });
+
final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
- Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, helper.getItemCount(),
+ Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
durationMillis, staleThumbnails, expiredMedia);
}
public void onPackageOrphaned(String packageName) {
- final DatabaseHelper helper = mExternalDatabase;
- final SQLiteDatabase db = helper.getWritableDatabase();
+ mExternalDatabase.runWithTransaction((db) -> {
+ onPackageOrphaned(db, packageName);
+ return null;
+ });
+ }
+ public void onPackageOrphaned(@NonNull SQLiteDatabase db, @NonNull String packageName) {
final ContentValues values = new ContentValues();
values.putNull(FileColumns.OWNER_PACKAGE_NAME);
@@ -990,7 +1002,7 @@
"owner_package_name=?", new String[] { packageName });
if (count > 0) {
Log.d(TAG, "Orphaned " + count + " items belonging to "
- + packageName + " on " + helper.mName);
+ + packageName + " on " + db.getPath());
}
}
@@ -1377,7 +1389,7 @@
computeAudioLocalizedValues(values);
computeAudioKeyValues(values);
}
- computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
return values;
}
@@ -2105,14 +2117,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 +2175,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 +2220,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 +2281,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;
}
@@ -2331,12 +2342,12 @@
}
}
- private long insertDirectory(DatabaseHelper helper, String path) {
+ private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) {
if (LOGV) Log.v(TAG, "inserting directory " + path);
ContentValues values = new ContentValues();
values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
values.put(FileColumns.DATA, path);
- values.put(FileColumns.PARENT, getParent(helper, path));
+ values.put(FileColumns.PARENT, getParent(db, path));
values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
@@ -2346,12 +2357,10 @@
if (file.exists()) {
values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
}
- final SQLiteDatabase db = helper.getWritableDatabase();
- long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
- return rowId;
+ return db.insert("files", FileColumns.DATE_MODIFIED, values);
}
- private long getParent(DatabaseHelper helper, String path) {
+ private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) {
final String parentPath = new File(path).getParent();
if (Objects.equals("/", parentPath)) {
return -1;
@@ -2364,13 +2373,12 @@
}
final long id;
- final SQLiteDatabase db = helper.getReadableDatabase();
try (Cursor c = db.query("files", new String[] { FileColumns._ID },
FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
if (c.moveToFirst()) {
id = c.getLong(0);
} else {
- id = insertDirectory(helper, parentPath);
+ id = insertDirectory(db, parentPath);
}
}
@@ -2490,13 +2498,13 @@
}
public void onLocaleChanged() {
- localizeTitles();
+ mInternalDatabase.runWithTransaction((db) -> {
+ localizeTitles(db);
+ return null;
+ });
}
- private void localizeTitles() {
- final DatabaseHelper helper = mInternalDatabase;
- final SQLiteDatabase db = helper.getWritableDatabase();
-
+ private void localizeTitles(@NonNull SQLiteDatabase db) {
try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"},
"title_resource_uri IS NOT NULL", null, null, null, null)) {
while (c.moveToNext()) {
@@ -2544,7 +2552,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);
@@ -2618,7 +2626,9 @@
Long parent = values.getAsLong(FileColumns.PARENT);
if (parent == null) {
if (path != null) {
- long parentId = getParent(helper, path);
+ final long parentId = helper.runWithTransaction((db) -> {
+ return getParent(db, path);
+ });
values.put(FileColumns.PARENT, parentId);
}
}
@@ -2641,7 +2651,7 @@
private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
@NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
throws SQLiteConstraintException {
- return helper.runWithTransaction(() -> {
+ return helper.runWithTransaction((db) -> {
try {
return qb.insert(helper, values);
} catch (SQLiteConstraintException e) {
@@ -2939,7 +2949,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));
}
@@ -3818,16 +3828,19 @@
// Update any playlists that reference this item
if ((mediaType == FileColumns.MEDIA_TYPE_AUDIO)
&& helper.isExternal()) {
- final SQLiteDatabase db = helper.getReadableDatabase();
- try (Cursor cc = db.query("audio_playlists_map",
- new String[] { "playlist_id" }, "audio_id=" + id,
- null, "playlist_id", null, null)) {
- while (cc.moveToNext()) {
- final Uri playlistUri = ContentUris.withAppendedId(
- Playlists.getContentUri(volumeName), cc.getLong(0));
- resolvePlaylistMembers(playlistUri);
+ helper.runWithTransaction((db) -> {
+ try (Cursor cc = db.query("audio_playlists_map",
+ new String[] { "playlist_id" }, "audio_id=" + id,
+ null, "playlist_id", null, null)) {
+ while (cc.moveToNext()) {
+ final Uri playlistUri = ContentUris.withAppendedId(
+ Playlists.getContentUri(volumeName),
+ cc.getLong(0));
+ resolvePlaylistMembers(playlistUri);
+ }
}
- }
+ return null;
+ });
}
}
} finally {
@@ -3897,7 +3910,7 @@
synchronized (mDirectoryCache) {
mDirectoryCache.clear();
- return (int) helper.runWithTransaction(() -> {
+ return (int) helper.runWithTransaction((db) -> {
int n = 0;
int total = 0;
do {
@@ -3982,15 +3995,16 @@
case MediaStore.GET_VERSION_CALL: {
final String volumeName = extras.getString(Intent.EXTRA_TEXT);
- final SQLiteDatabase db;
+ final DatabaseHelper helper;
try {
- db = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName))
- .getReadableDatabase();
+ helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
} catch (VolumeNotFoundException e) {
throw e.rethrowAsIllegalArgumentException();
}
- final String version = db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db);
+ final String version = helper.runWithoutTransaction((db) -> {
+ return db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db);
+ });
final Bundle res = new Bundle();
res.putString(Intent.EXTRA_TEXT, version);
@@ -3999,14 +4013,17 @@
case MediaStore.GET_GENERATION_CALL: {
final String volumeName = extras.getString(Intent.EXTRA_TEXT);
- final long generation;
+ final DatabaseHelper helper;
try {
- generation = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName))
- .getGeneration();
+ helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
} catch (VolumeNotFoundException e) {
throw e.rethrowAsIllegalArgumentException();
}
+ final long generation = helper.runWithoutTransaction((db) -> {
+ return DatabaseHelper.getGeneration(db);
+ });
+
final Bundle res = new Bundle();
res.putLong(Intent.EXTRA_INDEX, generation);
return res;
@@ -4163,9 +4180,11 @@
mInternalDatabase,
mExternalDatabase
}) {
- final SQLiteDatabase db = helper.getReadableDatabase();
- db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);",
- new String[] { locale, collationName });
+ helper.runWithoutTransaction((db) -> {
+ db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);",
+ new String[] { locale, collationName });
+ return null;
+ });
}
mCustomCollators.add(collationName);
}
@@ -4173,12 +4192,9 @@
return collationName;
}
- private int pruneThumbnails(@NonNull CancellationSignal signal) {
+ private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) {
int prunedCount = 0;
- final DatabaseHelper helper = mExternalDatabase;
- final SQLiteDatabase db = helper.getReadableDatabase();
-
// Determine all known media items
final LongArray knownIds = new LongArray();
try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID },
@@ -4365,27 +4381,28 @@
}
final DatabaseHelper helper;
- final SQLiteDatabase db;
try {
helper = getDatabaseForUri(uri);
- db = helper.getWritableDatabase();
} catch (VolumeNotFoundException e) {
Log.w(TAG, e);
return;
}
- final String idString = Long.toString(id);
- try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?"
- + " union all select _data from videothumbnails where video_id=?",
- new String[] { idString, idString })) {
- while (c.moveToNext()) {
- String path = c.getString(0);
- deleteIfAllowed(uri, Bundle.EMPTY, path);
+ helper.runWithTransaction((db) -> {
+ final String idString = Long.toString(id);
+ try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?"
+ + " union all select _data from videothumbnails where video_id=?",
+ new String[] { idString, idString })) {
+ while (c.moveToNext()) {
+ String path = c.getString(0);
+ deleteIfAllowed(uri, Bundle.EMPTY, path);
+ }
}
- }
- db.execSQL("delete from thumbnails where image_id=?", new String[] { idString });
- db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString });
+ db.execSQL("delete from thumbnails where image_id=?", new String[] { idString });
+ db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString });
+ return null;
+ });
}
/**
@@ -4597,7 +4614,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 +4641,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 +4755,7 @@
case IMAGES_MEDIA_ID:
case FILES_ID:
case DOWNLOADS_ID: {
- computeDataValues(values);
+ FileUtils.computeValuesFromData(values);
break;
}
}
@@ -4787,21 +4805,24 @@
private void resolvePlaylistMembers(@NonNull Uri playlistUri) {
Trace.beginSection("resolvePlaylistMembers");
try {
- resolvePlaylistMembersInternal(playlistUri);
+ final DatabaseHelper helper;
+ try {
+ helper = getDatabaseForUri(playlistUri);
+ } catch (VolumeNotFoundException e) {
+ throw e.rethrowAsIllegalArgumentException();
+ }
+
+ helper.runWithTransaction((db) -> {
+ resolvePlaylistMembersInternal(playlistUri, db);
+ return null;
+ });
} finally {
Trace.endSection();
}
}
- private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri) {
- final SQLiteDatabase db;
- try {
- db = getDatabaseForUri(playlistUri).getWritableDatabase();
- } catch (VolumeNotFoundException e) {
- throw e.rethrowAsIllegalArgumentException();
- }
-
- db.beginTransaction();
+ private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
+ @NonNull SQLiteDatabase db) {
try {
// Refresh playlist members based on what we parse from disk
final long playlistId = ContentUris.parseId(playlistUri);
@@ -4827,11 +4848,8 @@
}
}
}
- db.setTransactionSuccessful();
} catch (IOException e) {
Log.w(TAG, "Failed to refresh playlist", e);
- } finally {
- db.endTransaction();
}
}
@@ -6509,8 +6527,11 @@
ForegroundThread.getExecutor().execute(() -> {
final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
? mInternalDatabase : mExternalDatabase;
- ensureDefaultFolders(volume, helper);
- ensureThumbnailsValid(volume, helper);
+ helper.runWithTransaction((db) -> {
+ ensureDefaultFolders(volume, db);
+ ensureThumbnailsValid(volume, db);
+ return null;
+ });
});
}
return uri;
@@ -6753,6 +6774,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/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java
index 932b20b..b9fba09 100644
--- a/src/com/android/providers/media/MediaUpgradeReceiver.java
+++ b/src/com/android/providers/media/MediaUpgradeReceiver.java
@@ -20,7 +20,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.database.sqlite.SQLiteDatabase;
import android.provider.Column;
import android.util.Log;
@@ -65,19 +64,19 @@
if (MediaProvider.isMediaDatabaseName(file)) {
long startTime = System.currentTimeMillis();
Log.i(TAG, "---> Start upgrade of media database " + file);
- SQLiteDatabase db = null;
try {
DatabaseHelper helper = new DatabaseHelper(
context, file, MediaProvider.isInternalMediaDatabaseName(file),
false, false, Column.class, Metrics::logSchemaChange, null, null,
null);
- db = helper.getWritableDatabase();
+ helper.runWithTransaction((db) -> {
+ // Perform just enough to force database upgrade
+ return db.getVersion();
+ });
+ helper.close();
} catch (Throwable t) {
Log.wtf(TAG, "Error during upgrade of media db " + file, t);
} finally {
- if (db != null) {
- db.close();
- }
}
Log.i(TAG, "<--- Finished upgrade of media database " + file
+ " in " + (System.currentTimeMillis()-startTime) + "ms");
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/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index da279aa..e3e0e71 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -425,8 +425,10 @@
public Cursor query(DatabaseHelper helper, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
- return query(helper.getReadableDatabase(), projectionIn, selection, selectionArgs, groupBy,
- having, sortOrder, limit, cancellationSignal);
+ return helper.runWithoutTransaction((db) -> {
+ return query(db, projectionIn, selection, selectionArgs, groupBy,
+ having, sortOrder, limit, cancellationSignal);
+ });
}
/**
@@ -524,8 +526,8 @@
public long insert(@NonNull DatabaseHelper helper, @NonNull ContentValues values) {
// We force wrap in a transaction to ensure that all mutations increment
// the generation counter
- return (int) helper.runWithTransaction(() -> {
- return insert(helper.getWritableDatabase(), values);
+ return helper.runWithTransaction((db) -> {
+ return insert(db, values);
});
}
@@ -568,8 +570,8 @@
@Nullable String selection, @Nullable String[] selectionArgs) {
// We force wrap in a transaction to ensure that all mutations increment
// the generation counter
- return (int) helper.runWithTransaction(() -> {
- return update(helper.getWritableDatabase(), values, selection, selectionArgs);
+ return helper.runWithTransaction((db) -> {
+ return update(db, values, selection, selectionArgs);
});
}
@@ -654,8 +656,8 @@
@Nullable String[] selectionArgs) {
// We force wrap in a transaction to ensure that all mutations increment
// the generation counter
- return (int) helper.runWithTransaction(() -> {
- return delete(helper.getWritableDatabase(), selection, selectionArgs);
+ return helper.runWithTransaction((db) -> {
+ return delete(db, selection, selectionArgs);
});
}
diff --git a/tests/Android.bp b/tests/Android.bp
index ba71320..e74d9bb 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -17,6 +17,7 @@
"res",
],
srcs: [
+ ":framework-mediaprovider-sources",
":mediaprovider-sources",
"src/**/*.java",
],
@@ -38,6 +39,7 @@
"androidx.test.rules",
"guava",
"mockito-target",
+ "truth-prebuilt",
],
certificate: "media",
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/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index 1149854..a83cc13 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -74,7 +74,7 @@
@Test
public void testFilterVolumeNames() throws Exception {
try (DatabaseHelper helper = new DatabaseHelperR(getContext(), TEST_CLEAN_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
{
final ContentValues values = new ContentValues();
values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
@@ -163,7 +163,7 @@
helper.endTransaction();
}
- helper.runWithTransaction(() -> {
+ helper.runWithTransaction((db) -> {
return 0;
});
}
@@ -188,7 +188,7 @@
Class<? extends DatabaseHelper> after) throws Exception {
try (DatabaseHelper helper = before.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_DOWNGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
{
final ContentValues values = new ContentValues();
values.put(FileColumns.DATA,
@@ -207,7 +207,7 @@
// Downgrade will wipe data, but at least we don't crash
try (DatabaseHelper helper = after.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_DOWNGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
try (Cursor c = db.query("files", null, null, null, null, null, null, null)) {
assertEquals(0, c.getCount());
}
@@ -235,7 +235,7 @@
Class<? extends DatabaseHelper> after) throws Exception {
try (DatabaseHelper helper = before.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_RECOMPUTE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
{
final ContentValues values = new ContentValues();
values.put(FileColumns.DATA,
@@ -299,7 +299,7 @@
try (DatabaseHelper helper = after.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_RECOMPUTE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
try (Cursor c = db.query("files", null, FileColumns.DISPLAY_NAME + "='global.jpg'",
null, null, null, null)) {
assertEquals(1, c.getCount());
@@ -369,18 +369,18 @@
Class<? extends DatabaseHelper> after) throws Exception {
try (DatabaseHelper helper = before.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_UPGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
}
try (DatabaseHelper helper = after.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_UPGRADE_DB)) {
- SQLiteDatabase db = helper.getWritableDatabase();
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
// Create a second isolated instance from scratch and assert that
// upgraded schema is identical
try (DatabaseHelper helper2 = after.getConstructor(Context.class, String.class)
.newInstance(getContext(), TEST_CLEAN_DB)) {
- SQLiteDatabase db2 = helper2.getWritableDatabase();
+ SQLiteDatabase db2 = helper2.getWritableDatabaseForTest();
try (Cursor c1 = db.query("sqlite_master",
null, null, null, null, null, SQLITE_MASTER_ORDER_BY);
@@ -413,8 +413,8 @@
private static Set<String> queryValues(@NonNull DatabaseHelper helper, @NonNull String table,
@NonNull String columnName) {
- try (Cursor c = helper.getReadableDatabase().query(table, new String[] { columnName },
- null, null, null, null, null)) {
+ try (Cursor c = helper.getWritableDatabaseForTest().query(table,
+ new String[] { columnName }, null, null, null, null, null)) {
final ArraySet<String> res = new ArraySet<>();
while (c.moveToNext()) {
res.add(c.getString(0));
@@ -433,11 +433,6 @@
public void onCreate(SQLiteDatabase db) {
createOSchema(db, false);
}
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- // Purposefully empty to leave views intact
- }
}
private static class DatabaseHelperP extends DatabaseHelper {
@@ -450,11 +445,6 @@
public void onCreate(SQLiteDatabase db) {
createPSchema(db, false);
}
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- // Purposefully empty to leave views intact
- }
}
private static class DatabaseHelperQ extends DatabaseHelper {
@@ -467,11 +457,6 @@
public void onCreate(SQLiteDatabase db) {
createQSchema(db, false);
}
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- // Purposefully empty to leave views intact
- }
}
private static class DatabaseHelperR extends DatabaseHelper {
diff --git a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
new file mode 100644
index 0000000..127cb35
--- /dev/null
+++ b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 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 android.Manifest;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
+
+import com.google.common.truth.Truth;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Arrays;
+
+/**
+ * This class is purely here to convince internal code coverage tools that
+ * {@code FuseDaemonHostTest} is actually covering all of these methods; the
+ * current coverage infrastructure doesn't support host tests yet.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaProviderForFuseTest {
+
+ private static Context sIsolatedContext;
+ private static ContentResolver sIsolatedResolver;
+ private static MediaProvider sMediaProvider;
+
+ private static int sTestUid;
+ private static File sTestDir;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.UPDATE_APP_OPS_STATS);
+
+ final Context context = InstrumentationRegistry.getTargetContext();
+ sIsolatedContext = new IsolatedContext(context, "modern");
+ sIsolatedResolver = sIsolatedContext.getContentResolver();
+ sMediaProvider = (MediaProvider) sIsolatedResolver
+ .acquireContentProviderClient(MediaStore.AUTHORITY).getLocalContentProvider();
+
+ // Use a random app without any permissions
+ sTestUid = context.getPackageManager().getPackageUid("com.android.egg",
+ PackageManager.MATCH_ALL);
+ sTestDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testTypical() throws Exception {
+ final File file = new File(sTestDir, "test" + System.nanoTime());
+
+ // We can create our file
+ Truth.assertThat(sMediaProvider.insertFileIfNecessaryForFuse(
+ file.getPath(), sTestUid)).isEqualTo(0);
+ Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
+ sTestDir.getPath(), sTestUid))).contains(file.getName());
+
+ // Touch on disk so we can rename below
+ file.createNewFile();
+
+ // We can write our file
+ Truth.assertThat(sMediaProvider.isOpenAllowedForFuse(
+ file.getPath(), sTestUid, true)).isEqualTo(0);
+
+ // We should have no redaction
+ Truth.assertThat(sMediaProvider.getRedactionRangesForFuse(
+ file.getPath(), sTestUid)).isEqualTo(new long[0]);
+
+ // We can rename our file
+ final File renamed = new File(sTestDir, "renamed" + System.nanoTime());
+ Truth.assertThat(sMediaProvider.renameForFuse(
+ file.getPath(), renamed.getPath(), sTestUid)).isEqualTo(0);
+ Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
+ sTestDir.getPath(), sTestUid))).doesNotContain(file.getName());
+ Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
+ sTestDir.getPath(), sTestUid))).contains(renamed.getName());
+
+ // And we can delete it
+ Truth.assertThat(sMediaProvider.deleteFileForFuse(
+ renamed.getPath(), sTestUid)).isEqualTo(0);
+ Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
+ sTestDir.getPath(), sTestUid))).doesNotContain(renamed.getName());
+ }
+
+ @Test
+ public void test_scanFileForFuse() throws Exception {
+ final File file = File.createTempFile("test", ".jpg", sTestDir);
+ sMediaProvider.scanFileForFuse(file.getPath());
+ }
+
+ @Test
+ public void test_isOpendirAllowedForFuse() throws Exception {
+ Truth.assertThat(sMediaProvider.isOpendirAllowedForFuse(
+ sTestDir.getPath(), sTestUid)).isEqualTo(0);
+ }
+
+ @Test
+ public void test_isDirectoryCreationOrDeletionAllowedForFuse() throws Exception {
+ Truth.assertThat(sMediaProvider.isDirectoryCreationOrDeletionAllowedForFuse(
+ sTestDir.getPath(), sTestUid, true)).isEqualTo(0);
+ }
+}
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")));