Merge "Support Windows style file path for a playlist"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index dfb2139..ecd5943 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -18,12 +18,14 @@
<uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_RESERVED_DISK" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:name="com.android.providers.media.MediaApplication"
android:label="@string/app_label"
android:allowBackup="false"
android:supportsRtl="true"
+ android:forceQueryable="true"
android:usesCleartextTraffic="true">
<provider
android:name="com.android.providers.media.MediaProvider"
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 952649c..5a16a9c 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1247,6 +1247,24 @@
@Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
public static final String GENERATION_MODIFIED = "generation_modified";
+ /**
+ * Indexed XMP metadata extracted from this media item.
+ * <p>
+ * The structure of this metadata is defined by the <a href=
+ * "https://en.wikipedia.org/wiki/Extensible_Metadata_Platform"><em>XMP
+ * Media Management</em> standard</a>, published as ISO 16684-1:2012.
+ * <p>
+ * This metadata is typically extracted from a
+ * {@link ExifInterface#TAG_XMP} contained inside an image file or from
+ * a {@code XMP_} box contained inside an ISO/IEC base media file format
+ * (MPEG-4 Part 12).
+ * <p>
+ * Note that any location details are redacted from this metadata for
+ * privacy reasons.
+ */
+ @Column(value = Cursor.FIELD_TYPE_BLOB, readOnly = true)
+ public static final String XMP = "xmp";
+
// =======================================
// ==== MediaMetadataRetriever values ====
// =======================================
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 7c78666..1c10335 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -417,10 +417,8 @@
return NULL;
}
- node = parent->LookupChildByName(name);
- if (node) {
- node->Acquire();
- } else {
+ node = parent->LookupChildByName(name, true /* acquire */);
+ if (!node) {
node = ::node::Create(parent, name, &fuse->lock);
fuse->NodeCreated(node);
}
@@ -726,7 +724,7 @@
return;
}
- node* child_node = parent_node->LookupChildByName(name);
+ node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
if (child_node) {
child_node->SetDeleted();
}
@@ -756,7 +754,7 @@
return;
}
- node* child_node = parent_node->LookupChildByName(name);
+ node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
if (child_node) {
child_node->SetDeleted();
}
@@ -797,8 +795,7 @@
<< safe_name(old_parent_node) << ") -> " << new_parent << " ("
<< safe_name(new_parent_node) << ")";
- node* child_node = old_parent_node->LookupChildByName(name);
- child_node->Acquire();
+ node* child_node = old_parent_node->LookupChildByName(name, true /* acquire */);
const string old_child_path = child_node->BuildPath();
const string new_child_path = new_parent_path + "/" + new_name;
@@ -1481,7 +1478,7 @@
};
static struct fuse_loop_config config = {
- .clone_fd = 0,
+ .clone_fd = 1,
.max_idle_threads = 10,
};
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 6cfcb57..821dc8a 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -358,7 +358,7 @@
void MediaProviderWrapper::ScanFile(const string& path) {
// Don't send in path by reference, since the memory might be deleted before we get the chances
// to perfrom the task.
- PostAsyncTask([this, path](JNIEnv* env) {
+ PostAndWaitForTask([this, path](JNIEnv* env) {
scanFileInternal(env, media_provider_object_, mid_scan_file_, path);
});
}
diff --git a/jni/node-inl.h b/jni/node-inl.h
index 487583c..4b688f1 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -89,14 +89,6 @@
return static_cast<__u64>(reinterpret_cast<uintptr_t>(node));
}
- // Acquires a reference to a node. This maps to the "lookup count" specified
- // by the FUSE documentation and must only happen under the circumstances
- // documented in libfuse/include/fuse_lowlevel.h.
- inline void Acquire() {
- std::lock_guard<std::recursive_mutex> guard(*lock_);
- refcount_++;
- }
-
// Releases a reference to a node. Returns true iff the refcount dropped to
// zero as a result of this call to Release, meaning that it's no longer
// safe to perform any operations on references to this node.
@@ -120,8 +112,9 @@
// associated with its descendants.
std::string BuildPath() const;
- // Looks up a direct descendant of this node by name.
- node* LookupChildByName(const std::string& name) const {
+ // Looks up a direct descendant of this node by name. If |acquire| is true,
+ // also Acquire the node before returning a reference to it.
+ node* LookupChildByName(const std::string& name, bool acquire) const {
std::lock_guard<std::recursive_mutex> guard(*lock_);
for (node* child : children_) {
@@ -131,6 +124,10 @@
// will not work because the source and target nodes are the same.
if ((name == child->name_) && !child->deleted_) {
+ if (acquire) {
+ child->Acquire();
+ }
+
return child;
}
}
@@ -219,6 +216,14 @@
}
}
+ // Acquires a reference to a node. This maps to the "lookup count" specified
+ // by the FUSE documentation and must only happen under the circumstances
+ // documented in libfuse/include/fuse_lowlevel.h.
+ inline void Acquire() {
+ std::lock_guard<std::recursive_mutex> guard(*lock_);
+ refcount_++;
+ }
+
// Adds this node to a specified parent.
void AddToParent(node* parent) {
std::lock_guard<std::recursive_mutex> guard(*lock_);
diff --git a/jni/node.cpp b/jni/node.cpp
index 1974a91..8898b7b 100644
--- a/jni/node.cpp
+++ b/jni/node.cpp
@@ -68,7 +68,7 @@
const node* node = root;
for (const std::string& segment : segments) {
- node = node->LookupChildByName(segment);
+ node = node->LookupChildByName(segment, false /* acquire */);
if (!node) {
return nullptr;
}
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index c72c8ee..ba876a0 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -22,6 +22,8 @@
// Forward destruction here, as NodeTest is a friend class.
static void destroy(node* node) { delete node; }
+ static void acquire(node* node) { node->Acquire(); }
+
typedef std::unique_ptr<node, decltype(&NodeTest::destroy)> unique_node_ptr;
unique_node_ptr CreateNode(node* parent, const std::string& path) {
@@ -46,21 +48,14 @@
ASSERT_EQ(2, GetRefCount(parent.get()));
// Make sure the node has been added to the parents list of children.
- ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
-}
-
-TEST_F(NodeTest, TestAcquire) {
- unique_node_ptr node = CreateNode(nullptr, "/path");
-
- ASSERT_EQ(1, GetRefCount(node.get()));
- node->Acquire();
- ASSERT_EQ(2, GetRefCount(node.get()));
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+ ASSERT_EQ(1, GetRefCount(child.get()));
}
TEST_F(NodeTest, TestRelease) {
node* node = node::Create(nullptr, "/path", &lock_);
- node->Acquire();
- node->Acquire();
+ acquire(node);
+ acquire(node);
ASSERT_EQ(3, GetRefCount(node));
ASSERT_FALSE(node->Release(1));
@@ -79,15 +74,16 @@
unique_node_ptr child = CreateNode(parent.get(), "subdir");
ASSERT_EQ(2, GetRefCount(parent.get()));
- ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
child->Rename("subdir_new", parent.get());
ASSERT_EQ(2, GetRefCount(parent.get()));
- ASSERT_EQ(nullptr, parent->LookupChildByName("subdir"));
- ASSERT_EQ(child.get(), parent->LookupChildByName("subdir_new"));
+ ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir_new", false /* acquire */));
ASSERT_EQ("/path/subdir_new", child->BuildPath());
+ ASSERT_EQ(1, GetRefCount(child.get()));
}
TEST_F(NodeTest, TestRenameWithParent) {
@@ -96,16 +92,17 @@
unique_node_ptr child = CreateNode(parent1.get(), "subdir");
ASSERT_EQ(2, GetRefCount(parent1.get()));
- ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir"));
+ ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir", false /* acquire */));
child->Rename("subdir", parent2.get());
ASSERT_EQ(1, GetRefCount(parent1.get()));
- ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir"));
+ ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir", false /* acquire */));
ASSERT_EQ(2, GetRefCount(parent2.get()));
- ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir"));
+ ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir", false /* acquire */));
ASSERT_EQ("/path2/subdir", child->BuildPath());
+ ASSERT_EQ(1, GetRefCount(child.get()));
}
TEST_F(NodeTest, TestRenameWithNameAndParent) {
@@ -114,17 +111,18 @@
unique_node_ptr child = CreateNode(parent1.get(), "subdir");
ASSERT_EQ(2, GetRefCount(parent1.get()));
- ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir"));
+ ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir", false /* acquire */));
child->Rename("subdir_new", parent2.get());
ASSERT_EQ(1, GetRefCount(parent1.get()));
- ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir"));
- ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir_new"));
+ ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir", false /* acquire */));
+ ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir_new", false /* acquire */));
ASSERT_EQ(2, GetRefCount(parent2.get()));
- ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir_new"));
+ ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir_new", false /* acquire */));
ASSERT_EQ("/path2/subdir_new", child->BuildPath());
+ ASSERT_EQ(1, GetRefCount(child.get()));
}
TEST_F(NodeTest, TestBuildPath) {
@@ -145,9 +143,9 @@
unique_node_ptr parent = CreateNode(nullptr, "/path");
unique_node_ptr child = CreateNode(parent.get(), "subdir");
- ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
child->SetDeleted();
- ASSERT_EQ(nullptr, parent->LookupChildByName("subdir"));
+ ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
}
TEST_F(NodeTest, DeleteTree) {
@@ -159,17 +157,28 @@
node* subchild2 = node::Create(child, "s2", &lock_);
node::Create(subchild2, "sc2", &lock_);
- ASSERT_EQ(child, parent->LookupChildByName("subdir"));
+ ASSERT_EQ(child, parent->LookupChildByName("subdir", false /* acquire */));
node::DeleteTree(child);
- ASSERT_EQ(nullptr, parent->LookupChildByName("subdir"));
+ ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
}
TEST_F(NodeTest, LookupChildByName_empty) {
unique_node_ptr parent = CreateNode(nullptr, "/path");
unique_node_ptr child = CreateNode(parent.get(), "subdir");
- ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
- ASSERT_EQ(nullptr, parent->LookupChildByName(""));
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+ ASSERT_EQ(nullptr, parent->LookupChildByName("", false /* acquire */));
+}
+
+TEST_F(NodeTest, LookupChildByName_refcounts) {
+ unique_node_ptr parent = CreateNode(nullptr, "/path");
+ unique_node_ptr child = CreateNode(parent.get(), "subdir");
+
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+ ASSERT_EQ(1, GetRefCount(child.get()));
+
+ ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", true /* acquire */));
+ ASSERT_EQ(2, GetRefCount(child.get()));
}
TEST_F(NodeTest, LookupAbsolutePath) {
diff --git a/legacy/AndroidManifest.xml b/legacy/AndroidManifest.xml
index 15349b1..960e5ba 100644
--- a/legacy/AndroidManifest.xml
+++ b/legacy/AndroidManifest.xml
@@ -11,6 +11,7 @@
android:process="android.process.media"
android:allowBackup="false"
android:supportsRtl="true"
+ android:forceQueryable="true"
android:usesCleartextTraffic="true">
<provider
android:name="com.android.providers.media.LegacyMediaProvider"
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index b96358a..4943d6b 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -335,7 +335,7 @@
* {@link ContentResolver#notifyChange}, but are instead being collected
* due to this ongoing transaction.
*/
- public final List<Uri> notifyChanges = new ArrayList<>();
+ public final Set<Uri> notifyChanges = new ArraySet<>();
}
public void beginTransaction() {
@@ -556,7 +556,7 @@
+ "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL,"
+ "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
+ "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
- + "generation_modified INTEGER DEFAULT 0)");
+ + "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL)");
db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
if (!mInternal) {
@@ -892,7 +892,7 @@
db.execSQL("ALTER TABLE files ADD COLUMN secondary_directory TEXT DEFAULT NULL;");
}
- private static void updateAddXmp(SQLiteDatabase db, boolean internal) {
+ private static void updateAddXmpMm(SQLiteDatabase db, boolean internal) {
db.execSQL("ALTER TABLE files ADD COLUMN document_id TEXT DEFAULT NULL;");
db.execSQL("ALTER TABLE files ADD COLUMN instance_id TEXT DEFAULT NULL;");
db.execSQL("ALTER TABLE files ADD COLUMN original_document_id TEXT DEFAULT NULL;");
@@ -988,6 +988,10 @@
db.execSQL("ALTER TABLE files ADD COLUMN generation_modified INTEGER DEFAULT 0;");
}
+ private static void updateAddXmp(SQLiteDatabase db, boolean internal) {
+ db.execSQL("ALTER TABLE files ADD COLUMN xmp BLOB DEFAULT NULL;");
+ }
+
private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA },
null, null, null, null, null, null)) {
@@ -1043,7 +1047,7 @@
static final int VERSION_O = 800;
static final int VERSION_P = 900;
static final int VERSION_Q = 1023;
- static final int VERSION_R = 1111;
+ static final int VERSION_R = 1112;
static final int VERSION_LATEST = VERSION_R;
/**
@@ -1106,7 +1110,7 @@
recomputeDataValues = true;
}
if (fromVersion < 1014) {
- updateAddXmp(db, internal);
+ updateAddXmpMm(db, internal);
}
if (fromVersion < 1015) {
// Empty version bump to ensure views are recreated
@@ -1177,6 +1181,9 @@
if (fromVersion < 1111) {
recomputeMediaTypeValues(db);
}
+ if (fromVersion < 1112) {
+ updateAddXmp(db, internal);
+ }
if (recomputeDataValues) {
recomputeDataValues(db, internal);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index bc74663..ad8f840 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -186,7 +186,6 @@
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -3907,27 +3906,62 @@
public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal)
throws IOException;
- public File ensureThumbnail(Uri uri, CancellationSignal signal) throws IOException {
+ public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
+ throws IOException {
+ // First attempt to fast-path by opening the thumbnail; if it
+ // doesn't exist we fall through to create it below
final File thumbFile = getThumbnailFile(uri);
+ try {
+ return ParcelFileDescriptor.open(thumbFile,
+ ParcelFileDescriptor.MODE_READ_ONLY);
+ } catch (FileNotFoundException ignored) {
+ }
+
final File thumbDir = thumbFile.getParentFile();
thumbDir.mkdirs();
- if (!thumbFile.exists()) {
- // When multiple threads race for the same thumbnail, the second
- // thread could return a file with a thumbnail still in
- // progress. We could add heavy per-ID locking to mitigate this
- // rare race condition, but it's simpler to have both threads
- // generate the same thumbnail using temporary files and rename
- // them into place once finished.
- final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
+
+ // When multiple threads race for the same thumbnail, the second
+ // thread could return a file with a thumbnail still in
+ // progress. We could add heavy per-ID locking to mitigate this
+ // rare race condition, but it's simpler to have both threads
+ // generate the same thumbnail using temporary files and rename
+ // them into place once finished.
+ final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
+
+ ParcelFileDescriptor thumbWrite = null;
+ ParcelFileDescriptor thumbRead = null;
+ try {
+ // Open our temporary file twice: once for local writing, and
+ // once for remote reading. Both FDs point at the same
+ // underlying inode on disk, so they're stable across renames
+ // to avoid race conditions between threads.
+ thumbWrite = ParcelFileDescriptor.open(thumbTempFile,
+ ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
+ thumbRead = ParcelFileDescriptor.open(thumbTempFile,
+ ParcelFileDescriptor.MODE_READ_ONLY);
+
final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
- try (OutputStream out = new FileOutputStream(thumbTempFile)) {
- thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, out);
+ thumbnail.compress(Bitmap.CompressFormat.JPEG, 90,
+ new FileOutputStream(thumbWrite.getFileDescriptor()));
+
+ try {
+ // Use direct syscall for better failure logs
+ Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath());
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
}
- if (!thumbTempFile.renameTo(thumbFile)) {
- thumbTempFile.delete();
- }
+
+ // Everything above went peachy, so return a duplicate of our
+ // already-opened read FD to keep our finally logic below simple
+ return thumbRead.dup();
+
+ } finally {
+ // Regardless of success or failure, try cleaning up any
+ // remaining temporary file and close all our local FDs
+ FileUtils.closeQuietly(thumbWrite);
+ FileUtils.closeQuietly(thumbRead);
+ thumbTempFile.delete();
}
- return thumbFile;
}
public void invalidateThumbnail(Uri uri) throws IOException {
@@ -4468,30 +4502,25 @@
final long albumId = Long.parseLong(uri.getPathSegments().get(3));
final Uri targetUri = ContentUris
.withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
- return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
- ParcelFileDescriptor.MODE_READ_ONLY);
-
+ return ensureThumbnail(targetUri, signal);
}
case AUDIO_ALBUMART_FILE_ID: {
final long audioId = Long.parseLong(uri.getPathSegments().get(3));
final Uri targetUri = ContentUris
.withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
- return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
- ParcelFileDescriptor.MODE_READ_ONLY);
+ return ensureThumbnail(targetUri, signal);
}
case VIDEO_MEDIA_ID_THUMBNAIL: {
final long videoId = Long.parseLong(uri.getPathSegments().get(3));
final Uri targetUri = ContentUris
.withAppendedId(Video.Media.getContentUri(volumeName), videoId);
- return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
- ParcelFileDescriptor.MODE_READ_ONLY);
+ return ensureThumbnail(targetUri, signal);
}
case IMAGES_MEDIA_ID_THUMBNAIL: {
final long imageId = Long.parseLong(uri.getPathSegments().get(3));
final Uri targetUri = ContentUris
.withAppendedId(Images.Media.getContentUri(volumeName), imageId);
- return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
- ParcelFileDescriptor.MODE_READ_ONLY);
+ return ensureThumbnail(targetUri, signal);
}
}
@@ -4520,10 +4549,8 @@
final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
&& (mimeTypeFilter != null) && mimeTypeFilter.startsWith("image/");
if (wantsThumb) {
- final File thumbFile = ensureThumbnail(uri, signal);
- return new AssetFileDescriptor(
- ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.MODE_READ_ONLY),
- 0, AssetFileDescriptor.UNKNOWN_LENGTH);
+ final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
+ return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}
// Worst case, return the underlying file
@@ -4531,14 +4558,14 @@
AssetFileDescriptor.UNKNOWN_LENGTH);
}
- private File ensureThumbnail(Uri uri, CancellationSignal signal) throws FileNotFoundException {
+ private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
+ throws FileNotFoundException {
final boolean allowHidden = isCallingPackageAllowedHidden();
final int match = matchUri(uri, allowHidden);
Trace.beginSection("ensureThumbnail");
final LocalCallingIdentity token = clearLocalCallingIdentity();
try {
- final File thumbFile;
switch (match) {
case AUDIO_ALBUMS_ID: {
final String volumeName = MediaStore.getVolumeName(uri);
@@ -4970,6 +4997,9 @@
IsoInterface.BOX_GPS0,
};
+ public static final Set<String> sRedactedExifTags = new ArraySet<>(
+ Arrays.asList(REDACTED_EXIF_TAGS));
+
private static final class RedactionInfo {
public final long[] redactionRanges;
public final long[] freeOffsets;
@@ -5078,7 +5108,6 @@
final LongArray res = new LongArray();
final LongArray freeOffsets = new LongArray();
try (FileInputStream is = new FileInputStream(file)) {
- final Set<String> redactedXmpTags = new ArraySet<>(Arrays.asList(REDACTED_EXIF_TAGS));
final String mimeType = MimeUtils.resolveMimeType(file);
if (ExifInterface.isSupportedMimeType(mimeType)) {
final ExifInterface exif = new ExifInterface(is.getFD());
@@ -5090,7 +5119,7 @@
}
}
// Redact xmp where present
- final XmpInterface exifXmp = XmpInterface.fromContainer(exif, redactedXmpTags);
+ final XmpInterface exifXmp = XmpInterface.fromContainer(exif);
res.addAll(exifXmp.getRedactionRanges());
}
@@ -5106,7 +5135,7 @@
}
}
// Redact xmp where present
- final XmpInterface isoXmp = XmpInterface.fromContainer(iso, redactedXmpTags);
+ final XmpInterface isoXmp = XmpInterface.fromContainer(iso);
res.addAll(isoXmp.getRedactionRanges());
}
} catch (FileNotFoundException ignored) {
@@ -5853,6 +5882,10 @@
final String volumeName = resolveVolumeName(uri);
synchronized (mAttachedVolumeNames) {
if (!mAttachedVolumeNames.contains(volumeName)) {
+ // Maybe we are racing onVolumeStateChanged, update our cache and try again
+ updateVolumes();
+ }
+ if (!mAttachedVolumeNames.contains(volumeName)) {
throw new VolumeNotFoundException(volumeName);
}
}
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 4a3ab82..4e8318f 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -337,6 +337,7 @@
}
try {
Files.walkFileTree(mRoot.toPath(), this);
+ applyPending();
} catch (IOException e) {
// This should never happen, so yell loudly
throw new IllegalStateException(e);
@@ -346,7 +347,6 @@
}
Trace.endSection();
}
- applyPending();
}
}
@@ -801,6 +801,7 @@
op.withValue(MediaColumns.DOCUMENT_ID, xmp.getDocumentId());
op.withValue(MediaColumns.INSTANCE_ID, xmp.getInstanceId());
op.withValue(MediaColumns.ORIGINAL_DOCUMENT_ID, xmp.getOriginalDocumentId());
+ op.withValue(MediaColumns.XMP, xmp.getRedactedXmp());
}
/**
@@ -1051,7 +1052,7 @@
}
private static @NonNull <T> Optional<T> parseOptionalOrZero(@Nullable T value) {
- if (value instanceof String && ((String) value).equals("0")) {
+ if (value instanceof String && isZero((String) value)) {
return Optional.empty();
} else if (value instanceof Number && ((Number) value).intValue() == 0) {
return Optional.empty();
@@ -1275,6 +1276,18 @@
return (path.size() == 4) && path.get(1).equals("audio") && path.get(2).equals("playlists");
}
+ static boolean isZero(@NonNull String value) {
+ if (value.length() == 0) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ if (value.charAt(i) != '0') {
+ return false;
+ }
+ }
+ return true;
+ }
+
static void logTroubleScanning(File file, Exception e) {
if (LOGW) Log.w(TAG, "Trouble scanning " + file + ": " + e);
}
diff --git a/src/com/android/providers/media/util/XmpInterface.java b/src/com/android/providers/media/util/XmpInterface.java
index 63f736f..1a316e7 100644
--- a/src/com/android/providers/media/util/XmpInterface.java
+++ b/src/com/android/providers/media/util/XmpInterface.java
@@ -28,16 +28,18 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.providers.media.MediaProvider;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
-import java.util.Collections;
+import java.nio.file.Files;
+import java.util.Arrays;
import java.util.Set;
import java.util.UUID;
@@ -62,35 +64,29 @@
private static final String NAME_ORIGINAL_DOCUMENT_ID = "OriginalDocumentID";
private static final String NAME_INSTANCE_ID = "InstanceID";
- private final ByteCountingInputStream mIn;
- private final Set<String> mRedactedExifTags;
- private final long mXmpOffset;
- private final LongArray mRedactedRanges;
+ private final LongArray mRedactedRanges = new LongArray();
+ private byte[] mRedactedXmp;
private String mFormat;
private String mDocumentId;
private String mInstanceId;
private String mOriginalDocumentId;
- private XmpInterface(@NonNull InputStream in) throws IOException {
- this(in, Collections.emptySet(), new long[0]);
- }
+ private XmpInterface(@NonNull byte[] rawXmp, @NonNull Set<String> redactedExifTags,
+ @NonNull long[] xmpOffsets) throws IOException {
+ mRedactedXmp = rawXmp;
- private XmpInterface(
- @NonNull InputStream in, @NonNull Set<String> redactedExifTags, long[] xmpOffsets)
- throws IOException {
- mIn = new ByteCountingInputStream(in);
- mRedactedExifTags = redactedExifTags;
- mXmpOffset = xmpOffsets.length == 0 ? 0 : xmpOffsets[0];
- mRedactedRanges = new LongArray();
+ final ByteCountingInputStream in = new ByteCountingInputStream(
+ new ByteArrayInputStream(rawXmp));
+ final long xmpOffset = xmpOffsets.length == 0 ? 0 : xmpOffsets[0];
try {
final XmlPullParser parser = Xml.newPullParser();
- parser.setInput(mIn, StandardCharsets.UTF_8.name());
+ parser.setInput(in, StandardCharsets.UTF_8.name());
long offset = 0;
int type;
while ((type = parser.next()) != END_DOCUMENT) {
if (type != START_TAG) {
- offset = mIn.getOffset(parser);
+ offset = in.getOffset(parser);
continue;
}
@@ -117,14 +113,19 @@
mInstanceId = maybeOverride(mInstanceId, parser.nextText());
} else if (NS_XMPMM.equals(ns) && NAME_ORIGINAL_DOCUMENT_ID.equals(name)) {
mOriginalDocumentId = maybeOverride(mOriginalDocumentId, parser.nextText());
- } else if (NS_EXIF.equals(ns) && mRedactedExifTags.contains(name)) {
+ } else if (NS_EXIF.equals(ns) && redactedExifTags.contains(name)) {
long start = offset;
do {
type = parser.next();
} while (type != END_TAG || !parser.getName().equals(name));
- offset = mIn.getOffset(parser);
- mRedactedRanges.add(mXmpOffset + start);
- mRedactedRanges.add(mXmpOffset + offset);
+ offset = in.getOffset(parser);
+
+ // Redact range within entire file
+ mRedactedRanges.add(xmpOffset + start);
+ mRedactedRanges.add(xmpOffset + offset);
+
+ // Redact range within local copy
+ Arrays.fill(mRedactedXmp, (int) start, (int) offset, (byte) ' ');
}
}
} catch (XmlPullParserException e) {
@@ -144,7 +145,7 @@
public static @NonNull XmpInterface fromContainer(@NonNull ExifInterface exif)
throws IOException {
- return fromContainer(exif, Collections.emptySet());
+ return fromContainer(exif, MediaProvider.sRedactedExifTags);
}
public static @NonNull XmpInterface fromContainer(@NonNull ExifInterface exif,
@@ -158,12 +159,12 @@
buf = new byte[0];
xmpOffsets = new long[0];
}
- return new XmpInterface(new ByteArrayInputStream(buf), redactedExifTags, xmpOffsets);
+ return new XmpInterface(buf, redactedExifTags, xmpOffsets);
}
public static @NonNull XmpInterface fromContainer(@NonNull IsoInterface iso)
throws IOException {
- return fromContainer(iso, Collections.emptySet());
+ return fromContainer(iso, MediaProvider.sRedactedExifTags);
}
public static @NonNull XmpInterface fromContainer(@NonNull IsoInterface iso,
@@ -183,12 +184,13 @@
buf = new byte[0];
xmpOffsets = new long[0];
}
- return new XmpInterface(new ByteArrayInputStream(buf), redactedExifTags, xmpOffsets);
+ return new XmpInterface(buf, redactedExifTags, xmpOffsets);
}
public static @NonNull XmpInterface fromSidecar(@NonNull File file)
throws IOException {
- return new XmpInterface(new FileInputStream(file));
+ return new XmpInterface(Files.readAllBytes(file.toPath()),
+ MediaProvider.sRedactedExifTags, new long[0]);
}
private static @Nullable String maybeOverride(@Nullable String existing,
@@ -221,6 +223,10 @@
return mOriginalDocumentId;
}
+ public @Nullable byte[] getRedactedXmp() {
+ return mRedactedXmp;
+ }
+
/** The [start, end] offsets in the original file where to-be redacted info is stored */
public LongArray getRedactionRanges() {
return mRedactedRanges;
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 2f0c78f..1c4afe6 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -43,9 +43,10 @@
name: "FuseDaemonLegacyTest",
manifest: "legacy/AndroidManifest.xml",
srcs: ["legacy/src/**/*.java"],
- static_libs: ["androidx.test.rules", "truth-prebuilt"],
+ static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
test_suites: ["general-tests", "mts"],
- sdk_version: "29"
+ sdk_version: "test_current",
+ target_sdk_version: "29"
}
java_test_host {
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 151ec50..1b09f5e 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,6 +16,8 @@
package com.android.tests.fused.legacy;
+import static com.android.tests.fused.lib.TestUtils.pollForExternalStorageState;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@@ -31,6 +33,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,6 +59,11 @@
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private static final long POLLING_SLEEP_MILLIS = 100;
+ @Before
+ public void setUp() throws Exception {
+ pollForExternalStorageState();
+ }
+
/**
* Tests that legacy apps bypass the type-path conformity restrictions imposed by MediaProvider.
* <p> Assumes we have WRITE_EXTERNAL_STORAGE.
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 25ccee2..e564915 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
@@ -38,6 +38,7 @@
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
@@ -58,6 +59,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* General helper functions for FuseDaemon tests.
@@ -71,6 +73,9 @@
public static final String CREATE_FILE_QUERY = "com.android.tests.fused.createfile";
public static final String DELETE_FILE_QUERY = "com.android.tests.fused.deletefile";
+ private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+ private static final long POLLING_SLEEP_MILLIS = 100;
+
private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
.getUiAutomation();
@@ -380,6 +385,17 @@
return path.delete();
}
+ public static void pollForExternalStorageState() throws Exception {
+ for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+ if(Environment.getExternalStorageState(Environment.getExternalStorageDirectory())
+ .equals(Environment.MEDIA_MOUNTED)) {
+ return;
+ }
+ Thread.sleep(POLLING_SLEEP_MILLIS);
+ }
+ fail("Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
+ }
+
/**
* <p>This method drops shell permission identity.
*/
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 a9b0274..00e698e 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -47,6 +47,7 @@
import static com.android.tests.fused.lib.TestUtils.uninstallApp;
import static com.android.tests.fused.lib.TestUtils.uninstallAppNoThrow;
import static com.android.tests.fused.lib.TestUtils.updateDisplayNameWithMediaProvider;
+import static com.android.tests.fused.lib.TestUtils.pollForExternalStorageState;
import static com.google.common.truth.Truth.assertThat;
@@ -131,10 +132,13 @@
private static final String[] SYSTEM_GALERY_APPOPS = { AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO };
- // skips all test cases if FUSE is not active.
+
@Before
- public void assumeFuseIsOn() {
+ public void setUp() throws Exception {
+ // skips all test cases if FUSE is not active.
assumeTrue(getBoolean("persist.sys.fuse", false));
+
+ pollForExternalStorageState();
EXTERNAL_FILES_DIR.mkdirs();
}
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index fbefb44..1bbcb45 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -229,6 +229,19 @@
}
@Test
+ public void testIsZero() throws Exception {
+ assertFalse(ModernMediaScanner.isZero(""));
+ assertFalse(ModernMediaScanner.isZero("meow"));
+ assertFalse(ModernMediaScanner.isZero("1"));
+ assertFalse(ModernMediaScanner.isZero("01"));
+ assertFalse(ModernMediaScanner.isZero("010"));
+
+ assertTrue(ModernMediaScanner.isZero("0"));
+ assertTrue(ModernMediaScanner.isZero("00"));
+ assertTrue(ModernMediaScanner.isZero("000"));
+ }
+
+ @Test
public void testPlaylistM3u() throws Exception {
doPlaylist(R.raw.test_m3u, "test.m3u");
}
diff --git a/tests/src/com/android/providers/media/util/XmpInterfaceTest.java b/tests/src/com/android/providers/media/util/XmpInterfaceTest.java
index e9876a3..6995219 100644
--- a/tests/src/com/android/providers/media/util/XmpInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/XmpInterfaceTest.java
@@ -18,7 +18,9 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.Context;
@@ -30,9 +32,6 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.R;
-import com.android.providers.media.util.FileUtils;
-import com.android.providers.media.util.IsoInterface;
-import com.android.providers.media.util.XmpInterface;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,14 +92,22 @@
redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP);
redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID);
- // The XMP contents start at byte 1809. These are the file offsets.
- final long[] expectedRanges = new long[]{2625,2675,2678,2730,2733,2792,2795,2841};
final Context context = InstrumentationRegistry.getContext();
try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg)) {
ExifInterface exif = new ExifInterface(in);
assertEquals(1809, exif.getAttributeRange(ExifInterface.TAG_XMP)[0]);
final XmpInterface xmp = XmpInterface.fromContainer(exif, redactionTags);
+
+ // Confirm redact range within entire file
+ // The XMP contents start at byte 1809. These are the file offsets.
+ final long[] expectedRanges = new long[]{2625,2675,2678,2730,2733,2792,2795,2841};
assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
+
+ // Confirm redact range within local copy
+ final String redactedXmp = new String(xmp.getRedactedXmp());
+ assertFalse(redactedXmp.contains("exif:GPSLatitude"));
+ assertFalse(redactedXmp.contains("exif:GPSLongitude"));
+ assertTrue(redactedXmp.contains("exif:ShutterSpeedValue"));
}
}
@@ -116,9 +123,16 @@
final IsoInterface mp4 = IsoInterface.fromFile(file);
final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags);
+ // Confirm redact range within entire file
// The XMP contents start at byte 30286. These are the file offsets.
final long[] expectedRanges = new long[]{37299,37349,37352,37404,37407,37466,37469,37515};
assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
+
+ // Confirm redact range within local copy
+ final String redactedXmp = new String(xmp.getRedactedXmp());
+ assertFalse(redactedXmp.contains("exif:GPSLatitude"));
+ assertFalse(redactedXmp.contains("exif:GPSLongitude"));
+ assertTrue(redactedXmp.contains("exif:ShutterSpeedValue"));
}
@Test