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