package org.odk.collect.android.jr.extensions;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.javarosa.core.model.Action;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.Recalculate;
import org.javarosa.core.model.data.AnswerDataFactory;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.AbstractTreeElement;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import org.odk.collect.android.application.Collect;
import org.odk.collect.android.utilities.GeoUtils;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
/**
* XForms Action extension to periodically poll a sensor and optionally save its value.
* @author jschweers
*/
public class PollSensorAction extends Action implements LocationListener {
private static String name = "pollsensor";
private TreeReference target;
private LocationManager mLocationManager;
private FormDef mModel;
private TreeReference mContextRef;
private class ProvidersChangedHandler extends BroadcastReceiver {
/*
* (non-Javadoc)
* @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
*/
@Override
public void onReceive(Context context, Intent intent) {
Set<String> providers = GeoUtils.evaluateProviders(mLocationManager);
requestLocationUpdates(providers);
}
}
private class StopPollingTask extends TimerTask {
/*
* (non-Javadoc)
* @see java.util.TimerTask#run()
*/
@Override
public void run() {
mLocationManager.removeUpdates(PollSensorAction.this);
}
}
public PollSensorAction() {
super(name);
}
public PollSensorAction(TreeReference target) {
super(name);
this.target = target;
}
/**
* Deal with a pollsensor action: start getting a GPS fix, and prepare to cancel after maximum amount of time.
* @param model The FormDef that triggered the action
* @param contextRef
*/
public void processAction(FormDef model, TreeReference contextRef) {
mModel = model;
mContextRef = contextRef;
// LocationManager needs to be dealt with in the main UI thread, so wrap GPS-checking logic in a Handler
new Handler(Looper.getMainLooper()).post(new Runnable() {
public void run() {
// Start requesting GPS updates
Context context = Collect.getStaticApplicationContext();
mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
Set<String> providers = GeoUtils.evaluateProviders(mLocationManager);
if (providers.isEmpty()) {
context.registerReceiver(
new ProvidersChangedHandler(),
new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
);
// This thread can't take action on the UI, so instead send a message that actual activities
// notice and then display a dialog asking user to enable location access
Intent noGPSIntent = new Intent(GeoUtils.ACTION_CHECK_GPS_ENABLED);
context.sendStickyBroadcast(noGPSIntent);
}
requestLocationUpdates(providers);
}
});
}
/**
* Start polling for location, based on whatever providers are given, and set up a timeout after MAXIMUM_WAIT is exceeded.
* @param providers Set of String objects that may contain LocationManager.GPS_PROVDER and/or LocationManager.NETWORK_PROVIDER
*/
private void requestLocationUpdates(Set<String> providers) {
if (providers.isEmpty()) {
mLocationManager.removeUpdates(PollSensorAction.this);
return;
}
for (String provider : providers) {
mLocationManager.requestLocationUpdates(provider, 0, 0, PollSensorAction.this);
}
// Cancel polling after maximum time is exceeded
Timer timeout = new Timer();
timeout.schedule(new StopPollingTask(), GeoUtils.MAXIMUM_WAIT);
}
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
super.readExternal(in, pf);
target = (TreeReference)ExtUtil.read(in, TreeReference.class, pf);
}
public void writeExternal(DataOutputStream out) throws IOException {
super.writeExternal(out);
ExtUtil.write(out, target);
}
/**
* (non-Javadoc)
* @see android.location.LocationListener#onLocationChanged(android.location.Location)
*
* If this action has a target node, update its value with the given location.
* @param location
*/
@Override
public void onLocationChanged(Location location) {
if (location != null) {
if (target != null) {
String result = GeoUtils.locationToString(location);
TreeReference qualifiedReference = mContextRef == null ? target : target.contextualize(mContextRef);
EvaluationContext context = new EvaluationContext(mModel.getEvaluationContext(), qualifiedReference);
AbstractTreeElement node = context.resolveReference(qualifiedReference);
if(node == null) { throw new NullPointerException("Target of TreeReference " + qualifiedReference.toString(true) +" could not be resolved!"); }
int dataType = node.getDataType();
IAnswerData val = Recalculate.wrapData(result, dataType);
mModel.setValue(val == null ? null: AnswerDataFactory.templateByDataType(dataType).cast(val.uncast()), qualifiedReference);
}
if (location.getAccuracy() <= GeoUtils.GOOD_ACCURACY) {
mLocationManager.removeUpdates(this);
}
}
}
/*
* (non-Javadoc)
* @see android.location.LocationListener#onProviderDisabled(java.lang.String)
*/
@Override
public void onProviderDisabled(String provider) { }
/*
* (non-Javadoc)
* @see android.location.LocationListener#onProviderEnabled(java.lang.String)
*/
@Override
public void onProviderEnabled(String provider) { }
/*
* (non-Javadoc)
* @see android.location.LocationListener#onStatusChanged(java.lang.String, int, android.os.Bundle)
*/
@Override
public void onStatusChanged(String provider, int status, Bundle extras) { }
}