Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to get location on non GMS devices #47

Open
jasonfish568 opened this issue Jan 23, 2022 · 4 comments
Open

Unable to get location on non GMS devices #47

jasonfish568 opened this issue Jan 23, 2022 · 4 comments

Comments

@jasonfish568
Copy link

Describe the bug
It is unable to get locations on non-GMS devices.

To Reproduce
Steps to reproduce the behavior:

  1. Install the app on a emulator or device with no GMS
  2. Monitor the on location event

Expected behavior
Locations should be available on all devices with GPS.

Smartphone (please complete the following information):

  • Device: Zebra TC25
  • OS: Android 8.1
  • Browser: ionic

So I basically moved over from cordova mauron85's plugin. I migrated the entire project from cordova to capacitor with a new generate ionic app. I also implemented the capactior Geolocation plugin. The Geolocation plugin clearly logged that without GMS the plugin cannot work.

I was just wondering is it possible to use the plugin without GMS services? I noticed that transistorsoft's plugin also requires GMS. Has things changed that location plugin needs to utilise GMS API to fetch locations?

As this plugin is fairly light weight, would it be possible to add support for devices without GMS?

In addition, would you like to open a donation channel so people use this plugin can at least financially help maintaining this repo?

@jasonfish568
Copy link
Author

Reading the code of mauron85's plugin, found out his plugin is using:

import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;

Where onLocation implements the LocationListener.

@jasonfish568
Copy link
Author

Just figured out that this plugin is mainly based on GMS APIs.

I tried to added in location service from Android APIs, it worked but I believe there is a much better way implementing this. Please see the below code of BackgroundGeolocationService.java. I referenced the code from mauron's plugin which include marianhello's and tenforwardconsulting's code.

I'm pretty bad with frontend so please don't judge what I am doing. Just an idea on how to get location without GMS services. Code not solid enough for a PR, if author is keen to include this feature, I will be waiting.

package com.equimaps.capacitor_background_geolocation;

import android.app.Notification;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;

import com.getcapacitor.Logger;
import com.getcapacitor.android.BuildConfig;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationAvailability;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;

import java.util.HashSet;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;

// A bound and started service that is promoted to a foreground service when
// location updates have been requested and the main activity is stopped.
//
// When an activity is bound to this service, frequent location updates are
// permitted. When the activity is removed from the foreground, the service
// promotes itself to a foreground service, and location updates continue. When
// the activity comes back to the foreground, the foreground service stops, and
// the notification associated with that service is removed.
public class BackgroundGeolocationService extends Service {
    static final String ACTION_BROADCAST = (
            BackgroundGeolocationService.class.getPackage().getName() + ".broadcast"
    );
    private final IBinder binder = new LocalBinder();

    // Must be unique for this application.
    private static final int NOTIFICATION_ID = 28351;

    private LocationManager locationManager;
    private Criteria criteria;
    private boolean isStarted = false;
    private LocationListener mLocationListener;

