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