Merge "Add column for indexed XMP metadata."
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/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index b96358a..77592eb 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -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 2f88183..2e731ca 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -4970,6 +4970,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 +5081,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 +5092,7 @@
}
}
// Redact xmp where present
- final XmpInterface exifXmp = XmpInterface.fromContainer(exif, redactedXmpTags);
+ final XmpInterface exifXmp = XmpInterface.fromContainer(exif);
res.addAll(exifXmp.getRedactionRanges());
}
@@ -5106,7 +5108,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) {
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 4a3ab82..116d0e7 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -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());
}
/**
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/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