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