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 if (!mEnabled) { 101 mEnabled = true; 102 updateRequirements(); 103 } 104 } 105 106 /** Called on mLooper thread */ 107 public void disable() { 108 if (mEnabled) { 109 mEnabled = false; 110 updateRequirements(); 111 } 112 } 113 114 /** Called on mLooper thread */ 115 public void setRequest(ProviderRequestUnbundled request, WorkSource source) { 116 mRequest = request; 117 mEnabled = request.getReportLocation(); 118 updateRequirements(); 119 } 120 121 private static class ProviderStats { 122 public boolean available; 123 public boolean requested; 124 public long requestTime; 125 public long minTime; 126 @Override 127 public String toString() { 128 StringBuilder s = new StringBuilder(); 129 s.append(available ? "AVAILABLE" : "UNAVAILABLE"); 130 s.append(requested ? " REQUESTED" : " ---"); 131 return s.toString(); 132 } 133 } 134 135 private void enableProvider(String name, long minTime) { 136 ProviderStats stats = mStats.get(name); 137 138 if (!stats.requested) { 139 stats.requestTime = SystemClock.elapsedRealtime(); 140 stats.requested = true; 141 stats.minTime = minTime; 142 mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); 143 } else if (stats.minTime != minTime) { 144 stats.minTime = minTime; 145 mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); 146 } 147 } 148 149 private void disableProvider(String name) { 150 ProviderStats stats = mStats.get(name); 151 152 if (stats.requested) { 153 stats.requested = false; 154 mLocationManager.removeUpdates(this); //TODO GLOBAL 155 } 156 } 157 158 private void updateRequirements() { 159 if (mEnabled == false || mRequest == null) { 160 mRequest = null; 161 disableProvider(NETWORK); 162 disableProvider(GPS); 163 return; 164 } 165 166 long networkInterval = Long.MAX_VALUE; 167 long gpsInterval = Long.MAX_VALUE; 168 for (LocationRequestUnbundled request : mRequest.getLocationRequests()) { 169 switch (request.getQuality()) { 170 case LocationRequestUnbundled.ACCURACY_FINE: 171 case LocationRequestUnbundled.POWER_HIGH: 172 if (request.getInterval() < gpsInterval) { 173 gpsInterval = request.getInterval(); 174 } 175 if (request.getInterval() < networkInterval) { 176 networkInterval = request.getInterval(); 177 } 178 break; 179 case LocationRequestUnbundled.ACCURACY_BLOCK: 180 case LocationRequestUnbundled.ACCURACY_CITY: 181 case LocationRequestUnbundled.POWER_LOW: 182 if (request.getInterval() < networkInterval) { 183 networkInterval = request.getInterval(); 184 } 185 break; 186 } 187 } 188 189 if (gpsInterval < Long.MAX_VALUE) { 190 enableProvider(GPS, gpsInterval); 191 } else { 192 disableProvider(GPS); 193 } 194 if (networkInterval < Long.MAX_VALUE) { 195 enableProvider(NETWORK, networkInterval); 196 } else { 197 disableProvider(NETWORK); 198 } 199 } 200 201 /** 202 * Test whether one location (a) is better to use than another (b). 203 */ 204 private static boolean isBetterThan(Location locationA, Location locationB) { 205 if (locationA == null) { 206 return false; 207 } 208 if (locationB == null) { 209 return true; 210 } 211 // A provider is better if the reading is sufficiently newer. Heading 212 // underground can cause GPS to stop reporting fixes. In this case it's 213 // appropriate to revert to cell, even when its accuracy is less. 214 if (locationA.getElapsedRealtimeNanos() > locationB.getElapsedRealtimeNanos() + SWITCH_ON_FRESHNESS_CLIFF_NS) { 215 return true; 216 } 217 218 // A provider is better if it has better accuracy. Assuming both readings 219 // are fresh (and by that accurate), choose the one with the smaller 220 // accuracy circle. 221 if (!locationA.hasAccuracy()) { 222 return false; 223 } 224 if (!locationB.hasAccuracy()) { 225 return true; 226 } 227 return locationA.getAccuracy() < locationB.getAccuracy(); 228 } 229 230 private void updateFusedLocation() { 231 // may the best location win! 232 if (isBetterThan(mGpsLocation, mNetworkLocation)) { 233 mFusedLocation = new Location(mGpsLocation); 234 } else { 235 mFusedLocation = new Location(mNetworkLocation); 236 } 237 mFusedLocation.setProvider(FUSED); 238 if (mNetworkLocation != null) { 239 // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation 240 Bundle srcExtras = mNetworkLocation.getExtras(); 241 if (srcExtras != null) { 242 Parcelable srcParcelable = 243 srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION); 244 if (srcParcelable instanceof Location) { 245 Bundle dstExtras = mFusedLocation.getExtras(); 246 if (dstExtras == null) { 247 dstExtras = new Bundle(); 248 mFusedLocation.setExtras(dstExtras); 249 } 250 dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION, 251 (Location) srcParcelable); 252 } 253 } 254 } 255 256 if (mCallback != null) { 257 mCallback.reportLocation(mFusedLocation); 258 } else { 259 Log.w(TAG, "Location updates received while fusion engine not started"); 260 } 261 } 262 263 /** Called on mLooper thread */ 264 @Override 265 public void onLocationChanged(Location location) { 266 if (GPS.equals(location.getProvider())) { 267 mGpsLocation = location; 268 updateFusedLocation(); 269 } else if (NETWORK.equals(location.getProvider())) { 270 mNetworkLocation = location; 271 updateFusedLocation(); 272 } 273 } 274 275 /** Called on mLooper thread */ 276 @Override 277 public void onStatusChanged(String provider, int status, Bundle extras) { } 278 279 /** Called on mLooper thread */ 280 @Override 281 public void onProviderEnabled(String provider) { 282 ProviderStats stats = mStats.get(provider); 283 if (stats == null) return; 284 285 stats.available = true; 286 } 287 288 /** Called on mLooper thread */ 289 @Override 290 public void onProviderDisabled(String provider) { 291 ProviderStats stats = mStats.get(provider); 292 if (stats == null) return; 293 294 stats.available = false; 295 } 296 297 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 298 StringBuilder s = new StringBuilder(); 299 s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n'); 300 s.append("fused=").append(mFusedLocation).append('\n'); 301 s.append(String.format("gps %s\n", mGpsLocation)); 302 s.append(" ").append(mStats.get(GPS)).append('\n'); 303 s.append(String.format("net %s\n", mNetworkLocation)); 304 s.append(" ").append(mStats.get(NETWORK)).append('\n'); 305 pw.append(s); 306 } 307 308 /** Called on mLooper thread */ 309 public void switchUser() { 310 // reset state to prevent location data leakage 311 mFusedLocation = null; 312 mGpsLocation = null; 313 mNetworkLocation = null; 314 } 315 } 316