1 /* 2 * Copyright (C) 2020 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.providers.media.util; 18 19 import static android.media.ExifInterface.TAG_DATETIME; 20 import static android.media.ExifInterface.TAG_DATETIME_DIGITIZED; 21 import static android.media.ExifInterface.TAG_DATETIME_ORIGINAL; 22 import static android.media.ExifInterface.TAG_GPS_DATESTAMP; 23 import static android.media.ExifInterface.TAG_GPS_TIMESTAMP; 24 import static android.media.ExifInterface.TAG_OFFSET_TIME; 25 import static android.media.ExifInterface.TAG_OFFSET_TIME_DIGITIZED; 26 import static android.media.ExifInterface.TAG_OFFSET_TIME_ORIGINAL; 27 import static android.media.ExifInterface.TAG_SUBSEC_TIME; 28 import static android.media.ExifInterface.TAG_SUBSEC_TIME_DIGITIZED; 29 import static android.media.ExifInterface.TAG_SUBSEC_TIME_ORIGINAL; 30 31 import android.annotation.CurrentTimeMillisLong; 32 import android.annotation.Nullable; 33 import android.media.ExifInterface; 34 35 import androidx.annotation.GuardedBy; 36 import androidx.annotation.NonNull; 37 import androidx.annotation.VisibleForTesting; 38 39 import java.text.ParsePosition; 40 import java.text.SimpleDateFormat; 41 import java.util.Date; 42 import java.util.TimeZone; 43 import java.util.regex.Pattern; 44 45 46 /** 47 * Utility methods borrowed from {@link ExifInterface} since they're not 48 * official APIs yet. 49 */ 50 public class ExifUtils { 51 // Pattern to check non zero timestamp 52 private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*"); 53 54 @GuardedBy("sFormatter") 55 private static final SimpleDateFormat sFormatter; 56 @GuardedBy("sFormatterTz") 57 private static final SimpleDateFormat sFormatterTz; 58 59 static { 60 sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); 61 sFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 62 sFormatterTz = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss XXX"); 63 sFormatterTz.setTimeZone(TimeZone.getTimeZone("UTC")); 64 } 65 66 /** 67 * Returns parsed {@code DateTime} value, or -1 if unavailable or invalid. 68 */ getDateTime(@onNull ExifInterface exif)69 public static @CurrentTimeMillisLong long getDateTime(@NonNull ExifInterface exif) { 70 return parseDateTime(exif.getAttribute(TAG_DATETIME), 71 exif.getAttribute(TAG_SUBSEC_TIME), 72 exif.getAttribute(TAG_OFFSET_TIME)); 73 } 74 75 /** 76 * Returns parsed {@code DateTimeDigitized} value, or -1 if unavailable or 77 * invalid. 78 */ getDateTimeDigitized(@onNull ExifInterface exif)79 public static @CurrentTimeMillisLong long getDateTimeDigitized(@NonNull ExifInterface exif) { 80 return parseDateTime(exif.getAttribute(TAG_DATETIME_DIGITIZED), 81 exif.getAttribute(TAG_SUBSEC_TIME_DIGITIZED), 82 exif.getAttribute(TAG_OFFSET_TIME_DIGITIZED)); 83 } 84 85 /** 86 * Returns parsed {@code DateTimeOriginal} value, or -1 if unavailable or 87 * invalid. 88 */ getDateTimeOriginal(@onNull ExifInterface exif)89 public static @CurrentTimeMillisLong long getDateTimeOriginal(@NonNull ExifInterface exif) { 90 return parseDateTime(exif.getAttribute(TAG_DATETIME_ORIGINAL), 91 exif.getAttribute(TAG_SUBSEC_TIME_ORIGINAL), 92 exif.getAttribute(TAG_OFFSET_TIME_ORIGINAL)); 93 } 94 95 /** 96 * Returns parsed {@code GPSDateStamp} value, or -1 if unavailable or 97 * invalid. 98 */ getGpsDateTime(ExifInterface exif)99 public static long getGpsDateTime(ExifInterface exif) { 100 String date = exif.getAttribute(TAG_GPS_DATESTAMP); 101 String time = exif.getAttribute(TAG_GPS_TIMESTAMP); 102 if (date == null || time == null 103 || (!sNonZeroTimePattern.matcher(date).matches() 104 && !sNonZeroTimePattern.matcher(time).matches())) { 105 return -1; 106 } 107 108 String dateTimeString = date + ' ' + time; 109 110 ParsePosition pos = new ParsePosition(0); 111 try { 112 final Date datetime; 113 synchronized (sFormatter) { 114 datetime = sFormatter.parse(dateTimeString, pos); 115 } 116 if (datetime == null) return -1; 117 return datetime.getTime(); 118 } catch (IllegalArgumentException e) { 119 return -1; 120 } 121 } 122 parseDateTime(@ullable String dateTimeString, @Nullable String subSecs, @Nullable String offsetString)123 private static @CurrentTimeMillisLong long parseDateTime(@Nullable String dateTimeString, 124 @Nullable String subSecs, @Nullable String offsetString) { 125 if (dateTimeString == null 126 || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1; 127 128 ParsePosition pos = new ParsePosition(0); 129 try { 130 // The exif field is in local time. Parsing it as if it is UTC will yield time 131 // since 1/1/1970 local time 132 Date datetime; 133 synchronized (sFormatter) { 134 datetime = sFormatter.parse(dateTimeString, pos); 135 } 136 137 if (offsetString != null) { 138 dateTimeString = dateTimeString + " " + offsetString; 139 ParsePosition position = new ParsePosition(0); 140 synchronized (sFormatterTz) { 141 datetime = sFormatterTz.parse(dateTimeString, position); 142 } 143 } 144 145 if (datetime == null) return -1; 146 long msecs = datetime.getTime(); 147 148 if (subSecs != null) { 149 msecs += parseSubSeconds(subSecs); 150 } 151 return msecs; 152 } catch (IllegalArgumentException e) { 153 return -1; 154 } 155 } 156 157 @VisibleForTesting parseSubSeconds(@onNull String subSec)158 static @CurrentTimeMillisLong long parseSubSeconds(@NonNull String subSec) { 159 try { 160 final int len = Math.min(subSec.length(), 3); 161 long sub = Long.parseLong(subSec.substring(0, len)); 162 for (int i = len; i < 3; i++) { 163 sub *= 10; 164 } 165 return sub; 166 } catch (NumberFormatException e) { 167 // Ignored 168 } 169 return 0L; 170 } 171 } 172