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        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
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
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     */
90    public void deinit() {
91        mRequest = null;
92        disable();
93        Log.i(TAG, "engine stopped (" + mContext.getPackageName() + ")");
94    }
95
96    /** Called on mLooper thread */
97    public void enable() {
98        if (!mEnabled) {
99            mEnabled = true;
100            updateRequirements();
101        }
102    }
103
104    /** Called on mLooper thread */
105    public void disable() {
106        if (mEnabled) {
107            mEnabled = false;
108            updateRequirements();
109        }
110    }
111
112    /** Called on mLooper thread */
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
124        public String toString() {
125            return (requested ? " REQUESTED" : " ---");
126        }
127    }
128
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
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
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     */
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                            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
281    /** Called on mLooper thread */
282    @Override
283    public void onProviderDisabled(String provider) {  }
284
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 */
297    public void switchUser() {
298        // reset state to prevent location data leakage
299        mFusedLocation = null;
300        mGpsLocation = null;
301        mNetworkLocation = null;
302    }
303}
304