/*
 * Copyright (C) 2012 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 android.location;

import static android.Manifest.permission.LOCATION_BYPASS;

import static java.lang.Math.max;
import static java.lang.Math.min;

import android.Manifest;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.WorkSource;
import android.util.TimeUtils;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;


/**
 * An encapsulation of various parameters for requesting location via {@link LocationManager}.
 */
public final class LocationRequest implements Parcelable {

    /**
     * For apps targeting Android S and above, all LocationRequest objects marked as low power will
     * throw exceptions if the caller does not have the LOCATION_HARDWARE permission, instead of
     * silently dropping the low power part of the request.
     *
     * @hide
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
    public static final long LOW_POWER_EXCEPTIONS = 168936375L;

    /**
     * Represents a passive only request. Such a request will not trigger any active locations or
     * power usage itself, but may receive locations generated in response to other requests.
     *
     * @see LocationRequest#getIntervalMillis()
     */
    public static final long PASSIVE_INTERVAL = Long.MAX_VALUE;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({QUALITY_LOW_POWER, QUALITY_BALANCED_POWER_ACCURACY, QUALITY_HIGH_ACCURACY})
    public @interface Quality {}

    /**
     * A quality constant indicating a location provider may choose to satisfy this request by
     * providing very accurate locations at the expense of potentially increased power usage. Each
     * location provider may interpret this field differently, but as an example, the network
     * provider may choose to return only wifi based locations rather than cell based locations in
     * order to have greater accuracy when this flag is present.
     */
    public static final int QUALITY_HIGH_ACCURACY = 100;

    /**
     * A quality constant indicating a location provider may choose to satisfy this request by
     * equally balancing power and accuracy constraints. Each location provider may interpret this
     * field differently, but location providers will generally use their default behavior when this
     * flag is present.
     */
    public static final int QUALITY_BALANCED_POWER_ACCURACY = 102;

    /**
     * A quality constant indicating a location provider may choose to satisfy this request by
     * providing less accurate locations in order to save power. Each location provider may
     * interpret this field differently, but as an example, the network provider may choose to
     * return cell based locations rather than wifi based locations in order to save power when this
     * flag is present.
     */
    public static final int QUALITY_LOW_POWER = 104;

    /**
     * Used with {@link #setQuality} to request the most accurate locations available.
     *
     * <p>This may be up to 1 meter accuracy, although this is implementation dependent.
     *
     * @hide
     * @deprecated Use {@link #QUALITY_HIGH_ACCURACY} instead.
     */
    @Deprecated
    @SystemApi
    public static final int ACCURACY_FINE = QUALITY_HIGH_ACCURACY;

    /**
     * Used with {@link #setQuality} to request "block" level accuracy.
     *
     * <p>Block level accuracy is considered to be about 100 meter accuracy,
     * although this is implementation dependent. Using a coarse accuracy
     * such as this often consumes less power.
     *
     * @hide
     * @deprecated Use {@link #QUALITY_BALANCED_POWER_ACCURACY} instead.
     */
    @Deprecated
    @SystemApi
    public static final int ACCURACY_BLOCK = QUALITY_BALANCED_POWER_ACCURACY;

    /**
     * Used with {@link #setQuality} to request "city" level accuracy.
     *
     * <p>City level accuracy is considered to be about 10km accuracy,
     * although this is implementation dependent. Using a coarse accuracy
     * such as this often consumes less power.
     *
     * @hide
     * @deprecated Use {@link #QUALITY_LOW_POWER} instead.
     */
    @Deprecated
    @SystemApi
    public static final int ACCURACY_CITY = QUALITY_LOW_POWER;

    /**
     * Used with {@link #setQuality} to require no direct power impact (passive locations).
     *
     * <p>This location request will not trigger any active location requests,
     * but will receive locations triggered by other applications. Your application
     * will not receive any direct power blame for location work.
     *
     * @hide
     * @deprecated Use {@link #PASSIVE_INTERVAL} instead.
     */
    @SystemApi
    @Deprecated
    public static final int POWER_NONE = 200;

    /**
     * Used with {@link #setQuality} to request low power impact.
     *
     * <p>This location request will avoid high power location work where
     * possible.
     *
     * @hide
     * @deprecated Use {@link #QUALITY_LOW_POWER} instead.
     */
    @Deprecated
    @SystemApi
    public static final int POWER_LOW = 201;

    /**
     * Used with {@link #setQuality} to allow high power consumption for location.
     *
     * <p>This location request will allow high power location work.
     *
     * @hide
     * @deprecated Use {@link #QUALITY_HIGH_ACCURACY} instead.
     */
    @Deprecated
    @SystemApi
    public static final int POWER_HIGH = 203;

    private static final long IMPLICIT_MIN_UPDATE_INTERVAL = -1;
    private static final double IMPLICIT_MIN_UPDATE_INTERVAL_FACTOR = 1D / 6D;

