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.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        mCallback.reportLocation(mFusedLocation);
253    }
254
255    /** Called on mLooper thread */
256    @Override
257    public void onLocationChanged(Location location) {
258        if (GPS.equals(location.getProvider())) {
259            mGpsLocation = location;
260            updateFusedLocation();
261        } else if (NETWORK.equals(location.getProvider())) {
262            mNetworkLocation = location;
263            updateFusedLocation();
264        }
265    }
266
267    /** Called on mLooper thread */
268    @Override
269    public void onStatusChanged(String provider, int status, Bundle extras) {  }
270
271    /** Called on mLooper thread */
272    @Override
273    public void onProviderEnabled(String provider) {
274        ProviderStats stats = mStats.get(provider);
275        if (stats == null) return;
276
277        stats.available = true;
278    }
279
280    /** Called on mLooper thread */
281    @Override
282    public void onProviderDisabled(String provider) {
283        ProviderStats stats = mStats.get(provider);
284        if (stats == null) return;
285
286        stats.available = false;
287    }
288
289    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
290        StringBuilder s = new StringBuilder();
291        s.append("mEnabled=" + mEnabled).append(' ').append(mRequest).append('\n');
292        s.append("fused=").append(mFusedLocation).append('\n');
293        s.append(String.format("gps %s\n", mGpsLocation));
294        s.append("    ").append(mStats.get(GPS)).append('\n');
295        s.append(String.format("net %s\n", mNetworkLocation));
296        s.append("    ").append(mStats.get(NETWORK)).append('\n');
297        pw.append(s);
298    }
299}
300