package interdroid.swan.sensors.impl;
import interdroid.swan.R;
import interdroid.swan.sensors.AbstractConfigurationActivity;
import interdroid.swan.sensors.AbstractSwanSensor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Looper;
/**
* A sensor for location.
*
* @author rkemp <rkemp@cs.vu.nl>
*
*/
public class SmartLocationSensor extends AbstractSwanSensor {
/**
* Access to logger.
*/
private static final Logger LOG = LoggerFactory
.getLogger(SmartLocationSensor.class);
private static final long SECOND = 1000; // ms
// TODO make this configurable?
private static final long MIN_TIME_BETWEEN_UPDATES = 20 * SECOND; // ms
/**
* The configuration activity for this sensor.
*
* @author rkemp <rkemp@cs.vu.nl>
*
*/
public static class ConfigurationActivity extends
AbstractConfigurationActivity {
@Override
public final int getPreferencesXML() {
return R.xml.smart_location_preferences;
}
}
/**
* Vicinity field name. Vicinity gives the 10 log of the distance in meters
* to the configured destination.
*/
public static final String VICINITY = "vicinity";
/**
* Within field name. Within gives "true" when within the specified rang to
* the configured destination, "false" otherwise.
*/
public static final String WITHIN = "within";
/**
* The type of provider desired.
*/
public static final String PROVIDER = "provider";
/**
* The latitude.
*/
public static final String LATITUDE = "latitude";
/**
* The longitude.
*/
public static final String LONGITUDE = "longitude";
/**
* The max speed.
*/
public static final String MAX_SPEED = "max_speed";
/**
* The within range.
*/
public static final String WITHIN_RANGE = "range";
/**
* The current provider we are using.
*/
private String currentProvider;
/**
* The location manager we use.
*/
private LocationManager locationManager;
/**
* The requestSingleUpdate method (API 9+)
*/
private Method mRequestSingleUpdateMethod;
/**
* The location listener.
*/
private LocationListener locationListener = new LocationListener() {
public void onLocationChanged(final Location location) {
// if we couldn't use the requestSingleUpdate method, we have to
// stop listening explicitly
if (mRequestSingleUpdateMethod == null) {
stopListener();
}
long now = System.currentTimeMillis();
// update for vicinity
float[] results = new float[3];
float distance;
long minDelay = Long.MAX_VALUE;
for (int i = 0; i < mVicinityLatitudes.size(); i++) {
Location.distanceBetween(location.getLatitude(),
location.getLongitude(), mVicinityLatitudes.get(i),
mVicinityLongitudes.get(i), results);
distance = results[0];
int vicinity = (int) Math.log10(distance);
long upperBound = (long) ((Math.pow(10, vicinity + 1) - distance) / mVicinityMaxSpeeds
.get(i));
long lowerBound = (long) ((distance - Math.pow(10, vicinity)) / mVicinityMaxSpeeds
.get(i));
minDelay = Math.min(minDelay,
(Math.min(lowerBound, upperBound)));
putValueTrimSize(VICINITY, null, now, vicinity);
}
// update for within
for (int i = 0; i < mWithinLatitudes.size(); i++) {
Location.distanceBetween(location.getLatitude(),
location.getLongitude(), mWithinLatitudes.get(i),
mWithinLongitudes.get(i), results);
distance = results[0];
minDelay = Math
.min(minDelay,
(long) (Math.abs((distance - mWithinRanges
.get(i))) / mWithinMaxSpeeds.get(i)));
putValueTrimSize(WITHIN, null, now, distance);
}
// don't get the value too often if we're close to a point where the
// value changes
minDelay = Math.max(minDelay * SECOND, MIN_TIME_BETWEEN_UPDATES);
// now sleep for minDelay
final long sleepTime = minDelay;
// TODO change this to alarm?
new Thread() {
public void run() {
try {
sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
requestSingleUpdate();
}
}.start();
}
public void onProviderDisabled(final String provider) {
LOG.debug("provider disabled: {}. I am using: {}", provider,
currentProvider);
if (provider.equals(currentProvider)) {
LOG.warn("location sensor disabled due to lack of provider");
}
}
public void onProviderEnabled(final String provider) {
LOG.debug("provider enabled: {}", provider);
}
public void onStatusChanged(final String provider, final int status,
final Bundle extras) {
if (provider.equals(currentProvider)
&& status != LocationProvider.AVAILABLE) {
LOG.warn("location sensor disabled because sensor unavailable");
}
}
};
@Override
public final String[] getValuePaths() {
return new String[] { VICINITY, WITHIN };
}
@Override
public final void initDefaultConfiguration(final Bundle defaults) {
defaults.putDouble(MAX_SPEED, 28); // 28 m/s ~ 100 km/h
defaults.putDouble(WITHIN_RANGE, 100); // meter
defaults.putString(PROVIDER, LocationManager.NETWORK_PROVIDER);
}
@Override
public final void onConnected() {
SENSOR_NAME = "Smart Location Sensor";
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
// check whether we can use the advanced methods of API 9+
try {
mRequestSingleUpdateMethod = locationManager.getClass().getMethod(
"requestSingleUpdate",
new Class[] { String.class, LocationListener.class,
Looper.class });
} catch (Throwable t) {
mRequestSingleUpdateMethod = null;
}
// // construct the mock location provider (for testing)
// try {
// locationManager.removeTestProvider("test");
// } catch (Throwable t) {
// // ignore
// }
// locationManager
// .addTestProvider("test", false, false, false, false, false,
// false, false, Criteria.POWER_LOW,
// Criteria.ACCURACY_FINE);
// // and enable it
// locationManager.setTestProviderEnabled("test", true);
//
// // and run the update thread
// mockUpdateThread.start();
}
private List<Double> mVicinityMaxSpeeds = new ArrayList<Double>();
private List<Double> mVicinityLatitudes = new ArrayList<Double>();
private List<Double> mVicinityLongitudes = new ArrayList<Double>();
private List<String> mVicinityIds = new ArrayList<String>();
private List<Double> mWithinMaxSpeeds = new ArrayList<Double>();
private List<Double> mWithinLatitudes = new ArrayList<Double>();
private List<Double> mWithinLongitudes = new ArrayList<Double>();
private List<Double> mWithinRanges = new ArrayList<Double>();
private List<String> mWithinIds = new ArrayList<String>();
@Override
public final void register(final String id, final String valuePath,
final Bundle configuration) {
if (valuePath.equals(WITHIN)) {
mWithinLatitudes.add(Double.valueOf(configuration
.getDouble(LATITUDE)));
mWithinLongitudes.add(Double.valueOf(configuration
.getDouble(LONGITUDE)));
mWithinMaxSpeeds.add(configuration.getDouble(MAX_SPEED,
mDefaultConfiguration.getDouble(MAX_SPEED)));
mWithinRanges.add(configuration.getDouble(WITHIN_RANGE,
mDefaultConfiguration.getDouble(WITHIN_RANGE)));
mWithinIds.add(id);
} else if (valuePath.equals(VICINITY)) {
mVicinityLatitudes.add(Double.valueOf(configuration
.getString(LATITUDE)));
mVicinityLongitudes.add(Double.valueOf(configuration
.getString(LONGITUDE)));
mVicinityMaxSpeeds.add(configuration.getDouble(MAX_SPEED,
mDefaultConfiguration.getDouble(MAX_SPEED)));
mVicinityIds.add(id);
} else {
throw new RuntimeException("invalid valuePath: '" + valuePath
+ "' for SmartLocationSensor");
}
requestSingleUpdate();
}
/**
* Updates the listener.
*/
private void requestSingleUpdate() {
// stop if we don't have any registerd configurations!
if (registeredConfigurations.size() == 0) {
return;
}
String mostAccurateProvider;
String passiveProvider = null;
// Reflect out PASSIVE_PROVIDER so we can still run on 7.
try {
Field passive = LocationManager.class.getField("PASSIVE_PROVIDER");
passiveProvider = (String) passive.get(null);
mostAccurateProvider = passiveProvider;
} catch (Exception e) {
LOG.warn("Caught exception checking for PASSIVE_PROVIDER.");
mostAccurateProvider = LocationManager.NETWORK_PROVIDER;
}
for (Bundle configuration : registeredConfigurations.values()) {
if (configuration.containsKey(PROVIDER)) {
if (mostAccurateProvider.equals(passiveProvider)) {
// if current is passive, anything is better
mostAccurateProvider = configuration.getString(PROVIDER);
} else if (LocationManager.NETWORK_PROVIDER
.equals(mostAccurateProvider)
&& LocationManager.GPS_PROVIDER.equals(configuration
.getString(PROVIDER))) {
// if current is network, only gps is better
mostAccurateProvider = LocationManager.GPS_PROVIDER;
}
// if it isn't PASSIVE or NETWORK, we can't do any better,
// it
// must be GPS
}
}
if (mRequestSingleUpdateMethod != null) {
requestSingleUpdateGingerBread(mostAccurateProvider);
} else {
locationManager.requestLocationUpdates(mostAccurateProvider, 0, 0,
locationListener, Looper.getMainLooper());
}
}
private void requestSingleUpdateGingerBread(String provider) {
try {
mRequestSingleUpdateMethod.invoke(locationManager, new Object[] {
provider, locationListener, Looper.getMainLooper() });
} catch (Throwable t) {
// something went wrong, revert to old method by setting
// mRequestSingleUpdateMethod to null
mRequestSingleUpdateMethod = null;
requestSingleUpdate();
}
}
private void stopListener() {
locationManager.removeUpdates(locationListener);
}
@Override
public final void unregister(final String id) {
if (registeredConfigurations.size() == 0) {
}
}
@Override
public void onDestroySensor() {
mockUpdateThread.interrupt();
try {
locationManager.removeTestProvider("test");
} catch (Throwable t) {
// ignore
}
super.onDestroySensor();
}
/**** TESTING CODE FOR MOCK LOCATIONS ***/
private static final double[][] MOCK_LOCATIONS = new double[][] {
{ 52.333943, 4.864549 /* VU */},
{ 52.321983, 4.927613 /* Duivendrecht */},
{ 52.279711, 5.157254 /* Naarden-Bussum */},
{ 52.154294, 5.36587 /* Amersfoort */},
{ 52.154346, 5.922947 /* Apeldoorn */} };
private static final double MOCK_SPEED = 750; // m/s
/*
* This thread simulates continuous movement between VU and Apeldoorn and
* back (with MOCK_SPEED), it updates about every single second.
*/
Thread mockUpdateThread = new Thread() {
public void run() {
boolean reverse = false;
int index = 0;
int next = 0;
Location location = new Location("test");
while (!isInterrupted()) {
next = reverse ? index - 1 : index + 1;
location.setLatitude(MOCK_LOCATIONS[index][0]);
location.setLongitude(MOCK_LOCATIONS[index][1]);
float[] distance = new float[3];
Location.distanceBetween(MOCK_LOCATIONS[index][0],
MOCK_LOCATIONS[index][1], MOCK_LOCATIONS[next][0],
MOCK_LOCATIONS[next][1], distance);
long timeBetween = (long) (distance[0] / MOCK_SPEED);
for (int i = 0; i < timeBetween; i++) {
float indexFraction = (float) i / (float) timeBetween;
float nextFraction = 1 - indexFraction;
location.setLatitude(MOCK_LOCATIONS[index][0]
* indexFraction + MOCK_LOCATIONS[next][0]
* nextFraction);
location.setLongitude(MOCK_LOCATIONS[index][1]
* indexFraction + MOCK_LOCATIONS[next][1]
* nextFraction);
locationManager.setTestProviderLocation("test", location);
try {
sleep(1000);
} catch (InterruptedException e) {
continue;
}
}
if (reverse) {
index--;
} else {
index++;
}
// turn around at the end
if (next == 0 || next == MOCK_LOCATIONS.length - 1) {
reverse = !reverse;
}
}
}
};
}