    private @Nullable String mProvider;
    private @Quality int mQuality;
    private long mIntervalMillis;
    private long mMinUpdateIntervalMillis;
    private long mExpireAtRealtimeMillis;
    private long mDurationMillis;
    private int mMaxUpdates;
    private float mMinUpdateDistanceMeters;
    private final long mMaxUpdateDelayMillis;
    private boolean mHideFromAppOps;
    private final boolean mAdasGnssBypass;
    private boolean mBypass;
    private boolean mLowPower;
    private @Nullable WorkSource mWorkSource;

    /**
     * @hide
     * @deprecated Use the Builder to construct new LocationRequests.
     */
    @SystemApi
    @Deprecated
    @NonNull
    public static LocationRequest create() {
        // 60 minutes is the default legacy interval
        return new LocationRequest.Builder(60 * 60 * 1000).build();
    }

    /**
     * @hide
     * @deprecated Use the Builder to construct new LocationRequests.
     */
    @SystemApi
    @Deprecated
    @NonNull
    public static LocationRequest createFromDeprecatedProvider(@NonNull String provider,
            long intervalMillis, float minUpdateDistanceMeters, boolean singleShot) {
        Preconditions.checkArgument(provider != null, "invalid null provider");

        if (intervalMillis < 0) {
            intervalMillis = 0;
        } else if (intervalMillis == PASSIVE_INTERVAL) {
            intervalMillis = Long.MAX_VALUE - 1;
        }
        if (minUpdateDistanceMeters < 0) {
            minUpdateDistanceMeters = 0;
        }

        int quality;
        if (LocationManager.PASSIVE_PROVIDER.equals(provider)) {
            quality = POWER_NONE;
        } else if (LocationManager.GPS_PROVIDER.equals(provider)) {
            quality = QUALITY_HIGH_ACCURACY;
        } else {
            quality = QUALITY_BALANCED_POWER_ACCURACY;
        }

        return new LocationRequest.Builder(intervalMillis)
                .setMinUpdateIntervalMillis(intervalMillis)
                .setMinUpdateDistanceMeters(minUpdateDistanceMeters)
                .setMaxUpdates(singleShot ? 1 : Integer.MAX_VALUE)
                .build()
                .setProvider(provider)
                .setQuality(quality);
    }

    /**
     * @hide
     * @deprecated Use the Builder to construct new LocationRequests.
     */
    @SystemApi
    @Deprecated
    @NonNull
    public static LocationRequest createFromDeprecatedCriteria(@NonNull Criteria criteria,
            long intervalMillis, float minUpdateDistanceMeters, boolean singleShot) {
        Preconditions.checkArgument(criteria != null, "invalid null criteria");

        if (intervalMillis < 0) {
            intervalMillis = 0;
        } else if (intervalMillis == PASSIVE_INTERVAL) {
            intervalMillis = Long.MAX_VALUE - 1;
        }
        if (minUpdateDistanceMeters < 0) {
            minUpdateDistanceMeters = 0;
        }

        return new LocationRequest.Builder(intervalMillis)
                .setQuality(criteria)
                .setMinUpdateIntervalMillis(intervalMillis)
                .setMinUpdateDistanceMeters(minUpdateDistanceMeters)
                .setMaxUpdates(singleShot ? 1 : Integer.MAX_VALUE)
                .build();
    }