    @Override
    public void onCreate() {
      super.onCreate();

      locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

      // Location criteria
      criteria = new Criteria();
      criteria.setAltitudeRequired(false);
      criteria.setBearingRequired(false);
      criteria.setSpeedRequired(true);
      criteria.setCostAllowed(true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    private class Watcher {
        public String id;
        public FusedLocationProviderClient client;
        public LocationRequest locationRequest;
        public LocationCallback locationCallback;
        public Notification backgroundNotification;
    }
    private HashSet<Watcher> watchers = new HashSet<Watcher>();

    Notification getNotification() {
        for (Watcher watcher : watchers) {
            if (watcher.backgroundNotification != null) {
                return watcher.backgroundNotification;
            }
        }
        return null;
    }
    // Handles requests from the activity.
    public class LocalBinder extends Binder {
        void addWatcher(
                final String id,
                Notification backgroundNotification,
                float distanceFilter
        ) {
            int gmsResultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getApplicationContext());
            mLocationListener = new LocationListener() {
              @Override
              public void onLocationChanged(final Location location) {
                Logger.debug("Location received");
                Intent intent = new Intent(ACTION_BROADCAST);
                intent.putExtra("location", location);
                intent.putExtra("id", id);
                LocalBroadcastManager.getInstance(
                  getApplicationContext()
                ).sendBroadcast(intent);
              }
              @Override
              public void onProviderDisabled(String provider) {}
              @Override
              public void onProviderEnabled(String provider) {}
              @Override
              public void onStatusChanged(String provider, int status, Bundle extras) {}
            };

            if (gmsResultCode == ConnectionResult.SUCCESS) {
              FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(
                BackgroundGeolocationService.this
              );
              LocationRequest locationRequest = new LocationRequest();
              locationRequest.setMaxWaitTime(1000);
              locationRequest.setInterval(1000);
              locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
              locationRequest.setSmallestDisplacement(distanceFilter);

              LocationCallback callback = new LocationCallback(){
                @Override
                public void onLocationResult(LocationResult locationResult) {
                  Location location = locationResult.getLastLocation();
                  Logger.debug(location.toString());
                  Intent intent = new Intent(ACTION_BROADCAST);
                  intent.putExtra("location", location);
                  intent.putExtra("id", id);
                  LocalBroadcastManager.getInstance(
                    getApplicationContext()
                  ).sendBroadcast(intent);
                }
                @Override
                public void onLocationAvailability(LocationAvailability availability) {
                  if (!availability.isLocationAvailable() && BuildConfig.DEBUG) {
                    Logger.debug("Location not available");
                  }
                }
              };
              Watcher watcher = new Watcher();
              watcher.id = id;
              watcher.client = client;
              watcher.locationRequest = locationRequest;
              watcher.locationCallback = callback;
              watcher.backgroundNotification = backgroundNotification;
              watchers.add(watcher);

              watcher.client.requestLocationUpdates(
                watcher.locationRequest,
                watcher.locationCallback,
                null
              );
            } else {
              Logger.debug("Google Play Services not available, using Android location APIs");
              criteria.setAccuracy(Criteria.ACCURACY_FINE);
              criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
              criteria.setPowerRequirement(Criteria.POWER_HIGH);
              locationManager.requestLocationUpdates(locationManager.getBestProvider(criteria, true), 1000, distanceFilter, mLocationListener);
            }
        }

        void removeWatcher(String id) {
            int gmsResultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getApplicationContext());
            if(gmsResultCode == ConnectionResult.SUCCESS){
              for (Watcher watcher : watchers) {
                if (watcher.id.equals(id)) {
                  watcher.client.removeLocationUpdates(watcher.locationCallback);
                  watchers.remove(watcher);
                  if (getNotification() == null) {
                    stopForeground(true);
                  }
                  return;
                }
              }
            } else {
              Logger.debug("Location Listener removed");
              locationManager.removeUpdates(mLocationListener);
              if (getNotification() == null) {
                stopForeground(true);
              }
              return;
            }
        }

        void onPermissionsGranted() {
            // If permissions were granted while the app was in the background, for example in
            // the Settings app, the watchers need restarting.
            for (Watcher watcher : watchers) {
                watcher.client.removeLocationUpdates(watcher.locationCallback);
                watcher.client.requestLocationUpdates(
                        watcher.locationRequest,
                        watcher.locationCallback,
                        null
                );
            }
        }

        void onActivityStarted() {
            stopForeground(true);
        }

        void onActivityStopped() {
            Notification notification = getNotification();
            if (notification != null) {
                startForeground(NOTIFICATION_ID, notification);
            }
        }

        void stopService() {
            BackgroundGeolocationService.this.stopSelf();
        }
    }
}

@jasonfish568
Copy link
Author

After all, I tried to combine extra part from mauron85's plugin into this one. For anyone who needs extra precision on location, please check the extra code that got copied over(I gotta say mauron85's plugin was solid until maintainence stopped). In the code, it checked whether GMS is installed(copied code from Geolocation plugin), if installed use this plugin's original method. If not, use mauron85's method.

