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
17package com.android.location.fused;
18
19import java.io.FileDescriptor;
20import java.io.PrintWriter;
21import java.util.HashMap;
22
23import com.android.location.provider.LocationProviderBase;
24import com.android.location.provider.LocationRequestUnbundled;
25import com.android.location.provider.ProviderRequestUnbundled;
26
27import android.content.Context;
28import android.location.Location;
29import android.location.LocationListener;
30import android.location.LocationManager;
31import android.os.Bundle;
32import android.os.Looper;
33import android.os.Parcelable;
34import android.os.SystemClock;
35import android.os.WorkSource;
36import android.util.Log;
37
38public 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.available) {
135            if (!stats.requested) {
136                stats.requestTime = SystemClock.elapsedRealtime();
137                stats.requested = true;
138                stats.minTime = minTime;
139                mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
140            } else if (stats.minTime != minTime) {
141                stats.minTime = minTime;
142                mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
143            }
144        }
145    }
146
147    private void disableProvider(String name) {
148        ProviderStats stats = mStats.get(name);
149
150        if (stats.requested) {
151            stats.requested = false;
152            mLocationManager.removeUpdates(this);  //TODO GLOBAL
153        }
154    }
155
156    private void updateRequirements() {
157        if (mEnabled == false || 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     */
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
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                            (Location) 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
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
275    public void onStatusChanged(String provider, int status, Bundle extras) {  }
276
277    /** Called on mLooper thread */
278    @Override
279    public void onProviderEnabled(String provider) {
280        ProviderStats stats = mStats.get(provider);
281        if (stats == null) return;
282
283        stats.available = true;
284    }
285
286    /** Called on mLooper thread */
287    @Override
288    public void onProviderDisabled(String provider) {
289        ProviderStats stats = mStats.get(provider);
290        if (stats == null) return;
291
292        stats.available = false;
293    }
294
295    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
296        StringBuilder s = new StringBuilder();
297        s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n');
298        s.append("fused=").append(mFusedLocation).append('\n');
299        s.append(String.format("gps %s\n", mGpsLocation));
300        s.append("    ").append(mStats.get(GPS)).append('\n');
301        s.append(String.format("net %s\n", mNetworkLocation));
302        s.append("    ").append(mStats.get(NETWORK)).append('\n');
303        pw.append(s);
304    }
305
306    /** Called on mLooper thread */
307    public void switchUser() {
308        // reset state to prevent location data leakage
309        mFusedLocation = null;
310        mGpsLocation = null;
311        mNetworkLocation = null;
312    }
313}
314