/* * Copyright 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 androidx.car.app.model; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import android.annotation.SuppressLint; import androidx.annotation.DoNotInline; import androidx.annotation.IntRange; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.car.app.annotations.CarProtocol; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.TextStyle; import java.util.Date; import java.util.Locale; import java.util.Objects; import java.util.TimeZone; /** * A time with an associated time zone information. * *

In order to avoid time zone databases being out of sync between the app and the host, this * model avoids using IANA database time zone IDs and * instead relies on the app passing the time zone offset and its abbreviated name. Apps can use * their time library of choice to retrieve the time zone information. * *

{@link #create(long, TimeZone)} and {@link #create(ZonedDateTime)} are provided for * convenience if using {@code java.util} and {@code java.time} respectively. If using another * library such as Joda time, {@link #create(long, int, String)} can be used. */ @SuppressWarnings("MissingSummary") @CarProtocol public final class DateTimeWithZone { /** The maximum allowed offset for a time zone, in seconds. */ private static final long MAX_ZONE_OFFSET_SECONDS = 18 * HOURS.toSeconds(1); @Keep private final long mTimeSinceEpochMillis; @Keep private final int mZoneOffsetSeconds; @Nullable @Keep private final String mZoneShortName; /** Returns the number of milliseconds from the epoch of 1970-01-01T00:00:00Z. */ public long getTimeSinceEpochMillis() { return mTimeSinceEpochMillis; } /** Returns the offset of the time zone from UTC. */ @SuppressLint("MethodNameUnits") public int getZoneOffsetSeconds() { return mZoneOffsetSeconds; } /** * Returns the abbreviated name of the time zone, for example "PST" for Pacific Standard * Time. */ @Nullable public String getZoneShortName() { return mZoneShortName; } @Override @NonNull public String toString() { return "[time since epoch (ms): " + mTimeSinceEpochMillis + "( " + new Date(mTimeSinceEpochMillis) + ") " + " zone offset (s): " + mZoneOffsetSeconds + ", zone: " + mZoneShortName + "]"; } @Override public int hashCode() { return Objects.hash(mTimeSinceEpochMillis, mZoneOffsetSeconds, mZoneShortName); } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof DateTimeWithZone)) { return false; } DateTimeWithZone otherDateTime = (DateTimeWithZone) other; return mTimeSinceEpochMillis == otherDateTime.mTimeSinceEpochMillis && mZoneOffsetSeconds == otherDateTime.mZoneOffsetSeconds && Objects.equals(mZoneShortName, otherDateTime.mZoneShortName); } /** * Returns an instance of a {@link DateTimeWithZone}. * * @param timeSinceEpochMillis The number of milliseconds from the epoch of * 1970-01-01T00:00:00Z * @param zoneOffsetSeconds The offset of the time zone from UTC at the date specified by * {@code timeInUtcMillis}. This offset must be in the range * {@code -18:00} to {@code +18:00}, which corresponds to -64800 * to +64800 * @param zoneShortName The abbreviated name of the time zone, for example, "PST" for * Pacific Standard Time. This string may be used to display to * the user along with the date when needed, for example, if this * time zone is different than the current system time zone * @throws IllegalArgumentException if {@code timeSinceEpochMillis} is a negative value, if * {@code zoneOffsetSeconds} is not within the required range, * or if {@code zoneShortName} is empty * @throws NullPointerException if {@code zoneShortName} is {@code null} */ @NonNull public static DateTimeWithZone create( long timeSinceEpochMillis, @IntRange(from = -64800, to = 64800) int zoneOffsetSeconds, @NonNull String zoneShortName) { if (timeSinceEpochMillis < 0) { throw new IllegalArgumentException( "Time since epoch must be greater than or equal to zero"); } if (Math.abs(zoneOffsetSeconds) > MAX_ZONE_OFFSET_SECONDS) { throw new IllegalArgumentException("Zone offset not in valid range: -18:00 to +18:00"); } if (requireNonNull(zoneShortName).isEmpty()) { throw new IllegalArgumentException("The time zone short name can not be null or empty"); } return new DateTimeWithZone(timeSinceEpochMillis, zoneOffsetSeconds, zoneShortName); } /** * Returns an instance of a {@link DateTimeWithZone}. * * @param timeSinceEpochMillis The number of milliseconds from the epoch of * 1970-01-01T00:00:00Z * @param timeZone The time zone at the date specified by {@code timeInUtcMillis}. * The abbreviated name of this time zone, formatted using the * default locale, may be displayed to the user when needed, for * example, if this time zone is different than the current * system time zone * @throws IllegalArgumentException if {@code timeSinceEpochMillis} is a negative value * @throws NullPointerException if {@code timeZone} is {@code null} */ @NonNull public static DateTimeWithZone create(long timeSinceEpochMillis, @NonNull TimeZone timeZone) { if (timeSinceEpochMillis < 0) { throw new IllegalArgumentException( "timeSinceEpochMillis must be greater than or equal to zero"); } return create( timeSinceEpochMillis, (int) MILLISECONDS.toSeconds( requireNonNull(timeZone).getOffset(timeSinceEpochMillis)), timeZone.getDisplayName(false, TimeZone.SHORT)); } /** * Returns an instance of a {@link DateTimeWithZone}. * * @param zonedDateTime The time with a time zone. The abbreviated name of this time zone, * formatted using the default locale, may be displayed to the user when * needed, for example, if this time zone is different than the current * system time zone * @throws NullPointerException if {@code zonedDateTime} is {@code null} */ @RequiresApi(26) @NonNull public static DateTimeWithZone create(@NonNull ZonedDateTime zonedDateTime) { return Api26Impl.create(zonedDateTime); } private DateTimeWithZone() { mTimeSinceEpochMillis = 0; mZoneOffsetSeconds = 0; mZoneShortName = null; } private DateTimeWithZone( long timeSinceEpochMillis, int zoneOffsetSeconds, @Nullable String timeZoneShortName) { mTimeSinceEpochMillis = timeSinceEpochMillis; mZoneOffsetSeconds = zoneOffsetSeconds; mZoneShortName = timeZoneShortName; } /** * Version-specific static inner class to avoid verification errors that negatively affect * run-time performance. */ @RequiresApi(26) private static final class Api26Impl { private Api26Impl() { } @DoNotInline @NonNull public static DateTimeWithZone create(@NonNull ZonedDateTime zonedDateTime) { LocalDateTime localDateTime = requireNonNull(zonedDateTime).toLocalDateTime(); ZoneId zoneId = zonedDateTime.getZone(); ZoneOffset zoneOffset = zoneId.getRules().getOffset(localDateTime); return DateTimeWithZone.create( SECONDS.toMillis(localDateTime.toEpochSecond(zoneOffset)), zoneOffset.getTotalSeconds(), zoneId.getDisplayName(TextStyle.SHORT, Locale.getDefault())); } } }