FusionEngine.java revision 20de160ca32a8f2936a80ffd70551a22e2371d25
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.location.fused;
18
19import java.io.FileDescriptor;
20import java.io.PrintWriter;
21import java.util.HashMap;
22
23import com.android.location.provider.ProviderRequestUnbundled;
24
25import android.content.Context;
26import android.location.Location;
27import android.location.LocationListener;
28import android.location.LocationManager;
29import android.location.LocationRequest;
30import android.os.Bundle;
31import android.os.Looper;
32import android.os.SystemClock;
33import android.os.WorkSource;
34import android.util.Log;
35
36public class FusionEngine implements LocationListener {
37    public interface Callback {
38        public void reportLocation(Location location);
39    }
40
41    private static final String TAG = "FusedLocation";
42    private static final String NETWORK = LocationManager.NETWORK_PROVIDER;
43    private static final String GPS = LocationManager.GPS_PROVIDER;
44
45    // threshold below which a location is considered stale enough
46    // that we shouldn't use its bearing, altitude, speed etc
47    private static final double WEIGHT_THRESHOLD = 0.5;
48    // accuracy in meters at which a Location's weight is halved (compared to 0 accuracy)
49    private static final double ACCURACY_HALFLIFE_M = 20.0;
50    // age in seconds at which a Location's weight is halved (compared to 0 age)
51    private static final double AGE_HALFLIFE_S = 60.0;
52
53    private static final double ACCURACY_DECAY_CONSTANT_M = Math.log(2) / ACCURACY_HALFLIFE_M;
54    private static final double AGE_DECAY_CONSTANT_S = Math.log(2) / AGE_HALFLIFE_S;
55
56    private final Context mContext;
57    private final LocationManager mLocationManager;
58    private final Looper mLooper;
59
60    // all fields are only used on mLooper thread. except for in dump() which is not thread-safe
61    private Callback mCallback;
62    private Location mFusedLocation;
63    private Location mGpsLocation;
64    private Location mNetworkLocation;
65    private double mNetworkWeight;
66    private double mGpsWeight;
67
68    private boolean mEnabled;
69    private ProviderRequestUnbundled mRequest;
70
71    private final HashMap<String, ProviderStats> mStats = new HashMap<String, ProviderStats>();
72
73    public FusionEngine(Context context, Looper looper) {
74        mContext = context;
75        mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
76        mNetworkLocation = new Location("");
77        mNetworkLocation.setAccuracy(Float.MAX_VALUE);
78        mGpsLocation = new Location("");
79        mGpsLocation.setAccuracy(Float.MAX_VALUE);
80        mLooper = looper;
81
82        mStats.put(GPS, new ProviderStats());
83        mStats.get(GPS).available = mLocationManager.isProviderEnabled(GPS);
84        mStats.put(NETWORK, new ProviderStats());
85        mStats.get(NETWORK).available = mLocationManager.isProviderEnabled(NETWORK);
86    }
87
88    public void init(Callback callback) {
89        Log.i(TAG, "engine started (" + mContext.getPackageName() + ")");
90        mCallback = callback;
91    }
92
93    /**
94     * Called to stop doing any work, and release all resources
95     * This can happen when a better fusion engine is installed
96     * in a different package, and this one is no longer needed.
97     * Called on mLooper thread
98     */
99    public void deinit() {
100        mRequest = null;
101        disable();
102        Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")");
103    }
104
105    private boolean isAvailable() {
106        return mStats.get(GPS).available || mStats.get(NETWORK).available;
107    }
108
109    /** Called on mLooper thread */
110    public void enable() {
111        mEnabled = true;
112        updateRequirements();
113    }
114
115    /** Called on mLooper thread */
116    public void disable() {
117        mEnabled = false;
118        updateRequirements();
119    }
120
121    /** Called on mLooper thread */
122    public void setRequest(ProviderRequestUnbundled request, WorkSource source) {
123        mRequest = request;
124        mEnabled = request.getReportLocation();
125        updateRequirements();
126    }
127
128    private static class ProviderStats {
129        public boolean available;
130        public boolean requested;
131        public long requestTime;
132        public long minTime;
133        public long lastRequestTtff;
134        @Override
135        public String toString() {
136            StringBuilder s = new StringBuilder();
137            s.append(available ? "AVAILABLE" : "UNAVAILABLE");
138            s.append(requested ? " REQUESTED" : " ---");
139            return s.toString();
140        }
141    }
142
143    private void enableProvider(String name, long minTime) {
144        ProviderStats stats = mStats.get(name);
145
146        if (!stats.requested) {
147            stats.requestTime = SystemClock.elapsedRealtime();
148            stats.requested = true;
149            stats.minTime = minTime;
150            mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
151        } else if (stats.minTime != minTime) {
152            stats.minTime = minTime;
153            mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
154        }
155    }
156
157    private void disableProvider(String name) {
158        ProviderStats stats = mStats.get(name);
159
160        if (stats.requested) {
161            stats.requested = false;
162            mLocationManager.removeUpdates(this);  //TODO GLOBAL
163        }
164    }
165
166    private void updateRequirements() {
167        if (mEnabled == false || mRequest == null) {
168            mRequest = null;
169            disableProvider(NETWORK);
170            disableProvider(GPS);
171            return;
172        }
173
174        ProviderStats gpsStats = mStats.get(GPS);
175        ProviderStats networkStats = mStats.get(NETWORK);
176
177        long networkInterval = Long.MAX_VALUE;
178        long gpsInterval = Long.MAX_VALUE;
179        for (LocationRequest request : mRequest.getLocationRequests()) {
180            switch (request.getQuality()) {
181                case LocationRequest.ACCURACY_FINE:
182                case LocationRequest.POWER_HIGH:
183                    if (request.getInterval() < gpsInterval) {
184                        gpsInterval = request.getInterval();
185                    }
186                    if (request.getInterval() < networkInterval) {
187                        networkInterval = request.getInterval();
188                    }
189                    break;
190                case LocationRequest.ACCURACY_BLOCK:
191                case LocationRequest.ACCURACY_CITY:
192                case LocationRequest.POWER_LOW:
193                    if (request.getInterval() < networkInterval) {
194                        networkInterval = request.getInterval();
195                    }
196                    break;
197            }
198        }
199
200        if (gpsInterval < Long.MAX_VALUE) {
201            enableProvider(GPS, gpsInterval);
202        } else {
203            disableProvider(GPS);
204        }
205        if (networkInterval < Long.MAX_VALUE) {
206            enableProvider(NETWORK, networkInterval);
207        } else {
208            disableProvider(NETWORK);
209        }
210    }
211
212    private static double weighAccuracy(Location loc) {
213        double accuracy = loc.getAccuracy();
214        return Math.exp(-accuracy * ACCURACY_DECAY_CONSTANT_M);
215    }
216
217    private static double weighAge(Location loc) {
218        long ageSeconds = SystemClock.elapsedRealtimeNanos() - loc.getElapsedRealtimeNanos();
219        ageSeconds /= 1000000000L;
220        if (ageSeconds < 0) ageSeconds = 0;
221        return Math.exp(-ageSeconds * AGE_DECAY_CONSTANT_S);
222    }
223
224    private double weigh(double gps, double network) {
225        return (gps * mGpsWeight) + (network * mNetworkWeight);
226    }
227
228    private double weigh(double gps, double network, double wrapMin, double wrapMax) {
229        // apply aliasing
230        double wrapWidth = wrapMax - wrapMin;
231        if (gps - network > wrapWidth / 2) network += wrapWidth;
232        else if (network - gps > wrapWidth / 2) gps += wrapWidth;
233
234        double result = weigh(gps, network);
235
236        // remove aliasing
237        if (result > wrapMax) result -= wrapWidth;
238        return result;
239    }
240
241    private void updateFusedLocation() {
242        // naive fusion
243        mNetworkWeight = weighAccuracy(mNetworkLocation) * weighAge(mNetworkLocation);
244        mGpsWeight = weighAccuracy(mGpsLocation) * weighAge(mGpsLocation);
245        // scale mNetworkWeight and mGpsWeight so that they add to 1
246        double totalWeight = mNetworkWeight + mGpsWeight;
247        mNetworkWeight /= totalWeight;
248        mGpsWeight /= totalWeight;
249
250        Location fused = new Location(LocationManager.FUSED_PROVIDER);
251        // fuse lat/long
252        // assumes the two locations are close enough that earth curvature doesn't matter
253        fused.setLatitude(weigh(mGpsLocation.getLatitude(), mNetworkLocation.getLatitude()));
254        fused.setLongitude(weigh(mGpsLocation.getLongitude(), mNetworkLocation.getLongitude(),
255                -180.0, 180.0));
256
257        // fused accuracy
258        //TODO: use some real math instead of this crude fusion
259        // one suggestion is to fuse in a quadratic manner, eg
260        // sqrt(weigh(gpsAcc^2, netAcc^2)).
261        // another direction to explore is to consider the difference in the 2
262        // locations. If the component locations overlap, the fused accuracy is
263        // better than the component accuracies. If they are far apart,
264        // the fused accuracy is much worse.
265        fused.setAccuracy((float)weigh(mGpsLocation.getAccuracy(), mNetworkLocation.getAccuracy()));
266
267        // fused time - now
268        fused.setTime(System.currentTimeMillis());
269        fused.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
270
271        // fuse altitude
272        if (mGpsLocation.hasAltitude() && !mNetworkLocation.hasAltitude() &&
273                mGpsWeight > WEIGHT_THRESHOLD) {
274            fused.setAltitude(mGpsLocation.getAltitude());   // use GPS
275        } else if (!mGpsLocation.hasAltitude() && mNetworkLocation.hasAltitude() &&
276                mNetworkWeight > WEIGHT_THRESHOLD) {
277            fused.setAltitude(mNetworkLocation.getAltitude());   // use Network
278        } else if (mGpsLocation.hasAltitude() && mNetworkLocation.hasAltitude()) {
279            fused.setAltitude(weigh(mGpsLocation.getAltitude(), mNetworkLocation.getAltitude()));
280        }
281
282        // fuse speed
283        if (mGpsLocation.hasSpeed() && !mNetworkLocation.hasSpeed() &&
284                mGpsWeight > WEIGHT_THRESHOLD) {
285            fused.setSpeed(mGpsLocation.getSpeed());   // use GPS if its not too old
286        } else if (!mGpsLocation.hasSpeed() && mNetworkLocation.hasSpeed() &&
287                mNetworkWeight > WEIGHT_THRESHOLD) {
288            fused.setSpeed(mNetworkLocation.getSpeed());   // use Network
289        } else if (mGpsLocation.hasSpeed() && mNetworkLocation.hasSpeed()) {
290            fused.setSpeed((float)weigh(mGpsLocation.getSpeed(), mNetworkLocation.getSpeed()));
291        }
292
293        // fuse bearing
294        if (mGpsLocation.hasBearing() && !mNetworkLocation.hasBearing() &&
295                mGpsWeight > WEIGHT_THRESHOLD) {
296            fused.setBearing(mGpsLocation.getBearing());   // use GPS if its not too old
297        } else if (!mGpsLocation.hasBearing() && mNetworkLocation.hasBearing() &&
298                mNetworkWeight > WEIGHT_THRESHOLD) {
299            fused.setBearing(mNetworkLocation.getBearing());   // use Network
300        } else if (mGpsLocation.hasBearing() && mNetworkLocation.hasBearing()) {
301            fused.setBearing((float)weigh(mGpsLocation.getBearing(), mNetworkLocation.getBearing(),
302                    0.0, 360.0));
303        }
304
305        if (mNetworkLocation != null) {
306            fused.setExtraLocation(Location.EXTRA_NO_GPS_LOCATION, mNetworkLocation);
307        }
308
309        mFusedLocation = fused;
310
311        mCallback.reportLocation(mFusedLocation);
312    }
313
314    /** Called on mLooper thread */
315    @Override
316    public void onLocationChanged(Location location) {
317        if (GPS.equals(location.getProvider())) {
318            mGpsLocation = location;
319            updateFusedLocation();
320        } else if (NETWORK.equals(location.getProvider())) {
321            mNetworkLocation = location;
322            updateFusedLocation();
323        }
324    }
325
326    /** Called on mLooper thread */
327    @Override
328    public void onStatusChanged(String provider, int status, Bundle extras) {  }
329
330    /** Called on mLooper thread */
331    @Override
332    public void onProviderEnabled(String provider) {
333        ProviderStats stats = mStats.get(provider);
334        if (stats == null) return;
335
336        stats.available = true;
337    }
338
339    /** Called on mLooper thread */
340    @Override
341    public void onProviderDisabled(String provider) {
342        ProviderStats stats = mStats.get(provider);
343        if (stats == null) return;
344
345        stats.available = false;
346    }
347
348    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
349        StringBuilder s = new StringBuilder();
350        s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n');
351        s.append("fused=").append(mFusedLocation).append('\n');
352        s.append(String.format("gps %.3f %s\n", mGpsWeight, mGpsLocation));
353        s.append("    ").append(mStats.get(GPS)).append('\n');
354        s.append(String.format("net %.3f %s\n", mNetworkWeight, mNetworkLocation));
355        s.append("    ").append(mStats.get(NETWORK)).append('\n');
356        pw.append(s);
357    }
358}
359