It's probably better to write a new plugin, but I'm not good enough on that. But author please consider the use case of non gms devices. I noticed Google is promoting using its Google Play services. But there is not only Google Play Store in the world. As Huawei promotes its App Gallery over the next few years or so, people still needs to face this problem. Well, yes, ionic is owned by Google so is Capacitor. But the point is here. It would be great to support non-GMS devices.

package com.equimaps.capacitor_background_geolocation;

import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Location;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.location.Criteria;
import android.location.LocationListener;
import android.location.LocationManager;

import com.getcapacitor.Logger;
import com.getcapacitor.android.BuildConfig;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationAvailability;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;

import java.util.HashSet;
import java.util.List;
import static java.lang.Math.abs;
import static java.lang.Math.pow;
import static java.lang.Math.round;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;

// A bound and started service that is promoted to a foreground service when
// location updates have been requested and the main activity is stopped.
//
// When an activity is bound to this service, frequent location updates are
// permitted. When the activity is removed from the foreground, the service
// promotes itself to a foreground service, and location updates continue. When
// the activity comes back to the foreground, the foreground service stops, and
// the notification associated with that service is removed.
public class BackgroundGeolocationService extends Service {
    static final String ACTION_BROADCAST = (
            BackgroundGeolocationService.class.getPackage().getName() + ".broadcast"
    );
    private final IBinder binder = new LocalBinder();

    // Must be unique for this application.
    private static final int NOTIFICATION_ID = 28351;

    //Background Geolocation Tenforwardconsult code
    private LocationManager locationManager;
    private Criteria criteria;
    private boolean isStarted = false;
    private LocationListener mLocationListener;
    private AlarmManager alarmManager;

    private static final String P_NAME = "com.equimaps.capacitor_background_geolocation";

    private static final String STATIONARY_REGION_ACTION        = P_NAME + ".STATIONARY_REGION_ACTION";
    private static final String STATIONARY_ALARM_ACTION         = P_NAME + ".STATIONARY_ALARM_ACTION";
    private static final String SINGLE_LOCATION_UPDATE_ACTION   = P_NAME + ".SINGLE_LOCATION_UPDATE_ACTION";
    private static final String STATIONARY_LOCATION_MONITOR_ACTION = P_NAME + ".STATIONARY_LOCATION_MONITOR_ACTION";

    private static final long STATIONARY_TIMEOUT                                = 5 * 1000 * 60;    // 5 minutes.
    private static final long STATIONARY_LOCATION_POLLING_INTERVAL_LAZY         = 3 * 1000 * 60;    // 3 minutes.
    private static final long STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE   = 1 * 1000 * 60;    // 1 minute.
    private static final int MAX_STATIONARY_ACQUISITION_ATTEMPTS = 5;
    private static final int MAX_SPEED_ACQUISITION_ATTEMPTS = 3;

    private Boolean isMoving = false;
    private Boolean isAcquiringStationaryLocation = false;
    private Boolean isAcquiringSpeed = false;
    private Integer locationAcquisitionAttempts = 0;

    private Location lastLocation;
    private Location stationaryLocation;
    private float stationaryRadius;
    private Integer scaledDistanceFilter;
    private long stationaryLocationPollingInterval;

    private PendingIntent stationaryAlarmPI;
    private PendingIntent stationaryLocationPollingPI;
    private PendingIntent stationaryRegionPI;
    private PendingIntent singleUpdatePI;

    protected String callbackId;
    //end of Background Geolocation Tenforwardconsult code

    /**
     * CommandId sent by the service to
     * any registered clients with error.
     */
    public static final int MSG_ON_ERROR = 100;

    /**
     * CommandId sent by the service to
     * any registered clients with the new position.
     */
    public static final int MSG_ON_LOCATION = 101;

    /**
     * CommandId sent by the service to
     * any registered clients whenever the devices enters "stationary-mode"
     */
    public static final int MSG_ON_STATIONARY = 102;

