• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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