am 60020e2f: Merge "Improve coarse locations." into jb-mr1-dev
* commit '60020e2fa3dd31280ccec91029113f88ee5338c8': Improve coarse locations.
This commit is contained in:
@@ -195,20 +195,6 @@ public final class LocationRequest implements Parcelable {
|
||||
return mSmallestDisplacement;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public LocationRequest applyCoarsePermissionRestrictions() {
|
||||
switch (mQuality) {
|
||||
case ACCURACY_FINE:
|
||||
mQuality = ACCURACY_BLOCK;
|
||||
break;
|
||||
}
|
||||
// cap fastest interval to 6 seconds
|
||||
if (mFastestInterval < 6 * 1000) mFastestInterval = 6 * 1000;
|
||||
// cap requested interval to 1 minute
|
||||
if (mInterval < 60 * 1000) mInterval = 60 * 1000;
|
||||
return this;
|
||||
}
|
||||
|
||||
private static void checkInterval(long millis) {
|
||||
if (millis < 0) {
|
||||
throw new IllegalArgumentException("invalid interval: " + millis);
|
||||
|
||||
@@ -47,7 +47,6 @@ import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
@@ -64,6 +63,7 @@ import com.android.internal.location.ProviderRequest;
|
||||
import com.android.server.location.GeocoderProxy;
|
||||
import com.android.server.location.GeofenceManager;
|
||||
import com.android.server.location.GpsLocationProvider;
|
||||
import com.android.server.location.LocationFudger;
|
||||
import com.android.server.location.LocationProviderInterface;
|
||||
import com.android.server.location.LocationProviderProxy;
|
||||
import com.android.server.location.MockProvider;
|
||||
@@ -110,19 +110,6 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
|
||||
private static final int MSG_LOCATION_CHANGED = 1;
|
||||
|
||||
// Accuracy in meters above which a location is considered coarse
|
||||
private static final double COARSE_ACCURACY_M = 100.0;
|
||||
private static final String EXTRA_COARSE_LOCATION = "coarseLocation";
|
||||
|
||||
private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000;
|
||||
|
||||
/**
|
||||
* Maximum latitude of 1 meter from the pole.
|
||||
* This keeps cosine(MAX_LATITUDE) to a non-zero value;
|
||||
*/
|
||||
private static final double MAX_LATITUDE =
|
||||
90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR);
|
||||
|
||||
// Location Providers may sometimes deliver location updates
|
||||
// slightly faster that requested - provide grace period so
|
||||
// we don't unnecessarily filter events that are otherwise on
|
||||
@@ -137,6 +124,7 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
private final Object mLock = new Object();
|
||||
|
||||
// --- fields below are final after init() ---
|
||||
private LocationFudger mLocationFudger;
|
||||
private GeofenceManager mGeofenceManager;
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
private PackageManager mPackageManager;
|
||||
@@ -221,6 +209,7 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
loadProvidersLocked();
|
||||
}
|
||||
mGeofenceManager = new GeofenceManager(mContext);
|
||||
mLocationFudger = new LocationFudger();
|
||||
|
||||
// Register for Network (Wifi or Mobile) updates
|
||||
IntentFilter filter = new IntentFilter();
|
||||
@@ -907,7 +896,22 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
String perm = checkPermission();
|
||||
|
||||
if (ACCESS_COARSE_LOCATION.equals(perm)) {
|
||||
request.applyCoarsePermissionRestrictions();
|
||||
switch (request.getQuality()) {
|
||||
case LocationRequest.ACCURACY_FINE:
|
||||
request.setQuality(LocationRequest.ACCURACY_BLOCK);
|
||||
break;
|
||||
case LocationRequest.POWER_HIGH:
|
||||
request.setQuality(LocationRequest.POWER_LOW);
|
||||
break;
|
||||
}
|
||||
// throttle fastest interval
|
||||
if (request.getFastestInterval() < LocationFudger.FASTEST_INTERVAL_MS) {
|
||||
request.setFastestInterval(LocationFudger.FASTEST_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
// throttle interval if its faster than the fastest interval
|
||||
if (request.getInterval () < request.getFastestInterval()) {
|
||||
request.setInterval(request.getFastestInterval());
|
||||
}
|
||||
return perm;
|
||||
}
|
||||
@@ -1075,7 +1079,7 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
if (ACCESS_FINE_LOCATION.equals(perm)) {
|
||||
return location;
|
||||
} else {
|
||||
return getCoarseLocationExtra(location);
|
||||
return mLocationFudger.getOrCreate(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1291,11 +1295,8 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
LocationProviderInterface p = mProvidersByName.get(provider);
|
||||
if (p == null) return;
|
||||
|
||||
// Add the coarse location as an extra, if not already present
|
||||
Location coarse = getCoarseLocationExtra(location);
|
||||
if (coarse == null) {
|
||||
coarse = addCoarseLocationExtra(location);
|
||||
}
|
||||
// Add the coarse location as an extra
|
||||
Location coarse = mLocationFudger.getOrCreate(location);
|
||||
|
||||
// Update last known locations
|
||||
Location lastLocation = mLastLocation.get(provider);
|
||||
@@ -1660,106 +1661,6 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
}
|
||||
}
|
||||
|
||||
private static double wrapLatitude(double lat) {
|
||||
if (lat > MAX_LATITUDE) lat = MAX_LATITUDE;
|
||||
if (lat < -MAX_LATITUDE) lat = -MAX_LATITUDE;
|
||||
return lat;
|
||||
}
|
||||
|
||||
private static double wrapLongitude(double lon) {
|
||||
if (lon >= 180.0) lon -= 360.0;
|
||||
if (lon < -180.0) lon += 360.0;
|
||||
return lon;
|
||||
}
|
||||
|
||||
private static double distanceToDegreesLatitude(double distance) {
|
||||
return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires latitude since longitudinal distances change with distance from equator.
|
||||
*/
|
||||
private static double distanceToDegreesLongitude(double distance, double lat) {
|
||||
return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(lat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fudge a location into a coarse location.
|
||||
* <p>Add a random offset, then quantize the result (snap-to-grid).
|
||||
* Random offsets alone can be low-passed pretty easily.
|
||||
* Snap-to-grid on its own is excellent unless you are sitting on a
|
||||
* grid boundary and bouncing between quantizations.
|
||||
* The combination is quite hard to reverse engineer.
|
||||
* <p>The random offset used is smaller than the goal accuracy
|
||||
* ({@link #COARSE_ACCURACY_M}), in order to give relatively stable
|
||||
* results after quantization.
|
||||
*/
|
||||
private static Location createCoarse(Location fine) {
|
||||
Location coarse = new Location(fine);
|
||||
|
||||
coarse.removeBearing();
|
||||
coarse.removeSpeed();
|
||||
coarse.removeAltitude();
|
||||
|
||||
double lat = coarse.getLatitude();
|
||||
double lon = coarse.getLongitude();
|
||||
|
||||
// wrap
|
||||
lat = wrapLatitude(lat);
|
||||
lon = wrapLongitude(lon);
|
||||
|
||||
if (coarse.getAccuracy() < COARSE_ACCURACY_M / 2) {
|
||||
// apply a random offset
|
||||
double fudgeDistance = COARSE_ACCURACY_M / 2.0 - coarse.getAccuracy();
|
||||
lat += (Math.random() - 0.5) * distanceToDegreesLatitude(fudgeDistance);
|
||||
lon += (Math.random() - 0.5) * distanceToDegreesLongitude(fudgeDistance, lat);
|
||||
}
|
||||
|
||||
// wrap
|
||||
lat = wrapLatitude(lat);
|
||||
lon = wrapLongitude(lon);
|
||||
|
||||
// quantize (snap-to-grid)
|
||||
double latGranularity = distanceToDegreesLatitude(COARSE_ACCURACY_M);
|
||||
double lonGranularity = distanceToDegreesLongitude(COARSE_ACCURACY_M, lat);
|
||||
long latQuantized = Math.round(lat / latGranularity);
|
||||
long lonQuantized = Math.round(lon / lonGranularity);
|
||||
lat = latQuantized * latGranularity;
|
||||
lon = lonQuantized * lonGranularity;
|
||||
|
||||
// wrap again
|
||||
lat = wrapLatitude(lat);
|
||||
lon = wrapLongitude(lon);
|
||||
|
||||
// apply
|
||||
coarse.setLatitude(lat);
|
||||
coarse.setLongitude(lon);
|
||||
coarse.setAccuracy((float)COARSE_ACCURACY_M);
|
||||
|
||||
return coarse;
|
||||
}
|
||||
|
||||
|
||||
private static Location getCoarseLocationExtra(Location location) {
|
||||
Bundle extras = location.getExtras();
|
||||
if (extras == null) return null;
|
||||
Parcelable parcel = extras.getParcelable(EXTRA_COARSE_LOCATION);
|
||||
if (parcel == null) return null;
|
||||
if (!(parcel instanceof Location)) return null;
|
||||
Location coarse = (Location) parcel;
|
||||
if (coarse.getAccuracy() < COARSE_ACCURACY_M) return null;
|
||||
return coarse;
|
||||
}
|
||||
|
||||
private static Location addCoarseLocationExtra(Location location) {
|
||||
Bundle extras = location.getExtras();
|
||||
if (extras == null) extras = new Bundle();
|
||||
Location coarse = createCoarse(location);
|
||||
extras.putParcelable(EXTRA_COARSE_LOCATION, coarse);
|
||||
location.setExtras(extras);
|
||||
return coarse;
|
||||
}
|
||||
|
||||
private void log(String log) {
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Slog.d(TAG, log);
|
||||
@@ -1819,6 +1720,9 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs
|
||||
}
|
||||
}
|
||||
|
||||
pw.append(" fudger: ");
|
||||
mLocationFudger.dump(fd, pw, args);
|
||||
|
||||
if (args.length > 0 && "short".equals(args[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
310
services/java/com/android/server/location/LocationFudger.java
Normal file
310
services/java/com/android/server/location/LocationFudger.java
Normal file
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* 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 com.android.server.location;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.security.SecureRandom;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* Contains the logic to obfuscate (fudge) locations for coarse applications.
|
||||
*
|
||||
* <p>The goal is just to prevent applications with only
|
||||
* the coarse location permission from receiving a fine location.
|
||||
*/
|
||||
public class LocationFudger {
|
||||
private static final boolean D = false;
|
||||
private static final String TAG = "LocationFudge";
|
||||
|
||||
private static final String EXTRA_COARSE_LOCATION = "coarseLocation";
|
||||
|
||||
/**
|
||||
* This is the main control: Best location accuracy allowed for coarse applications.
|
||||
*/
|
||||
private static final float ACCURACY_METERS = 200.0f;
|
||||
|
||||
/**
|
||||
* The distance between grids for snap-to-grid. See {@link #createCoarse}.
|
||||
*/
|
||||
private static final double GRID_SIZE_METERS = ACCURACY_METERS;
|
||||
|
||||
/**
|
||||
* Standard deviation of the (normally distributed) random offset applied
|
||||
* to coarse locations. It does not need to be as large as
|
||||
* {@link #COARSE_ACCURACY_METERS} because snap-to-grid is the primary obfuscation
|
||||
* method. See further details in the implementation.
|
||||
*/
|
||||
private static final double STANDARD_DEVIATION_METERS = GRID_SIZE_METERS / 4.0;
|
||||
|
||||
/**
|
||||
* This is the fastest interval that applications can receive coarse
|
||||
* locations.
|
||||
*/
|
||||
public static final long FASTEST_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
/**
|
||||
* The duration until we change the random offset.
|
||||
*/
|
||||
private static final long CHANGE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
/**
|
||||
* The percentage that we change the random offset at every interval.
|
||||
*
|
||||
* <p>0.0 indicates the random offset doesn't change. 1.0
|
||||
* indicates the random offset is completely replaced every interval.
|
||||
*/
|
||||
private static final double CHANGE_PER_INTERVAL = 0.03; // 3% change
|
||||
|
||||
// Pre-calculated weights used to move the random offset.
|
||||
//
|
||||
// The goal is to iterate on the previous offset, but keep
|
||||
// the resulting standard deviation the same. The variance of
|
||||
// two gaussian distributions summed together is equal to the
|
||||
// sum of the variance of each distribution. So some quick
|
||||
// algebra results in the following sqrt calculation to
|
||||
// weigh in a new offset while keeping the final standard
|
||||
// deviation unchanged.
|
||||
private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL;
|
||||
private static final double PREVIOUS_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT);
|
||||
|
||||
/**
|
||||
* This number actually varies because the earth is not round, but
|
||||
* 111,000 meters is considered generally acceptable.
|
||||
*/
|
||||
private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000;
|
||||
|
||||
/**
|
||||
* Maximum latitude.
|
||||
*
|
||||
* <p>We pick a value 1 meter away from 90.0 degrees in order
|
||||
* to keep cosine(MAX_LATITUDE) to a non-zero value, so that we avoid
|
||||
* divide by zero fails.
|
||||
*/
|
||||
private static final double MAX_LATITUDE = 90.0 -
|
||||
(1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR);
|
||||
|
||||
private final Object mLock = new Object();
|
||||
private final SecureRandom mRandom = new SecureRandom();
|
||||
|
||||
// all fields below protected by mLock
|
||||
private double mOffsetLatitudeMeters;
|
||||
private double mOffsetLongitudeMeters;
|
||||
private long mNextInterval;
|
||||
|
||||
public LocationFudger() {
|
||||
mOffsetLatitudeMeters = nextOffset();
|
||||
mOffsetLongitudeMeters = nextOffset();
|
||||
mNextInterval = SystemClock.elapsedRealtime() + CHANGE_INTERVAL_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached coarse location, or generate a new one and cache it.
|
||||
*/
|
||||
public Location getOrCreate(Location location) {
|
||||
Bundle extras = location.getExtras();
|
||||
if (extras == null) {
|
||||
return addCoarseLocationExtra(location);
|
||||
}
|
||||
Parcelable parcel = extras.getParcelable(EXTRA_COARSE_LOCATION);
|
||||
if (parcel == null) {
|
||||
return addCoarseLocationExtra(location);
|
||||
}
|
||||
if (!(parcel instanceof Location)) {
|
||||
return addCoarseLocationExtra(location);
|
||||
}
|
||||
Location coarse = (Location) parcel;
|
||||
if (coarse.getAccuracy() < ACCURACY_METERS) {
|
||||
return addCoarseLocationExtra(location);
|
||||
}
|
||||
return coarse;
|
||||
}
|
||||
|
||||
private Location addCoarseLocationExtra(Location location) {
|
||||
Bundle extras = location.getExtras();
|
||||
if (extras == null) extras = new Bundle();
|
||||
Location coarse = createCoarse(location);
|
||||
extras.putParcelable(EXTRA_COARSE_LOCATION, coarse);
|
||||
location.setExtras(extras);
|
||||
return coarse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a coarse location.
|
||||
*
|
||||
* <p>Two techniques are used: random offsets and snap-to-grid.
|
||||
*
|
||||
* <p>First we add a random offset. This mitigates against detecting
|
||||
* grid transitions. Without a random offset it is possible to detect
|
||||
* a users position very accurately when they cross a grid boundary.
|
||||
* The random offset changes very slowly over time, to mitigate against
|
||||
* taking many location samples and averaging them out.
|
||||
*
|
||||
* <p>Second we snap-to-grid (quantize). This has the nice property of
|
||||
* producing stable results, and mitigating against taking many samples
|
||||
* to average out a random offset.
|
||||
*/
|
||||
private Location createCoarse(Location fine) {
|
||||
Location coarse = new Location(fine);
|
||||
|
||||
// clean all the optional information off the location, because
|
||||
// this can leak detailed location information
|
||||
coarse.removeBearing();
|
||||
coarse.removeSpeed();
|
||||
coarse.removeAltitude();
|
||||
coarse.setExtras(null);
|
||||
|
||||
double lat = coarse.getLatitude();
|
||||
double lon = coarse.getLongitude();
|
||||
|
||||
// wrap
|
||||
lat = wrapLatitude(lat);
|
||||
lon = wrapLongitude(lon);
|
||||
|
||||
// Step 1) apply a random offset
|
||||
//
|
||||
// The goal of the random offset is to prevent the application
|
||||
// from determining that the device is on a grid boundary
|
||||
// when it crosses from one grid to the next.
|
||||
//
|
||||
// We apply the offset even if the location already claims to be
|
||||
// inaccurate, because it may be more accurate than claimed.
|
||||
synchronized (mLock) {
|
||||
updateRandomOffsetLocked();
|
||||
// perform lon first whilst lat is still within bounds
|
||||
lon += metersToDegreesLongitude(mOffsetLongitudeMeters, lat);
|
||||
lat += metersToDegreesLatitude(mOffsetLatitudeMeters);
|
||||
if (D) Log.d(TAG, String.format("applied offset of %.0f, %.0f (meters)",
|
||||
mOffsetLongitudeMeters, mOffsetLatitudeMeters));
|
||||
}
|
||||
|
||||
// wrap
|
||||
lat = wrapLatitude(lat);
|
||||
lon = wrapLongitude(lon);
|
||||
|
||||
// Step 2) Snap-to-grid (quantize)
|
||||
//
|
||||
// This is the primary means of obfuscation. It gives nice consistent
|
||||
// results and is very effective at hiding the true location
|
||||
// (as long as you are not sitting on a grid boundary, which
|
||||
// step 1 mitigates).
|
||||
//
|
||||
// Note we quantize the latitude first, since the longitude
|
||||
// quantization depends on the latitude value and so leaks information
|
||||
// about the latitude
|
||||
double latGranularity = metersToDegreesLatitude(GRID_SIZE_METERS);
|
||||
lat = Math.round(lat / latGranularity) * latGranularity;
|
||||
double lonGranularity = metersToDegreesLongitude(GRID_SIZE_METERS, lat);
|
||||
lon = Math.round(lon / lonGranularity) * lonGranularity;
|
||||
|
||||
// wrap again
|
||||
lat = wrapLatitude(lat);
|
||||
lon = wrapLongitude(lon);
|
||||
|
||||
// apply
|
||||
coarse.setLatitude(lat);
|
||||
coarse.setLongitude(lon);
|
||||
coarse.setAccuracy(Math.max(ACCURACY_METERS, coarse.getAccuracy()));
|
||||
|
||||
if (D) Log.d(TAG, "fudged " + fine + " to " + coarse);
|
||||
return coarse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the random offset over time.
|
||||
*
|
||||
* <p>If the random offset was new for every location
|
||||
* fix then an application can more easily average location results
|
||||
* over time,
|
||||
* especially when the location is near a grid boundary. On the
|
||||
* other hand if the random offset is constant then if an application
|
||||
* found a way to reverse engineer the offset they would be able
|
||||
* to detect location at grid boundaries very accurately. So
|
||||
* we choose a random offset and then very slowly move it, to
|
||||
* make both approaches very hard.
|
||||
*
|
||||
* <p>The random offset does not need to be large, because snap-to-grid
|
||||
* is the primary obfuscation mechanism. It just needs to be large
|
||||
* enough to stop information leakage as we cross grid boundaries.
|
||||
*/
|
||||
private void updateRandomOffsetLocked() {
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
if (now < mNextInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (D) Log.d(TAG, String.format("old offset: %.0f, %.0f (meters)",
|
||||
mOffsetLongitudeMeters, mOffsetLatitudeMeters));
|
||||
|
||||
// ok, need to update the random offset
|
||||
mNextInterval = now + CHANGE_INTERVAL_MS;
|
||||
|
||||
mOffsetLatitudeMeters *= PREVIOUS_WEIGHT;
|
||||
mOffsetLatitudeMeters += NEW_WEIGHT * nextOffset();
|
||||
mOffsetLongitudeMeters *= PREVIOUS_WEIGHT;
|
||||
mOffsetLongitudeMeters += NEW_WEIGHT * nextOffset();
|
||||
|
||||
if (D) Log.d(TAG, String.format("new offset: %.0f, %.0f (meters)",
|
||||
mOffsetLongitudeMeters, mOffsetLatitudeMeters));
|
||||
}
|
||||
|
||||
private double nextOffset() {
|
||||
return mRandom.nextGaussian() * STANDARD_DEVIATION_METERS;
|
||||
}
|
||||
|
||||
private static double wrapLatitude(double lat) {
|
||||
if (lat > MAX_LATITUDE) {
|
||||
lat = MAX_LATITUDE;
|
||||
}
|
||||
if (lat < -MAX_LATITUDE) {
|
||||
lat = -MAX_LATITUDE;
|
||||
}
|
||||
return lat;
|
||||
}
|
||||
|
||||
private static double wrapLongitude(double lon) {
|
||||
lon %= 360.0; // wraps into range (-360.0, +360.0)
|
||||
if (lon >= 180.0) {
|
||||
lon -= 360.0;
|
||||
}
|
||||
if (lon < -180.0) {
|
||||
lon += 360.0;
|
||||
}
|
||||
return lon;
|
||||
}
|
||||
|
||||
private static double metersToDegreesLatitude(double distance) {
|
||||
return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires latitude since longitudinal distances change with distance from equator.
|
||||
*/
|
||||
private static double metersToDegreesLongitude(double distance, double lat) {
|
||||
return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat));
|
||||
}
|
||||
|
||||
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
||||
pw.println(String.format("offset: %.0f, %.0f (meters)", mOffsetLongitudeMeters,
|
||||
mOffsetLatitudeMeters));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user