1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import static org.junit.Assert.assertArrayEquals; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertNull; 22 import static org.junit.Assert.fail; 23 24 import android.content.Context; 25 import android.content.res.AssetFileDescriptor; 26 import android.media.ExifInterface; 27 import android.os.FileUtils; 28 import android.util.LongArray; 29 import android.util.ArraySet; 30 import android.util.Xml; 31 32 import com.android.providers.media.tests.R; 33 import com.android.providers.media.util.IsoInterface; 34 import com.android.providers.media.util.XmpInterface; 35 36 import org.junit.Test; 37 import org.junit.runner.RunWith; 38 import org.xmlpull.v1.XmlPullParser; 39 import org.xmlpull.v1.XmlPullParserException; 40 import org.xmlpull.v1.XmlPullParserFactory; 41 42 import java.io.ByteArrayInputStream; 43 import java.io.File; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 import java.io.StringReader; 49 import java.nio.charset.StandardCharsets; 50 import java.util.Set; 51 52 import androidx.test.InstrumentationRegistry; 53 import androidx.test.runner.AndroidJUnit4; 54 55 @RunWith(AndroidJUnit4.class) 56 public class XmpInterfaceTest { 57 @Test testContainer_Empty()58 public void testContainer_Empty() throws Exception { 59 final Context context = InstrumentationRegistry.getContext(); 60 try (InputStream in = context.getResources().openRawResource(R.raw.test_image)) { 61 final XmpInterface xmp = XmpInterface.fromContainer(in); 62 assertNull(xmp.getFormat()); 63 assertNull(xmp.getDocumentId()); 64 assertNull(xmp.getInstanceId()); 65 assertNull(xmp.getOriginalDocumentId()); 66 } 67 } 68 69 @Test testContainer_ValidAttrs()70 public void testContainer_ValidAttrs() throws Exception { 71 final Context context = InstrumentationRegistry.getContext(); 72 try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg)) { 73 final XmpInterface xmp = XmpInterface.fromContainer(in); 74 assertEquals("image/dng", xmp.getFormat()); 75 assertEquals("xmp.did:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getDocumentId()); 76 assertEquals("xmp.iid:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getInstanceId()); 77 assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId()); 78 } 79 } 80 81 @Test testContainer_ValidTags()82 public void testContainer_ValidTags() throws Exception { 83 final Context context = InstrumentationRegistry.getContext(); 84 try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_dng)) { 85 final XmpInterface xmp = XmpInterface.fromContainer(in); 86 assertEquals("image/dng", xmp.getFormat()); 87 assertEquals("xmp.did:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getDocumentId()); 88 assertEquals("xmp.iid:041dfd42-0b46-4302-918a-836fba5016ed", xmp.getInstanceId()); 89 assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId()); 90 } 91 } 92 93 @Test testContainer_ExifRedactionRanges()94 public void testContainer_ExifRedactionRanges() throws Exception { 95 final Set<String> redactionTags = new ArraySet<>(); 96 redactionTags.add(ExifInterface.TAG_GPS_LATITUDE); 97 redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE); 98 redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP); 99 redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID); 100 101 // The XMP contents start at byte 1809. These are the file offsets. 102 final long[] expectedRanges = new long[]{2625,2675,2678,2730,2733,2792,2795,2841}; 103 final Context context = InstrumentationRegistry.getContext(); 104 try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg)) { 105 ExifInterface exif = new ExifInterface(in); 106 assertEquals(1809, exif.getAttributeRange(ExifInterface.TAG_XMP)[0]); 107 final XmpInterface xmp = XmpInterface.fromContainer(exif, redactionTags); 108 assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray()); 109 } 110 } 111 112 @Test testContainer_IsoRedactionRanges()113 public void testContainer_IsoRedactionRanges() throws Exception { 114 final Set<String> redactionTags = new ArraySet<>(); 115 redactionTags.add(ExifInterface.TAG_GPS_LATITUDE); 116 redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE); 117 redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP); 118 redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID); 119 120 final File file = stageFile(R.raw.test_video_xmp); 121 final IsoInterface mp4 = IsoInterface.fromFile(file); 122 final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags); 123 124 // The XMP contents start at byte 30286. These are the file offsets. 125 final long[] expectedRanges = new long[]{37299,37349,37352,37404,37407,37466,37469,37515}; 126 assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray()); 127 } 128 129 @Test testContainer_IsoRedactionRanges_BadTagValue()130 public void testContainer_IsoRedactionRanges_BadTagValue() throws Exception { 131 final Set<String> redactionTags = new ArraySet<>(); 132 redactionTags.add(ExifInterface.TAG_GPS_LATITUDE); 133 redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE); 134 redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP); 135 redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID); 136 137 // This file has some inner xml in the latitude tag. We should redact anyway. 138 final File file = stageFile(R.raw.test_video_xmp_bad_tag); 139 final IsoInterface mp4 = IsoInterface.fromFile(file); 140 final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags); 141 142 // The XMP contents start at byte 30286. These are the file offsets. 143 final long[] expectedRanges = new long[]{37299,37349,37352,37404,37407,37466,37469,37515}; 144 assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray()); 145 } 146 147 @Test testContainer_IsoRedactionRanges_MalformedXml()148 public void testContainer_IsoRedactionRanges_MalformedXml() throws Exception { 149 final Set<String> redactionTags = new ArraySet<>(); 150 redactionTags.add(ExifInterface.TAG_GPS_LATITUDE); 151 redactionTags.add(ExifInterface.TAG_GPS_LONGITUDE); 152 redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP); 153 redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID); 154 155 // This file has malformed XML in the latitude tag. XML parsing will fail 156 final File file = stageFile(R.raw.test_video_xmp_malformed); 157 final IsoInterface mp4 = IsoInterface.fromFile(file); 158 try { 159 final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags); 160 fail("Should throw IOException"); 161 } catch (IOException e) {} 162 } 163 164 @Test testStream_LineOffsets()165 public void testStream_LineOffsets() throws Exception { 166 final String xml = 167 "<a:b xmlns:a='a' xmlns:c='c' c:d=''\n c:f='g'>\n <c:i>j</c:i>\n </a:b>"; 168 final InputStream xmlStream = new ByteArrayInputStream(xml.getBytes("UTF-8")); 169 final XmpInterface.ByteCountingInputStream stream = 170 new XmpInterface.ByteCountingInputStream(xmlStream); 171 172 final long[] expectedElementOffsets = new long[]{46,54,61,70}; 173 XmlPullParser parser = Xml.newPullParser(); 174 parser.setInput(stream, StandardCharsets.UTF_8.name()); 175 int type; 176 int i = 0; 177 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 178 if (type == XmlPullParser.START_TAG || type == XmlPullParser.END_TAG) { 179 assertEquals(expectedElementOffsets[i++], stream.getOffset(parser)); 180 } 181 } 182 } 183 stageFile(int resId)184 private static File stageFile(int resId) throws Exception { 185 final Context context = InstrumentationRegistry.getContext(); 186 final File file = File.createTempFile("test", ".mp4"); 187 try (InputStream in = context.getResources().openRawResource(resId); 188 OutputStream out = new FileOutputStream(file)) { 189 FileUtils.copy(in, out); 190 } 191 return file; 192 } 193 } 194