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