    private LocationRequest(
            @Nullable String provider,
            long intervalMillis,
            @Quality int quality,
            long expireAtRealtimeMillis,
            long durationMillis,
            int maxUpdates,
            long minUpdateIntervalMillis,
            float minUpdateDistanceMeters,
            long maxUpdateDelayMillis,
            boolean hiddenFromAppOps,
            boolean adasGnssBypass,
            boolean bypass,
            boolean lowPower,
            WorkSource workSource) {
        mProvider = provider;
        mIntervalMillis = intervalMillis;
        mQuality = quality;
        mMinUpdateIntervalMillis = minUpdateIntervalMillis;
        mExpireAtRealtimeMillis = expireAtRealtimeMillis;
        mDurationMillis = durationMillis;
        mMaxUpdates = maxUpdates;
        mMinUpdateDistanceMeters = minUpdateDistanceMeters;
        mMaxUpdateDelayMillis = maxUpdateDelayMillis;
        mHideFromAppOps = hiddenFromAppOps;
        mAdasGnssBypass = adasGnssBypass;
        mBypass = bypass;
        mLowPower = lowPower;
        mWorkSource = Objects.requireNonNull(workSource);
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setProvider(@NonNull String provider) {
        Preconditions.checkArgument(provider != null);
        mProvider = provider;
        return this;
    }

    /**
     * @hide
     * @deprecated Providers are no longer an explicit part of a location request.
     */
    @SystemApi
    @Deprecated
    public @NonNull String getProvider() {
        return mProvider != null ? mProvider : LocationManager.FUSED_PROVIDER;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setQuality(int quality) {
        switch (quality) {
            case POWER_HIGH:
                // fall through
            case QUALITY_HIGH_ACCURACY:
                mQuality = QUALITY_HIGH_ACCURACY;
                break;
            case QUALITY_BALANCED_POWER_ACCURACY:
                mQuality = QUALITY_BALANCED_POWER_ACCURACY;
                break;
            case POWER_LOW:
                // fall through
            case QUALITY_LOW_POWER:
                mQuality = QUALITY_LOW_POWER;
                break;
            case POWER_NONE:
                mIntervalMillis = PASSIVE_INTERVAL;
                break;
            default:
                throw new IllegalArgumentException("invalid quality: " + quality);
        }

        return this;
    }

    /**
     * Returns the quality hint for this location request. The quality hint informs the provider how
     * it should attempt to manage any accuracy vs power tradeoffs while attempting to satisfy this
     * location request.
     *
     * @return the desired quality tradeoffs between accuracy and power
     */
    public @Quality int getQuality() {
        return mQuality;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setInterval(long millis) {
        Preconditions.checkArgument(millis >= 0);

        // legacy clients don't know about the passive interval
        if (millis == PASSIVE_INTERVAL) {
            millis = Long.MAX_VALUE - 1;
        }

        mIntervalMillis = millis;
        if (mMinUpdateIntervalMillis > mIntervalMillis) {
            mMinUpdateIntervalMillis = mIntervalMillis;
        }
        return this;
    }

    /**
     * @hide
     * @deprecated Use {@link #getIntervalMillis()} instead.
     */
    @SystemApi
    @Deprecated
    public long getInterval() {
        return getIntervalMillis();
    }

    /**
     * Returns the desired interval of location updates, or {@link #PASSIVE_INTERVAL} if this is a
     * passive, no power request. A passive request will not actively generate location updates
     * (and thus will not be power blamed for location), but may receive location updates generated
     * as a result of other location requests. A passive request must always have an explicit
     * minimum update interval set.
     *
     * <p>Locations may be available at a faster interval than specified here, see
     * {@link #getMinUpdateIntervalMillis()} for the behavior in that case.
     *
     * @return the desired interval of location updates
     */
    public @IntRange(from = 0) long getIntervalMillis() {
        return mIntervalMillis;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setFastestInterval(long millis) {
        Preconditions.checkArgument(millis >= 0);
        mMinUpdateIntervalMillis = millis;
        return this;
    }

    /**
     * @hide
     * @deprecated Use {@link #getMinUpdateIntervalMillis()} instead.
     */
    @SystemApi
    @Deprecated
    public long getFastestInterval() {
        return getMinUpdateIntervalMillis();
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setExpireAt(long millis) {
        mExpireAtRealtimeMillis = max(millis, 0);
        return this;
    }

    /**
     * @hide
     * @deprecated Prefer {@link #getDurationMillis()} where possible.
     */
    @SystemApi
    @Deprecated
    public long getExpireAt() {
        return mExpireAtRealtimeMillis;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setExpireIn(long millis) {
        mDurationMillis = millis;
        return this;
    }

    /**
     * @hide
     * @deprecated Use {@link #getDurationMillis()} instead.
     */
    @SystemApi
    @Deprecated
    public long getExpireIn() {
        return getDurationMillis();
    }

    /**
     * Returns the duration for which location will be provided before the request is automatically
     * removed. A duration of <code>Long.MAX_VALUE</code> represents an unlimited duration.
     *
     * @return the duration for which location will be provided
     */
    public @IntRange(from = 1) long getDurationMillis() {
        return mDurationMillis;
    }

    /**
     * @hide
     */
    public long getExpirationRealtimeMs(long startRealtimeMs) {
        long expirationRealtimeMs;
        // Check for > Long.MAX_VALUE overflow (elapsedRealtime > 0):
        if (mDurationMillis > Long.MAX_VALUE - startRealtimeMs) {
            expirationRealtimeMs = Long.MAX_VALUE;
        } else {
            expirationRealtimeMs = startRealtimeMs + mDurationMillis;
        }
        return min(expirationRealtimeMs, mExpireAtRealtimeMillis);
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setNumUpdates(int numUpdates) {
        if (numUpdates <= 0) {
            throw new IllegalArgumentException(
                    "invalid numUpdates: " + numUpdates);
        }
        mMaxUpdates = numUpdates;
        return this;
    }

    /**
     * @hide
     * @deprecated Use {@link #getMaxUpdates()} instead.
     */
    @SystemApi
    @Deprecated
    public int getNumUpdates() {
        return getMaxUpdates();
    }

    /**
     * Returns the maximum number of location updates for this request before the request is
     * automatically removed. A max updates value of <code>Integer.MAX_VALUE</code> represents an
     * unlimited number of updates.
     */
    public @IntRange(from = 1, to = Integer.MAX_VALUE) int getMaxUpdates() {
        return mMaxUpdates;
    }

    /**
     * Returns the minimum update interval. If location updates are available faster than the
     * request interval then locations will only be updated if the minimum update interval has
     * expired since the last location update.
     *
     * <p class=note><strong>Note:</strong> Some allowance for jitter is already built into the
     * minimum update interval, so you need not worry about updates blocked simply because they
     * arrived a fraction of a second earlier than expected.
     *
     * @return the minimum update interval
     */
    public @IntRange(from = 0) long getMinUpdateIntervalMillis() {
        if (mMinUpdateIntervalMillis == IMPLICIT_MIN_UPDATE_INTERVAL) {
            return (long) (mIntervalMillis * IMPLICIT_MIN_UPDATE_INTERVAL_FACTOR);
        } else {
            // the min is only necessary in case someone use a deprecated function to mess with the
            // interval or min update interval
            return min(mMinUpdateIntervalMillis, mIntervalMillis);
        }
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setSmallestDisplacement(float minDisplacementMeters) {
        mMinUpdateDistanceMeters = Preconditions.checkArgumentInRange(minDisplacementMeters, 0,
                Float.MAX_VALUE, "minDisplacementMeters");
        return this;
    }

    /**
     * @hide
     * @deprecated Use {@link #getMinUpdateDistanceMeters()} instead.
     */
    @SystemApi
    @Deprecated
    public float getSmallestDisplacement() {
        return getMinUpdateDistanceMeters();
    }

    /**
     * Returns the minimum distance between location updates. If a potential location update is
     * closer to the last location update than the minimum update distance, then the potential
     * location update will not occur. A value of 0 meters implies that no location update will ever
     * be rejected due to failing this constraint.
     *
     * @return the minimum distance between location updates
     */
    public @FloatRange(from = 0, to = Float.MAX_VALUE) float getMinUpdateDistanceMeters() {
        return mMinUpdateDistanceMeters;
    }

    /**
     * Returns the maximum time any location update may be delayed, and thus grouped with following
     * updates to enable location batching. If the maximum update delay is equal to or greater than
     * twice the interval, then location providers may provide batched results. The maximum batch
     * size is the maximum update delay divided by the interval. Not all devices or location
     * providers support batching, and use of this parameter does not guarantee that the client will
     * see batched results, or that batched results will always be of the maximum size.
     *
     * When available, batching can provide substantial power savings to the device, and clients are
     * encouraged to take advantage where appropriate for the use case.
     *
     * @see LocationListener#onLocationChanged(java.util.List)
     * @return the maximum time by which a location update may be delayed
     */
    public @IntRange(from = 0) long getMaxUpdateDelayMillis() {
        return mMaxUpdateDelayMillis;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public void setHideFromAppOps(boolean hiddenFromAppOps) {
        mHideFromAppOps = hiddenFromAppOps;
    }

    /**
     * @hide
     * @deprecated Use {@link #isHiddenFromAppOps()} instead.
     */
    @SystemApi
    @Deprecated
    public boolean getHideFromAppOps() {
        return isHiddenFromAppOps();
    }

    /**
     * Returns true if this request should be ignored while updating app ops with location usage.
     * This implies that someone else (usually the creator of the location request) is responsible
     * for updating app ops.
     *
     * @return true if this request should be ignored while updating app ops with location usage
     *
     * @hide
     */
    @SystemApi
    public boolean isHiddenFromAppOps() {
        return mHideFromAppOps;
    }

    /**
     * Returns true if this request may access GNSS even if location settings would normally deny
     * this, in order to enable automotive safety features. This field is only respected on
     * automotive devices, and only if the client is recognized as a legitimate ADAS (Advanced
     * Driving Assistance Systems) application.
     *
     * @return true if all limiting factors will be ignored to satisfy GNSS request
     *
     * @hide
     */
    @SystemApi
    public boolean isAdasGnssBypass() {
        return mAdasGnssBypass;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    @RequiresPermission(LOCATION_BYPASS)
    public @NonNull LocationRequest setLocationSettingsIgnored(boolean locationSettingsIgnored) {
        mBypass = locationSettingsIgnored;
        return this;
    }

    /**
     * Returns true if location settings, throttling, background location limits, and any other
     * possible limiting factors will be ignored in order to satisfy this request.
     *
     * @return true if all limiting factors will be ignored to satisfy this request
     *
     * @hide
     */
    @SystemApi
    public boolean isLocationSettingsIgnored() {
        return mBypass;
    }

    /**
     * Returns true if any bypass flag is set on this request. For internal use only.
     *
     * @hide
     */
    public boolean isBypass() {
        return mAdasGnssBypass || mBypass;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public @NonNull LocationRequest setLowPowerMode(boolean enabled) {
        mLowPower = enabled;
        return this;
    }

    /**
     * @hide
     * @deprecated Use {@link #isLowPower()} instead.
     */
    @Deprecated
    @SystemApi
    public boolean isLowPowerMode() {
        return isLowPower();
    }

    /**
     * Returns true if extreme trade-offs should be made to save power for this request. This
     * usually involves specialized hardware modes which can greatly affect the quality of
     * locations.
     *
     * @return true if extreme trade-offs should be made to save power for this request
     *
     * @hide
     */
    @SystemApi
    public boolean isLowPower() {
        return mLowPower;
    }

    /**
     * @hide
     * @deprecated LocationRequests should be treated as immutable.
     */
    @SystemApi
    @Deprecated
    public void setWorkSource(@Nullable WorkSource workSource) {
        if (workSource == null) {
            workSource = new WorkSource();
        }
        mWorkSource = workSource;
    }

    /**
     * Returns the work source used for power blame for this request. If empty, the system is free
     * to assign power blame as it deems most appropriate.
     *
     * @return the work source used for power blame for this request
     *
     * @hide
     */
    @SystemApi
    public @NonNull WorkSource getWorkSource() {
        return mWorkSource;
    }


    public static final @NonNull Parcelable.Creator<LocationRequest> CREATOR =
            new Parcelable.Creator<LocationRequest>() {
                @Override
                public LocationRequest createFromParcel(Parcel in) {
                    return new LocationRequest(
                            /* provider= */ in.readString(),
                            /* intervalMillis= */ in.readLong(),
                            /* quality= */ in.readInt(),
                            /* expireAtRealtimeMillis= */ in.readLong(),
                            /* durationMillis= */ in.readLong(),
                            /* maxUpdates= */ in.readInt(),
                            /* minUpdateIntervalMillis= */ in.readLong(),
                            /* minUpdateDistanceMeters= */ in.readFloat(),
                            /* maxUpdateDelayMillis= */ in.readLong(),
                            /* hiddenFromAppOps= */ in.readBoolean(),
                            /* adasGnssBypass= */ in.readBoolean(),
                            /* locationSettingsIgnored= */ in.readBoolean(),
                            /* lowPower= */ in.readBoolean(),
                            /* workSource= */ in.readTypedObject(WorkSource.CREATOR));
                }

                @Override
                public LocationRequest[] newArray(int size) {
                    return new LocationRequest[size];
                }
            };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel parcel, int flags) {
        parcel.writeString(mProvider);
        parcel.writeLong(mIntervalMillis);
        parcel.writeInt(mQuality);
        parcel.writeLong(mExpireAtRealtimeMillis);
        parcel.writeLong(mDurationMillis);
        parcel.writeInt(mMaxUpdates);
        parcel.writeLong(mMinUpdateIntervalMillis);
        parcel.writeFloat(mMinUpdateDistanceMeters);
        parcel.writeLong(mMaxUpdateDelayMillis);
        parcel.writeBoolean(mHideFromAppOps);
        parcel.writeBoolean(mAdasGnssBypass);
        parcel.writeBoolean(mBypass);
        parcel.writeBoolean(mLowPower);
        parcel.writeTypedObject(mWorkSource, 0);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        LocationRequest that = (LocationRequest) o;
        return mIntervalMillis == that.mIntervalMillis
                && mQuality == that.mQuality
                && mExpireAtRealtimeMillis == that.mExpireAtRealtimeMillis
                && mDurationMillis == that.mDurationMillis
                && mMaxUpdates == that.mMaxUpdates
                && mMinUpdateIntervalMillis == that.mMinUpdateIntervalMillis
                && Float.compare(that.mMinUpdateDistanceMeters, mMinUpdateDistanceMeters) == 0
                && mMaxUpdateDelayMillis == that.mMaxUpdateDelayMillis
                && mHideFromAppOps == that.mHideFromAppOps
                && mAdasGnssBypass == that.mAdasGnssBypass
                && mBypass == that.mBypass
                && mLowPower == that.mLowPower
                && Objects.equals(mProvider, that.mProvider)
                && Objects.equals(mWorkSource, that.mWorkSource);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mProvider, mIntervalMillis, mWorkSource);
    }

    @NonNull
    @Override
    public String toString() {
        StringBuilder s = new StringBuilder();
        s.append("Request[");
        if (mProvider != null) {
            s.append(mProvider).append(" ");
        }
        if (mIntervalMillis != PASSIVE_INTERVAL) {
            s.append("@");
            TimeUtils.formatDuration(mIntervalMillis, s);

            switch (mQuality) {
                case QUALITY_HIGH_ACCURACY:
                    s.append(" HIGH_ACCURACY");
                    break;
                case QUALITY_BALANCED_POWER_ACCURACY:
                    s.append(" BALANCED");
                    break;
                case QUALITY_LOW_POWER:
                    s.append(" LOW_POWER");
                    break;
            }
        } else {
            s.append("PASSIVE");
        }
        if (mExpireAtRealtimeMillis != Long.MAX_VALUE) {
            s.append(", expireAt=").append(TimeUtils.formatRealtime(mExpireAtRealtimeMillis));
        }
        if (mDurationMillis != Long.MAX_VALUE) {
            s.append(", duration=");
            TimeUtils.formatDuration(mDurationMillis, s);
        }
        if (mMaxUpdates != Integer.MAX_VALUE) {
            s.append(", maxUpdates=").append(mMaxUpdates);
        }
        if (mMinUpdateIntervalMillis != IMPLICIT_MIN_UPDATE_INTERVAL
                && mMinUpdateIntervalMillis < mIntervalMillis) {
            s.append(", minUpdateInterval=");
            TimeUtils.formatDuration(mMinUpdateIntervalMillis, s);
        }
        if (mMinUpdateDistanceMeters > 0.0) {
            s.append(", minUpdateDistance=").append(mMinUpdateDistanceMeters);
        }
        if (mMaxUpdateDelayMillis / 2 > mIntervalMillis) {
            s.append(", maxUpdateDelay=");
            TimeUtils.formatDuration(mMaxUpdateDelayMillis, s);
        }
        if (mLowPower) {
            s.append(", lowPower");
        }
        if (mHideFromAppOps) {
            s.append(", hiddenFromAppOps");
        }
        if (mAdasGnssBypass) {
            s.append(", adasGnssBypass");
        }
        if (mBypass) {
            s.append(", bypass");
        }
        if (mWorkSource != null && !mWorkSource.isEmpty()) {
            s.append(", ").append(mWorkSource);
        }
        s.append(']');
        return s.toString();
    }

    /**
     * A builder class for {@link LocationRequest}.
     */
    public static final class Builder {

        private long mIntervalMillis;
        private @Quality int mQuality;
        private long mDurationMillis;
        private int mMaxUpdates;
        private long mMinUpdateIntervalMillis;
        private float mMinUpdateDistanceMeters;
        private long mMaxUpdateDelayMillis;
        private boolean mHiddenFromAppOps;
        private boolean mAdasGnssBypass;
        private boolean mBypass;
        private boolean mLowPower;
        @Nullable private WorkSource mWorkSource;

        /**
         * Creates a new Builder with the given interval. See {@link #setIntervalMillis(long)} for
         * more information on the interval.
         */
        public Builder(long intervalMillis) {
            // gives us a range check
            setIntervalMillis(intervalMillis);

            mQuality = QUALITY_BALANCED_POWER_ACCURACY;
            mDurationMillis = Long.MAX_VALUE;
            mMaxUpdates = Integer.MAX_VALUE;
            mMinUpdateIntervalMillis = IMPLICIT_MIN_UPDATE_INTERVAL;
            mMinUpdateDistanceMeters = 0;
            mMaxUpdateDelayMillis = 0;
            mHiddenFromAppOps = false;
            mAdasGnssBypass = false;
            mBypass = false;
            mLowPower = false;
            mWorkSource = null;
        }

        /**
         * Creates a new Builder with all parameters copied from the given location request.
         */
        public Builder(@NonNull LocationRequest locationRequest) {
            mIntervalMillis = locationRequest.mIntervalMillis;
            mQuality = locationRequest.mQuality;
            mDurationMillis = locationRequest.mDurationMillis;
            mMaxUpdates = locationRequest.mMaxUpdates;
            mMinUpdateIntervalMillis = locationRequest.mMinUpdateIntervalMillis;
            mMinUpdateDistanceMeters = locationRequest.mMinUpdateDistanceMeters;
            mMaxUpdateDelayMillis = locationRequest.mMaxUpdateDelayMillis;
            mHiddenFromAppOps = locationRequest.mHideFromAppOps;
            mAdasGnssBypass = locationRequest.mAdasGnssBypass;
            mBypass = locationRequest.mBypass;
            mLowPower = locationRequest.mLowPower;
            mWorkSource = locationRequest.mWorkSource;

            // handle edge cases that can only happen with location request that has been modified
            // by deprecated SystemApi methods
            if (mIntervalMillis == PASSIVE_INTERVAL
                    && mMinUpdateIntervalMillis == IMPLICIT_MIN_UPDATE_INTERVAL) {
                // this is the legacy default minimum update interval, so if we're forced to
                // change the value, at least this should be unsuprising to legacy clients (which
                // should be the only clients capable of getting in this weird state).
                mMinUpdateIntervalMillis = 10 * 60 * 1000;
            }
        }

        /**
         * Sets the request interval. The request interval may be set to {@link #PASSIVE_INTERVAL}
         * which indicates this request will not actively generate location updates (and thus will
         * not be power blamed for location), but may receive location updates generated as a result
         * of other location requests. A passive request must always have an explicit minimum
         * update interval set.
         *
         * <p>Locations may be available at a faster interval than specified here, see
         * {@link #setMinUpdateIntervalMillis(long)} for the behavior in that case.
         */
        public @NonNull Builder setIntervalMillis(@IntRange(from = 0) long intervalMillis) {
            mIntervalMillis = Preconditions.checkArgumentInRange(intervalMillis, 0, Long.MAX_VALUE,
                    "intervalMillis");
            return this;
        }

        /**
         * Sets the request quality. The quality is a hint to providers on how they should weigh
         * power vs accuracy tradeoffs. High accuracy locations may cost more power to produce, and
         * lower accuracy locations may cost less power to produce. Defaults to
         * {@link #QUALITY_BALANCED_POWER_ACCURACY}.
         */
        public @NonNull Builder setQuality(@Quality int quality) {
            Preconditions.checkArgument(
                    quality == QUALITY_LOW_POWER || quality == QUALITY_BALANCED_POWER_ACCURACY
                            || quality == QUALITY_HIGH_ACCURACY,
                    "quality must be a defined QUALITY constant, not %d", quality);
            mQuality = quality;
            return this;
        }

        /**
         * @hide
         */
        public @NonNull Builder setQuality(@NonNull Criteria criteria) {
            switch (criteria.getAccuracy()) {
                case Criteria.ACCURACY_COARSE:
                    mQuality = QUALITY_BALANCED_POWER_ACCURACY;
                    break;
                case Criteria.ACCURACY_FINE:
                    mQuality = QUALITY_HIGH_ACCURACY;
                    break;
                default: {
                    if (criteria.getPowerRequirement() == Criteria.POWER_HIGH) {
                        mQuality = POWER_HIGH;
                    } else {
                        mQuality = POWER_LOW;
                    }
                }
            }
            return this;
        }

        /**
         * Sets the duration this request will continue before being automatically removed. Defaults
         * to <code>Long.MAX_VALUE</code>, which represents an unlimited duration.
         */
        public @NonNull Builder setDurationMillis(@IntRange(from = 1) long durationMillis) {
            mDurationMillis = Preconditions.checkArgumentInRange(durationMillis, 1, Long.MAX_VALUE,
                    "durationMillis");
            return this;
        }

        /**
         * Sets the maximum number of location updates for this request before this request is
         * automatically removed. Defaults to <code>Integer.MAX_VALUE</code>, which represents an
         * unlimited number of updates.
         */
        public @NonNull Builder setMaxUpdates(
                @IntRange(from = 1, to = Integer.MAX_VALUE) int maxUpdates) {
            mMaxUpdates = Preconditions.checkArgumentInRange(maxUpdates, 1, Integer.MAX_VALUE,
                    "maxUpdates");
            return this;
        }

        /**
         * Sets an explicit minimum update interval. If location updates are available faster than
         * the request interval then an update will only occur if the minimum update interval has
         * expired since the last location update. Defaults to no explicit minimum update interval
         * set, which means some sensible default between 0 and the interval will be chosen. The
         * exact value is not specified at the moment. If an exact known value is required, clients
         * should set an explicit value themselves.
         *
         * <p class=note><strong>Note:</strong> Some allowance for jitter is already built into the
         * minimum update interval, so you need not worry about updates blocked simply because they
         * arrived a fraction of a second earlier than expected.
         *
         * <p class="note"><strong>Note:</strong> When {@link #build()} is invoked, the minimum of
         * the interval and the minimum update interval will be used as the minimum update interval
         * of the built request.
         */
        public @NonNull Builder setMinUpdateIntervalMillis(
                @IntRange(from = 0) long minUpdateIntervalMillis) {
            mMinUpdateIntervalMillis = Preconditions.checkArgumentInRange(minUpdateIntervalMillis,
                    0, Long.MAX_VALUE, "minUpdateIntervalMillis");
            return this;
        }

        /**
         * Clears an explicitly set minimum update interval and reverts to an implicit minimum
         * update interval (ie, the minimum update interval is some sensible default between 0 and
         * the interval).
         */
        public @NonNull Builder clearMinUpdateIntervalMillis() {
            mMinUpdateIntervalMillis = IMPLICIT_MIN_UPDATE_INTERVAL;
            return this;
        }

        /**
         * Sets the minimum update distance between location updates. If a potential location
         * update is closer to the last location update than the minimum update distance, then
         * the potential location update will not occur. Defaults to 0, which represents no minimum
         * update distance.
         */
        public @NonNull Builder setMinUpdateDistanceMeters(
                @FloatRange(from = 0, to = Float.MAX_VALUE) float minUpdateDistanceMeters) {
            mMinUpdateDistanceMeters = Preconditions.checkArgumentInRange(minUpdateDistanceMeters,
                    0, Float.MAX_VALUE, "minUpdateDistanceMeters");
            return this;
        }

        /**
         * Sets the maximum time any location update may be delayed, and thus grouped with following
         * updates to enable location batching. If the maximum update delay is equal to or greater
         * than twice the interval, then location providers may provide batched results. Defaults to
         * 0, which represents no batching allowed.
         */
        public @NonNull Builder setMaxUpdateDelayMillis(
                @IntRange(from = 0) long maxUpdateDelayMillis) {
            mMaxUpdateDelayMillis = Preconditions.checkArgumentInRange(maxUpdateDelayMillis, 0,
                    Long.MAX_VALUE, "maxUpdateDelayMillis");
            return this;
        }

        /**
         * If set to true, indicates that app ops should not be updated with location usage due to
         * this request. This implies that someone else (usually the creator of the location
         * request) is responsible for updating app ops as appropriate. Defaults to false.
         *
         * <p>Permissions enforcement occurs when resulting location request is actually used, not
         * when this method is invoked.
         *
         * @hide
         */
        @SystemApi
        @RequiresPermission(Manifest.permission.UPDATE_APP_OPS_STATS)
        public @NonNull Builder setHiddenFromAppOps(boolean hiddenFromAppOps) {
            mHiddenFromAppOps = hiddenFromAppOps;
            return this;
        }

        /**
         * If set to true, indicates that the client is an ADAS (Advanced Driving Assistance
         * Systems) client, which requires access to GNSS even if location settings would normally
         * deny this, in order to enable auto safety features. This field is only respected on
         * automotive devices, and only if the client is recognized as a legitimate ADAS
         * application. Defaults to false.
         *
         * <p>Permissions enforcement occurs when resulting location request is actually used, not
         * when this method is invoked.
         *
         * @hide
         */
        @SystemApi
        @RequiresPermission(LOCATION_BYPASS)
        @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
        public @NonNull Builder setAdasGnssBypass(boolean adasGnssBypass) {
            mAdasGnssBypass = adasGnssBypass;
            return this;
        }

        /**
         * If set to true, indicates that location settings, throttling, background location limits,
         * and any other possible limiting factors should be ignored in order to satisfy this
         * request. This is only intended for use in user initiated emergency situations, and
         * should be used extremely cautiously. Defaults to false.
         *
         * <p>Permissions enforcement occurs when resulting location request is actually used, not
         * when this method is invoked.
         *
         * @hide
         */
        @SystemApi
        @RequiresPermission(LOCATION_BYPASS)
        public @NonNull Builder setLocationSettingsIgnored(boolean locationSettingsIgnored) {
            mBypass = locationSettingsIgnored;
            return this;
        }

        /**
         * It set to true, indicates that extreme trade-offs should be made if possible to save
         * power for this request. This usually involves specialized hardware modes which can
         * greatly affect the quality of locations. Not all devices may support this. Defaults to
         * false.
         *
         * <p>Permissions enforcement occurs when resulting location request is actually used, not
         * when this method is invoked.
         *
         * @hide
         */
        @SystemApi
        @RequiresPermission(Manifest.permission.LOCATION_HARDWARE)
        public @NonNull Builder setLowPower(boolean lowPower) {
            mLowPower = lowPower;
            return this;
        }

        /**
         * Sets the work source to use for power blame for this location request. Defaults to an
         * empty WorkSource, which implies the system is free to assign power blame as it determines
         * best for this request (which usually means blaming the owner of the location listener).
         *
         * <p>Permissions enforcement occurs when resulting location request is actually used, not
         * when this method is invoked.
         *
         * @hide
         */
        @SystemApi
        @RequiresPermission(Manifest.permission.UPDATE_DEVICE_STATS)
        public @NonNull Builder setWorkSource(@Nullable WorkSource workSource) {
            mWorkSource = workSource;
            return this;
        }

        /**
         * Builds a location request from this builder. If an explicit minimum update interval is
         * set, the minimum update interval of the location request will be the minimum of the
         * interval and minimum update interval.
         *
         * <p>If building a passive request then you must have set an explicit minimum update
         * interval.
         *
         * @throws IllegalStateException if building a passive request with no explicit minimum
         * update interval set
         * @return a new location request
         */
        public @NonNull LocationRequest build() {
            Preconditions.checkState(mIntervalMillis != PASSIVE_INTERVAL
                            || mMinUpdateIntervalMillis != IMPLICIT_MIN_UPDATE_INTERVAL,
                    "passive location requests must have an explicit minimum update interval");

            return new LocationRequest(
                    null,
                    mIntervalMillis,
                    mQuality,
                    Long.MAX_VALUE,
                    mDurationMillis,
                    mMaxUpdates,
                    min(mMinUpdateIntervalMillis, mIntervalMillis),
                    mMinUpdateDistanceMeters,
                    mMaxUpdateDelayMillis,
                    mHiddenFromAppOps,
                    mAdasGnssBypass,
                    mBypass,
                    mLowPower,
                    new WorkSource(mWorkSource));
        }
    }
}
