Merge "Remove FuseTargetPreparer" into rvc-dev
diff --git a/Android.bp b/Android.bp
index 49863ed..f59369f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -71,6 +71,7 @@
"src/com/android/providers/media/util/BackgroundThread.java",
"src/com/android/providers/media/util/DatabaseUtils.java",
"src/com/android/providers/media/util/FileUtils.java",
+ "src/com/android/providers/media/util/ForegroundThread.java",
"src/com/android/providers/media/util/HandlerExecutor.java",
"src/com/android/providers/media/util/Logging.java",
"src/com/android/providers/media/util/MimeUtils.java",
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 62fb282..ca32355 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -28,6 +28,7 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
+import android.annotation.WorkerThread;
import android.app.Activity;
import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
@@ -99,10 +100,27 @@
public static final @NonNull Uri AUTHORITY_URI =
Uri.parse("content://" + AUTHORITY);
- /** @hide */
+ /**
+ * The authority for a legacy instance of the media provider, before it was
+ * converted into a Mainline module. When initializing for the first time,
+ * the Mainline module will connect to this legacy instance to migrate
+ * important user settings, such as {@link BaseColumns#_ID},
+ * {@link MediaColumns#IS_FAVORITE}, and more.
+ * <p>
+ * The legacy instance is expected to meet the exact same API contract
+ * expressed here in {@link MediaStore}, to facilitate smooth data
+ * migrations. Interactions that would normally interact with
+ * {@link #AUTHORITY} can be redirected to work with the legacy instance
+ * using {@link #rewriteToLegacy(Uri)}.
+ *
+ * @hide
+ */
@SystemApi
public static final String AUTHORITY_LEGACY = "media_legacy";
- /** @hide */
+ /**
+ * @see #AUTHORITY_LEGACY
+ * @hide
+ */
@SystemApi
public static final @NonNull Uri AUTHORITY_LEGACY_URI =
Uri.parse("content://" + AUTHORITY_LEGACY);
@@ -163,6 +181,11 @@
public static final String GET_GENERATION_CALL = "get_generation";
/** {@hide} */
+ public static final String START_LEGACY_MIGRATION_CALL = "start_legacy_migration";
+ /** {@hide} */
+ public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration";
+
+ /** {@hide} */
@Deprecated
public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
"com.android.externalstorage.documents";
@@ -729,6 +752,7 @@
* Rewrite the given {@link Uri} to point at
* {@link MediaStore#AUTHORITY_LEGACY}.
*
+ * @see #AUTHORITY_LEGACY
* @hide
*/
@SystemApi
@@ -736,6 +760,29 @@
return uri.buildUpon().authority(MediaStore.AUTHORITY_LEGACY).build();
}
+ /**
+ * Called by the Mainline module to signal to {@link #AUTHORITY_LEGACY} that
+ * data migration is starting.
+ *
+ * @hide
+ */
+ public static void startLegacyMigration(@NonNull ContentResolver resolver,
+ @NonNull String volumeName) {
+ resolver.call(AUTHORITY_LEGACY, START_LEGACY_MIGRATION_CALL, volumeName, null);
+ }
+
+ /**
+ * Called by the Mainline module to signal to {@link #AUTHORITY_LEGACY} that
+ * data migration is finished. The legacy provider may choose to perform
+ * clean-up operations at this point, such as deleting databases.
+ *
+ * @hide
+ */
+ public static void finishLegacyMigration(@NonNull ContentResolver resolver,
+ @NonNull String volumeName) {
+ resolver.call(AUTHORITY_LEGACY, FINISH_LEGACY_MIGRATION_CALL, volumeName, null);
+ }
+
private static @NonNull PendingIntent createRequest(@NonNull ContentResolver resolver,
@NonNull String method, @NonNull Collection<Uri> uris, @Nullable ContentValues values) {
Objects.requireNonNull(resolver);
@@ -761,7 +808,8 @@
* call {@link Activity#startIntentSenderForResult} with
* {@link PendingIntent#getIntentSender()}. You can then determine if the
* user granted your request by testing for {@link Activity#RESULT_OK} in
- * {@link Activity#onActivityResult}.
+ * {@link Activity#onActivityResult}. The requested operation will have
+ * completely finished before this activity result is delivered.
* <p>
* Permissions granted through this mechanism are tied to the lifecycle of
* the {@link Activity} that requests them. If you need to retain
@@ -781,6 +829,11 @@
* For security and performance reasons this method does not support
* {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION} or
* {@link Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}.
+ * <p>
+ * The write access granted through this request is general-purpose, and
+ * once obtained you can directly {@link ContentResolver#update} columns
+ * like {@link MediaColumns#IS_FAVORITE}, {@link MediaColumns#IS_TRASHED},
+ * or {@link ContentResolver#delete}.
*
* @param resolver Used to connect with {@link MediaStore#AUTHORITY}.
* Typically this value is {@link Context#getContentResolver()},
@@ -805,7 +858,8 @@
* call {@link Activity#startIntentSenderForResult} with
* {@link PendingIntent#getIntentSender()}. You can then determine if the
* user granted your request by testing for {@link Activity#RESULT_OK} in
- * {@link Activity#onActivityResult}.
+ * {@link Activity#onActivityResult}. The requested operation will have
+ * completely finished before this activity result is delivered.
* <p>
* The displayed prompt will reflect all the media items you're requesting,
* including those for which you already hold write access. If you want to
@@ -848,7 +902,8 @@
* call {@link Activity#startIntentSenderForResult} with
* {@link PendingIntent#getIntentSender()}. You can then determine if the
* user granted your request by testing for {@link Activity#RESULT_OK} in
- * {@link Activity#onActivityResult}.
+ * {@link Activity#onActivityResult}. The requested operation will have
+ * completely finished before this activity result is delivered.
* <p>
* The displayed prompt will reflect all the media items you're requesting,
* including those for which you already hold write access. If you want to
@@ -888,7 +943,8 @@
* call {@link Activity#startIntentSenderForResult} with
* {@link PendingIntent#getIntentSender()}. You can then determine if the
* user granted your request by testing for {@link Activity#RESULT_OK} in
- * {@link Activity#onActivityResult}.
+ * {@link Activity#onActivityResult}. The requested operation will have
+ * completely finished before this activity result is delivered.
* <p>
* The displayed prompt will reflect all the media items you're requesting,
* including those for which you already hold write access. If you want to
@@ -1224,6 +1280,13 @@
* than using {@link #DATE_ADDED}, since those values may change in
* unexpected ways when apps use {@link File#setLastModified(long)} or
* when the system clock is set incorrectly.
+ * <p>
+ * Note that before comparing these detailed generation values, you
+ * should first confirm that the overall version hasn't changed by
+ * checking {@link MediaStore#getVersion(Context, String)}, since that
+ * indicates when a more radical change has occurred. If the overall
+ * version changes, you should assume that generation numbers have been
+ * reset and perform a full synchronization pass.
*
* @see MediaStore#getGeneration(Context, String)
*/
@@ -1241,6 +1304,13 @@
* using {@link #DATE_MODIFIED}, since those values may change in
* unexpected ways when apps use {@link File#setLastModified(long)} or
* when the system clock is set incorrectly.
+ * <p>
+ * Note that before comparing these detailed generation values, you
+ * should first confirm that the overall version hasn't changed by
+ * checking {@link MediaStore#getVersion(Context, String)}, since that
+ * indicates when a more radical change has occurred. If the overall
+ * version changes, you should assume that generation numbers have been
+ * reset and perform a full synchronization pass.
*
* @see MediaStore#getGeneration(Context, String)
*/
@@ -2255,7 +2325,13 @@
/**
* Return the typical {@link Size} (in pixels) used internally when
* the given thumbnail kind is requested.
+ *
+ * @deprecated Callers should migrate to using
+ * {@link ContentResolver#loadThumbnail}, since it
+ * offers richer control over requested thumbnail sizes
+ * and cancellation behavior.
*/
+ @Deprecated
public static @NonNull Size getKindSize(int kind) {
return ThumbnailConstants.getKindSize(kind);
}
@@ -3525,7 +3601,13 @@
/**
* Return the typical {@link Size} (in pixels) used internally when
* the given thumbnail kind is requested.
+ *
+ * @deprecated Callers should migrate to using
+ * {@link ContentResolver#loadThumbnail}, since it
+ * offers richer control over requested thumbnail sizes
+ * and cancellation behavior.
*/
+ @Deprecated
public static @NonNull Size getKindSize(int kind) {
return ThumbnailConstants.getKindSize(kind);
}
@@ -3720,6 +3802,13 @@
* {@link MediaColumns#DATE_MODIFIED}, since those values may change in
* unexpected ways when apps use {@link File#setLastModified(long)} or when
* the system clock is set incorrectly.
+ * <p>
+ * Note that before comparing these detailed generation values, you should
+ * first confirm that the overall version hasn't changed by checking
+ * {@link MediaStore#getVersion(Context, String)}, since that indicates when
+ * a more radical change has occurred. If the overall version changes, you
+ * should assume that generation numbers have been reset and perform a full
+ * synchronization pass.
*
* @param volumeName specific volume to obtain an generation value for. Must
* be one of the values returned from
@@ -3808,6 +3897,7 @@
*/
@SystemApi
@TestApi
+ @WorkerThread
public static void waitForIdle(@NonNull ContentResolver resolver) {
resolver.call(AUTHORITY, WAIT_FOR_IDLE_CALL, null, null);
}
@@ -3820,6 +3910,7 @@
*/
@SystemApi
@TestApi
+ @WorkerThread
@SuppressLint("StreamFiles")
public static @NonNull Uri scanFile(@NonNull ContentResolver resolver, @NonNull File file) {
final Bundle out = resolver.call(AUTHORITY, SCAN_FILE_CALL, file.getAbsolutePath(), null);
@@ -3833,6 +3924,7 @@
*/
@SystemApi
@TestApi
+ @WorkerThread
public static void scanVolume(@NonNull ContentResolver resolver, @NonNull String volumeName) {
resolver.call(AUTHORITY, SCAN_VOLUME_CALL, volumeName, null);
}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 42fdd05..95529a3 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -841,25 +841,30 @@
return;
}
- if (ri->isRedactionNeeded() || is_file_locked(fd, path)) {
- // We don't want to use the FUSE VFS cache in two cases:
- // 1. When redaction is needed because app A with EXIF access might access
- // a region that should have been redacted for app B without EXIF access, but app B on
- // a subsequent read, will be able to see the EXIF data because the read request for that
- // region will be served from cache and not get to the FUSE daemon
- // 2. When the file has a read or write lock on it. This means that the MediaProvider has
- // given an fd to the lower file system to an app. There are two cases where using the cache
- // in this case can be a problem:
- // a. Writing to a FUSE fd with caching enabled will use the write-back cache and a
- // subsequent read from the lower fs fd will not see the write.
- // b. Reading from a FUSE fd with caching enabled may not see the latest writes using the
- // lower fs fd because those writes did not go through the FUSE layer and reads from FUSE
- // after that write may be served from cache
- fi->direct_io = true;
- }
+ handle* h = nullptr;
+ {
+ std::lock_guard<std::recursive_mutex> guard(fuse->lock);
- handle* h = new handle(path, fd, ri.release(), /*owner_uid*/ -1, !fi->direct_io);
- node->AddHandle(h);
+ if (ri->isRedactionNeeded() || is_file_locked(fd, path)) {
+ // We don't want to use the FUSE VFS cache in two cases:
+ // 1. When redaction is needed because app A with EXIF access might access
+ // a region that should have been redacted for app B without EXIF access, but app B on
+ // a subsequent read, will be able to see the EXIF data because the read request for
+ // that region will be served from cache and not get to the FUSE daemon
+ // 2. When the file has a read or write lock on it. This means that the MediaProvider
+ // has given an fd to the lower file system to an app. There are two cases where using
+ // the cache in this case can be a problem:
+ // a. Writing to a FUSE fd with caching enabled will use the write-back cache and a
+ // subsequent read from the lower fs fd will not see the write.
+ // b. Reading from a FUSE fd with caching enabled may not see the latest writes using
+ // the lower fs fd because those writes did not go through the FUSE layer and reads from
+ // FUSE after that write may be served from cache
+ fi->direct_io = true;
+ }
+
+ h = new handle(path, fd, ri.release(), /*owner_uid*/ -1, !fi->direct_io);
+ node->AddHandle(h);
+ }
fi->fh = ptr_to_id(h);
fi->keep_cache = 1;
@@ -1468,6 +1473,7 @@
bool use_fuse = false;
if (active.load(std::memory_order_acquire)) {
+ std::lock_guard<std::recursive_mutex> guard(fuse->lock);
const node* node = node::LookupAbsolutePath(fuse->root, path);
if (node && node->HasCachedHandle()) {
use_fuse = true;
@@ -1550,8 +1556,6 @@
LOG(FATAL) << "mmap failed - could not start fuse! errno = " << errno;
}
- umask(0);
-
// Custom logging for libfuse
fuse_set_log_func(fuse_logger);
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index 93d1b8e..b79a955 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -26,6 +26,7 @@
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
@@ -37,6 +38,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.Objects;
/**
* Very limited subset of {@link MediaProvider} which only surfaces
@@ -46,6 +48,9 @@
private DatabaseHelper mInternalDatabase;
private DatabaseHelper mExternalDatabase;
+ public static final String START_LEGACY_MIGRATION_CALL = "start_legacy_migration";
+ public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration";
+
@Override
public void attachInfo(Context context, ProviderInfo info) {
// Sanity check our setup
@@ -68,9 +73,9 @@
Logging.initPersistent(persistentDir);
mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME,
- true, false, true, null, null, null);
+ true, false, true, null, null, null, null);
mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME,
- false, false, true, null, null, null);
+ false, false, true, null, null, null, null);
return true;
}
@@ -79,9 +84,9 @@
final String volumeName = MediaStore.getVolumeName(uri);
switch (volumeName) {
case MediaStore.VOLUME_INTERNAL:
- return mInternalDatabase;
+ return Objects.requireNonNull(mInternalDatabase, "Missing internal database");
default:
- return mExternalDatabase;
+ return Objects.requireNonNull(mExternalDatabase, "Missing external database");
}
}
@@ -124,6 +129,37 @@
}
@Override
+ public Bundle call(String authority, String method, String arg, Bundle extras) {
+ switch (method) {
+ case START_LEGACY_MIGRATION_CALL: {
+ // Nice to know, but nothing actionable
+ break;
+ }
+ case FINISH_LEGACY_MIGRATION_CALL: {
+ // We're only going to hear this once, since we've either
+ // successfully migrated legacy data, or we're never going to
+ // try again, so it's time to clean things up
+ final String volumeName = arg;
+ switch (volumeName) {
+ case MediaStore.VOLUME_INTERNAL: {
+ mInternalDatabase.close();
+ mInternalDatabase = null;
+ getContext().deleteDatabase(INTERNAL_DATABASE_NAME);
+ break;
+ }
+ default: {
+ mExternalDatabase.close();
+ mExternalDatabase = null;
+ getContext().deleteDatabase(EXTERNAL_DATABASE_NAME);
+ break;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Logging.dumpPersistent(writer);
}
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 85e2394..612e9cd 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -34,14 +34,10 @@
<item quantity="other">அத்துடன் கூடுதலாக <xliff:g id="COUNT_1">^1</xliff:g></item>
<item quantity="one">அத்துடன் கூடுதலாக <xliff:g id="COUNT_0">^1</xliff:g></item>
</plurals>
- <!-- no translation found for cache_clearing_dialog_title (543177167845854283) -->
- <skip />
- <!-- no translation found for cache_clearing_dialog_text (425995541409682360) -->
- <skip />
- <!-- no translation found for allow (8885707816848569619) -->
- <skip />
- <!-- no translation found for deny (6040983710442068936) -->
- <skip />
+ <string name="cache_clearing_dialog_title" msgid="543177167845854283">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> மூலம் தற்காலிகச் சேமிப்பை அகற்ற விரும்புகிறீர்களா?"</string>
+ <string name="cache_clearing_dialog_text" msgid="425995541409682360">"சில தற்காலிகக் கோப்புகளை நீக்குவதற்கு <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> அனுமதி கேட்கின்றது. ஏற்றுக்கொண்டால் பேட்டரி அல்லது டேட்டா உபயோகம் அதிகரிக்கக்கூடும்."</string>
+ <string name="allow" msgid="8885707816848569619">"அனுமதி"</string>
+ <string name="deny" msgid="6040983710442068936">"நிராகரி"</string>
<plurals name="permission_write_audio" formatted="false" msgid="3539998638571517689">
<item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களை மாற்ற <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
<item quantity="one">இந்த ஆடியோ ஃபைலை மாற்ற <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 29360c5..05276b3 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -51,15 +51,16 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.SparseArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.android.providers.media.util.BackgroundThread;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.FileUtils;
+import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.Logging;
import com.android.providers.media.util.MimeUtils;
@@ -67,8 +68,6 @@
import java.io.FilenameFilter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -101,6 +100,7 @@
final @Nullable Class<? extends Annotation> mColumnAnnotation;
final @Nullable OnSchemaChangeListener mSchemaListener;
final @Nullable OnFilesChangeListener mFilesListener;
+ final @Nullable OnLegacyMigrationListener mMigrationListener;
final Set<String> mFilterVolumeNames = new ArraySet<>();
long mScanStartTime;
long mScanStopTime;
@@ -122,20 +122,27 @@
int mediaType, boolean isDownload);
}
+ public interface OnLegacyMigrationListener {
+ public void onStarted(ContentProviderClient client, String volumeName);
+ public void onFinished(ContentProviderClient client, String volumeName);
+ }
+
public DatabaseHelper(Context context, String name,
boolean internal, boolean earlyUpgrade, boolean legacyProvider,
@Nullable Class<? extends Annotation> columnAnnotation,
@Nullable OnSchemaChangeListener schemaListener,
- @Nullable OnFilesChangeListener filesListener) {
+ @Nullable OnFilesChangeListener filesListener,
+ @Nullable OnLegacyMigrationListener migrationListener) {
this(context, name, getDatabaseVersion(context), internal, earlyUpgrade, legacyProvider,
- columnAnnotation, schemaListener, filesListener);
+ columnAnnotation, schemaListener, filesListener, migrationListener);
}
public DatabaseHelper(Context context, String name, int version,
boolean internal, boolean earlyUpgrade, boolean legacyProvider,
@Nullable Class<? extends Annotation> columnAnnotation,
@Nullable OnSchemaChangeListener schemaListener,
- @Nullable OnFilesChangeListener filesListener) {
+ @Nullable OnFilesChangeListener filesListener,
+ @Nullable OnLegacyMigrationListener migrationListener) {
super(context, name, null, version);
mContext = context;
mName = name;
@@ -147,6 +154,7 @@
mColumnAnnotation = columnAnnotation;
mSchemaListener = schemaListener;
mFilesListener = filesListener;
+ mMigrationListener = migrationListener;
// Configure default filters until we hear differently
if (mInternal) {
@@ -331,11 +339,11 @@
public boolean successful;
/**
- * List of {@link Uri} that would have been sent directly via
- * {@link ContentResolver#notifyChange}, but are instead being collected
- * due to this ongoing transaction.
+ * Map from {@code flags} value to set of {@link Uri} that would have
+ * been sent directly via {@link ContentResolver#notifyChange}, but are
+ * instead being collected due to this ongoing transaction.
*/
- public final Set<Uri> notifyChanges = new ArraySet<>();
+ public final SparseArray<ArraySet<Uri>> notifyChanges = new SparseArray<>();
}
public void beginTransaction() {
@@ -371,8 +379,11 @@
db.endTransaction();
if (state.successful) {
- BackgroundThread.getExecutor().execute(() -> {
- notifyChangeInternal(state.notifyChanges);
+ ForegroundThread.getExecutor().execute(() -> {
+ for (int i = 0; i < state.notifyChanges.size(); i++) {
+ notifyChangeInternal(state.notifyChanges.valueAt(i),
+ state.notifyChanges.keyAt(i));
+ }
});
}
}
@@ -399,36 +410,53 @@
}
}
+ public void notifyInsert(@NonNull Uri uri) {
+ notifyChange(uri, ContentResolver.NOTIFY_INSERT);
+ }
+
+ public void notifyUpdate(@NonNull Uri uri) {
+ notifyChange(uri, ContentResolver.NOTIFY_UPDATE);
+ }
+
+ public void notifyDelete(@NonNull Uri uri) {
+ notifyChange(uri, ContentResolver.NOTIFY_DELETE);
+ }
+
/**
* Notify that the given {@link Uri} has changed. This enqueues the
* notification if currently inside a transaction, and they'll be
* clustered and sent when the transaction completes.
*/
- public void notifyChange(@NonNull Uri uri) {
+ public void notifyChange(@NonNull Uri uri, int flags) {
if (LOGV) Log.v(TAG, "Notifying " + uri);
final TransactionState state = mTransactionState.get();
if (state != null) {
- state.notifyChanges.add(uri);
+ ArraySet<Uri> set = state.notifyChanges.get(flags);
+ if (set == null) {
+ set = new ArraySet<>();
+ state.notifyChanges.put(flags, set);
+ }
+ set.add(uri);
} else {
- BackgroundThread.getExecutor().execute(() -> {
- notifySingleChangeInternal(uri);
+ ForegroundThread.getExecutor().execute(() -> {
+ notifySingleChangeInternal(uri, flags);
});
}
}
- private void notifySingleChangeInternal(Uri uri) {
+ private void notifySingleChangeInternal(@NonNull Uri uri, int flags) {
Trace.beginSection("notifySingleChange");
try {
- mContext.getContentResolver().notifyChange(uri, null, 0);
+ mContext.getContentResolver().notifyChange(uri, null, flags);
} finally {
Trace.endSection();
}
}
- private void notifyChangeInternal(Iterable<Uri> uris) {
+ private void notifyChangeInternal(@NonNull Iterable<Uri> uris, int flags) {
Trace.beginSection("notifyChange");
try {
- mContext.getContentResolver().notifyChange(uris, null, 0);
+ mContext.getContentResolver().notifyChange(uris, null, flags);
} finally {
Trace.endSection();
}
@@ -617,6 +645,7 @@
db.execSQL("SAVEPOINT before_migrate");
Log.d(TAG, "Starting migration from legacy provider for " + mName);
+ mMigrationListener.onStarted(client, mVolumeName);
try (Cursor c = client.query(queryUri, sMigrateColumns.toArray(new String[0]),
extras, null)) {
final ContentValues values = new ContentValues();
@@ -635,11 +664,13 @@
db.execSQL("RELEASE before_migrate");
Log.d(TAG, "Finished migration from legacy provider for " + mName);
+ mMigrationListener.onFinished(client, mVolumeName);
} catch (Exception e) {
// 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);
+ mMigrationListener.onFinished(client, mVolumeName);
}
}
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 172aa41..d2af07d 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -162,6 +162,7 @@
import androidx.annotation.VisibleForTesting;
import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
+import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
import com.android.providers.media.fuse.ExternalStorageServiceImpl;
import com.android.providers.media.fuse.FuseDaemon;
import com.android.providers.media.scan.MediaScanner;
@@ -171,6 +172,7 @@
import com.android.providers.media.util.CachedSupplier;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.FileUtils;
+import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.IsoInterface;
import com.android.providers.media.util.Logging;
import com.android.providers.media.util.LongArray;
@@ -198,7 +200,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -475,7 +476,7 @@
@Override
public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
int mediaType, boolean isDownload) {
- acceptWithExpansion(helper::notifyChange, volumeName, id, mediaType, isDownload);
+ acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload);
if (helper.isExternal()) {
// Update the quota type on the filesystem
@@ -492,7 +493,7 @@
int oldMediaType, boolean oldIsDownload,
int newMediaType, boolean newIsDownload) {
final boolean isDownload = oldIsDownload || newIsDownload;
- acceptWithExpansion(helper::notifyChange, volumeName, id, oldMediaType, isDownload);
+ acceptWithExpansion(helper::notifyUpdate, volumeName, id, oldMediaType, isDownload);
// When media type changes, notify both old and new collections and
// invalidate any thumbnails
@@ -501,7 +502,7 @@
if (helper.isExternal()) {
updateQuotaTypeForUri(fileUri, newMediaType);
}
- acceptWithExpansion(helper::notifyChange, volumeName, id, newMediaType, isDownload);
+ acceptWithExpansion(helper::notifyUpdate, volumeName, id, newMediaType, isDownload);
invalidateThumbnails(fileUri);
}
}
@@ -512,7 +513,7 @@
// Both notify apps and revoke any outstanding permission grants
final Context context = getContext();
acceptWithExpansion((uri) -> {
- helper.notifyChange(uri);
+ helper.notifyDelete(uri);
context.revokeUriPermission(uri, ~0);
}, volumeName, id, mediaType, isDownload);
@@ -524,6 +525,18 @@
}
};
+ private final OnLegacyMigrationListener mMigrationListener = new OnLegacyMigrationListener() {
+ @Override
+ public void onStarted(ContentProviderClient client, String volumeName) {
+ MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName);
+ }
+
+ @Override
+ public void onFinished(ContentProviderClient client, String volumeName) {
+ MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName);
+ }
+ };
+
/**
* Apply {@link Consumer#accept} to the given item.
* <p>
@@ -583,6 +596,8 @@
Environment.DIRECTORY_MOVIES,
Environment.DIRECTORY_DOWNLOADS,
Environment.DIRECTORY_DCIM,
+ Environment.DIRECTORY_AUDIOBOOKS,
+ Environment.DIRECTORY_DOCUMENTS,
};
private static boolean isDefaultDirectoryName(@Nullable String dirName) {
@@ -671,10 +686,10 @@
mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME,
true, false, mLegacyProvider, Column.class,
- Metrics::logSchemaChange, mFilesListener);
+ Metrics::logSchemaChange, mFilesListener, mMigrationListener);
mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME,
false, false, mLegacyProvider, Column.class,
- Metrics::logSchemaChange, mFilesListener);
+ Metrics::logSchemaChange, mFilesListener, mMigrationListener);
final IntentFilter filter = new IntentFilter();
filter.setPriority(10);
@@ -1909,6 +1924,7 @@
defaultPrimary = Environment.DIRECTORY_MUSIC;
allowedPrimary = Arrays.asList(
Environment.DIRECTORY_ALARMS,
+ Environment.DIRECTORY_AUDIOBOOKS,
Environment.DIRECTORY_MUSIC,
Environment.DIRECTORY_NOTIFICATIONS,
Environment.DIRECTORY_PODCASTS,
@@ -3860,15 +3876,8 @@
private Bundle callInternal(String method, String arg, Bundle extras) {
switch (method) {
case MediaStore.WAIT_FOR_IDLE_CALL: {
- final CountDownLatch latch = new CountDownLatch(1);
- BackgroundThread.getExecutor().execute(() -> {
- latch.countDown();
- });
- try {
- latch.await(30, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- throw new IllegalStateException(e);
- }
+ ForegroundThread.waitForIdle();
+ BackgroundThread.waitForIdle();
return null;
}
case MediaStore.SCAN_FILE_CALL:
diff --git a/src/com/android/providers/media/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java
index ee3cb68..dd99fc4 100644
--- a/src/com/android/providers/media/MediaUpgradeReceiver.java
+++ b/src/com/android/providers/media/MediaUpgradeReceiver.java
@@ -69,7 +69,7 @@
try {
DatabaseHelper helper = new DatabaseHelper(
context, file, MediaProvider.isInternalMediaDatabaseName(file),
- false, false, Column.class, Metrics::logSchemaChange, null);
+ false, false, Column.class, Metrics::logSchemaChange, null, null);
db = helper.getWritableDatabase();
} catch (Throwable t) {
Log.wtf(TAG, "Error during upgrade of media db " + file, t);
diff --git a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
index 4ce2d2b..3fd1b4f 100644
--- a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
+++ b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
@@ -28,6 +28,7 @@
import com.android.providers.media.MediaProvider;
+import java.io.File;
import java.util.HashMap;
import java.util.Map;
@@ -42,8 +43,8 @@
@Override
public void onStartSession(String sessionId, /* @SessionFlag */ int flag,
- @NonNull ParcelFileDescriptor deviceFd, @NonNull String upperFileSystemPath,
- @NonNull String lowerFileSystemPath) {
+ @NonNull ParcelFileDescriptor deviceFd, @NonNull File upperFileSystemPath,
+ @NonNull File lowerFileSystemPath) {
MediaProvider mediaProvider = getMediaProvider();
synchronized (sLock) {
@@ -55,7 +56,7 @@
// REMOUNT_MODE_PASS_THROUGH which guarantees that all /storage paths are bind
// mounts of the lower filesystem.
FuseDaemon daemon = new FuseDaemon(mediaProvider, this, deviceFd, sessionId,
- upperFileSystemPath);
+ upperFileSystemPath.getPath());
daemon.start();
sFuseDaemons.put(sessionId, daemon);
}
diff --git a/src/com/android/providers/media/util/BackgroundThread.java b/src/com/android/providers/media/util/BackgroundThread.java
index 7b9cb54..dc03112 100644
--- a/src/com/android/providers/media/util/BackgroundThread.java
+++ b/src/com/android/providers/media/util/BackgroundThread.java
@@ -19,7 +19,9 @@
import android.os.Handler;
import android.os.HandlerThread;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
public final class BackgroundThread extends HandlerThread {
private static BackgroundThread sInstance;
@@ -27,7 +29,7 @@
private static HandlerExecutor sHandlerExecutor;
private BackgroundThread() {
- super("android.bg", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ super("bg", android.os.Process.THREAD_PRIORITY_BACKGROUND);
}
private static void ensureThreadLocked() {
@@ -59,4 +61,16 @@
return sHandlerExecutor;
}
}
+
+ public static void waitForIdle() {
+ final CountDownLatch latch = new CountDownLatch(1);
+ getExecutor().execute(() -> {
+ latch.countDown();
+ });
+ try {
+ latch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ }
}
diff --git a/src/com/android/providers/media/util/ForegroundThread.java b/src/com/android/providers/media/util/ForegroundThread.java
new file mode 100644
index 0000000..67388fc
--- /dev/null
+++ b/src/com/android/providers/media/util/ForegroundThread.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+public final class ForegroundThread extends HandlerThread {
+ private static ForegroundThread sInstance;
+ private static Handler sHandler;
+ private static HandlerExecutor sHandlerExecutor;
+
+ private ForegroundThread() {
+ super("fg", android.os.Process.THREAD_PRIORITY_FOREGROUND);
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new ForegroundThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ sHandlerExecutor = new HandlerExecutor(sHandler);
+ }
+ }
+
+ public static ForegroundThread get() {
+ synchronized (ForegroundThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+
+ public static Handler getHandler() {
+ synchronized (ForegroundThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+
+ public static Executor getExecutor() {
+ synchronized (ForegroundThread.class) {
+ ensureThreadLocked();
+ return sHandlerExecutor;
+ }
+ }
+
+ public static void waitForIdle() {
+ final CountDownLatch latch = new CountDownLatch(1);
+ getExecutor().execute(() -> {
+ latch.countDown();
+ });
+ try {
+ latch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
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 04e2b5c..ae63ac3 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -188,6 +188,9 @@
Assume.assumeNotNull(legacyProvider);
Assume.assumeNotNull(modernProvider);
+ // Clear data on the legacy provider so that we create a database
+ executeShellCommand("pm clear " + legacyProvider.applicationInfo.packageName, ui);
+
// Create a well-known entry in legacy provider, and write data into
// place to ensure the file is created on disk
final Uri legacyUri;
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index 2825040..10b063a 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -49,14 +49,13 @@
@Before
public void setup() throws Exception {
- executeShellCommand("mkdir /sdcard/Android/data/com.android.shell");
- executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files");
+ executeShellCommand("mkdir /sdcard/Android/data/com.android.shell -m 2770");
+ executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files -m 2770");
}
@After
public void tearDown() throws Exception {
executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
- executeShellCommand("rm -r /sdcard/Android/data/com.android.shell/files");
}
@Test
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java
index 852ab0e..9ebdfe8 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/LegacyAccessHostTest.java
@@ -195,4 +195,20 @@
public void testLegacyAppCanOwnAFile_hasW() throws Exception {
runDeviceTest("testLegacyAppCanOwnAFile_hasW");
}
+
+ @Test
+ public void testCreateAndRenameDoesntLeaveStaleDBRow_hasRW() throws Exception {
+ runDeviceTest("testCreateAndRenameDoesntLeaveStaleDBRow_hasRW");
+ }
+
+ @Test
+ public void testRenameDoesntInvalidateUri_hasRW() throws Exception {
+ runDeviceTest("testRenameDoesntInvalidateUri_hasRW");
+ }
+
+
+ @Test
+ public void testCanRenameAFileWithNoDBRow_hasRW() throws Exception {
+ runDeviceTest("testCanRenameAFileWithNoDBRow_hasRW");
+ }
}
diff --git a/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java b/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
index 8dfbb4d..b28f7fc 100644
--- a/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
+++ b/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
@@ -16,13 +16,17 @@
package com.android.tests.fused.legacy;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA1;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA2;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA1;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA2;
import static com.android.tests.fused.lib.TestUtils.assertCanRenameFile;
import static com.android.tests.fused.lib.TestUtils.assertCanRenameDirectory;
import static com.android.tests.fused.lib.TestUtils.assertCantRenameFile;
+import static com.android.tests.fused.lib.TestUtils.assertFileContent;
import static com.android.tests.fused.lib.TestUtils.createFileAs;
-
-
import static com.android.tests.fused.lib.TestUtils.deleteFileAsNoThrow;
+import static com.android.tests.fused.lib.TestUtils.getContentResolver;
import static com.android.tests.fused.lib.TestUtils.getFileOwnerPackageFromDatabase;
import static com.android.tests.fused.lib.TestUtils.getFileRowIdFromDatabase;
import static com.android.tests.fused.lib.TestUtils.installApp;
@@ -36,11 +40,17 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
import android.os.Environment;
+import android.provider.MediaStore;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
@@ -52,6 +62,8 @@
import com.android.cts.install.lib.TestApp;
import com.android.tests.fused.lib.ReaddirTestHelper;
+import com.google.common.io.Files;
+
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -59,7 +71,9 @@
import java.io.File;
import java.io.FileDescriptor;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
/**
@@ -76,6 +90,7 @@
private static final String TAG = "LegacyFileAccessTest";
static final String THIS_PACKAGE_NAME = InstrumentationRegistry.getContext().getPackageName();
+ static final String IMAGE_FILE_NAME = "FilePathAccessTest_file.jpg";
static final String VIDEO_FILE_NAME = "LegacyAccessTest_file.mp4";
static final String NONMEDIA_FILE_NAME = "LegacyAccessTest_file.pdf";
@@ -460,6 +475,134 @@
}
}
+ /**
+ * b/14966134: Test that FuseDaemon doesn't leave stale database entries after create() and
+ * rename().
+ */
+ @Test
+ public void testCreateAndRenameDoesntLeaveStaleDBRow_hasRW() throws Exception {
+ pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+ pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+ final File directoryDCIM = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DCIM);
+ final File videoFile = new File(directoryDCIM, VIDEO_FILE_NAME);
+ final File renamedVideoFile = new File(directoryDCIM, "Renamed_" + VIDEO_FILE_NAME);
+ final ContentResolver cr = getContentResolver();
+
+ try {
+ assertThat(videoFile.createNewFile()).isTrue();
+ assertThat(videoFile.renameTo(renamedVideoFile)).isTrue();
+
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.DATA, renamedVideoFile.getAbsolutePath());
+ // Insert new renamedVideoFile to database
+ final Uri uri = cr.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values, null);
+ assertNotNull(uri);
+
+ // Query for all images/videos in the device.
+ // This shouldn't list videoFile which was renamed to renamedVideoFile.
+ final ArrayList<String> imageAndVideoFiles = getImageAndVideoFilesFromDatabase();
+ assertThat(imageAndVideoFiles).contains(renamedVideoFile.getName());
+ assertThat(imageAndVideoFiles).doesNotContain(videoFile.getName());
+ } finally {
+ videoFile.delete();
+ renamedVideoFile.delete();
+ MediaStore.scanFile(cr, renamedVideoFile);
+ }
+ }
+
+ /**
+ * b/150147690,b/150193381: Test that file rename doesn't delete any existing Uri.
+ */
+ @Test
+ public void testRenameDoesntInvalidateUri_hasRW() throws Exception {
+ pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+ pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+ final File directoryDCIM = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DCIM);
+ final File imageFile = new File(directoryDCIM, IMAGE_FILE_NAME);
+ final File temporaryImageFile = new File(directoryDCIM, IMAGE_FILE_NAME + "_.tmp");
+ final ContentResolver cr = getContentResolver();
+
+ try {
+ assertThat(imageFile.createNewFile()).isTrue();
+ try (final FileOutputStream fos = new FileOutputStream(imageFile)) {
+ fos.write(BYTES_DATA1);
+ }
+ // Insert this file to database.
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.DATA, imageFile.getAbsolutePath());
+ final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, null);
+ assertNotNull(uri);
+
+ Files.copy(imageFile, temporaryImageFile);
+ // Write more bytes to temporaryImageFile
+ try (final FileOutputStream fos = new FileOutputStream(temporaryImageFile, true)) {
+ fos.write(BYTES_DATA2);
+ }
+ assertThat(imageFile.delete()).isTrue();
+ temporaryImageFile.renameTo(imageFile);
+
+ // Previous uri of imageFile is unaltered after delete & rename.
+ final Uri scannedUri = MediaStore.scanFile(cr, imageFile);
+ assertThat(scannedUri.getLastPathSegment()).isEqualTo(uri.getLastPathSegment());
+
+ final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
+ assertFileContent(imageFile, expected);
+ } finally {
+ imageFile.delete();
+ temporaryImageFile.delete();
+ MediaStore.scanFile(cr, imageFile);
+ }
+ }
+
+ /**
+ * b/150498564,b/150274099: Test that apps can rename files that are not in database.
+ */
+ @Test
+ public void testCanRenameAFileWithNoDBRow_hasRW() throws Exception {
+ pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+ pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+
+ final File directoryDCIM = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DCIM);
+ final File directoryNoMedia = new File(directoryDCIM, ".directoryNoMedia");
+ final File imageInNoMediaDir = new File(directoryNoMedia, IMAGE_FILE_NAME);
+ final File renamedImageInDCIM = new File(directoryDCIM, IMAGE_FILE_NAME);
+ final File noMediaFile = new File(directoryNoMedia, ".nomedia");
+ final ContentResolver cr = getContentResolver();
+
+ try {
+ if (!directoryNoMedia.exists()) {
+ assertThat(directoryNoMedia.mkdirs()).isTrue();
+ }
+ assertThat(noMediaFile.createNewFile()).isTrue();
+ assertThat(imageInNoMediaDir.createNewFile()).isTrue();
+ // Remove imageInNoMediaDir from database.
+ MediaStore.scanFile(cr, directoryNoMedia);
+
+ // Query for all images/videos in the device. This shouldn't list imageInNoMediaDir
+ assertThat(getImageAndVideoFilesFromDatabase())
+ .doesNotContain(imageInNoMediaDir.getName());
+
+ // Rename shouldn't throw error even if imageInNoMediaDir is not in database.
+ assertThat(imageInNoMediaDir.renameTo(renamedImageInDCIM)).isTrue();
+ // We can insert renamedImageInDCIM to database
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.DATA, renamedImageInDCIM.getAbsolutePath());
+ final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, null);
+ assertNotNull(uri);
+ } finally {
+ imageInNoMediaDir.delete();
+ renamedImageInDCIM.delete();
+ MediaStore.scanFile(cr, renamedImageInDCIM);
+ noMediaFile.delete();
+ }
+
+ }
+
private static void assertCanCreateFile(File file) throws IOException {
if (file.exists()) {
file.delete();
@@ -489,4 +632,27 @@
dir.delete();
}
}
+
+ /**
+ * Queries {@link ContentResolver} for all image and video files, returns display name of
+ * corresponding files.
+ */
+ private static ArrayList<String> getImageAndVideoFilesFromDatabase() {
+ ArrayList<String> mediaFiles = new ArrayList<>();
+ final String selection = "is_pending = 0 AND is_trashed = 0 AND "
+ + "(media_type = ? OR media_type = ?)";
+ final String[] selectionArgs = new String[] {
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)};
+
+ try (Cursor c = getContentResolver().query(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+ /* projection */ new String[]{MediaStore.MediaColumns.DISPLAY_NAME},
+ selection, selectionArgs, null)) {
+ while (c.moveToNext()) {
+ mediaFiles.add(c.getString(0));
+ }
+ }
+ return mediaFiles;
+ }
}
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
index 89863ed..75735a5 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
@@ -43,7 +43,9 @@
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.MediaStore;
+import android.system.ErrnoException;
import android.system.Os;
+import android.system.OsConstants;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -58,7 +60,10 @@
import com.google.common.io.ByteStreams;
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
@@ -76,6 +81,13 @@
public static final String CREATE_FILE_QUERY = "com.android.tests.fused.createfile";
public static final String DELETE_FILE_QUERY = "com.android.tests.fused.deletefile";
+
+ public static final String STR_DATA1 = "Just some random text";
+ public static final String STR_DATA2 = "More arbitrary stuff";
+
+ public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
+ public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
+
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private static final long POLLING_SLEEP_MILLIS = 100;
@@ -467,6 +479,32 @@
}
/**
+ * Asserts the entire content of the file equals exactly {@code expectedContent}.
+ */
+ public static void assertFileContent(File file, byte[] expectedContent) throws IOException {
+ try (final FileInputStream fis = new FileInputStream(file)) {
+ assertInputStreamContent(fis, expectedContent);
+ }
+ }
+
+ /**
+ * Asserts the entire content of the file equals exactly {@code expectedContent}.
+ * <p>Sets {@code fd} to beginning of file first.
+ */
+ public static void assertFileContent(FileDescriptor fd, byte[] expectedContent)
+ throws IOException, ErrnoException {
+ Os.lseek(fd, 0, OsConstants.SEEK_SET);
+ try (final FileInputStream fis = new FileInputStream(fd)) {
+ assertInputStreamContent(fis, expectedContent);
+ }
+ }
+
+ private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
+ throws IOException {
+ assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent);
+ }
+
+ /**
* Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED.
*/
private static boolean checkPermissionAndAppOp(String permission) {
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index a17e7b9..54caffe 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -25,12 +25,16 @@
import static com.android.tests.fused.lib.RedactionTestHelper.assertExifMetadataMismatch;
import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadata;
import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA1;
+import static com.android.tests.fused.lib.TestUtils.BYTES_DATA2;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA1;
+import static com.android.tests.fused.lib.TestUtils.STR_DATA2;
import static com.android.tests.fused.lib.TestUtils.assertCanRenameFile;
import static com.android.tests.fused.lib.TestUtils.assertCanRenameDirectory;
-import static com.android.tests.fused.lib.TestUtils.adoptShellPermissionIdentity;
import static com.android.tests.fused.lib.TestUtils.allowAppOpsToUid;
import static com.android.tests.fused.lib.TestUtils.assertCantRenameDirectory;
import static com.android.tests.fused.lib.TestUtils.assertCantRenameFile;
+import static com.android.tests.fused.lib.TestUtils.assertFileContent;
import static com.android.tests.fused.lib.TestUtils.assertThrows;
import static com.android.tests.fused.lib.TestUtils.createFileAs;
import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
@@ -38,7 +42,6 @@
import static com.android.tests.fused.lib.TestUtils.deleteRecursively;
import static com.android.tests.fused.lib.TestUtils.deleteWithMediaProvider;
import static com.android.tests.fused.lib.TestUtils.denyAppOpsToUid;
-import static com.android.tests.fused.lib.TestUtils.dropShellPermissionIdentity;
import static com.android.tests.fused.lib.TestUtils.executeShellCommand;
import static com.android.tests.fused.lib.TestUtils.getContentResolver;
import static com.android.tests.fused.lib.TestUtils.getFileMimeTypeFromDatabase;
@@ -79,8 +82,6 @@
import com.android.cts.install.lib.TestApp;
import com.android.tests.fused.lib.ReaddirTestHelper;
-import com.google.common.io.ByteStreams;
-
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -104,13 +105,22 @@
static final File EXTERNAL_STORAGE_DIR = Environment.getExternalStorageDirectory();
+ // Default top-level directories
+ static final File ALARMS_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_ALARMS);
+ static final File AUDIOBOOKS_DIR = new File(EXTERNAL_STORAGE_DIR,
+ Environment.DIRECTORY_AUDIOBOOKS);
static final File DCIM_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_DCIM);
- static final File PICTURES_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_PICTURES);
- static final File MUSIC_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MUSIC);
- static final File MOVIES_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MOVIES);
+ static final File DOCUMENTS_DIR = new File(EXTERNAL_STORAGE_DIR,
+ Environment.DIRECTORY_DOCUMENTS);
static final File DOWNLOAD_DIR = new File(EXTERNAL_STORAGE_DIR,
Environment.DIRECTORY_DOWNLOADS);
+ static final File MUSIC_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MUSIC);
+ static final File MOVIES_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_MOVIES);
+ static final File NOTIFICATIONS_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_NOTIFICATIONS);
+ static final File PICTURES_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_PICTURES);
static final File PODCASTS_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_PODCASTS);
+ static final File RINGTONES_DIR = new File(EXTERNAL_STORAGE_DIR, Environment.DIRECTORY_RINGTONES);
+
static final File ANDROID_DATA_DIR = new File(EXTERNAL_STORAGE_DIR, "Android/data");
static final File ANDROID_MEDIA_DIR = new File(EXTERNAL_STORAGE_DIR, "Android/media");
static final String TEST_DIRECTORY_NAME = "FilePathAccessTestDirectory";
@@ -118,17 +128,11 @@
static final File EXTERNAL_FILES_DIR = getContext().getExternalFilesDir(null);
static final File EXTERNAL_MEDIA_DIR = getContext().getExternalMediaDirs()[0];
- static final String MUSIC_FILE_NAME = "FilePathAccessTest_file.mp3";
+ static final String AUDIO_FILE_NAME = "FilePathAccessTest_file.mp3";
static final String VIDEO_FILE_NAME = "FilePathAccessTest_file.mp4";
static final String IMAGE_FILE_NAME = "FilePathAccessTest_file.jpg";
static final String NONMEDIA_FILE_NAME = "FilePathAccessTest_file.pdf";
- static final String STR_DATA1 = "Just some random text";
- static final String STR_DATA2 = "More arbitrary stuff";
-
- static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
- static final byte[] BYTES_DATA2 = STR_DATA2.getBytes();
-
static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
private static final TestApp TEST_APP_A = new TestApp("TestAppA",
@@ -154,7 +158,7 @@
*/
@Test
public void testTypePathConformity() throws Exception {
- // Only music files can be created in Music
+ // Only audio files can be created in Music
assertThrows(IOException.class, "Operation not permitted", () -> {
new File(MUSIC_DIR, NONMEDIA_FILE_NAME).createNewFile();
});
@@ -169,7 +173,7 @@
new File(MOVIES_DIR, NONMEDIA_FILE_NAME).createNewFile();
});
assertThrows(IOException.class, "Operation not permitted", () -> {
- new File(MOVIES_DIR, MUSIC_FILE_NAME).createNewFile();
+ new File(MOVIES_DIR, AUDIO_FILE_NAME).createNewFile();
});
assertThrows(IOException.class, "Operation not permitted", () -> {
new File(MOVIES_DIR, IMAGE_FILE_NAME).createNewFile();
@@ -179,28 +183,42 @@
new File(DCIM_DIR, NONMEDIA_FILE_NAME).createNewFile();
});
assertThrows(IOException.class, "Operation not permitted", () -> {
- new File(DCIM_DIR, MUSIC_FILE_NAME).createNewFile();
+ new File(DCIM_DIR, AUDIO_FILE_NAME).createNewFile();
});
// Only image and video files can be created in Pictures
assertThrows(IOException.class, "Operation not permitted", () -> {
new File(PICTURES_DIR, NONMEDIA_FILE_NAME).createNewFile();
});
assertThrows(IOException.class, "Operation not permitted", () -> {
- new File(PICTURES_DIR, MUSIC_FILE_NAME).createNewFile();
+ new File(PICTURES_DIR, AUDIO_FILE_NAME).createNewFile();
});
+ assertCanCreateFile(new File(ALARMS_DIR, AUDIO_FILE_NAME));
+ assertCanCreateFile(new File(AUDIOBOOKS_DIR, AUDIO_FILE_NAME));
assertCanCreateFile(new File(DCIM_DIR, IMAGE_FILE_NAME));
- assertCanCreateFile(new File(MUSIC_DIR, MUSIC_FILE_NAME));
- assertCanCreateFile(new File(MOVIES_DIR, VIDEO_FILE_NAME));
+ assertCanCreateFile(new File(DCIM_DIR, VIDEO_FILE_NAME));
+ assertCanCreateFile(new File(DOCUMENTS_DIR, AUDIO_FILE_NAME));
+ assertCanCreateFile(new File(DOCUMENTS_DIR, IMAGE_FILE_NAME));
+ assertCanCreateFile(new File(DOCUMENTS_DIR, NONMEDIA_FILE_NAME));
+ assertCanCreateFile(new File(DOCUMENTS_DIR, VIDEO_FILE_NAME));
+ assertCanCreateFile(new File(DOWNLOAD_DIR, AUDIO_FILE_NAME));
+ assertCanCreateFile(new File(DOWNLOAD_DIR, IMAGE_FILE_NAME));
assertCanCreateFile(new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME));
+ assertCanCreateFile(new File(DOWNLOAD_DIR, VIDEO_FILE_NAME));
+ assertCanCreateFile(new File(MOVIES_DIR, VIDEO_FILE_NAME));
+ assertCanCreateFile(new File(MUSIC_DIR, AUDIO_FILE_NAME));
+ assertCanCreateFile(new File(NOTIFICATIONS_DIR, AUDIO_FILE_NAME));
+ assertCanCreateFile(new File(PICTURES_DIR, IMAGE_FILE_NAME));
assertCanCreateFile(new File(PICTURES_DIR, VIDEO_FILE_NAME));
+ assertCanCreateFile(new File(PODCASTS_DIR, AUDIO_FILE_NAME));
+ assertCanCreateFile(new File(RINGTONES_DIR, AUDIO_FILE_NAME));
// No file whatsoever can be created in the top level directory
assertThrows(IOException.class, "Operation not permitted", () -> {
new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME).createNewFile();
});
assertThrows(IOException.class, "Operation not permitted", () -> {
- new File(EXTERNAL_STORAGE_DIR, MUSIC_FILE_NAME).createNewFile();
+ new File(EXTERNAL_STORAGE_DIR, AUDIO_FILE_NAME).createNewFile();
});
assertThrows(IOException.class, "Operation not permitted", () -> {
new File(EXTERNAL_STORAGE_DIR, IMAGE_FILE_NAME).createNewFile();
@@ -931,9 +949,9 @@
@Test
public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
- final File otherAppAudioFile = new File(MUSIC_DIR, "other_" + MUSIC_FILE_NAME);
- final File topLevelAudioFile = new File(EXTERNAL_STORAGE_DIR, MUSIC_FILE_NAME);
- final File audioInAnObviouslyWrongPlace = new File(PICTURES_DIR, MUSIC_FILE_NAME);
+ final File otherAppAudioFile = new File(MUSIC_DIR, "other_" + AUDIO_FILE_NAME);
+ final File topLevelAudioFile = new File(EXTERNAL_STORAGE_DIR, AUDIO_FILE_NAME);
+ final File audioInAnObviouslyWrongPlace = new File(PICTURES_DIR, AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
@@ -980,7 +998,7 @@
final File imageFile = new File(PICTURES_DIR, IMAGE_FILE_NAME);
final File videoFile = new File(PICTURES_DIR, VIDEO_FILE_NAME);
final File topLevelVideoFile = new File(EXTERNAL_STORAGE_DIR, VIDEO_FILE_NAME);
- final File musicFile = new File(MUSIC_DIR, MUSIC_FILE_NAME);
+ final File musicFile = new File(MUSIC_DIR, AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
@@ -1262,7 +1280,7 @@
@Test
public void testManageExternalStorageCanCreateFilesAnywhere() throws Exception {
final File topLevelPdf = new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME);
- final File musicFileInMovies = new File(MOVIES_DIR, MUSIC_FILE_NAME);
+ final File musicFileInMovies = new File(MOVIES_DIR, AUDIO_FILE_NAME);
final File imageFileInDcim = new File(DCIM_DIR, IMAGE_FILE_NAME);
try {
allowAppOpsToUid(Process.myUid(), OPSTR_MANAGE_EXTERNAL_STORAGE);
@@ -1304,7 +1322,7 @@
public void testManageExternalStorageCanDeleteOtherAppsContents() throws Exception {
final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
final File otherAppImage = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
- final File otherAppMusic = new File(MUSIC_DIR, "other" + MUSIC_FILE_NAME);
+ final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
@@ -1339,7 +1357,7 @@
final File pdf = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
final File pdfInObviouslyWrongPlace = new File(PICTURES_DIR, NONMEDIA_FILE_NAME);
final File topLevelPdf = new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME);
- final File musicFile = new File(MUSIC_DIR, MUSIC_FILE_NAME);
+ final File musicFile = new File(MUSIC_DIR, AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
@@ -1398,7 +1416,7 @@
public void testManageExternalStorageQueryOtherAppsFile() throws Exception {
final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
- final File otherAppMusic = new File(MUSIC_DIR, "other" + MUSIC_FILE_NAME);
+ final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
assertCreateFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
@@ -1421,7 +1439,7 @@
public void testQueryOtherAppsFiles() throws Exception {
final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
- final File otherAppMusic = new File(MUSIC_DIR, "other" + MUSIC_FILE_NAME);
+ final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
assertCreateFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
@@ -1441,7 +1459,7 @@
public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
final File otherAppImg = new File(DCIM_DIR, "other" + IMAGE_FILE_NAME);
- final File otherAppMusic = new File(MUSIC_DIR, "other" + MUSIC_FILE_NAME);
+ final File otherAppMusic = new File(MUSIC_DIR, "other" + AUDIO_FILE_NAME);
try {
installApp(TEST_APP_A, false);
assertCreateFilesAs(TEST_APP_A, otherAppImg, otherAppMusic, otherAppPdf);
@@ -1591,30 +1609,4 @@
+ "running the test!");
}
}
-
- /**
- * Asserts the entire content of the file equals exactly {@code expectedContent}.
- */
- private static void assertFileContent(File file, byte[] expectedContent) throws IOException {
- try (final FileInputStream fis = new FileInputStream(file)) {
- assertInputStreamContent(fis, expectedContent);
- }
- }
-
- /**
- * Asserts the entire content of the file equals exactly {@code expectedContent}.
- * <p>Sets {@code fd} to beginning of file first.
- */
- private static void assertFileContent(FileDescriptor fd, byte[] expectedContent)
- throws IOException, ErrnoException {
- Os.lseek(fd, 0, OsConstants.SEEK_SET);
- try (final FileInputStream fis = new FileInputStream(fd)) {
- assertInputStreamContent(fis, expectedContent);
- }
- }
-
- private static void assertInputStreamContent(InputStream in, byte[] expectedContent)
- throws IOException {
- assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent);
- }
}
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index 5923573..a655fe6 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -426,7 +426,7 @@
private static class DatabaseHelperO extends DatabaseHelper {
public DatabaseHelperO(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_O,
- false, false, true, Column.class, null, null);
+ false, false, true, Column.class, null, null, null);
}
@Override
@@ -438,7 +438,7 @@
private static class DatabaseHelperP extends DatabaseHelper {
public DatabaseHelperP(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_P,
- false, false, true, Column.class, null, null);
+ false, false, true, Column.class, null, null, null);
}
@Override
@@ -450,7 +450,7 @@
private static class DatabaseHelperQ extends DatabaseHelper {
public DatabaseHelperQ(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_Q,
- false, false, true, Column.class, null, null);
+ false, false, true, Column.class, null, null, null);
}
@Override
@@ -462,7 +462,7 @@
private static class DatabaseHelperR extends DatabaseHelper {
public DatabaseHelperR(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_R,
- false, false, true, Column.class, null, null);
+ false, false, true, Column.class, null, null, null);
}
}