/* * 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 android.location; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.location.flags.Flags; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.util.Log; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; /** * A location result representing a list of locations, ordered from earliest to latest. * * @hide */ public final class LocationResult implements Parcelable { private static final String TAG = "LocationResult"; // maximum reasonable accuracy, somewhat arbitrarily chosen. this is a very high upper limit, it // could likely be lower, but we only want to throw out really absurd values. private static final float MAX_ACCURACY_M = 1000000; // maximum reasonable speed we expect a device to travel at is currently mach 1 (top speed of // current fastest private jet). Higher speed than the value is considered as a malfunction // than a correct reading. private static final float MAX_SPEED_MPS = 343; /** Exception representing an invalid location within a {@link LocationResult}. */ public static class BadLocationException extends Exception { public BadLocationException(String message) { super(message); } } /** * Creates a new LocationResult from the given locations, making a copy of each location. * Locations must be ordered in the same order they were derived (earliest to latest). */ public static @NonNull LocationResult create(@NonNull List locations) { Preconditions.checkArgument(!locations.isEmpty()); ArrayList locationsCopy = new ArrayList<>(locations.size()); for (Location location : locations) { locationsCopy.add(new Location(Objects.requireNonNull(location))); } return new LocationResult(locationsCopy); } /** * Creates a new LocationResult from the given locations, making a copy of each location. * Locations must be ordered in the same order they were derived (earliest to latest). */ public static @NonNull LocationResult create(@NonNull Location... locations) { Preconditions.checkArgument(locations.length > 0); ArrayList locationsCopy = new ArrayList<>(locations.length); for (Location location : locations) { locationsCopy.add(new Location(Objects.requireNonNull(location))); } return new LocationResult(locationsCopy); } /** * Creates a new LocationResult that takes ownership of the given locations without copying * them. Callers must ensure the given locations are never mutated after this method is called. * Locations must be ordered in the same order they were derived (earliest to latest). */ public static @NonNull LocationResult wrap(@NonNull List locations) { Preconditions.checkArgument(!locations.isEmpty()); return new LocationResult(new ArrayList<>(locations)); } /** * Creates a new LocationResult that takes ownership of the given locations without copying * them. Callers must ensure the given locations are never mutated after this method is called. * Locations must be ordered in the same order they were derived (earliest to latest). */ public static @NonNull LocationResult wrap(@NonNull Location... locations) { Preconditions.checkArgument(locations.length > 0); ArrayList newLocations = new ArrayList<>(locations.length); for (Location location : locations) { newLocations.add(Objects.requireNonNull(location)); } return new LocationResult(newLocations); } private final ArrayList mLocations; private LocationResult(ArrayList locations) { Preconditions.checkArgument(!locations.isEmpty()); mLocations = locations; } /** * Throws an IllegalArgumentException if the ordering of locations does not appear to generally * be from earliest to latest, or if any individual location is incomplete. * * @hide */ public @NonNull LocationResult validate() throws BadLocationException { long prevElapsedRealtimeNs = 0; final int size = mLocations.size(); for (int i = 0; i < size; ++i) { Location location = mLocations.get(i); if (Flags.locationValidation()) { if (location.getLatitude() < -90.0 || location.getLatitude() > 90.0 || location.getLongitude() < -180.0 || location.getLongitude() > 180.0 || Double.isNaN(location.getLatitude()) || Double.isNaN(location.getLongitude())) { throw new BadLocationException("location must have valid lat/lng"); } if (!location.hasAccuracy()) { throw new BadLocationException("location must have accuracy"); } if (location.getAccuracy() < 0 || location.getAccuracy() > MAX_ACCURACY_M) { throw new BadLocationException("location must have reasonable accuracy"); } if (location.getTime() < 0) { throw new BadLocationException("location must have valid time"); } if (prevElapsedRealtimeNs > location.getElapsedRealtimeNanos()) { throw new BadLocationException( "location must have valid monotonically increasing realtime"); } if (location.getElapsedRealtimeNanos() > SystemClock.elapsedRealtimeNanos()) { throw new BadLocationException("location must not have realtime in the future"); } if (!location.isMock()) { if (location.getProvider() == null) { throw new BadLocationException("location must have valid provider"); } if (location.getLatitude() == 0 && location.getLongitude() == 0) { throw new BadLocationException("location must not be at 0,0"); } } if (location.hasSpeed() && (location.getSpeed() < 0 || location.getSpeed() > MAX_SPEED_MPS)) { Log.w(TAG, "removed bad location speed: " + location.getSpeed()); location.removeSpeed(); } } else { if (!location.isComplete()) { throw new IllegalArgumentException( "incomplete location at index " + i + ": " + mLocations); } if (location.getElapsedRealtimeNanos() < prevElapsedRealtimeNs) { throw new IllegalArgumentException( "incorrectly ordered location at index " + i + ": " + mLocations); } } prevElapsedRealtimeNs = location.getElapsedRealtimeNanos(); } return this; } /** * Returns the latest location in this location result, ie, the location at the highest index. */ public @NonNull Location getLastLocation() { return mLocations.get(mLocations.size() - 1); } /** * Returns the number of locations in this location result. */ public @IntRange(from = 1) int size() { return mLocations.size(); } /** * Returns the location at the given index, from 0 to {@link #size()} - 1. Locations at lower * indices are from earlier in time than location at higher indices. */ public @NonNull Location get(@IntRange(from = 0) int i) { return mLocations.get(i); } /** * Returns an unmodifiable list of locations in this location result. */ public @NonNull List asList() { return Collections.unmodifiableList(mLocations); } /** * Returns a deep copy of this LocationResult. * * @hide */ public @NonNull LocationResult deepCopy() { final int size = mLocations.size(); ArrayList copy = new ArrayList<>(size); for (int i = 0; i < size; i++) { copy.add(new Location(mLocations.get(i))); } return new LocationResult(copy); } /** * Returns a LocationResult with only the last location from this location result. * * @hide */ public @NonNull LocationResult asLastLocationResult() { if (mLocations.size() == 1) { return this; } else { return LocationResult.wrap(getLastLocation()); } } /** * Returns a LocationResult with only locations that pass the given predicate. This * implementation will avoid allocations when no locations are filtered out. The predicate is * guaranteed to be invoked once per location, in order from earliest to latest. If all * locations are filtered out a null value is returned. * * @hide */ public @Nullable LocationResult filter(Predicate predicate) { ArrayList filtered = mLocations; final int size = mLocations.size(); for (int i = 0; i < size; ++i) { if (!predicate.test(mLocations.get(i))) { if (filtered == mLocations) { filtered = new ArrayList<>(mLocations.size() - 1); for (int j = 0; j < i; ++j) { filtered.add(mLocations.get(j)); } } } else if (filtered != mLocations) { filtered.add(mLocations.get(i)); } } if (filtered == mLocations) { return this; } else if (filtered.isEmpty()) { return null; } else { return new LocationResult(filtered); } } /** * Returns a LocationResult with locations mapped to other locations. This implementation will * avoid allocations when all locations are mapped to the same location. The function is * guaranteed to be invoked once per location, in order from earliest to latest. * * @hide */ public @NonNull LocationResult map(Function function) { ArrayList mapped = mLocations; final int size = mLocations.size(); for (int i = 0; i < size; ++i) { Location location = mLocations.get(i); Location newLocation = function.apply(location); if (mapped != mLocations) { mapped.add(newLocation); } else if (newLocation != location) { mapped = new ArrayList<>(mLocations.size()); for (int j = 0; j < i; ++j) { mapped.add(mLocations.get(j)); } mapped.add(newLocation); } } if (mapped == mLocations) { return this; } else { return new LocationResult(mapped); } } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public LocationResult createFromParcel(Parcel in) { return new LocationResult( Objects.requireNonNull(in.createTypedArrayList(Location.CREATOR))); } @Override public LocationResult[] newArray(int size) { return new LocationResult[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel parcel, int flags) { parcel.writeTypedList(mLocations); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LocationResult that = (LocationResult) o; return mLocations.equals(that.mLocations); } @Override public int hashCode() { return Objects.hash(mLocations); } @Override public String toString() { return mLocations.toString(); } }