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
17package com.android.mediaframeworktest.unit;
18
19import com.android.mediaframeworktest.R;
20
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.media.ExifInterface;
25import android.os.Environment;
26import android.test.AndroidTestCase;
27import android.util.Log;
28import android.system.ErrnoException;
29import android.system.Os;
30import android.system.OsConstants;
31
32import java.io.BufferedInputStream;
33import java.io.ByteArrayInputStream;
34import java.io.File;
35import java.io.FileDescriptor;
36import java.io.FileInputStream;
37import java.io.FileOutputStream;
38import java.io.InputStream;
39import java.io.IOException;
40
41import libcore.io.IoUtils;
42import libcore.io.Streams;
43
44public 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
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
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
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
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
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
240    private void assertIntTag(ExifInterface exifInterface, String tag, int expectedValue) {
241        int intValue = exifInterface.getAttributeInt(tag, 0);
242        assertEquals(expectedValue, intValue);
243    }
244
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
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
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
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
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
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
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
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
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
456    public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
457        testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
458    }
459
460    public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
461        testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
462    }
463
464    public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
465        testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
466    }
467
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
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