/* * Copyright (c) 2013, Psiphon Inc. * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package ca.psiphon.ploggy; import android.content.Context; import android.location.Address; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import ca.psiphon.ploggy.Utils.ApplicationError; /** * Schedule and monitor location events from Android OS. * * Implements best practices from: * - http://developer.android.com/guide/topics/location/strategies.html * - http://code.google.com/p/android-protips-location/ * * Does not use the newer, higher level Play Services location API as it's * not available on open source Android builds and its source is not available * to review (e.g., verify that location isn't sent to 3rd party). */ public class LocationMonitor implements android.location.LocationListener { private static final String LOG_TAG = "Location Monitor"; Engine mEngine; Handler mHandler; Runnable mStartLocationFixTask; Runnable mFinishLocationFixTask; Runnable mStopLocationUpdatesTask; Location mLastReportedLocation; Location mCurrentLocation; LocationMonitor(Engine engine) { // TODO: use Utils.FixedDelayExecutor mEngine = engine; mHandler = new Handler(); initRunnables(); } public void start() throws Utils.ApplicationError { // Using a Handler for LocationManager calls, which need to run on a Looper thread. StartLocationFixTask // kicks off location updates and schedules FinishLocationFixTask which reports the "best" location fix // and stops updates after a set time period. FinishLocationFixTask also schedules the next location fix. mHandler.post(mStartLocationFixTask); } public void stop() { mHandler.removeCallbacks(mStartLocationFixTask); mHandler.removeCallbacks(mFinishLocationFixTask); // Ensure removeUpdates is called // TODO: ok that posting this task makes stop() asynchronous? mHandler.post(mStopLocationUpdatesTask); } private void initRunnables() { final LocationMonitor finalLocationMonitor = this; mStartLocationFixTask = new Runnable() { @Override public void run() { try { LocationManager locationManager = (LocationManager)mEngine.getContext().getSystemService(Context.LOCATION_SERVICE); // Use last known location already present in all providers (they don't need to be enabled) for (String provider: locationManager.getAllProviders()) { updateCurrentLocation(locationManager.getLastKnownLocation(provider)); } if (locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)) { locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 1000, 1, finalLocationMonitor); } if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, finalLocationMonitor); } // TODO: previously had a preference to allow use of Network location provider, since this provider // sends data to a 3rd party. But is the provider always sending this data? I.e., is there any privacy // benefit to not using it if it's available? if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 1, finalLocationMonitor); } mHandler.postDelayed( mFinishLocationFixTask, 1000*mEngine.getIntPreference(R.string.preferenceLocationFixPeriodInSeconds)); } catch (Utils.ApplicationError e) { Log.addEntry(LOG_TAG, "start location fix failed"); } } }; mFinishLocationFixTask = new Runnable() { @Override public void run() { try { LocationManager locationManager = (LocationManager)mEngine.getContext().getSystemService(Context.LOCATION_SERVICE); locationManager.removeUpdates(finalLocationMonitor); reportLocation(); // TODO: simulate scheduleAtFixedrate by adjusting next fix delay to account for elapsed fix time period mHandler.postDelayed( mStartLocationFixTask, 60*1000*mEngine.getIntPreference(R.string.preferenceLocationFixFrequencyInMinutes)); } catch (Utils.ApplicationError e) { Log.addEntry(LOG_TAG, "finish location fix failed"); } } }; mStopLocationUpdatesTask = new Runnable() { @Override public void run() { LocationManager locationManager = (LocationManager)mEngine.getContext().getSystemService(Context.LOCATION_SERVICE); locationManager.removeUpdates(finalLocationMonitor); } }; } public void reportLocation() throws Utils.ApplicationError { if (mCurrentLocation == null) { return; } mLastReportedLocation = mCurrentLocation; if (mEngine.getBooleanPreference(R.string.preferenceUseGeoCoder)) { // Run a background task to map and reverse geocode the location Runnable task = new Runnable() { @Override public void run() { int torSocksProxyPort; try { torSocksProxyPort = mEngine.getTorSocksProxyPort(); } catch (ApplicationError e) { Log.addEntry(LOG_TAG, "failed to get Tor SOCKS port: " + e.getMessage()); Events.post(new Events.NewSelfLocation(mLastReportedLocation, null)); return; } Address address = Nominatim.getFromLocation( torSocksProxyPort, mLastReportedLocation.getLatitude(), mLastReportedLocation.getLongitude()); // TODO: get map Events.post(new Events.NewSelfLocation(mLastReportedLocation, address)); } }; mEngine.submitTask(task); } else { Events.post(new Events.NewSelfLocation(mLastReportedLocation, null)); } } @Override public void onLocationChanged(Location location) { updateCurrentLocation(location); } @Override public void onProviderDisabled(String provider) { restart(); } @Override public void onProviderEnabled(String provider) { restart(); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { restart(); } private void restart() { try { stop(); start(); } catch (Utils.ApplicationError e) { Log.addEntry(LOG_TAG, "failed to restart"); } } private void updateCurrentLocation(Location location) { if (location != null) { if (isBetterLocation(location, mCurrentLocation)) { mCurrentLocation = location; } } } // From: http://developer.android.com/guide/topics/location/strategies.html private boolean isBetterLocation(Location location, Location currentBestLocation) { final int TWO_MINUTES = 1000 * 60 * 2; if (currentBestLocation == null) { // A new location is always better than no location return true; } // Check whether the new location fix is newer or older long timeDelta = location.getTime() - currentBestLocation.getTime(); boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; boolean isNewer = timeDelta > 0; // If it's been more than two minutes since the current location, use the new location // because the user has likely moved if (isSignificantlyNewer) { return true; // If the new location is more than two minutes older, it must be worse } else if (isSignificantlyOlder) { return false; } // Check whether the new location fix is more or less accurate int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); boolean isLessAccurate = accuracyDelta > 0; boolean isMoreAccurate = accuracyDelta < 0; boolean isSignificantlyLessAccurate = accuracyDelta > 200; // Check if the old and new location are from the same provider boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider()); // Determine location quality using a combination of timeliness and accuracy if (isMoreAccurate) { return true; } else if (isNewer && !isLessAccurate) { return true; } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { return true; } return false; } private boolean isSameProvider(String provider1, String provider2) { if (provider1 == null) { return provider2 == null; } return provider1.equals(provider2); } }