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