package org.commcare.utils; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.os.Looper; import android.support.v4.content.ContextCompat; import android.util.Log; import org.commcare.CommCareApplication; import org.commcare.activities.EntitySelectActivity; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.condition.IFunctionHandler; import org.javarosa.core.model.data.GeoPointData; import java.util.Set; import java.util.Vector; /** * Allows evaluation contexts to support the here() XPath function, * which returns the current location. * * In addition, an EntitySelectActivity can register itself to be refreshed whenever * a new value of here() is obtained (whenever the location changes). * * No locations are requested if here() is never evaluated. * * @author Forest Tong, Phillip Mates */ public class HereFunctionHandler implements IFunctionHandler, LocationListener { public static final String HERE_NAME = "here"; // only trigger entity list refresh if distance has changed by this amount private static final int REFRESH_METER_DELTA = 30; private Location location; // last location used to render the distance properties of the entity list private Location lastDisplayedLocation; private boolean requestingLocationUpdates; private boolean locationGoodEnough; private final Context context = CommCareApplication.instance().getApplicationContext(); private final LocationManager mLocationManager = (LocationManager)context.getSystemService( Context.LOCATION_SERVICE); // If there are more general uses for HereFunctionHandler, the type of this field can be // generalized to a listener interface. private EntitySelectActivity entitySelectActivity; public HereFunctionHandler() { } public void registerEvalLocationListener(EntitySelectActivity entitySelectActivity) { this.entitySelectActivity = entitySelectActivity; } public void unregisterEvalLocationListener() { this.entitySelectActivity = null; } // The EntitySelectActivity must subscribe before this method is called if a fresh location is desired. @Override public Object eval(Object[] args, EvaluationContext ec) { if (entitySelectActivity != null) { entitySelectActivity.onHereFunctionEvaluated(); } if (location == null) { return ""; } return toGeoPointData(location).getDisplayText(); } public void allowGpsUse() { if (!locationGoodEnough && !requestingLocationUpdates) { requestLocationUpdates(); } } public void forbidGpsUse() { if (requestingLocationUpdates) { removeLocationUpdates(); } } public void refreshLocation() { this.locationGoodEnough = false; } @Override public void onLocationChanged(Location updatedLocation) { this.location = updatedLocation; Log.i("HereFunctionHandler", "location has been set to " + this.location); if (this.location.getAccuracy() <= GeoUtils.DEFAULT_ACCEPTABLE_ACCURACY) { locationGoodEnough = true; forbidGpsUse(); } if (entitySelectActivity != null && shouldRefreshEntityList()) { lastDisplayedLocation = location; entitySelectActivity.onEvalLocationChanged(); } } private boolean shouldRefreshEntityList() { boolean isDistanceDeltaSufficient = true; if (lastDisplayedLocation != null) { float distanceFromLastLocation = lastDisplayedLocation.distanceTo(location); isDistanceDeltaSufficient = distanceFromLastLocation > REFRESH_METER_DELTA; } return isDistanceDeltaSufficient; } public boolean locationProvidersFound() { return GeoUtils.evaluateProvidersWithPermissions(mLocationManager, context).size() > 0; } private void requestLocationUpdates() { Set<String> mProviders = GeoUtils.evaluateProvidersWithPermissions(mLocationManager, context); for (String provider : mProviders) { // Ignore the inspector warnings; the permissions are already checked in evaluateProvidersWithPermissions. if (location == null) { Location lastKnownLocation = mLocationManager.getLastKnownLocation(provider); if (lastKnownLocation != null) { this.location = lastKnownLocation; this.lastDisplayedLocation = lastKnownLocation; Log.i("HereFunctionHandler", "last known location: " + this.location); } } // Looper is necessary because requestLocationUpdates is called inside an AsyncTask (EntityLoaderTask). // What values for minTime and minDistance? mLocationManager.requestLocationUpdates(provider, 0, 0, this, Looper.getMainLooper()); requestingLocationUpdates = true; } } // Clients must call this when done using handler. private void removeLocationUpdates() { // stops the GPS. Note that this will turn off the GPS if the screen goes to sleep. if (ContextCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) { mLocationManager.removeUpdates(this); requestingLocationUpdates = false; } } private static GeoPointData toGeoPointData(Location location) { return new GeoPointData(new double[]{ location.getLatitude(), location.getLongitude(), location.getAltitude(), (double)location.getAccuracy() }); } @Override public void onProviderDisabled(String provider) { } @Override public void onProviderEnabled(String provider) { } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public String getName() { return HERE_NAME; } @Override public Vector getPrototypes() { Vector p = new Vector(); p.addElement(new Class[0]); return p; } @Override public boolean rawArgs() { return false; } }