TravelEstimate.java

/*
 * 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.navigation.model;

import static java.util.Objects.requireNonNull;

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.model.CarColor;
import androidx.car.app.model.DateTimeWithZone;
import androidx.car.app.model.Distance;
import androidx.car.app.model.constraints.CarColorConstraints;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Objects;

/**
 * Represents the travel estimates to a destination of a trip or for a trip segment, including the
 * remaining time and distance to the destination.
 */
@SuppressWarnings("MissingSummary")
public final class TravelEstimate {
    /** A value used to represent an unknown remaining amount of time. */
    public static final long REMAINING_TIME_UNKNOWN = -1L;

    @Keep
    @Nullable
    private final Distance mRemainingDistance;
    @Keep
    private final long mRemainingTimeSeconds;
    @Keep
    @Nullable
    private final DateTimeWithZone mArrivalTimeAtDestination;
    @Keep
    private final CarColor mRemainingTimeColor;
    @Keep
    private final CarColor mRemainingDistanceColor;

    /**
     * Returns the remaining {@link Distance} until arriving at the destination,  or {@code null}
     * if not set.
     *
     * @see Builder#Builder(Distance, DateTimeWithZone)
     */
    @Nullable
    public Distance getRemainingDistance() {
        return mRemainingDistance;
    }

    /**
     * Returns the remaining time until arriving at the destination, in seconds.
     *
     * @see Builder#setRemainingTimeSeconds(long)
     */
    @SuppressWarnings("MethodNameUnits")
    public long getRemainingTimeSeconds() {
        return mRemainingTimeSeconds >= 0 ? mRemainingTimeSeconds : REMAINING_TIME_UNKNOWN;
    }

    /**
     * Returns the arrival time until at the destination or {@code null} if not set.
     *
     * @see Builder#Builder(Distance, DateTimeWithZone)
     */
    @Nullable
    public DateTimeWithZone getArrivalTimeAtDestination() {
        return mArrivalTimeAtDestination;
    }

    /**
     * Sets the color of the remaining time text or {@code null} if not set.
     *
     * @see Builder#setRemainingTimeColor(CarColor)
     */
    @Nullable
    public CarColor getRemainingTimeColor() {
        return mRemainingTimeColor;
    }

    /**
     * Sets the color of the remaining distance text or {@code null} if not set.
     *
     * @see Builder#setRemainingDistanceColor(CarColor)
     */
    @Nullable
    public CarColor getRemainingDistanceColor() {
        return mRemainingDistanceColor;
    }

