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