1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.mozstumbler.service.stumblerthread.scanners;
6 
7 import android.annotation.SuppressLint;
8 import android.content.Context;
9 import android.content.Intent;
10 import android.location.GpsSatellite;
11 import android.location.GpsStatus;
12 import android.location.Location;
13 import android.location.LocationListener;
14 import android.location.LocationManager;
15 import android.location.LocationProvider;
16 import android.os.Bundle;
17 import android.support.v4.content.LocalBroadcastManager;
18 import android.util.Log;
19 import org.mozilla.mozstumbler.service.AppGlobals;
20 import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
21 import org.mozilla.mozstumbler.service.Prefs;
22 import org.mozilla.mozstumbler.service.utils.TelemetryWrapper;
23 
24 import java.text.SimpleDateFormat;
25 import java.util.Date;
26 
27 public class GPSScanner implements LocationListener {
28     public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".GPSScanner.";
29     public static final String ACTION_GPS_UPDATED = ACTION_BASE + "GPS_UPDATED";
30     public static final String ACTION_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
31     public static final String SUBJECT_NEW_STATUS = "new_status";
32     public static final String SUBJECT_LOCATION_LOST = "location_lost";
33     public static final String SUBJECT_NEW_LOCATION = "new_location";
34     public static final String NEW_STATUS_ARG_FIXES = "fixes";
35     public static final String NEW_STATUS_ARG_SATS = "sats";
36     public static final String NEW_LOCATION_ARG_LOCATION = "location";
37 
38     private static final String LOG_TAG = AppGlobals.makeLogTag(GPSScanner.class.getSimpleName());
39     private static final int MIN_SAT_USED_IN_FIX = 3;
40     private static final long ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS = 1000;
41     private static final float ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M = 10;
42     private static final long PASSIVE_GPS_MIN_UPDATE_FREQ_MS = 3000;
43     private static final float PASSIVE_GPS_MOVEMENT_MIN_DELTA_M = 30;
44 
45     private final LocationBlockList mBlockList = new LocationBlockList();
46     private final Context mContext;
47     private GpsStatus.Listener mGPSListener;
48     private int mLocationCount;
49     private Location mLocation = new Location("internal");
50     private boolean mAutoGeofencing;
51     private boolean mIsPassiveMode;
52     private long mTelemetry_lastStartedMs;
53     private final ScanManager mScanManager;
54 
GPSScanner(Context context, ScanManager scanManager)55     public GPSScanner(Context context, ScanManager scanManager) {
56         mContext = context;
57         mScanManager = scanManager;
58     }
59 
start(final ActiveOrPassiveStumbling stumblingMode)60     public void start(final ActiveOrPassiveStumbling stumblingMode) {
61         mIsPassiveMode = (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
62         if (mIsPassiveMode ) {
63             startPassiveMode();
64         } else {
65             startActiveMode();
66         }
67     }
68 
isGpsAvailable(LocationManager locationManager)69     private boolean isGpsAvailable(LocationManager locationManager) {
70         if (locationManager == null ||
71             locationManager.getProvider(LocationManager.GPS_PROVIDER) == null) {
72             String msg = "No GPS available, scanning not started.";
73             Log.d(LOG_TAG, msg);
74             AppGlobals.guiLogError(msg);
75             return false;
76         }
77         return true;
78     }
79 
80     @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
startPassiveMode()81     private void startPassiveMode() {
82         LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
83         if (!isGpsAvailable(locationManager)) {
84             return;
85         }
86 
87         locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
88 
89         final int timeDiffSec = Long.valueOf((System.currentTimeMillis() - mTelemetry_lastStartedMs) / 1000).intValue();
90         if (mTelemetry_lastStartedMs > 0 && timeDiffSec > 0) {
91             TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_STARTS_SEC, timeDiffSec);
92         }
93         mTelemetry_lastStartedMs = System.currentTimeMillis();
94     }
95 
96     @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
startActiveMode()97     private void startActiveMode() {
98         LocationManager lm = getLocationManager();
99         if (!isGpsAvailable(lm)) {
100             return;
101         }
102 
103         lm.requestLocationUpdates(LocationManager.GPS_PROVIDER,
104                                   ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS,
105                                   ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M,
106                                   this);
107 
108         reportLocationLost();
109         mGPSListener = new GpsStatus.Listener() {
110                 @Override
111                 public void onGpsStatusChanged(int event) {
112                 if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
113                     GpsStatus status = getLocationManager().getGpsStatus(null);
114                     Iterable<GpsSatellite> sats = status.getSatellites();
115 
116                     int satellites = 0;
117                     int fixes = 0;
118 
119                     for (GpsSatellite sat : sats) {
120                         satellites++;
121                         if (sat.usedInFix()) {
122                             fixes++;
123                         }
124                     }
125                     reportNewGpsStatus(fixes, satellites);
126                     if (fixes < MIN_SAT_USED_IN_FIX) {
127                         reportLocationLost();
128                     }
129 
130                     if (AppGlobals.isDebug) {
131                         Log.v(LOG_TAG, "onGpsStatusChange - satellites: " + satellites + " fixes: " + fixes);
132                     }
133                 } else if (event == GpsStatus.GPS_EVENT_STOPPED) {
134                     reportLocationLost();
135                 }
136             }
137         };
138 
139         lm.addGpsStatusListener(mGPSListener);
140     }
141 
142     @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
stop()143     public void stop() {
144         LocationManager lm = getLocationManager();
145         lm.removeUpdates(this);
146         reportLocationLost();
147 
148         if (mGPSListener != null) {
149           lm.removeGpsStatusListener(mGPSListener);
150           mGPSListener = null;
151         }
152     }
153 
getLocationCount()154     public int getLocationCount() {
155         return mLocationCount;
156     }
157 
getLatitude()158     public double getLatitude() {
159         return mLocation.getLatitude();
160     }
161 
getLongitude()162     public double getLongitude() {
163         return mLocation.getLongitude();
164     }
165 
getLocation()166     public Location getLocation() {
167         return mLocation;
168     }
169 
checkPrefs()170     public void checkPrefs() {
171         if (mBlockList != null) {
172             mBlockList.updateBlocks();
173         }
174 
175         Prefs prefs = Prefs.getInstanceWithoutContext();
176         if (prefs == null) {
177             return;
178         }
179         mAutoGeofencing = prefs.getGeofenceHere();
180     }
181 
isGeofenced()182     public boolean isGeofenced() {
183         return (mBlockList != null) && mBlockList.isGeofenced();
184     }
185 
sendToLogActivity(String msg)186     private void sendToLogActivity(String msg) {
187         AppGlobals.guiLogInfo(msg, "#33ccff", false);
188     }
189 
190     @Override
onLocationChanged(Location location)191     public void onLocationChanged(Location location) {
192         if (location == null) { // TODO: is this even possible??
193             reportLocationLost();
194             return;
195         }
196 
197         String logMsg = (mIsPassiveMode)? "[Passive] " : "[Active] ";
198 
199         String provider = location.getProvider();
200         if (!provider.toLowerCase().contains("gps")) {
201             Log.d(LOG_TAG, "Discard fused/network location.");
202             // only interested in GPS locations
203             return;
204         }
205 
206         final long timeDeltaMs = location.getTime() - mLocation.getTime();
207 
208         // Seem to get greater likelihood of non-fused location with higher update freq.
209         // Check dist and time threshold here, not set on the listener.
210         if (mIsPassiveMode) {
211             final boolean hasMoved = location.distanceTo(mLocation) > PASSIVE_GPS_MOVEMENT_MIN_DELTA_M;
212 
213             if (timeDeltaMs < PASSIVE_GPS_MIN_UPDATE_FREQ_MS || !hasMoved) {
214                 return;
215             }
216         }
217 
218         Date date = new Date(location.getTime());
219         SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
220         String time = formatter.format(date);
221         logMsg += String.format("%s Coord: %.4f,%.4f, Acc: %.0f, Speed: %.0f, Alt: %.0f, Bearing: %.1f", time, location.getLatitude(),
222                 location.getLongitude(), location.getAccuracy(), location.getSpeed(), location.getAltitude(), location.getBearing());
223         sendToLogActivity(logMsg);
224 
225         if (mBlockList.contains(location)) {
226             reportLocationLost();
227             return;
228         }
229 
230         mLocation = location;
231 
232         if (!mAutoGeofencing) {
233             reportNewLocationReceived(location);
234         }
235         mLocationCount++;
236 
237         if (mIsPassiveMode) {
238             mScanManager.newPassiveGpsLocation();
239         }
240 
241         if (timeDeltaMs > 0) {
242             TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC,
243                     Long.valueOf(timeDeltaMs).intValue() / 1000);
244         }
245     }
246 
247     @Override
onProviderDisabled(String provider)248     public void onProviderDisabled(String provider) {
249         if (LocationManager.GPS_PROVIDER.equals(provider)) {
250             reportLocationLost();
251         }
252     }
253 
254     @Override
onProviderEnabled(String provider)255     public void onProviderEnabled(String provider) {
256     }
257 
258     @Override
onStatusChanged(String provider, int status, Bundle extras)259     public void onStatusChanged(String provider, int status, Bundle extras) {
260         if ((status != LocationProvider.AVAILABLE) &&
261             (LocationManager.GPS_PROVIDER.equals(provider))) {
262             reportLocationLost();
263         }
264     }
265 
getLocationManager()266     private LocationManager getLocationManager() {
267         return (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
268     }
269 
reportNewLocationReceived(Location location)270     private void reportNewLocationReceived(Location location) {
271         Intent i = new Intent(ACTION_GPS_UPDATED);
272         i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_NEW_LOCATION);
273         i.putExtra(NEW_LOCATION_ARG_LOCATION, location);
274         i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
275         LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
276     }
277 
reportLocationLost()278     private void reportLocationLost() {
279         Intent i = new Intent(ACTION_GPS_UPDATED);
280         i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_LOCATION_LOST);
281         i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
282         LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
283     }
284 
reportNewGpsStatus(int fixes, int sats)285     private void reportNewGpsStatus(int fixes, int sats) {
286         Intent i = new Intent(ACTION_GPS_UPDATED);
287         i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_NEW_STATUS);
288         i.putExtra(NEW_STATUS_ARG_FIXES, fixes);
289         i.putExtra(NEW_STATUS_ARG_SATS, sats);
290         i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
291         LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
292     }
293 }
294