    @Override
    @NonNull
    public String toString() {
        return "[ remaining distance: "
                + mRemainingDistance
                + ", time (s): " + mRemainingTimeSeconds
                + ", ETA: " + mArrivalTimeAtDestination
                + "]";
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                mRemainingDistance,
                mRemainingTimeSeconds,
                mArrivalTimeAtDestination,
                mRemainingTimeColor,
                mRemainingDistanceColor);
    }

    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof TravelEstimate)) {
            return false;
        }
        TravelEstimate otherInfo = (TravelEstimate) other;

        return Objects.equals(mRemainingDistance, otherInfo.mRemainingDistance)
                && mRemainingTimeSeconds == otherInfo.mRemainingTimeSeconds
                && Objects.equals(mArrivalTimeAtDestination, otherInfo.mArrivalTimeAtDestination)
                && Objects.equals(mRemainingTimeColor, otherInfo.mRemainingTimeColor)
                && Objects.equals(mRemainingDistanceColor, otherInfo.mRemainingDistanceColor);
    }

    /** Constructs an empty instance, used by serialization code. */
    private TravelEstimate() {
        mRemainingDistance = null;
        mRemainingTimeSeconds = 0;
        mArrivalTimeAtDestination = null;
        mRemainingTimeColor = CarColor.DEFAULT;
        mRemainingDistanceColor = CarColor.DEFAULT;
    }

    TravelEstimate(Builder builder) {
        mRemainingDistance = builder.mRemainingDistance;
        mRemainingTimeSeconds = builder.mRemainingTimeSeconds;
        mArrivalTimeAtDestination = builder.mArrivalTimeAtDestination;
        mRemainingTimeColor = builder.mRemainingTimeColor;
        mRemainingDistanceColor = builder.mRemainingDistanceColor;
    }

    /** A builder of {@link TravelEstimate}. */
    public static final class Builder {
        final Distance mRemainingDistance;
        long mRemainingTimeSeconds = REMAINING_TIME_UNKNOWN;
        final DateTimeWithZone mArrivalTimeAtDestination;
        CarColor mRemainingTimeColor = CarColor.DEFAULT;
        CarColor mRemainingDistanceColor = CarColor.DEFAULT;

        /**
         * Constructs a new builder of {@link TravelEstimate}.
         *
         * @param remainingDistance        The estimated remaining {@link Distance} until
         *                                 arriving at the destination
         * @param arrivalTimeAtDestination The arrival time with the time zone information
         *                                 provided for the destination
         * @throws NullPointerException if {@code remainingDistance} or
         *                              {@code arrivalTimeAtDestination} are {@code null}
         */
        public Builder(
                @NonNull Distance remainingDistance,
                @NonNull DateTimeWithZone arrivalTimeAtDestination) {
            mRemainingDistance = requireNonNull(remainingDistance);
            mArrivalTimeAtDestination = requireNonNull(arrivalTimeAtDestination);
        }

        /**
         * Constructs a new builder of {@link TravelEstimate}.
         *
         * @param remainingDistance        The estimated remaining {@link Distance} until
         *                                 arriving at the destination
         * @param arrivalTimeAtDestination The arrival time with the time zone information
         *                                 provided for the destination
         * @throws NullPointerException if {@code remainingDistance} or
         *                              {@code arrivalTimeAtDestination} are {@code null}
         */
        @SuppressLint("UnsafeNewApiCall")
        @RequiresApi(26)
        @SuppressWarnings("AndroidJdkLibsChecker")
        public Builder(
                @NonNull Distance remainingDistance,
                @NonNull ZonedDateTime arrivalTimeAtDestination) {
            mRemainingDistance = requireNonNull(remainingDistance);
            mArrivalTimeAtDestination =
                    DateTimeWithZone.create(requireNonNull(arrivalTimeAtDestination));
        }

        /**
         * Sets the estimated time remaining until arriving at the destination, in seconds.
         *
         * <p>If not set, {@link #REMAINING_TIME_UNKNOWN} will be used.
         *
         * <p>Note that {@link #REMAINING_TIME_UNKNOWN} may not be supported depending on where the
         * {@link TravelEstimate} is used. See the documentation of where {@link TravelEstimate}
         * is used for any restrictions that might apply.
         *
         * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value
         *                                  but not {@link #REMAINING_TIME_UNKNOWN}
         */
        @NonNull
        public Builder setRemainingTimeSeconds(@IntRange(from = -1) long remainingTimeSeconds) {
            mRemainingTimeSeconds = validateRemainingTime(remainingTimeSeconds);
            return this;
        }

        /**
         * Sets the estimated time remaining until arriving at the destination.
         *
         * <p>If not set, {@link #REMAINING_TIME_UNKNOWN} will be used.
         *
         * @throws IllegalArgumentException if {@code remainingTime} is a negative duration
         *                                  but not {@link #REMAINING_TIME_UNKNOWN}
         * @throws NullPointerException     if {@code remainingTime} is {@code null}
         */
        @SuppressLint({"MissingGetterMatchingBuilder"})
        @RequiresApi(26)
        @NonNull
        public Builder setRemainingTime(@NonNull Duration remainingTime) {
            return Api26Impl.setRemainingTime(this, remainingTime);
        }

        /**
         * Sets the color of the remaining time text.
         *
         * <p>The host may ignore this color depending on the capabilities of the target screen.
         *
         * <p>If not set, {@link CarColor#DEFAULT} will be used.
         *
         * <p>Custom colors created with {@link CarColor#createCustom} are not supported.
         *
         * @throws IllegalArgumentException if {@code remainingTimeColor} is not supported
         * @throws NullPointerException     if {@code remainingTimecolor} is {@code null}
         */
        @NonNull
        public Builder setRemainingTimeColor(@NonNull CarColor remainingTimeColor) {
            CarColorConstraints.STANDARD_ONLY.validateOrThrow(requireNonNull(remainingTimeColor));
            mRemainingTimeColor = remainingTimeColor;
            return this;
        }

        /**
         * Sets the color of the remaining distance text.
         *
         * <p>The host may ignore this color depending on the capabilities of the target screen.
         *
         * <p>If not set, {@link CarColor#DEFAULT} will be used.
         *
         * <p>Custom colors created with {@link CarColor#createCustom} are not supported.
         *
         * @throws IllegalArgumentException if {@code remainingDistanceColor} is not supported
         * @throws NullPointerException     if {@code remainingDistanceColor} is {@code null}
         */
        @NonNull
        public Builder setRemainingDistanceColor(@NonNull CarColor remainingDistanceColor) {
            CarColorConstraints.STANDARD_ONLY.validateOrThrow(
                    requireNonNull(remainingDistanceColor));
            mRemainingDistanceColor = remainingDistanceColor;
            return this;
        }

        /** Constructs the {@link TravelEstimate} defined by this builder. */
        @NonNull
        public TravelEstimate build() {
            return new TravelEstimate(this);
        }

        static long validateRemainingTime(long remainingTimeSeconds) {
            if (remainingTimeSeconds < 0 && remainingTimeSeconds != REMAINING_TIME_UNKNOWN) {
                throw new IllegalArgumentException(
                        "Remaining time must be a larger than or equal to zero, or set to"
                                + " REMAINING_TIME_UNKNOWN");
            }
            return remainingTimeSeconds;
        }

        /**
         * 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 Builder setRemainingTime(Builder builder,
                    @NonNull Duration remainingTime) {
                requireNonNull(remainingTime);
                builder.mRemainingTimeSeconds =
                        Builder.validateRemainingTime(remainingTime.getSeconds());
                return builder;
            }
        }
    }
}