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         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 
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.get(GPS).available = mLocationManager.isProviderEnabled(GPS);
76         mStats.put(NETWORK, new ProviderStats());
77         mStats.get(NETWORK).available = mLocationManager.isProviderEnabled(NETWORK);
78 
79     }
80 
init(Callback callback)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      */
deinit()92     public void deinit() {
93         mRequest = null;
94         disable();
95         Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")");
96     }
97 
98     /** Called on mLooper thread */
enable()99     public void enable() {
100         if (!mEnabled) {
101             mEnabled = true;
102             updateRequirements();
103         }
104     }
105 
106     /** Called on mLooper thread */
disable()107     public void disable() {
108         if (mEnabled) {
109             mEnabled = false;
110             updateRequirements();
111         }
112     }
113 
114     /** Called on mLooper thread */
setRequest(ProviderRequestUnbundled request, WorkSource source)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
toString()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 
enableProvider(String name, long minTime)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 
disableProvider(String name)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 
updateRequirements()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      */
isBetterThan(Location locationA, Location locationB)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 
updateFusedLocation()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
onLocationChanged(Location location)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
onStatusChanged(String provider, int status, Bundle extras)277     public void onStatusChanged(String provider, int status, Bundle extras) {  }
278 
279     /** Called on mLooper thread */
280     @Override
onProviderEnabled(String provider)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
onProviderDisabled(String provider)290     public void onProviderDisabled(String provider) {
291         ProviderStats stats = mStats.get(provider);
292         if (stats == null) return;
293 
294         stats.available = false;
295     }
296 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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 */
switchUser()309     public void switchUser() {
310         // reset state to prevent location data leakage
311         mFusedLocation = null;
312         mGpsLocation = null;
313         mNetworkLocation = null;
314     }
315 }
316