1 /*
2  * Copyright 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 androidx.camera.core.impl.utils;
18 
19 import android.location.Location;
20 
21 import androidx.annotation.VisibleForTesting;
22 import androidx.camera.core.ImageProxy;
23 import androidx.camera.core.Logger;
24 import androidx.exifinterface.media.ExifInterface;
25 
26 import org.jspecify.annotations.NonNull;
27 import org.jspecify.annotations.Nullable;
28 
29 import java.io.ByteArrayInputStream;
30 import java.io.File;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.nio.ByteBuffer;
34 import java.text.ParseException;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Date;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Objects;
42 
43 /**
44  * Utility class for modifying metadata on JPEG files.
45  *
46  * <p>Call {@link #save()} to persist changes to disc.
47  */
48 public final class Exif {
49 
50     /** Timestamp value indicating a timestamp value that is either not set or not valid */
51     public static final long INVALID_TIMESTAMP = -1;
52     // Forked from ExifInterface.TAG_THUMBNAIL_ORIENTATION. The value is library-internal so we
53     // can't depend on it directly.
54     public static final String TAG_THUMBNAIL_ORIENTATION = "ThumbnailOrientation";
55 
56     private static final String TAG = Exif.class.getSimpleName();
57 
58     private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
59             new ThreadLocal<SimpleDateFormat>() {
60                 @Override
61                 public SimpleDateFormat initialValue() {
62                     return new SimpleDateFormat("yyyy:MM:dd", Locale.US);
63                 }
64             };
65     private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT =
66             new ThreadLocal<SimpleDateFormat>() {
67                 @Override
68                 public SimpleDateFormat initialValue() {
69                     return new SimpleDateFormat("HH:mm:ss", Locale.US);
70                 }
71             };
72     private static final ThreadLocal<SimpleDateFormat> DATETIME_FORMAT =
73             new ThreadLocal<SimpleDateFormat>() {
74                 @Override
75                 public SimpleDateFormat initialValue() {
76                     return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
77                 }
78             };
79 
80     private static final String KILOMETERS_PER_HOUR = "K";
81     private static final String MILES_PER_HOUR = "M";
82     private static final String KNOTS = "N";
83 
84     /** All public tags in {@link ExifInterface}. */
85     private static final List<String> ALL_EXIF_TAGS = getAllExifTags();
86     // Exif tags that should not be copied to the cropped image.
87     private static final List<String> DO_NOT_COPY_EXIF_TAGS = Arrays.asList(
88             // Dimension-related tags, which might change after cropping.
89             ExifInterface.TAG_IMAGE_WIDTH,
90             ExifInterface.TAG_IMAGE_LENGTH,
91             ExifInterface.TAG_PIXEL_X_DIMENSION,
92             ExifInterface.TAG_PIXEL_Y_DIMENSION,
93             // Thumbnail-related tags. Currently we do not create thumbnail for cropped images.
94             ExifInterface.TAG_COMPRESSION, // Our primary image is always Jpeg.
95             ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
96             ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
97             ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
98             ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
99             TAG_THUMBNAIL_ORIENTATION);
100 
101     private final ExifInterface mExifInterface;
102 
103     // When true, avoid saving any time. This is a privacy issue.
104     private boolean mRemoveTimestamp = false;
105 
Exif(ExifInterface exifInterface)106     private Exif(ExifInterface exifInterface) {
107         mExifInterface = exifInterface;
108     }
109 
110     /**
111      * Returns an Exif from the exif data contained in the file.
112      *
113      * @param file the file to read exif data from
114      */
createFromFile(@onNull File file)115     public static @NonNull Exif createFromFile(@NonNull File file) throws IOException {
116         return createFromFileString(file.toString());
117     }
118 
119     /**
120      * Returns an Exif extracted from the given {@link ImageProxy}.
121      *
122      * <p> This method rewinds and reads the given buffer.
123      */
createFromImageProxy(@onNull ImageProxy imageProxy)124     public static @NonNull Exif createFromImageProxy(@NonNull ImageProxy imageProxy)
125             throws IOException {
126         ByteBuffer buffer = imageProxy.getPlanes()[0].getBuffer();
127         // Rewind to make sure it is at the beginning of the buffer
128         buffer.rewind();
129 
130         byte[] data = new byte[buffer.capacity()];
131         buffer.get(data);
132         InputStream inputStream = new ByteArrayInputStream(data);
133         return Exif.createFromInputStream(inputStream);
134     }
135 
136     /**
137      * Returns an Exif from the exif data contained in the file at the filePath
138      *
139      * @param filePath the path to the file to read exif data from
140      */
createFromFileString(@onNull String filePath)141     public static @NonNull Exif createFromFileString(@NonNull String filePath) throws IOException {
142         return new Exif(new ExifInterface(filePath));
143     }
144 
145     /**
146      * Returns an Exif from the exif data contain in the input stream.
147      *
148      * @param is the input stream to read exif data from
149      */
createFromInputStream(@onNull InputStream is)150     public static @NonNull Exif createFromInputStream(@NonNull InputStream is) throws IOException {
151         return new Exif(new ExifInterface(is));
152     }
153 
convertToExifDateTime(long timestamp)154     private static String convertToExifDateTime(long timestamp) {
155         return DATETIME_FORMAT.get().format(new Date(timestamp));
156     }
157 
convertFromExifDateTime(String dateTime)158     private static Date convertFromExifDateTime(String dateTime) throws ParseException {
159         return DATETIME_FORMAT.get().parse(dateTime);
160     }
161 
convertFromExifDate(String date)162     private static Date convertFromExifDate(String date) throws ParseException {
163         return DATE_FORMAT.get().parse(date);
164     }
165 
convertFromExifTime(String time)166     private static Date convertFromExifTime(String time) throws ParseException {
167         return TIME_FORMAT.get().parse(time);
168     }
169 
170     /** Persists changes to disc. */
save()171     public void save() throws IOException {
172         if (!mRemoveTimestamp) {
173             attachLastModifiedTimestamp();
174         }
175         mExifInterface.saveAttributes();
176     }
177 
178     /**
179      * Copies Exif values to the given instance.
180      *
181      * <p> This methods is for copying exif data from the original image to the cropped image. Tags
182      * affected by cropping are not copied.
183      */
copyToCroppedImage(@onNull Exif croppedExif)184     public void copyToCroppedImage(@NonNull Exif croppedExif) {
185         List<String> exifTags = new ArrayList<>(ALL_EXIF_TAGS);
186         exifTags.removeAll(DO_NOT_COPY_EXIF_TAGS);
187         for (String tag : exifTags) {
188             String originalValue = mExifInterface.getAttribute(tag);
189             String croppedExifValue = croppedExif.mExifInterface.getAttribute(tag);
190             if (originalValue != null && !Objects.equals(originalValue, croppedExifValue)) {
191                 croppedExif.mExifInterface.setAttribute(tag, originalValue);
192             }
193         }
194     }
195 
196     @Override
toString()197     public String toString() {
198         return String.format(
199                 Locale.ENGLISH,
200                 "Exif{width=%s, height=%s, rotation=%d, "
201                         + "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, "
202                         + "timestamp=%s, description=%s}",
203                 getWidth(),
204                 getHeight(),
205                 getRotation(),
206                 isFlippedVertically(),
207                 isFlippedHorizontally(),
208                 getLocation(),
209                 getTimestamp(),
210                 getDescription());
211     }
212 
getOrientation()213     public int getOrientation() {
214         return mExifInterface.getAttributeInt(
215                 ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
216     }
217 
218     /** Sets the orientation for the exif. */
setOrientation(int orientation)219     public void setOrientation(int orientation) {
220         mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
221     }
222 
223     /** Returns the width of the photo in pixels. */
getWidth()224     public int getWidth() {
225         return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
226     }
227 
228     /** Returns the height of the photo in pixels. */
getHeight()229     public int getHeight() {
230         return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
231     }
232 
getDescription()233     public @Nullable String getDescription() {
234         return mExifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION);
235     }
236 
237     /** Sets the description for the exif. */
setDescription(@ullable String description)238     public void setDescription(@Nullable String description) {
239         mExifInterface.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, description);
240     }
241 
242     /** @return The degree of rotation (eg. 0, 90, 180, 270). */
getRotation()243     public int getRotation() {
244         switch (getOrientation()) {
245             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
246                 return 0;
247             case ExifInterface.ORIENTATION_ROTATE_180:
248                 return 180;
249             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
250                 return 180;
251             case ExifInterface.ORIENTATION_TRANSPOSE:
252                 return 270;
253             case ExifInterface.ORIENTATION_ROTATE_90:
254                 return 90;
255             case ExifInterface.ORIENTATION_TRANSVERSE:
256                 return 90;
257             case ExifInterface.ORIENTATION_ROTATE_270:
258                 return 270;
259             case ExifInterface.ORIENTATION_NORMAL:
260                 // Fall-through
261             case ExifInterface.ORIENTATION_UNDEFINED:
262                 // Fall-through
263             default:
264                 return 0;
265         }
266     }
267 
268     /** @return True if the image is flipped vertically after rotation. */
isFlippedVertically()269     public boolean isFlippedVertically() {
270         switch (getOrientation()) {
271             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
272                 return false;
273             case ExifInterface.ORIENTATION_ROTATE_180:
274                 return false;
275             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
276                 return true;
277             case ExifInterface.ORIENTATION_TRANSPOSE:
278                 return true;
279             case ExifInterface.ORIENTATION_ROTATE_90:
280                 return false;
281             case ExifInterface.ORIENTATION_TRANSVERSE:
282                 return true;
283             case ExifInterface.ORIENTATION_ROTATE_270:
284                 return false;
285             case ExifInterface.ORIENTATION_NORMAL:
286                 // Fall-through
287             case ExifInterface.ORIENTATION_UNDEFINED:
288                 // Fall-through
289             default:
290                 return false;
291         }
292     }
293 
294     /** @return True if the image is flipped horizontally after rotation. */
isFlippedHorizontally()295     public boolean isFlippedHorizontally() {
296         switch (getOrientation()) {
297             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
298                 return true;
299             case ExifInterface.ORIENTATION_ROTATE_180:
300                 return false;
301             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
302                 return false;
303             case ExifInterface.ORIENTATION_TRANSPOSE:
304                 return false;
305             case ExifInterface.ORIENTATION_ROTATE_90:
306                 return false;
307             case ExifInterface.ORIENTATION_TRANSVERSE:
308                 return false;
309             case ExifInterface.ORIENTATION_ROTATE_270:
310                 return false;
311             case ExifInterface.ORIENTATION_NORMAL:
312                 // Fall-through
313             case ExifInterface.ORIENTATION_UNDEFINED:
314                 // Fall-through
315             default:
316                 return false;
317         }
318     }
319 
attachLastModifiedTimestamp()320     private void attachLastModifiedTimestamp() {
321         long now = System.currentTimeMillis();
322         String datetime = convertToExifDateTime(now);
323 
324         mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime);
325 
326         try {
327             String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
328             mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, subsec);
329         } catch (ParseException e) {
330         }
331     }
332 
333     /**
334      * @return The timestamp (in millis) that this picture was modified, or {@link
335      * #INVALID_TIMESTAMP} if no time is available.
336      */
getLastModifiedTimestamp()337     public long getLastModifiedTimestamp() {
338         long timestamp = parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME));
339         if (timestamp == INVALID_TIMESTAMP) {
340             return INVALID_TIMESTAMP;
341         }
342 
343         String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME);
344         if (subSecs != null) {
345             try {
346                 long sub = Long.parseLong(subSecs);
347                 while (sub > 1000) {
348                     sub /= 10;
349                 }
350                 timestamp += sub;
351             } catch (NumberFormatException e) {
352                 // Ignored
353             }
354         }
355 
356         return timestamp;
357     }
358 
359     /**
360      * @return The timestamp (in millis) that this picture was taken, or {@link #INVALID_TIMESTAMP}
361      * if no time is available.
362      */
getTimestamp()363     public long getTimestamp() {
364         long timestamp =
365                 parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
366         if (timestamp == INVALID_TIMESTAMP) {
367             return INVALID_TIMESTAMP;
368         }
369 
370         String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL);
371         if (subSecs != null) {
372             try {
373                 long sub = Long.parseLong(subSecs);
374                 while (sub > 1000) {
375                     sub /= 10;
376                 }
377                 timestamp += sub;
378             } catch (NumberFormatException e) {
379                 // Ignored
380             }
381         }
382 
383         return timestamp;
384     }
385 
386     /** @return The location this picture was taken, or null if no location is available. */
getLocation()387     public @Nullable Location getLocation() {
388         String provider = mExifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
389         double[] latlng = mExifInterface.getLatLong();
390         double altitude = mExifInterface.getAltitude(0);
391         double speed = mExifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0);
392         String speedRef = mExifInterface.getAttribute(ExifInterface.TAG_GPS_SPEED_REF);
393         speedRef = speedRef == null ? KILOMETERS_PER_HOUR : speedRef; // Ensure speedRef is not null
394         long timestamp =
395                 parseTimestamp(
396                         mExifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP),
397                         mExifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
398         if (latlng == null) {
399             return null;
400         }
401         if (provider == null) {
402             provider = TAG;
403         }
404 
405         Location location = new Location(provider);
406         location.setLatitude(latlng[0]);
407         location.setLongitude(latlng[1]);
408         if (altitude != 0) {
409             location.setAltitude(altitude);
410         }
411         if (speed != 0) {
412             switch (speedRef) {
413                 case MILES_PER_HOUR:
414                     speed = Speed.fromMilesPerHour(speed).toMetersPerSecond();
415                     break;
416                 case KNOTS:
417                     speed = Speed.fromKnots(speed).toMetersPerSecond();
418                     break;
419                 case KILOMETERS_PER_HOUR:
420                     // fall through
421                 default:
422                     speed = Speed.fromKilometersPerHour(speed).toMetersPerSecond();
423                     break;
424             }
425 
426             location.setSpeed((float) speed);
427         }
428         if (timestamp != INVALID_TIMESTAMP) {
429             location.setTime(timestamp);
430         }
431         return location;
432     }
433 
434     /**
435      * Rotates the image by the given degrees. Can only rotate by right angles (eg. 90, 180, -90).
436      * Other increments will set the orientation to undefined.
437      */
rotate(int degrees)438     public void rotate(int degrees) {
439         if (degrees % 90 != 0) {
440             Logger.w(
441                     TAG,
442                     String.format(Locale.US,
443                             "Can only rotate in right angles (eg. 0, 90, 180, 270). %d is "
444                                     + "unsupported.",
445                             degrees));
446             mExifInterface.setAttribute(
447                     ExifInterface.TAG_ORIENTATION,
448                     String.valueOf(ExifInterface.ORIENTATION_UNDEFINED));
449             return;
450         }
451 
452         degrees %= 360;
453 
454         int orientation = getOrientation();
455         while (degrees < 0) {
456             degrees += 90;
457 
458             switch (orientation) {
459                 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
460                     orientation = ExifInterface.ORIENTATION_TRANSPOSE;
461                     break;
462                 case ExifInterface.ORIENTATION_ROTATE_180:
463                     orientation = ExifInterface.ORIENTATION_ROTATE_90;
464                     break;
465                 case ExifInterface.ORIENTATION_FLIP_VERTICAL:
466                     orientation = ExifInterface.ORIENTATION_TRANSVERSE;
467                     break;
468                 case ExifInterface.ORIENTATION_TRANSPOSE:
469                     orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
470                     break;
471                 case ExifInterface.ORIENTATION_ROTATE_90:
472                     orientation = ExifInterface.ORIENTATION_NORMAL;
473                     break;
474                 case ExifInterface.ORIENTATION_TRANSVERSE:
475                     orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
476                     break;
477                 case ExifInterface.ORIENTATION_ROTATE_270:
478                     orientation = ExifInterface.ORIENTATION_ROTATE_90;
479                     break;
480                 case ExifInterface.ORIENTATION_NORMAL:
481                     // Fall-through
482                 case ExifInterface.ORIENTATION_UNDEFINED:
483                     // Fall-through
484                 default:
485                     orientation = ExifInterface.ORIENTATION_ROTATE_270;
486                     break;
487             }
488         }
489         while (degrees > 0) {
490             degrees -= 90;
491 
492             switch (orientation) {
493                 case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
494                     orientation = ExifInterface.ORIENTATION_TRANSVERSE;
495                     break;
496                 case ExifInterface.ORIENTATION_ROTATE_180:
497                     orientation = ExifInterface.ORIENTATION_ROTATE_270;
498                     break;
499                 case ExifInterface.ORIENTATION_FLIP_VERTICAL:
500                     orientation = ExifInterface.ORIENTATION_TRANSPOSE;
501                     break;
502                 case ExifInterface.ORIENTATION_TRANSPOSE:
503                     orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
504                     break;
505                 case ExifInterface.ORIENTATION_ROTATE_90:
506                     orientation = ExifInterface.ORIENTATION_ROTATE_180;
507                     break;
508                 case ExifInterface.ORIENTATION_TRANSVERSE:
509                     orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
510                     break;
511                 case ExifInterface.ORIENTATION_ROTATE_270:
512                     orientation = ExifInterface.ORIENTATION_NORMAL;
513                     break;
514                 case ExifInterface.ORIENTATION_NORMAL:
515                     // Fall-through
516                 case ExifInterface.ORIENTATION_UNDEFINED:
517                     // Fall-through
518                 default:
519                     orientation = ExifInterface.ORIENTATION_ROTATE_90;
520                     break;
521             }
522         }
523         mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
524     }
525 
526     /**
527      * Sets attributes to represent a flip of the image over the horizon so that the top and bottom
528      * are reversed.
529      */
flipVertically()530     public void flipVertically() {
531         int orientation;
532         switch (getOrientation()) {
533             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
534                 orientation = ExifInterface.ORIENTATION_ROTATE_180;
535                 break;
536             case ExifInterface.ORIENTATION_ROTATE_180:
537                 orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
538                 break;
539             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
540                 orientation = ExifInterface.ORIENTATION_NORMAL;
541                 break;
542             case ExifInterface.ORIENTATION_TRANSPOSE:
543                 orientation = ExifInterface.ORIENTATION_ROTATE_270;
544                 break;
545             case ExifInterface.ORIENTATION_ROTATE_90:
546                 orientation = ExifInterface.ORIENTATION_TRANSVERSE;
547                 break;
548             case ExifInterface.ORIENTATION_TRANSVERSE:
549                 orientation = ExifInterface.ORIENTATION_ROTATE_90;
550                 break;
551             case ExifInterface.ORIENTATION_ROTATE_270:
552                 orientation = ExifInterface.ORIENTATION_TRANSPOSE;
553                 break;
554             case ExifInterface.ORIENTATION_NORMAL:
555                 // Fall-through
556             case ExifInterface.ORIENTATION_UNDEFINED:
557                 // Fall-through
558             default:
559                 orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
560                 break;
561         }
562         mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
563     }
564 
565     /**
566      * Sets attributes to represent a flip of the image over the vertical so that the left and right
567      * are reversed.
568      */
flipHorizontally()569     public void flipHorizontally() {
570         int orientation;
571         switch (getOrientation()) {
572             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
573                 orientation = ExifInterface.ORIENTATION_NORMAL;
574                 break;
575             case ExifInterface.ORIENTATION_ROTATE_180:
576                 orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
577                 break;
578             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
579                 orientation = ExifInterface.ORIENTATION_ROTATE_180;
580                 break;
581             case ExifInterface.ORIENTATION_TRANSPOSE:
582                 orientation = ExifInterface.ORIENTATION_ROTATE_90;
583                 break;
584             case ExifInterface.ORIENTATION_ROTATE_90:
585                 orientation = ExifInterface.ORIENTATION_TRANSPOSE;
586                 break;
587             case ExifInterface.ORIENTATION_TRANSVERSE:
588                 orientation = ExifInterface.ORIENTATION_ROTATE_270;
589                 break;
590             case ExifInterface.ORIENTATION_ROTATE_270:
591                 orientation = ExifInterface.ORIENTATION_TRANSVERSE;
592                 break;
593             case ExifInterface.ORIENTATION_NORMAL:
594                 // Fall-through
595             case ExifInterface.ORIENTATION_UNDEFINED:
596                 // Fall-through
597             default:
598                 orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
599                 break;
600         }
601         mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
602     }
603 
604     @VisibleForTesting
getMetadata()605     public @Nullable String getMetadata() {
606         return mExifInterface.getAttribute(ExifInterface.TAG_XMP);
607     }
608 
609     @VisibleForTesting
getExifInterface()610     public @NonNull ExifInterface getExifInterface() {
611         return mExifInterface;
612     }
613 
614     /** Attaches the current timestamp to the file. */
attachTimestamp()615     public void attachTimestamp() {
616         long now = System.currentTimeMillis();
617         String datetime = convertToExifDateTime(now);
618 
619         mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime);
620         mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime);
621 
622         try {
623             String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
624             mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec);
625             mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec);
626         } catch (ParseException e) {
627         }
628 
629         mRemoveTimestamp = false;
630     }
631 
632     /** Removes the timestamp from the file. */
removeTimestamp()633     public void removeTimestamp() {
634         mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, null);
635         mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
636         mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
637         mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null);
638         mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null);
639         mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null);
640         mRemoveTimestamp = true;
641     }
642 
643     /** Attaches the given location to the file. */
attachLocation(@onNull Location location)644     public void attachLocation(@NonNull Location location) {
645         mExifInterface.setGpsInfo(location);
646     }
647 
648     /** Removes the location from the file. */
removeLocation()649     public void removeLocation() {
650         mExifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null);
651         mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null);
652         mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null);
653         mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null);
654         mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null);
655         mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null);
656         mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null);
657         mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, null);
658         mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null);
659         mExifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null);
660         mExifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null);
661     }
662 
663     /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */
parseTimestamp(@ullable String date, @Nullable String time)664     private long parseTimestamp(@Nullable String date, @Nullable String time) {
665         if (date == null && time == null) {
666             return INVALID_TIMESTAMP;
667         }
668         if (time == null) {
669             try {
670                 return convertFromExifDate(date).getTime();
671             } catch (ParseException e) {
672                 return INVALID_TIMESTAMP;
673             }
674         }
675         if (date == null) {
676             try {
677                 return convertFromExifTime(time).getTime();
678             } catch (ParseException e) {
679                 return INVALID_TIMESTAMP;
680             }
681         }
682         return parseTimestamp(date + " " + time);
683     }
684 
685     /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */
parseTimestamp(@ullable String datetime)686     private long parseTimestamp(@Nullable String datetime) {
687         if (datetime == null) {
688             return INVALID_TIMESTAMP;
689         }
690         try {
691             return convertFromExifDateTime(datetime).getTime();
692         } catch (ParseException e) {
693             return INVALID_TIMESTAMP;
694         }
695     }
696 
697     private static final class Speed {
fromKilometersPerHour(double kph)698         static Converter fromKilometersPerHour(double kph) {
699             return new Converter(kph * 0.621371);
700         }
fromMilesPerHour(double mph)701         static Converter fromMilesPerHour(double mph) {
702             return new Converter(mph);
703         }
704 
fromKnots(double knots)705         static Converter fromKnots(double knots) {
706             return new Converter(knots * 1.15078);
707         }
708 
709         static final class Converter {
710             final double mMph;
711 
Converter(double mph)712             Converter(double mph) {
713                 mMph = mph;
714             }
715 
toMetersPerSecond()716             double toMetersPerSecond() {
717                 return mMph / 2.23694;
718             }
719         }
720     }
721 
722     /**
723      * Creates a list that contains all public tags defined in {@link ExifInterface}.
724      *
725      * <p> Deprecated tags are not included.
726      */
getAllExifTags()727     public static @NonNull List<String> getAllExifTags() {
728         return Arrays.asList(
729                 ExifInterface.TAG_IMAGE_WIDTH,
730                 ExifInterface.TAG_IMAGE_LENGTH,
731                 ExifInterface.TAG_BITS_PER_SAMPLE,
732                 ExifInterface.TAG_COMPRESSION,
733                 ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
734                 ExifInterface.TAG_ORIENTATION,
735                 ExifInterface.TAG_SAMPLES_PER_PIXEL,
736                 ExifInterface.TAG_PLANAR_CONFIGURATION,
737                 ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
738                 ExifInterface.TAG_Y_CB_CR_POSITIONING,
739                 ExifInterface.TAG_X_RESOLUTION,
740                 ExifInterface.TAG_Y_RESOLUTION,
741                 ExifInterface.TAG_RESOLUTION_UNIT,
742                 ExifInterface.TAG_STRIP_OFFSETS,
743                 ExifInterface.TAG_ROWS_PER_STRIP,
744                 ExifInterface.TAG_STRIP_BYTE_COUNTS,
745                 ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
746                 ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
747                 ExifInterface.TAG_TRANSFER_FUNCTION,
748                 ExifInterface.TAG_WHITE_POINT,
749                 ExifInterface.TAG_PRIMARY_CHROMATICITIES,
750                 ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
751                 ExifInterface.TAG_REFERENCE_BLACK_WHITE,
752                 ExifInterface.TAG_DATETIME,
753                 ExifInterface.TAG_IMAGE_DESCRIPTION,
754                 ExifInterface.TAG_MAKE,
755                 ExifInterface.TAG_MODEL,
756                 ExifInterface.TAG_SOFTWARE,
757                 ExifInterface.TAG_ARTIST,
758                 ExifInterface.TAG_COPYRIGHT,
759                 ExifInterface.TAG_EXIF_VERSION,
760                 ExifInterface.TAG_FLASHPIX_VERSION,
761                 ExifInterface.TAG_COLOR_SPACE,
762                 ExifInterface.TAG_GAMMA,
763                 ExifInterface.TAG_PIXEL_X_DIMENSION,
764                 ExifInterface.TAG_PIXEL_Y_DIMENSION,
765                 ExifInterface.TAG_COMPONENTS_CONFIGURATION,
766                 ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
767                 ExifInterface.TAG_MAKER_NOTE,
768                 ExifInterface.TAG_USER_COMMENT,
769                 ExifInterface.TAG_RELATED_SOUND_FILE,
770                 ExifInterface.TAG_DATETIME_ORIGINAL,
771                 ExifInterface.TAG_DATETIME_DIGITIZED,
772                 ExifInterface.TAG_OFFSET_TIME,
773                 ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
774                 ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
775                 ExifInterface.TAG_SUBSEC_TIME,
776                 ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
777                 ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
778                 ExifInterface.TAG_EXPOSURE_TIME,
779                 ExifInterface.TAG_F_NUMBER,
780                 ExifInterface.TAG_EXPOSURE_PROGRAM,
781                 ExifInterface.TAG_SPECTRAL_SENSITIVITY,
782                 ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
783                 ExifInterface.TAG_OECF,
784                 ExifInterface.TAG_SENSITIVITY_TYPE,
785                 ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
786                 ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
787                 ExifInterface.TAG_ISO_SPEED,
788                 ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
789                 ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
790                 ExifInterface.TAG_SHUTTER_SPEED_VALUE,
791                 ExifInterface.TAG_APERTURE_VALUE,
792                 ExifInterface.TAG_BRIGHTNESS_VALUE,
793                 ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
794                 ExifInterface.TAG_MAX_APERTURE_VALUE,
795                 ExifInterface.TAG_SUBJECT_DISTANCE,
796                 ExifInterface.TAG_METERING_MODE,
797                 ExifInterface.TAG_LIGHT_SOURCE,
798                 ExifInterface.TAG_FLASH,
799                 ExifInterface.TAG_SUBJECT_AREA,
800                 ExifInterface.TAG_FOCAL_LENGTH,
801                 ExifInterface.TAG_FLASH_ENERGY,
802                 ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
803                 ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
804                 ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
805                 ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
806                 ExifInterface.TAG_SUBJECT_LOCATION,
807                 ExifInterface.TAG_EXPOSURE_INDEX,
808                 ExifInterface.TAG_SENSING_METHOD,
809                 ExifInterface.TAG_FILE_SOURCE,
810                 ExifInterface.TAG_SCENE_TYPE,
811                 ExifInterface.TAG_CFA_PATTERN,
812                 ExifInterface.TAG_CUSTOM_RENDERED,
813                 ExifInterface.TAG_EXPOSURE_MODE,
814                 ExifInterface.TAG_WHITE_BALANCE,
815                 ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
816                 ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
817                 ExifInterface.TAG_SCENE_CAPTURE_TYPE,
818                 ExifInterface.TAG_GAIN_CONTROL,
819                 ExifInterface.TAG_CONTRAST,
820                 ExifInterface.TAG_SATURATION,
821                 ExifInterface.TAG_SHARPNESS,
822                 ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
823                 ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
824                 ExifInterface.TAG_IMAGE_UNIQUE_ID,
825                 ExifInterface.TAG_CAMERA_OWNER_NAME,
826                 ExifInterface.TAG_BODY_SERIAL_NUMBER,
827                 ExifInterface.TAG_LENS_SPECIFICATION,
828                 ExifInterface.TAG_LENS_MAKE,
829                 ExifInterface.TAG_LENS_MODEL,
830                 ExifInterface.TAG_LENS_SERIAL_NUMBER,
831                 ExifInterface.TAG_GPS_VERSION_ID,
832                 ExifInterface.TAG_GPS_LATITUDE_REF,
833                 ExifInterface.TAG_GPS_LATITUDE,
834                 ExifInterface.TAG_GPS_LONGITUDE_REF,
835                 ExifInterface.TAG_GPS_LONGITUDE,
836                 ExifInterface.TAG_GPS_ALTITUDE_REF,
837                 ExifInterface.TAG_GPS_ALTITUDE,
838                 ExifInterface.TAG_GPS_TIMESTAMP,
839                 ExifInterface.TAG_GPS_SATELLITES,
840                 ExifInterface.TAG_GPS_STATUS,
841                 ExifInterface.TAG_GPS_MEASURE_MODE,
842                 ExifInterface.TAG_GPS_DOP,
843                 ExifInterface.TAG_GPS_SPEED_REF,
844                 ExifInterface.TAG_GPS_SPEED,
845                 ExifInterface.TAG_GPS_TRACK_REF,
846                 ExifInterface.TAG_GPS_TRACK,
847                 ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
848                 ExifInterface.TAG_GPS_IMG_DIRECTION,
849                 ExifInterface.TAG_GPS_MAP_DATUM,
850                 ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
851                 ExifInterface.TAG_GPS_DEST_LATITUDE,
852                 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
853                 ExifInterface.TAG_GPS_DEST_LONGITUDE,
854                 ExifInterface.TAG_GPS_DEST_BEARING_REF,
855                 ExifInterface.TAG_GPS_DEST_BEARING,
856                 ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
857                 ExifInterface.TAG_GPS_DEST_DISTANCE,
858                 ExifInterface.TAG_GPS_PROCESSING_METHOD,
859                 ExifInterface.TAG_GPS_AREA_INFORMATION,
860                 ExifInterface.TAG_GPS_DATESTAMP,
861                 ExifInterface.TAG_GPS_DIFFERENTIAL,
862                 ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
863                 ExifInterface.TAG_INTEROPERABILITY_INDEX,
864                 ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
865                 ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
866                 TAG_THUMBNAIL_ORIENTATION,
867                 ExifInterface.TAG_DNG_VERSION,
868                 ExifInterface.TAG_DEFAULT_CROP_SIZE,
869                 ExifInterface.TAG_ORF_THUMBNAIL_IMAGE,
870                 ExifInterface.TAG_ORF_PREVIEW_IMAGE_START,
871                 ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH,
872                 ExifInterface.TAG_ORF_ASPECT_FRAME,
873                 ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER,
874                 ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER,
875                 ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER,
876                 ExifInterface.TAG_RW2_SENSOR_TOP_BORDER,
877                 ExifInterface.TAG_RW2_ISO,
878                 ExifInterface.TAG_RW2_JPG_FROM_RAW,
879                 ExifInterface.TAG_XMP,
880                 ExifInterface.TAG_NEW_SUBFILE_TYPE,
881                 ExifInterface.TAG_SUBFILE_TYPE);
882     }
883 }
884 
885