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 { reportLocation(Location location)40 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 * 1000000000L; // 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<>(); 64 FusionEngine(Context context, Looper looper)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.put(NETWORK, new ProviderStats()); 76 77 } 78 init(Callback callback)79 public void init(Callback callback) { 80 Log.i(TAG, "engine started (" + mContext.getPackageName() + ")"); 81 mCallback = callback; 82 } 83 84 /** 85 * Called to stop doing any work, and release all resources 86 * This can happen when a better fusion engine is installed 87 * in a different package, and this one is no longer needed. 88 * Called on mLooper thread 89 */ deinit()90 public void deinit() { 91 mRequest = null; 92 disable(); 93 Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")"); 94 } 95 96 /** Called on mLooper thread */ enable()97 public void enable() { 98 if (!mEnabled) { 99 mEnabled = true; 100 updateRequirements(); 101 } 102 } 103 104 /** Called on mLooper thread */ disable()105 public void disable() { 106 if (mEnabled) { 107 mEnabled = false; 108 updateRequirements(); 109 } 110 } 111 112 /** Called on mLooper thread */ setRequest(ProviderRequestUnbundled request, WorkSource source)113 public void setRequest(ProviderRequestUnbundled request, WorkSource source) { 114 mRequest = request; 115 mEnabled = request.getReportLocation(); 116 updateRequirements(); 117 } 118 119 private static class ProviderStats { 120 public boolean requested; 121 public long requestTime; 122 public long minTime; 123 @Override toString()124 public String toString() { 125 return (requested ? " REQUESTED" : " ---"); 126 } 127 } 128 enableProvider(String name, long minTime)129 private void enableProvider(String name, long minTime) { 130 ProviderStats stats = mStats.get(name); 131 if (stats == null) return; 132 133 if (mLocationManager.isProviderEnabled(name)) { 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 disableProvider(String name)146 private void disableProvider(String name) { 147 ProviderStats stats = mStats.get(name); 148 if (stats == null) return; 149 150 if (stats.requested) { 151 stats.requested = false; 152 mLocationManager.removeUpdates(this); //TODO GLOBAL 153 } 154 } 155 updateRequirements()156 private void updateRequirements() { 157 if (!mEnabled || 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 */ isBetterThan(Location locationA, Location locationB)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 updateFusedLocation()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 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 onLocationChanged(Location location)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 onStatusChanged(String provider, int status, Bundle extras)275 public void onStatusChanged(String provider, int status, Bundle extras) { } 276 277 /** Called on mLooper thread */ 278 @Override onProviderEnabled(String provider)279 public void onProviderEnabled(String provider) { } 280 281 /** Called on mLooper thread */ 282 @Override onProviderDisabled(String provider)283 public void onProviderDisabled(String provider) { } 284 dump(FileDescriptor fd, PrintWriter pw, String[] args)285 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 286 StringBuilder s = new StringBuilder(); 287 s.append("mEnabled=").append(mEnabled).append(' ').append(mRequest).append('\n'); 288 s.append("fused=").append(mFusedLocation).append('\n'); 289 s.append(String.format("gps %s\n", mGpsLocation)); 290 s.append(" ").append(mStats.get(GPS)).append('\n'); 291 s.append(String.format("net %s\n", mNetworkLocation)); 292 s.append(" ").append(mStats.get(NETWORK)).append('\n'); 293 pw.append(s); 294 } 295 296 /** Called on mLooper thread */ switchUser()297 public void switchUser() { 298 // reset state to prevent location data leakage 299 mFusedLocation = null; 300 mGpsLocation = null; 301 mNetworkLocation = null; 302 } 303 } 304