| /* |
| * 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; |
| } |
| } |