Merge "Add support for reading/writing secondary format of DateTime tag" into androidx-master-dev
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 9646ab1..5899803 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -93,18 +93,23 @@
     private static final String WEBP_WITHOUT_EXIF = "webp_without_exif.webp";
     private static final String WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING =
             "webp_lossless_without_exif.webp";
-    private static final String JPEG_WITH_DATETIME_TAG = "jpeg_with_datetime_tag.jpg";
+    private static final String JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT =
+            "jpeg_with_datetime_tag_primary_format.jpg";
+    private static final String JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT =
+            "jpeg_with_datetime_tag_secondary_format.jpg";
     private static final int[] IMAGE_RESOURCES = new int[] {
             R.raw.jpeg_with_exif_byte_order_ii, R.raw.jpeg_with_exif_byte_order_mm,
             R.raw.dng_with_exif_with_xmp, R.raw.jpeg_with_exif_with_xmp,
             R.raw.png_with_exif_byte_order_ii, R.raw.png_without_exif, R.raw.webp_with_exif,
             R.raw.webp_with_anim_without_exif, R.raw.webp_without_exif,
-            R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag};
+            R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag_primary_format,
+            R.raw.jpeg_with_datetime_tag_secondary_format};
     private static final String[] IMAGE_FILENAMES = new String[] {
             JPEG_WITH_EXIF_BYTE_ORDER_II, JPEG_WITH_EXIF_BYTE_ORDER_MM, DNG_WITH_EXIF_WITH_XMP,
             JPEG_WITH_EXIF_WITH_XMP, PNG_WITH_EXIF_BYTE_ORDER_II, PNG_WITHOUT_EXIF,
             WEBP_WITH_EXIF, WEBP_WITHOUT_EXIF_WITH_ANIM_DATA, WEBP_WITHOUT_EXIF,
-            WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG};
+            WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT,
+            JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT};
 
     private static final int USER_READ_WRITE = 0600;
     private static final String TEST_TEMP_FILE_NAME = "testImage";
@@ -632,7 +637,7 @@
         final String dateTimeValue = "2017:02:02 22:22:22";
         final String dateTimeOriginalValue = "2017:01:01 11:11:11";
 
-        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG);
+        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
         assertEquals(expectedDatetimeValue, (long) exif.getDateTime());
         assertEquals(expectedDatetimeValue, (long) exif.getDateTimeOriginal());
@@ -670,6 +675,42 @@
         assertEquals(currentTimeStamp - expectedDatetimeOffsetLongValue, (long) exif.getDateTime());
     }
 
