• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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