1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.location;
18
19import java.io.FileDescriptor;
20import java.io.PrintWriter;
21import java.security.SecureRandom;
22import android.content.Context;
23import android.database.ContentObserver;
24import android.location.Location;
25import android.location.LocationManager;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.Parcelable;
29import android.os.SystemClock;
30import android.provider.Settings;
31import android.util.Log;
32
33
34/**
35 * Contains the logic to obfuscate (fudge) locations for coarse applications.
36 *
37 * <p>The goal is just to prevent applications with only
38 * the coarse location permission from receiving a fine location.
39 */
40public class LocationFudger {
41    private static final boolean D = false;
42    private static final String TAG = "LocationFudge";
43
44    /**
45     * Default coarse accuracy in meters.
46     */
47    private static final float DEFAULT_ACCURACY_IN_METERS = 2000.0f;
48
49    /**
50     * Minimum coarse accuracy in meters.
51     */
52    private static final float MINIMUM_ACCURACY_IN_METERS = 200.0f;
53
54    /**
55     * Secure settings key for coarse accuracy.
56     */
57    private static final String COARSE_ACCURACY_CONFIG_NAME = "locationCoarseAccuracy";
58
59    /**
60     * This is the fastest interval that applications can receive coarse
61     * locations.
62     */
63    public static final long FASTEST_INTERVAL_MS = 10 * 60 * 1000;  // 10 minutes
64
65    /**
66     * The duration until we change the random offset.
67     */
68    private static final long CHANGE_INTERVAL_MS = 60 * 60 * 1000;  // 1 hour
69
70    /**
71     * The percentage that we change the random offset at every interval.
72     *
73     * <p>0.0 indicates the random offset doesn't change. 1.0
74     * indicates the random offset is completely replaced every interval.
75     */
76    private static final double CHANGE_PER_INTERVAL = 0.03;  // 3% change
77
78    // Pre-calculated weights used to move the random offset.
79    //
80    // The goal is to iterate on the previous offset, but keep
81    // the resulting standard deviation the same. The variance of
82    // two gaussian distributions summed together is equal to the
83    // sum of the variance of each distribution. So some quick
84    // algebra results in the following sqrt calculation to
85    // weigh in a new offset while keeping the final standard
86    // deviation unchanged.
87    private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL;
88    private static final double PREVIOUS_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT);
89
90    /**
91     * This number actually varies because the earth is not round, but
92     * 111,000 meters is considered generally acceptable.
93     */
94    private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000;
95
96    /**
97     * Maximum latitude.
98     *
99     * <p>We pick a value 1 meter away from 90.0 degrees in order
100     * to keep cosine(MAX_LATITUDE) to a non-zero value, so that we avoid
101     * divide by zero fails.
102     */
103    private static final double MAX_LATITUDE = 90.0 -
104            (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR);
105
106    private final Object mLock = new Object();
107    private final SecureRandom mRandom = new SecureRandom();
108
109    /**
110     * Used to monitor coarse accuracy secure setting for changes.
111     */
112    private final ContentObserver mSettingsObserver;
113
114    /**
115     * Used to resolve coarse accuracy setting.
116     */
117    private final Context mContext;
118
119    // all fields below protected by mLock
120    private double mOffsetLatitudeMeters;
121    private double mOffsetLongitudeMeters;
122    private long mNextInterval;
123
124    /**
125     * Best location accuracy allowed for coarse applications.
126     * This value should only be set by {@link #setAccuracyInMetersLocked(float)}.
127     */
128    private float mAccuracyInMeters;
129
130    /**
131     * The distance between grids for snap-to-grid. See {@link #createCoarse}.
132     * This value should only be set by {@link #setAccuracyInMetersLocked(float)}.
133     */
134    private double mGridSizeInMeters;
135
136    /**
137     * Standard deviation of the (normally distributed) random offset applied
138     * to coarse locations. It does not need to be as large as
139     * {@link #COARSE_ACCURACY_METERS} because snap-to-grid is the primary obfuscation
140     * method. See further details in the implementation.
141     * This value should only be set by {@link #setAccuracyInMetersLocked(float)}.
142     */
143    private double mStandardDeviationInMeters;
144
145    public LocationFudger(Context context, Handler handler) {
146        mContext = context;
147        mSettingsObserver = new ContentObserver(handler) {
148            @Override
149            public void onChange(boolean selfChange) {
150                setAccuracyInMeters(loadCoarseAccuracy());
151            }
152        };
153        mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
154                COARSE_ACCURACY_CONFIG_NAME), false, mSettingsObserver);
155
156        float accuracy = loadCoarseAccuracy();
157        synchronized (mLock) {
158            setAccuracyInMetersLocked(accuracy);
159            mOffsetLatitudeMeters = nextOffsetLocked();
160            mOffsetLongitudeMeters = nextOffsetLocked();
161            mNextInterval = SystemClock.elapsedRealtime() + CHANGE_INTERVAL_MS;
162        }
163    }
164
165    /**
166     * Get the cached coarse location, or generate a new one and cache it.
167     */
168    public Location getOrCreate(Location location) {
169        synchronized (mLock) {
170            Location coarse = location.getExtraLocation(Location.EXTRA_COARSE_LOCATION);
171            if (coarse == null) {
172                return addCoarseLocationExtraLocked(location);
173            }
174            if (coarse.getAccuracy() < mAccuracyInMeters) {
175                return addCoarseLocationExtraLocked(location);
176            }
177            return coarse;
178        }
179    }
180
181    private Location addCoarseLocationExtraLocked(Location location) {
182        Location coarse = createCoarseLocked(location);
183        location.setExtraLocation(Location.EXTRA_COARSE_LOCATION, coarse);
184        return coarse;
185    }
186
187    /**
188     * Create a coarse location.
189     *
190     * <p>Two techniques are used: random offsets and snap-to-grid.
191     *
192     * <p>First we add a random offset. This mitigates against detecting
193     * grid transitions. Without a random offset it is possible to detect
194     * a users position very accurately when they cross a grid boundary.
195     * The random offset changes very slowly over time, to mitigate against
196     * taking many location samples and averaging them out.
197     *
198     * <p>Second we snap-to-grid (quantize). This has the nice property of
199     * producing stable results, and mitigating against taking many samples
200     * to average out a random offset.
201     */
202    private Location createCoarseLocked(Location fine) {
203        Location coarse = new Location(fine);
204
205        // clean all the optional information off the location, because
206        // this can leak detailed location information
207        coarse.removeBearing();
208        coarse.removeSpeed();
209        coarse.removeAltitude();
210        coarse.setExtras(null);
211
212        double lat = coarse.getLatitude();
213        double lon = coarse.getLongitude();
214
215        // wrap
216        lat = wrapLatitude(lat);
217        lon = wrapLongitude(lon);
218
219        // Step 1) apply a random offset
220        //
221        // The goal of the random offset is to prevent the application
222        // from determining that the device is on a grid boundary
223        // when it crosses from one grid to the next.
224        //
225        // We apply the offset even if the location already claims to be
226        // inaccurate, because it may be more accurate than claimed.
227        updateRandomOffsetLocked();
228        // perform lon first whilst lat is still within bounds
229        lon += metersToDegreesLongitude(mOffsetLongitudeMeters, lat);
230        lat += metersToDegreesLatitude(mOffsetLatitudeMeters);
231        if (D) Log.d(TAG, String.format("applied offset of %.0f, %.0f (meters)",
232                mOffsetLongitudeMeters, mOffsetLatitudeMeters));
233
234        // wrap
235        lat = wrapLatitude(lat);
236        lon = wrapLongitude(lon);
237
238        // Step 2) Snap-to-grid (quantize)
239        //
240        // This is the primary means of obfuscation. It gives nice consistent
241        // results and is very effective at hiding the true location
242        // (as long as you are not sitting on a grid boundary, which
243        // step 1 mitigates).
244        //
245        // Note we quantize the latitude first, since the longitude
246        // quantization depends on the latitude value and so leaks information
247        // about the latitude
248        double latGranularity = metersToDegreesLatitude(mGridSizeInMeters);
249        lat = Math.round(lat / latGranularity) * latGranularity;
250        double lonGranularity = metersToDegreesLongitude(mGridSizeInMeters, lat);
251        lon = Math.round(lon / lonGranularity) * lonGranularity;
252
253        // wrap again
254        lat = wrapLatitude(lat);
255        lon = wrapLongitude(lon);
256
257        // apply
258        coarse.setLatitude(lat);
259        coarse.setLongitude(lon);
260        coarse.setAccuracy(Math.max(mAccuracyInMeters, coarse.getAccuracy()));
261
262        if (D) Log.d(TAG, "fudged " + fine + " to " + coarse);
263        return coarse;
264    }
265
266    /**
267     * Update the random offset over time.
268     *
269     * <p>If the random offset was new for every location
270     * fix then an application can more easily average location results
271     * over time,
272     * especially when the location is near a grid boundary. On the
273     * other hand if the random offset is constant then if an application
274     * found a way to reverse engineer the offset they would be able
275     * to detect location at grid boundaries very accurately. So
276     * we choose a random offset and then very slowly move it, to
277     * make both approaches very hard.
278     *
279     * <p>The random offset does not need to be large, because snap-to-grid
280     * is the primary obfuscation mechanism. It just needs to be large
281     * enough to stop information leakage as we cross grid boundaries.
282     */
283    private void updateRandomOffsetLocked() {
284        long now = SystemClock.elapsedRealtime();
285        if (now < mNextInterval) {
286            return;
287        }
288
289        if (D) Log.d(TAG, String.format("old offset: %.0f, %.0f (meters)",
290                mOffsetLongitudeMeters, mOffsetLatitudeMeters));
291
292        // ok, need to update the random offset
293        mNextInterval = now + CHANGE_INTERVAL_MS;
294
295        mOffsetLatitudeMeters *= PREVIOUS_WEIGHT;
296        mOffsetLatitudeMeters += NEW_WEIGHT * nextOffsetLocked();
297        mOffsetLongitudeMeters *= PREVIOUS_WEIGHT;
298        mOffsetLongitudeMeters += NEW_WEIGHT * nextOffsetLocked();
299
300        if (D) Log.d(TAG, String.format("new offset: %.0f, %.0f (meters)",
301                mOffsetLongitudeMeters, mOffsetLatitudeMeters));
302    }
303
304    private double nextOffsetLocked() {
305        return mRandom.nextGaussian() * mStandardDeviationInMeters;
306    }
307
308    private static double wrapLatitude(double lat) {
309         if (lat > MAX_LATITUDE) {
310             lat = MAX_LATITUDE;
311         }
312         if (lat < -MAX_LATITUDE) {
313             lat = -MAX_LATITUDE;
314         }
315         return lat;
316    }
317
318    private static double wrapLongitude(double lon) {
319        lon %= 360.0;  // wraps into range (-360.0, +360.0)
320        if (lon >= 180.0) {
321            lon -= 360.0;
322        }
323        if (lon < -180.0) {
324            lon += 360.0;
325        }
326        return lon;
327    }
328
329    private static double metersToDegreesLatitude(double distance) {
330        return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR;
331    }
332
333    /**
334     * Requires latitude since longitudinal distances change with distance from equator.
335     */
336    private static double metersToDegreesLongitude(double distance, double lat) {
337        return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat));
338    }
339
340    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
341        pw.println(String.format("offset: %.0f, %.0f (meters)", mOffsetLongitudeMeters,
342                mOffsetLatitudeMeters));
343    }
344
345    /**
346     * This is the main control: call this to set the best location accuracy
347     * allowed for coarse applications and all derived values.
348     */
349    private void setAccuracyInMetersLocked(float accuracyInMeters) {
350        mAccuracyInMeters = Math.max(accuracyInMeters, MINIMUM_ACCURACY_IN_METERS);
351        if (D) {
352            Log.d(TAG, "setAccuracyInMetersLocked: new accuracy = " + mAccuracyInMeters);
353        }
354        mGridSizeInMeters = mAccuracyInMeters;
355        mStandardDeviationInMeters = mGridSizeInMeters / 4.0;
356    }
357
358    /**
359     * Same as setAccuracyInMetersLocked without the pre-lock requirement.
360     */
361    private void setAccuracyInMeters(float accuracyInMeters) {
362        synchronized (mLock) {
363            setAccuracyInMetersLocked(accuracyInMeters);
364        }
365    }
366
367    /**
368     * Loads the coarse accuracy value from secure settings.
369     */
370    private float loadCoarseAccuracy() {
371        String newSetting = Settings.Secure.getString(mContext.getContentResolver(),
372                COARSE_ACCURACY_CONFIG_NAME);
373        if (D) {
374            Log.d(TAG, "loadCoarseAccuracy: newSetting = \"" + newSetting + "\"");
375        }
376        if (newSetting == null) {
377            return DEFAULT_ACCURACY_IN_METERS;
378        }
379        try {
380            return Float.parseFloat(newSetting);
381        } catch (NumberFormatException e) {
382            return DEFAULT_ACCURACY_IN_METERS;
383        }
384    }
385}
386