88import java .io .File ;
99import java .io .IOException ;
1010import java .io .InputStream ;
11+ import java .text .ParseException ;
12+ import java .text .ParsePosition ;
1113import java .text .SimpleDateFormat ;
1214import java .util .Date ;
1315import java .util .Locale ;
1921public class Exif {
2022 private static final String TAG = Exif .class .getSimpleName ();
2123
22- private static final String DEFAULT_TIMEZONE = "UTC" ;
23- private static final String DATE_FORMAT = "yyyy:MM:dd" ;
24- private static final String TIME_FORMAT = "HH:mm:ss" ;
25- private static final String DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT ;
24+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat ("yyyy:MM:dd" , Locale .ENGLISH );
25+ private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat ("HH:mm:ss" , Locale .ENGLISH );
26+ private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat ("yyyy:MM:dd HH:mm:ss" , Locale .ENGLISH );
2627
2728 private final ExifInterface mExifInterface ;
2829
30+ // When true, avoid saving any time. This is a privacy issue.
31+ private boolean mRemoveTimestamp = false ;
32+
2933 public Exif (File file ) throws IOException {
3034 this (file .toString ());
3135 }
@@ -46,19 +50,47 @@ private Exif(ExifInterface exifInterface) {
4650 * Persists changes to disc.
4751 */
4852 public void save () throws IOException {
53+ if (!mRemoveTimestamp ) {
54+ attachLastModifiedTimestamp ();
55+ }
4956 mExifInterface .saveAttributes ();
5057 }
5158
5259 @ Override
5360 public String toString () {
54- return String .format (Locale .ENGLISH , "Exif{location=%s, rotation=%d, isFlippedVertically=%s, isFlippedHorizontally=%s, timestamp=%s}" ,
55- getLocation (), getRotation (), isFlippedVertically (), isFlippedHorizontally (), getTimestamp ());
61+ return String .format (Locale .ENGLISH , "Exif{width=%s, height=%s, rotation=%d, "
62+ + "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, "
63+ + "timestamp=%s, description=%s}" ,
64+ getWidth (), getHeight (), getRotation (), isFlippedVertically (), isFlippedHorizontally (),
65+ getLocation (), getTimestamp (), getDescription ());
5666 }
5767
5868 private int getOrientation () {
5969 return mExifInterface .getAttributeInt (ExifInterface .TAG_ORIENTATION , ExifInterface .ORIENTATION_UNDEFINED );
6070 }
6171
72+ /**
73+ * Returns the width of the photo in pixels.
74+ */
75+ public int getWidth () {
76+ return mExifInterface .getAttributeInt (ExifInterface .TAG_IMAGE_WIDTH , 0 );
77+ }
78+
79+ /**
80+ * Returns the height of the photo in pixels.
81+ */
82+ public int getHeight () {
83+ return mExifInterface .getAttributeInt (ExifInterface .TAG_IMAGE_LENGTH , 0 );
84+ }
85+
86+ public String getDescription () {
87+ return mExifInterface .getAttribute (ExifInterface .TAG_IMAGE_DESCRIPTION );
88+ }
89+
90+ public void setDescription (@ Nullable String description ) {
91+ mExifInterface .setAttribute (ExifInterface .TAG_IMAGE_DESCRIPTION , description );
92+ }
93+
6294 /**
6395 * @return The degree of rotation (eg. 0, 90, 180, 270).
6496 */
@@ -143,11 +175,66 @@ public boolean isFlippedHorizontally() {
143175 }
144176 }
145177
178+ private void attachLastModifiedTimestamp () {
179+ long now = System .currentTimeMillis ();
180+ String datetime = convertToExifDateTime (now );
181+
182+ mExifInterface .setAttribute (ExifInterface .TAG_DATETIME , datetime );
183+
184+ try {
185+ String subsec = Long .toString (now - convertFromExifDateTime (datetime ).getTime ());
186+ mExifInterface .setAttribute (ExifInterface .TAG_SUBSEC_TIME , subsec );
187+ } catch (ParseException e ) {}
188+ }
189+
190+ /**
191+ * @return The timestamp (in millis) that this picture was modified, or -1 if no time is available.
192+ */
193+ public long getLastModifiedTimestamp () {
194+ long timestamp = parseTimestamp (mExifInterface .getAttribute (ExifInterface .TAG_DATETIME ));
195+ if (timestamp == -1 ) {
196+ return -1 ;
197+ }
198+
199+ String subSecs = mExifInterface .getAttribute (ExifInterface .TAG_SUBSEC_TIME );
200+ if (subSecs != null ) {
201+ try {
202+ long sub = Long .parseLong (subSecs );
203+ while (sub > 1000 ) {
204+ sub /= 10 ;
205+ }
206+ timestamp += sub ;
207+ } catch (NumberFormatException e ) {
208+ // Ignored
209+ }
210+ }
211+
212+ return timestamp ;
213+ }
214+
146215 /**
147216 * @return The timestamp (in millis) that this picture was taken, or -1 if no time is available.
148217 */
149218 public long getTimestamp () {
150- return mExifInterface .getDateTime ();
219+ long timestamp = parseTimestamp (mExifInterface .getAttribute (ExifInterface .TAG_DATETIME_ORIGINAL ));
220+ if (timestamp == -1 ) {
221+ return -1 ;
222+ }
223+
224+ String subSecs = mExifInterface .getAttribute (ExifInterface .TAG_SUBSEC_TIME_ORIGINAL );
225+ if (subSecs != null ) {
226+ try {
227+ long sub = Long .parseLong (subSecs );
228+ while (sub > 1000 ) {
229+ sub /= 10 ;
230+ }
231+ timestamp += sub ;
232+ } catch (NumberFormatException e ) {
233+ // Ignored
234+ }
235+ }
236+
237+ return timestamp ;
151238 }
152239
153240 /**
@@ -157,7 +244,12 @@ public long getTimestamp() {
157244 public Location getLocation () {
158245 String provider = mExifInterface .getAttribute (ExifInterface .TAG_GPS_PROCESSING_METHOD );
159246 double [] latlng = mExifInterface .getLatLong ();
160- long timestamp = mExifInterface .getGpsDateTime ();
247+ double altitude = mExifInterface .getAltitude (0 );
248+ double speed = mExifInterface .getAttributeDouble (ExifInterface .TAG_GPS_SPEED , 0 )
249+ * mExifInterface .getAttributeDouble (ExifInterface .TAG_GPS_SPEED_REF , 1 );
250+ long timestamp = parseTimestamp (
251+ mExifInterface .getAttribute (ExifInterface .TAG_GPS_DATESTAMP ),
252+ mExifInterface .getAttribute (ExifInterface .TAG_GPS_TIMESTAMP ));
161253 if (latlng == null ) {
162254 return null ;
163255 }
@@ -168,6 +260,12 @@ public Location getLocation() {
168260 Location location = new Location (provider );
169261 location .setLatitude (latlng [0 ]);
170262 location .setLongitude (latlng [1 ]);
263+ if (altitude != 0 ) {
264+ location .setAltitude (altitude );
265+ }
266+ if (speed != 0 ) {
267+ location .setSpeed ((float ) speed );
268+ }
171269 if (timestamp != -1 ) {
172270 location .setTime (timestamp );
173271 }
@@ -339,17 +437,32 @@ public void flipHorizontally() {
339437 * Attaches the current timestamp to the file.
340438 */
341439 public void attachTimestamp () {
342- String timestamp = convertToExifDateTime (System .currentTimeMillis ());
343- mExifInterface .setAttribute (ExifInterface .TAG_DATETIME_ORIGINAL , timestamp );
344- mExifInterface .setAttribute (ExifInterface .TAG_DATETIME , timestamp );
440+ long now = System .currentTimeMillis ();
441+ String datetime = convertToExifDateTime (now );
442+
443+ mExifInterface .setAttribute (ExifInterface .TAG_DATETIME_ORIGINAL , datetime );
444+ mExifInterface .setAttribute (ExifInterface .TAG_DATETIME_DIGITIZED , datetime );
445+
446+ try {
447+ String subsec = Long .toString (now - convertFromExifDateTime (datetime ).getTime ());
448+ mExifInterface .setAttribute (ExifInterface .TAG_SUBSEC_TIME_ORIGINAL , subsec );
449+ mExifInterface .setAttribute (ExifInterface .TAG_SUBSEC_TIME_DIGITIZED , subsec );
450+ } catch (ParseException e ) {}
451+
452+ mRemoveTimestamp = false ;
345453 }
346454
347455 /**
348456 * Removes the timestamp from the file.
349457 */
350458 public void removeTimestamp () {
351- mExifInterface .setAttribute (ExifInterface .TAG_DATETIME_ORIGINAL , null );
352459 mExifInterface .setAttribute (ExifInterface .TAG_DATETIME , null );
460+ mExifInterface .setAttribute (ExifInterface .TAG_DATETIME_ORIGINAL , null );
461+ mExifInterface .setAttribute (ExifInterface .TAG_DATETIME_DIGITIZED , null );
462+ mExifInterface .setAttribute (ExifInterface .TAG_SUBSEC_TIME , null );
463+ mExifInterface .setAttribute (ExifInterface .TAG_SUBSEC_TIME_ORIGINAL , null );
464+ mExifInterface .setAttribute (ExifInterface .TAG_SUBSEC_TIME_DIGITIZED , null );
465+ mRemoveTimestamp = true ;
353466 }
354467
355468 /**
@@ -358,6 +471,14 @@ public void removeTimestamp() {
358471 public void attachLocation (Location location ) {
359472 mExifInterface .setAttribute (ExifInterface .TAG_GPS_PROCESSING_METHOD , location .getProvider ());
360473 mExifInterface .setLatLong (location .getLatitude (), location .getLongitude ());
474+ if (location .hasAltitude ()) {
475+ mExifInterface .setAttribute (ExifInterface .TAG_GPS_ALTITUDE , Double .toString (location .getAltitude ()));
476+ mExifInterface .setAttribute (ExifInterface .TAG_GPS_ALTITUDE_REF , Integer .toString (1 ));
477+ }
478+ if (location .hasSpeed ()) {
479+ mExifInterface .setAttribute (ExifInterface .TAG_GPS_SPEED , Float .toString (location .getSpeed ()));
480+ mExifInterface .setAttribute (ExifInterface .TAG_GPS_SPEED_REF , Integer .toString (1 ));
481+ }
361482 mExifInterface .setAttribute (ExifInterface .TAG_GPS_DATESTAMP , convertToExifDate (location .getTime ()));
362483 mExifInterface .setAttribute (ExifInterface .TAG_GPS_TIMESTAMP , convertToExifTime (location .getTime ()));
363484 }
@@ -375,21 +496,65 @@ public void removeLocation() {
375496 mExifInterface .setAttribute (ExifInterface .TAG_GPS_TIMESTAMP , null );
376497 }
377498
499+ /**
500+ * @return The timestamp (in millis), or -1 if no time is available.
501+ */
502+ private long parseTimestamp (@ Nullable String date , @ Nullable String time ) {
503+ if (date == null && time == null ) {
504+ return -1 ;
505+ }
506+ if (time == null ) {
507+ try {
508+ return convertFromExifDate (date ).getTime ();
509+ } catch (ParseException e ) {
510+ return -1 ;
511+ }
512+ }
513+ if (date == null ) {
514+ try {
515+ return convertFromExifTime (time ).getTime ();
516+ } catch (ParseException e ) {
517+ return -1 ;
518+ }
519+ }
520+ return parseTimestamp (date + " " + time );
521+ }
522+
523+ /**
524+ * @return The timestamp (in millis), or -1 if no time is available.
525+ */
526+ private long parseTimestamp (@ Nullable String datetime ) {
527+ if (datetime == null ) {
528+ return -1 ;
529+ }
530+ try {
531+ return convertFromExifDateTime (datetime ).getTime ();
532+ } catch (ParseException e ) {
533+ return -1 ;
534+ }
535+ }
536+
378537 private static String convertToExifDateTime (long timestamp ) {
379- SimpleDateFormat format = new SimpleDateFormat (DATETIME_FORMAT , Locale .ENGLISH );
380- format .setTimeZone (TimeZone .getTimeZone (DEFAULT_TIMEZONE ));
381- return format .format (new Date (timestamp ));
538+ return DATETIME_FORMAT .format (new Date (timestamp ));
539+ }
540+
541+ private static Date convertFromExifDateTime (String dateTime ) throws ParseException {
542+ return DATETIME_FORMAT .parse (dateTime );
382543 }
383544
384545 private static String convertToExifDate (long timestamp ) {
385- SimpleDateFormat format = new SimpleDateFormat (DATE_FORMAT , Locale .ENGLISH );
386- format .setTimeZone (TimeZone .getTimeZone (DEFAULT_TIMEZONE ));
387- return format .format (new Date (timestamp ));
546+ return DATE_FORMAT .format (new Date (timestamp ));
547+ }
548+
549+ private static Date convertFromExifDate (String date ) throws ParseException {
550+ return DATE_FORMAT .parse (date );
388551 }
389552
390553 private static String convertToExifTime (long timestamp ) {
391- SimpleDateFormat format = new SimpleDateFormat (TIME_FORMAT , Locale .ENGLISH );
392- format .setTimeZone (TimeZone .getTimeZone (DEFAULT_TIMEZONE ));
393- return format .format (new Date (timestamp ));
554+ return TIME_FORMAT .format (new Date (timestamp ));
555+ }
556+
557+ private static Date convertFromExifTime (String time ) throws ParseException {
558+ return TIME_FORMAT .parse (time );
394559 }
395560}
0 commit comments