+    /**
+     * Test whether ExifInterface can correctly get and set datetime value for a secondary format:
+     * Primary format example: 2020:01:01 00:00:00
+     * Secondary format example: 2020-01-01 00:00:00
+     *
+     * Getting a datetime tag value with the secondary format should work for both
+     * {@link ExifInterface#getAttribute(String)} and {@link ExifInterface#getDateTime()}.
+     * Setting a datetime tag value with the secondary format with
+     * {@link ExifInterface#setAttribute(String, String)} should automatically convert it to the
+     * primary format.
+     */
+    @Test
+    @SmallTest
+    public void testGetSetDateTimeForSecondaryFormat() throws Exception {
+        final long dateTimePrimaryFormatLongValue = 1604075491000L;
+        final String dateTimePrimaryFormatStringValue = "2020-10-30 16:31:31";
+        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT);
+
+        // Check that secondary format value is read correctly.
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertEquals(dateTimePrimaryFormatStringValue,
+                exif.getAttribute(ExifInterface.TAG_DATETIME));
+        assertEquals(dateTimePrimaryFormatLongValue, (long) exif.getDateTime());
+
+        final long dateTimeSecondaryFormatLongValue = 1577836800000L;
+        final String dateTimeSecondaryFormatStringValue = "2020-01-01 00:00:00";
+        final String modifiedDateTimeSecondaryFormatStringValue = "2020:01:01 00:00:00";
+
+        // Check that secondary format value is written correctly.
+        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeSecondaryFormatStringValue);
+        exif.saveAttributes();
+        assertEquals(modifiedDateTimeSecondaryFormatStringValue,
+                exif.getAttribute(ExifInterface.TAG_DATETIME));
+        assertEquals(dateTimeSecondaryFormatLongValue, (long) exif.getDateTime());
+    }
+
     @Test
     @LargeTest
     public void testRotation() throws IOException {
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
similarity index 100%
rename from exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag.jpg
rename to exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
new file mode 100644
index 0000000..53ad4e9
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 9766912..c6ed238 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -454,6 +454,10 @@
      *      <li>Length = 19</li>
      *      <li>Default = None</li>
      *  </ul>
+     *
+     *  <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+     *  however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+     *  format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
      */
     public static final String TAG_DATETIME = "DateTime";
     /**
@@ -745,6 +749,10 @@
      *      <li>Length = 19</li>
      *      <li>Default = None</li>
      *  </ul>
+     *
+     *  <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+     *  however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+     *  format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
      */
     public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
     /**
@@ -763,6 +771,10 @@
      *      <li>Length = 19</li>
      *      <li>Default = None</li>
      *  </ul>
+     *
+     *  <p>Note: The format "YYYY-MM-DD HH:MM:SS" is also supported for reading. For writing,
+     *  however, calling {@link #setAttribute(String, String)} with the "YYYY-MM-DD HH:MM:SS"
+     *  format will automatically convert it to the primary format, "YYYY:MM:DD HH:MM:SS".
      */
     public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
     /**
@@ -2999,7 +3011,8 @@
     private static final int WEBP_CHUNK_TYPE_BYTE_LENGTH = 4;
     private static final int WEBP_CHUNK_SIZE_BYTE_LENGTH = 4;
 
-    private static SimpleDateFormat sFormatter;
+    private static SimpleDateFormat sFormatterPrimary;
+    private static SimpleDateFormat sFormatterSecondary;
 
     // See Exchangeable image file format for digital still cameras: Exif version 2.2.
     // The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
@@ -3840,8 +3853,10 @@
     private static final int IMAGE_TYPE_WEBP = 14;
 
     static {
-        sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
-        sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+        sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
+        sFormatterPrimary.setTimeZone(TimeZone.getTimeZone("UTC"));
+        sFormatterSecondary = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
+        sFormatterSecondary.setTimeZone(TimeZone.getTimeZone("UTC"));
 
         // Build up the hash tables to look up Exif tags for reading Exif tags.
         for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
@@ -3890,10 +3905,17 @@
     private boolean mXmpIsFromSeparateMarker;
 
     // Pattern to check non zero timestamp
-    private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
+    private static final Pattern NON_ZERO_TIME_PATTERN = Pattern.compile(".*[1-9].*");
     // Pattern to check gps timestamp
-    private static final Pattern sGpsTimestampPattern =
-            Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$");
+    private static final Pattern GPS_TIMESTAMP_PATTERN =
+            Pattern.compile("^(\\d{2}):(\\d{2}):(\\d{2})$");
+    // Pattern to check date time primary format (e.g. 2020:01:01 00:00:00)
+    private static final Pattern DATETIME_PRIMARY_FORMAT_PATTERN =
+            Pattern.compile("^(\\d{4}):(\\d{2}):(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+    // Pattern to check date time secondary format (e.g. 2020-01-01 00:00:00)
+    private static final Pattern DATETIME_SECONDARY_FORMAT_PATTERN =
+            Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+    private static final int DATETIME_VALUE_STRING_LENGTH = 19;
 
     /**
      * Reads Exif tags from the specified image file.
@@ -4192,6 +4214,28 @@
         if (tag == null) {
             throw new NullPointerException("tag shouldn't be null");
         }
+        // Validate and convert if necessary.
+        if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
+                || TAG_DATETIME_DIGITIZED.equals(tag)) {
+            if (value != null) {
+                boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
+                boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(value).find();
+                // Validate
+                if (value.length() != DATETIME_VALUE_STRING_LENGTH
+                        || (!isPrimaryFormat && !isSecondaryFormat)) {
+                    Log.w(TAG, "Invalid value for " + tag + " : " + value);
+                    return;
+                }
+                // If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it to
+                // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
+                // official documentation.
+                // See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
+                if (isSecondaryFormat) {
+                    // Replace "-" with ":" to match the primary format.
+                    value = value.replaceAll("-", ":");
+                }
+            }
+        }
         // Maintain compatibility.
         if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
             if (DEBUG) {
@@ -4203,7 +4247,7 @@
         // Convert the given value to rational values for backwards compatibility.
         if (value != null && sTagSetForCompatibility.contains(tag)) {
             if (tag.equals(TAG_GPS_TIMESTAMP)) {
-                Matcher m = sGpsTimestampPattern.matcher(value);
+                Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
                 if (!m.find()) {
                     Log.w(TAG, "Invalid value for " + tag + " : " + value);
                     return;
@@ -5017,7 +5061,8 @@
         setAttribute(TAG_GPS_SPEED_REF, "K");
         setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
                 * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
-        String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+", -1);
+        String[] dateTime = sFormatterPrimary.format(
+                new Date(location.getTime())).split("\\s+", -1);
         setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
         setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
     }
@@ -5080,7 +5125,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     public void setDateTime(@NonNull Long timeStamp) {
         long sub = timeStamp % 1000;
-        setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
+        setAttribute(TAG_DATETIME, sFormatterPrimary.format(new Date(timeStamp)));
         setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
     }
 
@@ -5131,16 +5176,22 @@
 
     private static Long parseDateTime(@Nullable String dateTimeString, @Nullable String subSecs,
             @Nullable String offsetString) {
-        if (dateTimeString == null
-                || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return null;
+        if (dateTimeString == null || !NON_ZERO_TIME_PATTERN.matcher(dateTimeString).matches()) {
+            return null;
+        }
 
         ParsePosition pos = new ParsePosition(0);
         try {
             // The exif field is in local time. Parsing it as if it is UTC will yield time
             // since 1/1/1970 local time
-            Date datetime = sFormatter.parse(dateTimeString, pos);
-            if (datetime == null) return null;
-            long msecs = datetime.getTime();
+            Date dateTime = sFormatterPrimary.parse(dateTimeString, pos);
+            if (dateTime == null) {
+                dateTime = sFormatterSecondary.parse(dateTimeString, pos);
+                if (dateTime == null) {
+                    return null;
+                }
+            }
+            long msecs = dateTime.getTime();
             if (offsetString != null) {
                 String sign = offsetString.substring(0, 1);
                 int hour = Integer.parseInt(offsetString.substring(1, 3));
@@ -5179,8 +5230,8 @@
         String date = getAttribute(TAG_GPS_DATESTAMP);
         String time = getAttribute(TAG_GPS_TIMESTAMP);
         if (date == null || time == null
-                || (!sNonZeroTimePattern.matcher(date).matches()
-                && !sNonZeroTimePattern.matcher(time).matches())) {
+                || (!NON_ZERO_TIME_PATTERN.matcher(date).matches()
+                && !NON_ZERO_TIME_PATTERN.matcher(time).matches())) {
             return null;
         }
 
@@ -5188,9 +5239,14 @@
 
         ParsePosition pos = new ParsePosition(0);
         try {
-            Date datetime = sFormatter.parse(dateTimeString, pos);
-            if (datetime == null) return null;
-            return datetime.getTime();
+            Date dateTime = sFormatterPrimary.parse(dateTimeString, pos);
+            if (dateTime == null) {
+                dateTime = sFormatterSecondary.parse(dateTimeString, pos);
+                if (dateTime == null) {
+                    return null;
+                }
+            }
+            return dateTime.getTime();
         } catch (IllegalArgumentException e) {
             return null;
         }