blob: f3a23eb11040c4e8c871a5c1ae72f385199454e5 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.media.util;
import static android.media.ExifInterface.TAG_DATETIME;
import static android.media.ExifInterface.TAG_DATETIME_DIGITIZED;
import static android.media.ExifInterface.TAG_DATETIME_ORIGINAL;
import static android.media.ExifInterface.TAG_GPS_DATESTAMP;
import static android.media.ExifInterface.TAG_GPS_TIMESTAMP;
import static android.media.ExifInterface.TAG_OFFSET_TIME;
import static android.media.ExifInterface.TAG_OFFSET_TIME_DIGITIZED;
import static android.media.ExifInterface.TAG_OFFSET_TIME_ORIGINAL;
import static android.media.ExifInterface.TAG_SUBSEC_TIME;
import static android.media.ExifInterface.TAG_SUBSEC_TIME_DIGITIZED;
import static android.media.ExifInterface.TAG_SUBSEC_TIME_ORIGINAL;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.Nullable;
import android.media.ExifInterface;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Pattern;
/**
* Utility methods borrowed from {@link ExifInterface} since they're not
* official APIs yet.
*/
public class ExifUtils {
// Pattern to check non zero timestamp
private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
@GuardedBy("sFormatter")
private static final SimpleDateFormat sFormatter;
@GuardedBy("sFormatterTz")
private static final SimpleDateFormat sFormatterTz;
static {
sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
sFormatterTz = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss XXX");
sFormatterTz.setTimeZone(TimeZone.getTimeZone("UTC"));
}
/**
* Returns parsed {@code DateTime} value, or -1 if unavailable or invalid.
*/
public static @CurrentTimeMillisLong long getDateTime(@NonNull ExifInterface exif) {
return parseDateTime(exif.getAttribute(TAG_DATETIME),
exif.getAttribute(TAG_SUBSEC_TIME),
exif.getAttribute(TAG_OFFSET_TIME));
}
/**
* Returns parsed {@code DateTimeDigitized} value, or -1 if unavailable or
* invalid.
*/
public static @CurrentTimeMillisLong long getDateTimeDigitized(@NonNull ExifInterface exif) {
return parseDateTime(exif.getAttribute(TAG_DATETIME_DIGITIZED),
exif.getAttribute(TAG_SUBSEC_TIME_DIGITIZED),
exif.getAttribute(TAG_OFFSET_TIME_DIGITIZED));
}
/**
* Returns parsed {@code DateTimeOriginal} value, or -1 if unavailable or
* invalid.
*/
public static @CurrentTimeMillisLong long getDateTimeOriginal(@NonNull ExifInterface exif) {
return parseDateTime(exif.getAttribute(TAG_DATETIME_ORIGINAL),
exif.getAttribute(TAG_SUBSEC_TIME_ORIGINAL),
exif.getAttribute(TAG_OFFSET_TIME_ORIGINAL));
}
/**
* Returns parsed {@code GPSDateStamp} value, or -1 if unavailable or
* invalid.
*/
public static long getGpsDateTime(ExifInterface exif) {
String date = exif.getAttribute(TAG_GPS_DATESTAMP);
String time = exif.getAttribute(TAG_GPS_TIMESTAMP);
if (date == null || time == null
|| (!sNonZeroTimePattern.matcher(date).matches()
&& !sNonZeroTimePattern.matcher(time).matches())) {
return -1;
}
String dateTimeString = date + ' ' + time;
ParsePosition pos = new ParsePosition(0);
try {
final Date datetime;
synchronized (sFormatter) {
datetime = sFormatter.parse(dateTimeString, pos);
}
if (datetime == null) return -1;
return datetime.getTime();
} catch (IllegalArgumentException e) {
return -1;
}
}
private static @CurrentTimeMillisLong long parseDateTime(@Nullable String dateTimeString,
@Nullable String subSecs, @Nullable String offsetString) {
if (dateTimeString == null
|| !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1;
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;
synchronized (sFormatter) {
datetime = sFormatter.parse(dateTimeString, pos);
}
if (offsetString != null) {
dateTimeString = dateTimeString + " " + offsetString;
ParsePosition position = new ParsePosition(0);
synchronized (sFormatterTz) {
datetime = sFormatterTz.parse(dateTimeString, position);
}
}
if (datetime == null) return -1;
long msecs = datetime.getTime();
if (subSecs != null) {
msecs += parseSubSeconds(subSecs);
}
return msecs;
} catch (IllegalArgumentException e) {
return -1;
}
}
@VisibleForTesting
static @CurrentTimeMillisLong long parseSubSeconds(@NonNull String subSec) {
try {
final int len = Math.min(subSec.length(), 3);
long sub = Long.parseLong(subSec.substring(0, len));
for (int i = len; i < 3; i++) {
sub *= 10;
}
return sub;
} catch (NumberFormatException e) {
// Ignored
}
return 0L;
}
}