1 /*
2  * Copyright (C) 2016 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.mediaframeworktest.unit;
18 
19 import com.android.mediaframeworktest.R;
20 
21 import android.content.res.TypedArray;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.media.ExifInterface;
25 import android.os.Environment;
26 import android.test.AndroidTestCase;
27 import android.util.Log;
28 import android.system.ErrnoException;
29 import android.system.Os;
30 import android.system.OsConstants;
31 
32 import java.io.BufferedInputStream;
33 import java.io.ByteArrayInputStream;
34 import java.io.File;
35 import java.io.FileDescriptor;
36 import java.io.FileInputStream;
37 import java.io.FileOutputStream;
38 import java.io.InputStream;
39 import java.io.IOException;
40 
41 import libcore.io.IoUtils;
42 import libcore.io.Streams;
43 
44 public class ExifInterfaceTest extends AndroidTestCase {
45     private static final String TAG = ExifInterface.class.getSimpleName();
46     private static final boolean VERBOSE = false;  // lots of logging
47 
48     private static final double DIFFERENCE_TOLERANCE = .001;
49 
50     // List of files.
51     private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg";
52     private static final String EXIF_BYTE_ORDER_MM_JPEG = "image_exif_byte_order_mm.jpg";
53     private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng";
54     private static final String VOLANTIS_JPEG = "volantis.jpg";
55     private static final int[] IMAGE_RESOURCES = new int[] {
56             R.raw.image_exif_byte_order_ii,  R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800,
57             R.raw.volantis };
58     private static final String[] IMAGE_FILENAMES = new String[] {
59             EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG, VOLANTIS_JPEG };
60 
61     private static final String[] EXIF_TAGS = {
62             ExifInterface.TAG_MAKE,
63             ExifInterface.TAG_MODEL,
64             ExifInterface.TAG_F_NUMBER,
65             ExifInterface.TAG_DATETIME,
66             ExifInterface.TAG_EXPOSURE_TIME,
67             ExifInterface.TAG_FLASH,
68             ExifInterface.TAG_FOCAL_LENGTH,
69             ExifInterface.TAG_GPS_ALTITUDE,
70             ExifInterface.TAG_GPS_ALTITUDE_REF,
71             ExifInterface.TAG_GPS_DATESTAMP,
72             ExifInterface.TAG_GPS_LATITUDE,
73             ExifInterface.TAG_GPS_LATITUDE_REF,
74             ExifInterface.TAG_GPS_LONGITUDE,
75             ExifInterface.TAG_GPS_LONGITUDE_REF,
76             ExifInterface.TAG_GPS_PROCESSING_METHOD,
77             ExifInterface.TAG_GPS_TIMESTAMP,
78             ExifInterface.TAG_IMAGE_LENGTH,
79             ExifInterface.TAG_IMAGE_WIDTH,
80             ExifInterface.TAG_ISO_SPEED_RATINGS,
81             ExifInterface.TAG_ORIENTATION,
82             ExifInterface.TAG_WHITE_BALANCE
83     };
84 
85     private static class ExpectedValue {
86         // Thumbnail information.
87         public final boolean hasThumbnail;
88         public final int thumbnailWidth;
89         public final int thumbnailHeight;
90 
91         // GPS information.
92         public final boolean hasLatLong;
93         public final float latitude;
94         public final float longitude;
95         public final float altitude;
96 
97         // Values.
98         public final String make;
99         public final String model;
100         public final float fNumber;
101         public final String datetime;
102         public final float exposureTime;
103         public final float flash;
104         public final String focalLength;
105         public final String gpsAltitude;
106         public final String gpsAltitudeRef;
107         public final String gpsDatestamp;
108         public final String gpsLatitude;
109         public final String gpsLatitudeRef;
110         public final String gpsLongitude;
111         public final String gpsLongitudeRef;
112         public final String gpsProcessingMethod;
113         public final String gpsTimestamp;
114         public final int imageLength;
115         public final int imageWidth;
116         public final String iso;
117         public final int orientation;
118         public final int whiteBalance;
119 
getString(TypedArray typedArray, int index)120         private static String getString(TypedArray typedArray, int index) {
121             String stringValue = typedArray.getString(index);
122             if (stringValue == null || stringValue.equals("")) {
123                 return null;
124             }
125             return stringValue.trim();
126         }
127 
ExpectedValue(TypedArray typedArray)128         public ExpectedValue(TypedArray typedArray) {
129             // Reads thumbnail information.
130             hasThumbnail = typedArray.getBoolean(0, false);
131             thumbnailWidth = typedArray.getInt(1, 0);
132             thumbnailHeight = typedArray.getInt(2, 0);
133 
134             // Reads GPS information.
135             hasLatLong = typedArray.getBoolean(3, false);
136             latitude = typedArray.getFloat(4, 0f);
137             longitude = typedArray.getFloat(5, 0f);
138             altitude = typedArray.getFloat(6, 0f);
139 
140             // Reads values.
141             make = getString(typedArray, 7);
142             model = getString(typedArray, 8);
143             fNumber = typedArray.getFloat(9, 0f);
144             datetime = getString(typedArray, 10);
145             exposureTime = typedArray.getFloat(11, 0f);
146             flash = typedArray.getFloat(12, 0f);
147             focalLength = getString(typedArray, 13);
148             gpsAltitude = getString(typedArray, 14);
149             gpsAltitudeRef = getString(typedArray, 15);
150             gpsDatestamp = getString(typedArray, 16);
151             gpsLatitude = getString(typedArray, 17);
152             gpsLatitudeRef = getString(typedArray, 18);
153             gpsLongitude = getString(typedArray, 19);
154             gpsLongitudeRef = getString(typedArray, 20);
155             gpsProcessingMethod = getString(typedArray, 21);
156             gpsTimestamp = getString(typedArray, 22);
157             imageLength = typedArray.getInt(23, 0);
158             imageWidth = typedArray.getInt(24, 0);
159             iso = getString(typedArray, 25);
160             orientation = typedArray.getInt(26, 0);
161             whiteBalance = typedArray.getInt(27, 0);
162 
163             typedArray.recycle();
164         }
165     }
166 
167     @Override
setUp()168     protected void setUp() throws Exception {
169         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
170             String outputPath = new File(Environment.getExternalStorageDirectory(),
171                     IMAGE_FILENAMES[i]).getAbsolutePath();
172             try (InputStream inputStream = getContext().getResources().openRawResource(
173                     IMAGE_RESOURCES[i])) {
174                 try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
175                     Streams.copy(inputStream, outputStream);
176                 }
177             }
178         }
179         super.setUp();
180     }
181 
182     @Override
tearDown()183     protected void tearDown() throws Exception {
184         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
185             String imageFilePath = new File(Environment.getExternalStorageDirectory(),
186                     IMAGE_FILENAMES[i]).getAbsolutePath();
187             File imageFile = new File(imageFilePath);
188             if (imageFile.exists()) {
189                 imageFile.delete();
190             }
191         }
192 
193         super.tearDown();
194     }
195 
printExifTagsAndValues(String fileName, ExifInterface exifInterface)196     private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
197         // Prints thumbnail information.
198         if (exifInterface.hasThumbnail()) {
199             byte[] thumbnailBytes = exifInterface.getThumbnailBytes();
200             if (thumbnailBytes != null) {
201                 Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length);
202                 Bitmap bitmap = exifInterface.getThumbnailBitmap();
203                 if (bitmap == null) {
204                     Log.e(TAG, fileName + " Corrupted thumbnail!");
205                 } else {
206                     Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", "
207                             + bitmap.getHeight());
208                 }
209             } else {
210                 Log.e(TAG, fileName + " Unexpected result: No thumbnails were found. "
211                         + "A thumbnail is expected.");
212             }
213         } else {
214             if (exifInterface.getThumbnail() != null) {
215                 Log.e(TAG, fileName + " Unexpected result: A thumbnail was found. "
216                         + "No thumbnail is expected.");
217             } else {
218                 Log.v(TAG, fileName + " No thumbnail");
219             }
220         }
221 
222         // Prints GPS information.
223         Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
224 
225         float[] latLong = new float[2];
226         if (exifInterface.getLatLong(latLong)) {
227             Log.v(TAG, fileName + " Latitude = " + latLong[0]);
228             Log.v(TAG, fileName + " Longitude = " + latLong[1]);
229         } else {
230             Log.v(TAG, fileName + " No latlong data");
231         }
232 
233         // Prints values.
234         for (String tagKey : EXIF_TAGS) {
235             String tagValue = exifInterface.getAttribute(tagKey);
236             Log.v(TAG, fileName + " Key{" + tagKey + "} = '" + tagValue + "'");
237         }
238     }
239 
assertIntTag(ExifInterface exifInterface, String tag, int expectedValue)240     private void assertIntTag(ExifInterface exifInterface, String tag, int expectedValue) {
241         int intValue = exifInterface.getAttributeInt(tag, 0);
242         assertEquals(expectedValue, intValue);
243     }
244 
assertDoubleTag(ExifInterface exifInterface, String tag, float expectedValue)245     private void assertDoubleTag(ExifInterface exifInterface, String tag, float expectedValue) {
246         double doubleValue = exifInterface.getAttributeDouble(tag, 0.0);
247         assertEquals(expectedValue, doubleValue, DIFFERENCE_TOLERANCE);
248     }
249 
assertStringTag(ExifInterface exifInterface, String tag, String expectedValue)250     private void assertStringTag(ExifInterface exifInterface, String tag, String expectedValue) {
251         String stringValue = exifInterface.getAttribute(tag);
252         if (stringValue != null) {
253             stringValue = stringValue.trim();
254         }
255         stringValue = (stringValue == "") ? null : stringValue;
256 
257         assertEquals(expectedValue, stringValue);
258     }
259 
compareWithExpectedValue(ExifInterface exifInterface, ExpectedValue expectedValue, String verboseTag)260     private void compareWithExpectedValue(ExifInterface exifInterface,
261             ExpectedValue expectedValue, String verboseTag) {
262         if (VERBOSE) {
263             printExifTagsAndValues(verboseTag, exifInterface);
264         }
265         // Checks a thumbnail image.
266         assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail());
267         if (expectedValue.hasThumbnail) {
268             byte[] thumbnailBytes = exifInterface.getThumbnailBytes();
269             assertNotNull(thumbnailBytes);
270             Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap();
271             assertNotNull(thumbnailBitmap);
272             assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
273             assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
274         } else {
275             assertNull(exifInterface.getThumbnail());
276         }
277 
278         // Checks GPS information.
279         float[] latLong = new float[2];
280         assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
281         if (expectedValue.hasLatLong) {
282             assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
283             assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
284         }
285         assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE);
286 
287         // Checks values.
288         assertStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make);
289         assertStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model);
290         assertDoubleTag(exifInterface, ExifInterface.TAG_F_NUMBER, expectedValue.fNumber);
291         assertStringTag(exifInterface, ExifInterface.TAG_DATETIME, expectedValue.datetime);
292         assertDoubleTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime);
293         assertDoubleTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash);
294         assertStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength);
295         assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude);
296         assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF,
297                 expectedValue.gpsAltitudeRef);
298         assertStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP, expectedValue.gpsDatestamp);
299         assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude);
300         assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF,
301                 expectedValue.gpsLatitudeRef);
302         assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE, expectedValue.gpsLongitude);
303         assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF,
304                 expectedValue.gpsLongitudeRef);
305         assertStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD,
306                 expectedValue.gpsProcessingMethod);
307         assertStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP, expectedValue.gpsTimestamp);
308         assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength);
309         assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth);
310         assertStringTag(exifInterface, ExifInterface.TAG_ISO_SPEED_RATINGS, expectedValue.iso);
311         assertIntTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation);
312         assertIntTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE, expectedValue.whiteBalance);
313     }
314 
testExifInterfaceCommon(File imageFile, ExpectedValue expectedValue)315     private void testExifInterfaceCommon(File imageFile, ExpectedValue expectedValue)
316             throws IOException {
317         String verboseTag = imageFile.getName();
318 
319         // Creates via path.
320         ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
321         compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
322 
323         // Creates from an asset file.
324         InputStream in = null;
325         try {
326             in = mContext.getAssets().open(imageFile.getName());
327             exifInterface = new ExifInterface(in);
328             compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
329         } finally {
330             IoUtils.closeQuietly(in);
331         }
332 
333         // Creates via InputStream.
334         in = null;
335         try {
336             in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
337             exifInterface = new ExifInterface(in);
338             compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
339         } finally {
340             IoUtils.closeQuietly(in);
341         }
342 
343         // Creates via FileDescriptor.
344         FileDescriptor fd = null;
345         try {
346             fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDONLY, 0600);
347             exifInterface = new ExifInterface(fd);
348             compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
349         } catch (ErrnoException e) {
350             throw e.rethrowAsIOException();
351         } finally {
352             IoUtils.closeQuietly(fd);
353         }
354     }
355 
testSaveAttributes_withFileName(File imageFile, ExpectedValue expectedValue)356     private void testSaveAttributes_withFileName(File imageFile, ExpectedValue expectedValue)
357             throws IOException {
358         String verboseTag = imageFile.getName();
359 
360         ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
361         exifInterface.saveAttributes();
362         exifInterface = new ExifInterface(imageFile.getAbsolutePath());
363         compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
364 
365         // Test for modifying one attribute.
366         String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
367         exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
368         exifInterface.saveAttributes();
369         exifInterface = new ExifInterface(imageFile.getAbsolutePath());
370         assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
371         // Restore the backup value.
372         exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue);
373         exifInterface.saveAttributes();
374         exifInterface = new ExifInterface(imageFile.getAbsolutePath());
375         compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
376     }
377 
testSaveAttributes_withFileDescriptor(File imageFile, ExpectedValue expectedValue)378     private void testSaveAttributes_withFileDescriptor(File imageFile, ExpectedValue expectedValue)
379             throws IOException {
380         String verboseTag = imageFile.getName();
381 
382         FileDescriptor fd = null;
383         try {
384             fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
385             ExifInterface exifInterface = new ExifInterface(fd);
386             exifInterface.saveAttributes();
387             Os.lseek(fd, 0, OsConstants.SEEK_SET);
388             exifInterface = new ExifInterface(fd);
389             compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
390 
391             // Test for modifying one attribute.
392             String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
393             exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
394             exifInterface.saveAttributes();
395             Os.lseek(fd, 0, OsConstants.SEEK_SET);
396             exifInterface = new ExifInterface(fd);
397             assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
398             // Restore the backup value.
399             exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue);
400             exifInterface.saveAttributes();
401             Os.lseek(fd, 0, OsConstants.SEEK_SET);
402             exifInterface = new ExifInterface(fd);
403             compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
404         } catch (ErrnoException e) {
405             throw e.rethrowAsIOException();
406         } finally {
407             IoUtils.closeQuietly(fd);
408         }
409     }
410 
testSaveAttributes_withInputStream(File imageFile, ExpectedValue expectedValue)411     private void testSaveAttributes_withInputStream(File imageFile, ExpectedValue expectedValue)
412             throws IOException {
413         InputStream in = null;
414         try {
415             in = getContext().getAssets().open(imageFile.getName());
416             ExifInterface exifInterface = new ExifInterface(in);
417             exifInterface.saveAttributes();
418         } catch (IOException e) {
419             // Expected. saveAttributes is not supported with an ExifInterface object which was
420             // created with InputStream.
421             return;
422         } finally {
423             IoUtils.closeQuietly(in);
424         }
425         fail("Should not reach here!");
426     }
427 
testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)428     private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
429             throws IOException {
430         ExpectedValue expectedValue = new ExpectedValue(
431                 getContext().getResources().obtainTypedArray(typedArrayResourceId));
432         File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
433 
434         // Test for reading from various inputs.
435         testExifInterfaceCommon(imageFile, expectedValue);
436 
437         // Test for saving attributes.
438         testSaveAttributes_withFileName(imageFile, expectedValue);
439         testSaveAttributes_withFileDescriptor(imageFile, expectedValue);
440         testSaveAttributes_withInputStream(imageFile, expectedValue);
441     }
442 
testExifInterfaceForRaw(String fileName, int typedArrayResourceId)443     private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId)
444             throws IOException {
445         ExpectedValue expectedValue = new ExpectedValue(
446                 getContext().getResources().obtainTypedArray(typedArrayResourceId));
447         File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
448 
449         // Test for reading from various inputs.
450         testExifInterfaceCommon(imageFile, expectedValue);
451 
452         // Since ExifInterface does not support for saving attributes for RAW files, do not test
453         // about writing back in here.
454     }
455 
testReadExifDataFromExifByteOrderIIJpeg()456     public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
457         testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
458     }
459 
testReadExifDataFromExifByteOrderMMJpeg()460     public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
461         testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
462     }
463 
testReadExifDataFromLgG4Iso800Dng()464     public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
465         testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
466     }
467 
testDoNotFailOnCorruptedImage()468     public void testDoNotFailOnCorruptedImage() throws Throwable {
469         // To keep the compatibility with old versions of ExifInterface, even on a corrupted image,
470         // it shouldn't raise any exceptions except an IOException when unable to open a file.
471         byte[] bytes = new byte[1024];
472         try {
473             new ExifInterface(new ByteArrayInputStream(bytes));
474             // Always success
475         } catch (IOException e) {
476             fail("Should not reach here!");
477         }
478     }
479 
testReadExifDataFromVolantisJpg()480     public void testReadExifDataFromVolantisJpg() throws Throwable {
481         // Test if it is possible to parse the volantis generated JPEG smoothly.
482         testExifInterfaceForJpeg(VOLANTIS_JPEG, R.array.volantis_jpg);
483     }
484 }
485