    /**
     * CommandId sent by the service to
     * any registered clients with new detected activity.
     */
    public static final int MSG_ON_ACTIVITY = 103;

    public static final int MSG_ON_SERVICE_STARTED = 104;

    public static final int MSG_ON_SERVICE_STOPPED = 105;

    public static final int MSG_ON_ABORT_REQUESTED = 106;

    public static final int MSG_ON_HTTP_AUTHORIZATION = 107;

    @Override
    public void onCreate() {
      super.onCreate();

      locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
      alarmManager = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
      // Stop-detection PI
      stationaryAlarmPI = PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent(STATIONARY_ALARM_ACTION), 0);

      registerReceiver(stationaryAlarmReceiver, new IntentFilter(STATIONARY_ALARM_ACTION));

      // Stationary region PI
      stationaryRegionPI = PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent(STATIONARY_REGION_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
      registerReceiver(stationaryRegionReceiver, new IntentFilter(STATIONARY_REGION_ACTION));

      // Stationary location monitor PI
      stationaryLocationPollingPI = PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent(STATIONARY_LOCATION_MONITOR_ACTION), 0);
      registerReceiver(stationaryLocationMonitorReceiver, new IntentFilter(STATIONARY_LOCATION_MONITOR_ACTION));

      // One-shot PI (TODO currently unused)
      singleUpdatePI = PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent(SINGLE_LOCATION_UPDATE_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
      registerReceiver(singleUpdateReceiver, new IntentFilter(SINGLE_LOCATION_UPDATE_ACTION));

      // Location criteria
      criteria = new Criteria();
      criteria.setAltitudeRequired(false);
      criteria.setBearingRequired(false);
      criteria.setSpeedRequired(true);
      criteria.setCostAllowed(true);
    }

