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 17 package com.android.location.fused; 18 19 import java.io.FileDescriptor; 20 import java.io.PrintWriter; 21 import java.util.HashMap; 22 23 import com.android.location.provider.LocationProviderBase; 24 import com.android.location.provider.LocationRequestUnbundled; 25 import com.android.location.provider.ProviderRequestUnbundled; 26 27 import android.content.Context; 28 import android.location.Location; 29 import android.location.LocationListener; 30 import android.location.LocationManager; 31 import android.os.Bundle; 32 import android.os.Looper; 33 import android.os.Parcelable; 34 import android.os.SystemClock; 35 import android.os.WorkSource; 36 import android.util.Log; 37 38 public class FusionEngine implements LocationListener { 39 public interface Callback { 40 public void reportLocation(Location location); 41 } 42 43 private static final String TAG = "FusedLocation"; 44 private static final String NETWORK = LocationManager.NETWORK_PROVIDER; 45 private static final String GPS = LocationManager.GPS_PROVIDER; 46 private static final String FUSED = LocationProviderBase.FUSED_PROVIDER; 47 48 public static final long SWITCH_ON_FRESHNESS_CLIFF_NS = 11 * 1000000000; // 11 seconds 49 50 private final Context mContext; 51 private final LocationManager mLocationManager; 52 private final Looper mLooper; 53 54 // all fields are only used on mLooper thread. except for in dump() which is not thread-safe 55 private Callback mCallback; 56 private Location mFusedLocation; 57 private Location mGpsLocation; 58 private Location mNetworkLocation; 59 60 private boolean mEnabled; 61 private ProviderRequestUnbundled mRequest; 62 63 private final HashMap<String, ProviderStats> mStats = new HashMap<String, ProviderStats>(); 64 65 public FusionEngine(Context context, Looper looper) { 66 mContext = context; 67 mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 68 mNetworkLocation = new Location(""); 69 mNetworkLocation.setAccuracy(Float.MAX_VALUE); 70 mGpsLocation = new Location(""); 71 mGpsLocation.setAccuracy(Float.MAX_VALUE); 72 mLooper = looper; 73 74 mStats.put(GPS, new ProviderStats()); 75 mStats.get(GPS).available = mLocationManager.isProviderEnabled(GPS); 76 mStats.put(NETWORK, new ProviderStats()); 77 mStats.get(NETWORK).available = mLocationManager.isProviderEnabled(NETWORK); 78 79 } 80 81 public void init(Callback callback) { 82 Log.i(TAG, "engine started (" + mContext.getPackageName() + ")"); 83 mCallback = callback; 84 } 85 86 /** 87 * Called to stop doing any work, and release all resources 88 * This can happen when a better fusion engine is installed 89 * in a different package, and this one is no longer needed. 90 * Called on mLooper thread 91 */ 92 public void deinit() { 93 mRequest = null; 94 disable(); 95 Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")"); 96 } 97 98 /** Called on mLooper thread */ 99 public void enable() { 100 mEnabled = true; 101 updateRequirements(); 102 } 103 104 /** Called on mLooper thread */ 105 public void disable() { 106 mEnabled = false; 107 updateRequirements(); 108 } 109 110 /** Called on mLooper thread */ 111 public void setRequest(ProviderRequestUnbundled request, WorkSource source) { 112 mRequest = request; 113 mEnabled = request.getReportLocation(); 114 updateRequirements(); 115 } 116 117 private static class ProviderStats { 118 public boolean available; 119 public boolean requested; 120 public long requestTime; 121 public long minTime; 122 @Override 123 public String toString() { 124 StringBuilder s = new StringBuilder(); 125 s.append(available ? "AVAILABLE" : "UNAVAILABLE"); 126 s.append(requested ? " REQUESTED" : " ---"); 127 return s.toString(); 128 } 129 } 130 131 private void enableProvider(String name, long minTime) { 132 ProviderStats stats = mStats.get(name); 133 134 if (stats.available) { 135 if (!stats.requested) { 136 stats.requestTime = SystemClock.elapsedRealtime(); 137 stats.requested = true; 138 stats.minTime = minTime; 139 mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); 140 } else if (stats.minTime != minTime) { 141 stats.minTime = minTime; 142 mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); 143 } 144 } 145 } 146 147 private void disableProvider(String name) { 148 ProviderStats stats = mStats.get(name); 149 150 if (stats.requested) { 151 stats.requested = false; 152 mLocationManager.removeUpdates(this); //TODO GLOBAL 153 } 154 } 155 156 private void updateRequirements() { 157 if (mEnabled == false || mRequest == null) { 158 mRequest = null; 159 disableProvider(NETWORK); 160 disableProvider(GPS); 161 return; 162 } 163 164 long networkInterval = Long.MAX_VALUE; 165 long gpsInterval = Long.MAX_VALUE; 166 for (LocationRequestUnbundled request : mRequest.getLocationRequests()) { 167 switch (request.getQuality()) { 168 case LocationRequestUnbundled.ACCURACY_FINE: 169 case LocationRequestUnbundled.POWER_HIGH: 170 if (request.getInterval() < gpsInterval) { 171 gpsInterval = request.getInterval(); 172 } 173 if (request.getInterval() < networkInterval) { 174 networkInterval = request.getInterval(); 175 } 176 break; 177 case LocationRequestUnbundled.ACCURACY_BLOCK: 178 case LocationRequestUnbundled.ACCURACY_CITY: 179 case LocationRequestUnbundled.POWER_LOW: 180 if (request.getInterval() < networkInterval) { 181 networkInterval = request.getInterval(); 182 } 183 break; 184 } 185 } 186 187 if (gpsInterval < Long.MAX_VALUE) { 188 enableProvider(GPS, gpsInterval); 189 } else { 190 disableProvider(GPS); 191 } 192 if (networkInterval < Long.MAX_VALUE) { 193 enableProvider(NETWORK, networkInterval); 194 } else { 195 disableProvider(NETWORK); 196 } 197 } 198 199 /** 200 * Test whether one location (a) is better to use than another (b). 201 */ 202 private static boolean isBetterThan(Location locationA, Location locationB) { 203 if (locationA == null) { 204 return false; 205 } 206 if (locationB == null) { 207 return true; 208 } 209 // A provider is better if the reading is sufficiently newer. Heading 210 // underground can cause GPS to stop reporting fixes. In this case it's 211 // appropriate to revert to cell, even when its accuracy is less. 212 if (locationA.getElapsedRealtimeNanos() > locationB.getElapsedRealtimeNanos() + SWITCH_ON_FRESHNESS_CLIFF_NS) { 213 return true; 214 } 215 216 // A provider is better if it has better accuracy. Assuming both readings 217 // are fresh (and by that accurate), choose the one with the smaller 218 // accuracy circle. 219 if (!locationA.hasAccuracy()) { 220 return false; 221 } 222 if (!locationB.hasAccuracy()) { 223 return true; 224 } 225 return locationA.getAccuracy() < locationB.getAccuracy(); 226 } 227 228 private void updateFusedLocation() { 229 // may the best location win! 230 if (isBetterThan(mGpsLocation, mNetworkLocation)) { 231 mFusedLocation = new Location(mGpsLocation); 232 } else { 233 mFusedLocation = new Location(mNetworkLocation); 234 } 235 mFusedLocation.setProvider(FUSED); 236 if (mNetworkLocation != null) { 237 // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation 238 Bundle srcExtras = mNetworkLocation.getExtras(); 239 if (srcExtras != null) { 240 Parcelable srcParcelable = 241 srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION); 242 if (srcParcelable instanceof Location) { 243 Bundle dstExtras = mFusedLocation.getExtras(); 244 if (dstExtras == null) { 245 dstExtras = new Bundle(); 246 mFusedLocation.setExtras(dstExtras); 247 } 248 dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION, 249 (Location) srcParcelable); 250 } 251 } 252 } 253 254 if (mCallback != null) { 255 mCallback.reportLocation(mFusedLocation); 256 } else { 257 Log.w(TAG, "Location updates received while fusion engine not started"); 258 } 259 } 260 261 /** Called on mLooper thread */ 262 @Override 263 public void onLocationChanged(Location location) { 264 if (GPS.equals(location.getProvider())) { 265 mGpsLocation = location; 266 updateFusedLocation(); 267 } else if (NETWORK.equals(location.getProvider())) { 268 mNetworkLocation = location; 269 updateFusedLocation(); 270 } 271 } 272 273 /** Called on mLooper thread */ 274 @Override 275 public void onStatusChanged(String provider, int status, Bundle extras) { } 276 277 /** Called on mLooper thread */ 278 @Override 279 public void onProviderEnabled(String provider) { 280 ProviderStats stats = mStats.get(provider); 281 if (stats == null) return; 282 283 stats.available = true; 284 } 285 286 /** Called on mLooper thread */ 287 @Override 288 public void onProviderDisabled(String provider) { 289 ProviderStats stats = mStats.get(provider); 290 if (stats == null) return; 291 292 stats.available = false; 293 } 294 295 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 296 StringBuilder s = new StringBuilder(); 297 s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n'); 298 s.append("fused=").append(mFusedLocation).append('\n'); 299 s.append(String.format("gps %s\n", mGpsLocation)); 300 s.append(" ").append(mStats.get(GPS)).append('\n'); 301 s.append(String.format("net %s\n", mNetworkLocation)); 302 s.append(" ").append(mStats.get(NETWORK)).append('\n'); 303 pw.append(s); 304 } 305 306 /** Called on mLooper thread */ 307 public void switchUser() { 308 // reset state to prevent location data leakage 309 mFusedLocation = null; 310 mGpsLocation = null; 311 mNetworkLocation = null; 312 } 313 } 314