    @Override
    public void onDestroy(){
      alarmManager.cancel(stationaryAlarmPI);
      alarmManager.cancel(stationaryLocationPollingPI);

      unregisterReceiver(stationaryAlarmReceiver);
      unregisterReceiver(singleUpdateReceiver);
      unregisterReceiver(stationaryRegionReceiver);
      unregisterReceiver(stationaryLocationMonitorReceiver);
      super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    private class Watcher {
        public String id;
        public FusedLocationProviderClient client;
        public LocationRequest locationRequest;
        public LocationCallback locationCallback;
        public Notification backgroundNotification;
    }
    private HashSet<Watcher> watchers = new HashSet<Watcher>();

    Notification getNotification() {
        for (Watcher watcher : watchers) {
            if (watcher.backgroundNotification != null) {
                return watcher.backgroundNotification;
            }
        }
        return null;
    }
    // Handles requests from the activity.
    public class LocalBinder extends Binder {
      private float distanceFilter;

      void addWatcher(
                final String id,
                Notification backgroundNotification,
                float distanceFilter
        ) {
            callbackId=id;
            int gmsResultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getApplicationContext());
            mLocationListener = new LocationListener() {
              @Override
              public void onLocationChanged(final Location location) {
                Logger.debug("Received location {}", location.toString());
                if (!isMoving && !isAcquiringStationaryLocation && stationaryLocation==null) {
                  // Perhaps our GPS signal was interupted, re-acquire a stationaryLocation now.
                  setPace(false);
                }
                Logger.debug("Is acquiring stationary location: "+isAcquiringStationaryLocation+". Is moving: "+isMoving+" Is acquiring speed: "+isAcquiringSpeed);

                if (isAcquiringStationaryLocation) {
                  if (stationaryLocation == null || stationaryLocation.getAccuracy() > location.getAccuracy()) {
                    stationaryLocation = location;
                  }
                  if (++locationAcquisitionAttempts == MAX_STATIONARY_ACQUISITION_ATTEMPTS) {
                    isAcquiringStationaryLocation = false;
                    startMonitoringStationaryRegion(stationaryLocation,30);
                    handleStationary(stationaryLocation, stationaryRadius);
                    return;
                  } else {
                    // Unacceptable stationary-location: bail-out and wait for another.
                    return;
                  }
                } else if (isAcquiringSpeed) {
                  if (++locationAcquisitionAttempts == MAX_SPEED_ACQUISITION_ATTEMPTS) {
                    // Got enough samples, assume we're confident in reported speed now.  Play "woohoo" sound.
                    isAcquiringSpeed = false;
                    scaledDistanceFilter = calculateDistanceFilter(location.getSpeed(),distanceFilter);
                    setPace(true);
                  } else {
                    return;
                  }
                } else if (isMoving) {

                  // Only reset stationaryAlarm when accurate speed is detected, prevents spurious locations from resetting when stopped.
                  if ( (location.getSpeed() >= 1) && (location.getAccuracy() <= 30) ) {
                    resetStationaryAlarm();
                  }
                  // Calculate latest distanceFilter, if it changed by 5 m/s, we'll reconfigure our pace.
                  Integer newDistanceFilter = calculateDistanceFilter(location.getSpeed(),distanceFilter);
                  if (newDistanceFilter != scaledDistanceFilter.intValue()) {
                    Logger.info("Updating distanceFilter: new="+newDistanceFilter+" old="+scaledDistanceFilter);
                    scaledDistanceFilter = newDistanceFilter;
                    setPace(true);
                  }
                  if (lastLocation != null && location.distanceTo(lastLocation) < distanceFilter) {
                    return;
                  }
                } else if (stationaryLocation != null) {
                  return;
                }
                // Go ahead and cache, push to server
                lastLocation = location;
                handleLocation(location);
              }
              @Override
              public void onProviderDisabled(String provider) {}
              @Override
              public void onProviderEnabled(String provider) {}
              @Override
              public void onStatusChanged(String provider, int status, Bundle extras) {}
            };

            if (gmsResultCode == ConnectionResult.SUCCESS) {
              FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(
                BackgroundGeolocationService.this
              );
              LocationRequest locationRequest = new LocationRequest();
              locationRequest.setMaxWaitTime(1000);
              locationRequest.setInterval(1000);
              locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
              locationRequest.setSmallestDisplacement(distanceFilter);

              LocationCallback callback = new LocationCallback(){
                @Override
                public void onLocationResult(LocationResult locationResult) {
                  Location location = locationResult.getLastLocation();
                  Logger.debug(location.toString());
                  Intent intent = new Intent(ACTION_BROADCAST);
                  intent.putExtra("location", location);
                  intent.putExtra("id", id);
                  LocalBroadcastManager.getInstance(
                    getApplicationContext()
                  ).sendBroadcast(intent);
                }
                @Override
                public void onLocationAvailability(LocationAvailability availability) {
                  if (!availability.isLocationAvailable() && BuildConfig.DEBUG) {
                    Logger.debug("Location not available");
                  }
                }
              };
              Watcher watcher = new Watcher();
              watcher.id = id;
              watcher.client = client;
              watcher.locationRequest = locationRequest;
              watcher.locationCallback = callback;
              watcher.backgroundNotification = backgroundNotification;
              watchers.add(watcher);

              watcher.client.requestLocationUpdates(
                watcher.locationRequest,
                watcher.locationCallback,
                null
              );
            } else {
              Logger.debug("Google Play Services not available, using Android location APIs");
              scaledDistanceFilter=(int)distanceFilter;
              isStarted = true;
              setPace(true);
            }
        }

        void removeWatcher(String id) {
            int gmsResultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getApplicationContext());
            if(gmsResultCode == ConnectionResult.SUCCESS){
              for (Watcher watcher : watchers) {
                if (watcher.id.equals(id)) {
                  watcher.client.removeLocationUpdates(watcher.locationCallback);
                  watchers.remove(watcher);
                  if (getNotification() == null) {
                    stopForeground(true);
                  }
                  return;
                }
              }
            } else {
              Logger.debug("Location Listener removed");
              locationManager.removeUpdates(mLocationListener);
              if (getNotification() == null) {
                stopForeground(true);
              }
              return;
            }
        }

        void onPermissionsGranted() {
            // If permissions were granted while the app was in the background, for example in
            // the Settings app, the watchers need restarting.
            for (Watcher watcher : watchers) {
                watcher.client.removeLocationUpdates(watcher.locationCallback);
                watcher.client.requestLocationUpdates(
                        watcher.locationRequest,
                        watcher.locationCallback,
                        null
                );
            }
        }

        void onActivityStarted() {
            stopForeground(true);
        }

        void onActivityStopped() {
            Notification notification = getNotification();
            if (notification != null) {
                startForeground(NOTIFICATION_ID, notification);
            }
        }

        void stopService() {
            BackgroundGeolocationService.this.stopSelf();
        }
    }

    /**
     *
     * @param value set true to engage "aggressive", battery-consuming tracking, false for stationary-region tracking
     */
    private void setPace(Boolean value) {
      if (!isStarted) {
        return;
      }

      Logger.debug("Setting pace: {}", value.toString());

      Boolean wasMoving   = isMoving;
      isMoving            = value;
      isAcquiringStationaryLocation = false;
      isAcquiringSpeed    = false;
      stationaryLocation  = null;

      try {
        locationManager.removeUpdates(mLocationListener);
        criteria.setAccuracy(Criteria.ACCURACY_FINE);
        criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
        criteria.setPowerRequirement(Criteria.POWER_HIGH);

        if (isMoving) {
          // setPace can be called while moving, after distanceFilter has been recalculated.  We don't want to re-acquire velocity in this case.
          if (!wasMoving) {
            isAcquiringSpeed = true;
          }
        } else {
          isAcquiringStationaryLocation = true;
        }

        // Temporarily turn on super-aggressive geolocation on all providers when acquiring velocity or stationary location.
        if (isAcquiringSpeed || isAcquiringStationaryLocation) {
          locationAcquisitionAttempts = 0;
          // Turn on each provider aggressively for a short period of time
          List<String> matchingProviders = locationManager.getAllProviders();
          for (String provider: matchingProviders) {
            if (provider != LocationManager.PASSIVE_PROVIDER) {
              locationManager.requestLocationUpdates(provider, 0, 0, mLocationListener);
            }
          }
        } else {
          locationManager.requestLocationUpdates(locationManager.getBestProvider(criteria, true), 1000, scaledDistanceFilter, mLocationListener);
        }
      } catch (SecurityException e) {
        Logger.error("Security exception: "+e.getMessage());
      }
    }

    private void startMonitoringStationaryRegion(Location location,float providedStationaryRadius) {
      try {
        locationManager.removeUpdates(mLocationListener);

        float _stationaryRadius = providedStationaryRadius;
        float proximityRadius = Math.max(location.getAccuracy(), _stationaryRadius);
        stationaryLocation = location;

        Logger.info("startMonitoringStationaryRegion: lat="+location.getLatitude()+" lon="+location.getLongitude()+" acy="+proximityRadius);

        // Here be the execution of the stationary region monitor
        locationManager.addProximityAlert(
          location.getLatitude(),
          location.getLongitude(),
          proximityRadius,
          (long)-1,
          stationaryRegionPI
        );

        stationaryRadius = proximityRadius;

        startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_LAZY);
      } catch (SecurityException e) {
        Logger.error("Security exception: "+e.getMessage());
      }
    }

    public void startPollingStationaryLocation(long interval) {
      // proximity-alerts don't seem to work while suspended in latest Android 4.42 (works in 4.03).  Have to use AlarmManager to sample
      //  location at regular intervals with a one-shot.
      stationaryLocationPollingInterval = interval;
      alarmManager.cancel(stationaryLocationPollingPI);
      long start = System.currentTimeMillis() + (60 * 1000);
      alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, start, interval, stationaryLocationPollingPI);
    }

    /**
     * User has exit his stationary region!  Initiate aggressive geolocation!
     */
    public void onExitStationaryRegion(Location location) {
      // Filter-out spurious region-exits:  must have at least a little speed to move out of stationary-region

      Logger.info("Exited stationary: lat="+location.getLatitude()+" long="+location.getLongitude()+" acy="+location.getAccuracy());

      try {
        // Cancel the periodic stationary location monitor alarm.
        alarmManager.cancel(stationaryLocationPollingPI);
        // Kill the current region-monitor we just walked out of.
        locationManager.removeProximityAlert(stationaryRegionPI);
        // Engage aggressive tracking.
        this.setPace(true);
      } catch (SecurityException e) {
        Logger.error("Security exception: "+e.getMessage());
      }
    }

    public void onPollStationaryLocation(Location location) {
      float stationaryRadius = 30.0f;
      if (isMoving) {
        return;
      }

      float distance = 0.0f;
      if (stationaryLocation != null) {
        distance = abs(location.distanceTo(stationaryLocation) - stationaryLocation.getAccuracy() - location.getAccuracy());
      }

      Logger.debug("Stationary exit in " + (stationaryRadius-distance) + "m");

      // TODO http://www.cse.buffalo.edu/~demirbas/publications/proximity.pdf
      // determine if we're almost out of stationary-distance and increase monitoring-rate.
      Logger.info("Distance from stationary location: "+ distance);
      if (distance > stationaryRadius) {
        onExitStationaryRegion(location);
      } else if (distance > 0) {
        startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE);
      } else if (stationaryLocationPollingInterval != STATIONARY_LOCATION_POLLING_INTERVAL_LAZY) {
        startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_LAZY);
      }
    }

    /**
     * Handle stationary location with radius
     *
     * @param location
     * @param radius radius of stationary region
     */
    protected void handleStationary (Location location, float radius) {
      this.onStationary(location);
    }

    /**
     * Handle location as recorder by provider
     * @param location
     */
    protected void handleLocation (Location location) {
      this.onLocation(location);
    }

    private Integer calculateDistanceFilter(Float speed,Float distanceFilter) {
      Double newDistanceFilter = (double) distanceFilter;
      if (speed < 100) {
        float roundedDistanceFilter = (round(speed / 5) * 5);
        newDistanceFilter = pow(roundedDistanceFilter, 2) + distanceFilter;
      }
      return (newDistanceFilter.intValue() < 1000) ? newDistanceFilter.intValue() : 1000;
    }

    public void resetStationaryAlarm() {
      alarmManager.cancel(stationaryAlarmPI);
      alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + STATIONARY_TIMEOUT, stationaryAlarmPI); // Millisec * Second * Minute
    }

    public void onLocation(Location location) {
      Logger.debug("New location {}", location.toString());

      if (location == null) {
        Logger.debug("Skipping location as requested by the locationTransform");
        return;
      }

      Intent intent = new Intent(ACTION_BROADCAST);
      intent.putExtra("id", callbackId);
      intent.putExtra("location", location);
      LocalBroadcastManager.getInstance(
        getApplicationContext()
      ).sendBroadcast(intent);
    }

    public void onStationary(Location location) {
      Logger.debug("New stationary {}", location.toString());

      if (location == null) {
        Logger.debug("Skipping location as requested by the locationTransform");
        return;
      }

      Bundle bundle = new Bundle();
      bundle.putInt("action", MSG_ON_STATIONARY);
      bundle.putParcelable("location", location);
      bundle.putString("location", callbackId);
      broadcastMessage(bundle);
    }

    private void broadcastMessage(Bundle bundle) {
      Intent intent = new Intent(ACTION_BROADCAST);
      intent.putExtras(bundle);
      LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
    }

    /**
     * Broadcast receiver which detects a user has stopped for a long enough time to be determined as STOPPED
     */
    private BroadcastReceiver stationaryAlarmReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent)
      {
        Logger.info("stationaryAlarm fired");
        setPace(false);
      }
    };
    /**
     * Broadcast receiver to handle stationaryMonitor alarm, fired at low frequency while monitoring stationary-region.
     * This is required because latest Android proximity-alerts don't seem to operate while suspended.  Regularly polling
     * the location seems to trigger the proximity-alerts while suspended.
     */
    private BroadcastReceiver stationaryLocationMonitorReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent)
      {
        Logger.info("Stationary location monitor fired");

        criteria.setAccuracy(Criteria.ACCURACY_FINE);
        criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
        criteria.setPowerRequirement(Criteria.POWER_HIGH);

        try {
          locationManager.requestSingleUpdate(criteria, singleUpdatePI);
        } catch (SecurityException e) {
          Logger.error("Security exception:"+ e.getMessage());
        }
      }
    };
    /**
     * Broadcast receiver which detects a user has exit his circular stationary-region determined by the greater of stationaryLocation.getAccuracy() OR stationaryRadius
     */
    private BroadcastReceiver stationaryRegionReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        String key = LocationManager.KEY_PROXIMITY_ENTERING;
        Boolean entering = intent.getBooleanExtra(key, false);

        if (entering) {
          Logger.debug("Entering stationary region");
          if (isMoving) {
            setPace(false);
          }
        }
        else {
          Logger.debug("Exiting stationary region");
          // There MUST be a valid, recent location if this event-handler was called.
          Location location = getLastBestLocation();
          if (location != null) {
            onExitStationaryRegion(location);
          }
        }
      }
    };

    /**
     * Broadcast receiver for receiving a single-update from LocationManager.
     */
    private BroadcastReceiver singleUpdateReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        String key = LocationManager.KEY_LOCATION_CHANGED;
        Location location = (Location)intent.getExtras().get(key);
        if (location != null) {
          Logger.debug("Single location update: " + location.toString());
          onPollStationaryLocation(location);
        }
      }
    };

    /**
     * Returns the most accurate and timely previously detected location.
     * Where the last result is beyond the specified maximum distance or
     * latency a one-off location update is returned via the {@link LocationListener}
     * specified in {@link setChangedLocationListener}.
     * @param minTime Minimum time required between location updates.
     * @return The most accurate and / or timely previously detected location.
     */
    public Location getLastBestLocation() {
      Location bestResult = null;
      String bestProvider = null;
      float bestAccuracy = Float.MAX_VALUE;
      long bestTime = Long.MIN_VALUE;
      long minTime = System.currentTimeMillis() - 1000;

      Logger.info("Fetching last best location: radius="+30+" minTime="+minTime);

      try {
        // Iterate through all the providers on the system, keeping
        // note of the most accurate result within the acceptable time limit.
        // If no result is found within maxTime, return the newest Location.
        List<String> matchingProviders = locationManager.getAllProviders();
        for (String provider: matchingProviders) {
          Location location = locationManager.getLastKnownLocation(provider);
          if (location != null) {
            Logger.debug("Test provider="+provider+" lat="+location.getLatitude()+" lon="+location.getLongitude()+" acy="+location.getAccuracy()+" v="+location.getSpeed()+"m/s time="+location.getTime());
            float accuracy = location.getAccuracy();
            long time = location.getTime();
            if ((time > minTime && accuracy < bestAccuracy)) {
              bestProvider = provider;
              bestResult = location;
              bestAccuracy = accuracy;
              bestTime = time;
            }
          }
        }

        if (bestResult != null) {
          Logger.debug("Best result found provider="+bestProvider+" lat="+bestResult.getLatitude()+" lon="+bestResult.getLongitude()+" acy="+bestResult.getAccuracy()+" v="+bestResult.getSpeed()+"m/s time="+bestResult.getTime());
        }
      } catch (SecurityException e) {
        Logger.error("Security exception: "+e.getMessage());
      }

      return bestResult;
    }
}

@diachedelic
Copy link
Collaborator

This is very interesting. I wasn't aware that there was an alternative to Google Location Services. I agree that non-Google devices should be supported. If anyone has the time to neatly incorporate this alternate strategy I will consider a pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants