/*******************************************************************************
* Copyright 2011 The Regents of the University of California
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package org.ohmage.triggers.types.location;
/*
* The service which performs energy efficient
* location sampling and location change detection.
*
* The location updates are duty cycled based on the
* user speed and proximity to the nearest defined
* location.
*/
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import com.google.android.maps.GeoPoint;
import edu.ucla.cens.accelservice.IAccelService;
import edu.ucla.cens.wifigpslocation.ILocationChangedCallback;
import edu.ucla.cens.wifigpslocation.IWiFiGPSLocationService;
import org.json.JSONException;
import org.json.JSONObject;
import org.ohmage.db.DbHelper;
import org.ohmage.db.Models.Campaign;
import org.ohmage.logprobe.Analytics;
import org.ohmage.logprobe.Log;
import org.ohmage.logprobe.LogProbe.Status;
import org.ohmage.triggers.config.LocTrigConfig;
import org.ohmage.triggers.utils.SimpleTime;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
public class LocTrigService extends Service
implements LocationListener {
private static final String TAG = "LocTrigService";
private static final String REMOTE_CLIENT_NAME =
LocTrigService.class.getName() + ".remote_client";
private static final String WAKE_LOCK_TAG =
LocTrigService.class.getName() + ".wake_lock";
private static final String RECR_WAKE_LOCK_TAG =
LocTrigService.class.getName() + ".recr_wake_lock";
public static final String ACTION_START_TRIGGER =
LocTrigService.class.getName() + ".start_trigger";
public static final String ACTION_REMOVE_TRIGGER =
LocTrigService.class.getName() + ".stop_trigger";
public static final String ACTION_RESET_TRIGGER =
LocTrigService.class.getName() + ".reset_trigger";
public static final String ACTION_UPDATE_LOCATIONS =
LocTrigService.class.getName() + ".update_locations";
private static final String ACTION_HANDLE_ALARM =
LocTrigService.class.getName() + ".handle_alarm";
public static final String ACTION_UPDATE_TRACING_STATUS =
LocTrigService.class.getName() + ".update_tracing";
public static final String KEY_TRIG_ID = "trigger_id";
public static final String KEY_TRIG_DESC = "trigger_description";
private static final String KEY_ALARM_ACTION = "alarm_action";
private static final String KEY_SAMPLING_ALARM_EXTRA = "alarm_extra";
/* Invalid category id */
private static final int CATEG_ID_INVAL = -1;
/* Alarm actions */
private static final String ACTION_ALRM_PASS_THROUGH =
"org.ohmage.triggers.types.location.LocTrigService.PASS_THROUGH";
private static final String ACTION_ALRM_GPS_SAMPLE =
"org.ohmage.triggers.types.location.LocTrigService.GPS_SAMPLE";
private static final String ACTION_ALRM_GPS_TIMEOUT =
"org.ohmage.triggers.types.location.LocTrigService.GPS_TIMEOUT";
private static final String ACTION_ALRM_SRV_KEEP_ALIVE =
"org.ohmage.triggers.types.location.LocTrigService.KEEP_ALIVE";
private static final String ACTION_ALM_TRIGGER_ALWAYS =
"org.ohmage.triggers.types.location.LocTrigService.TRIGGER_ALWAYS";
private static final String DATA_PREFIX_TRIG_ALWAYS_ALM =
"locationtrigger://edu.ucla.cens.triggers.types.location/";
//Time value for the alarm to keep the service alive
private static final long SERV_KEEP_ALIVE_TIME = 300000; //5min
//Time value for the alarm to check if the user is passing through
private static final long PASS_THROUGH_TIME = 180000; //3min
private static final long STALE_LOC_TIME = 180000; //3min
//The number of accurate samples to collect during every
//sampling instance
private static final int SAMPLES_LIMIT = 10;
//GPS timeout alarm time value. The GPS is timed out after
//this if it cannot obtain the above number of accurate
//samples.
private static final long GPS_TIMEOUT = 45000; //45s
//The threshold value to use when checking if a location
//belongs to a category
private static final float CATEG_ACCURACY_MARGIN = 20; //m
//The maximum value of GPS duty cycle interval
private static final long MAX_SLEEP_TIME = 360000; //6 mins
//The minimum value of GPS duty cycle interval
private static final long MIN_SLEEP_TIME = 30000; //30sec
//The actual sleep time is calculated based on the speed.
//The distance (the maximum of which is given above) is calculated
//from the proximity. The proximity is divided by this value get
//the distance value which should be divided by the speed to get the
//sleep time.
private static final float SLEEP_DIST_FACTOR = 10;
//Discard samples below this accuracy
private static final float INACCURATE_SAMPLE_THRESHOLD = 17; //m
//Ignore the speed below this value
private static final float SPEED_MIN_THRESHOLD = 0.05F; //m/s
//Calculate the speed from the displacement only if the user has
//moved at least this much.
private static final float SPEED_CALC_MIN_DISPLACEMENT
= 4 * INACCURATE_SAMPLE_THRESHOLD;
//Minimum accelerometer value for motion detection
private static final double MOTION_DETECT_ACCEL_THRESHOLD = 5;
//Use motion detection only if the sleep time is greater than this value
private static final long SLEEP_TIME_MIN_MOTION_DETECT = 60000; //1 min
//Ignore a movement callback if the motion detection was started
//within this interval
private static final long MOTION_DETECT_DELAY = 60000; //1min
//Wake lock for GPS sampling
private PowerManager.WakeLock mWakeLock = null;
//Wake lock for alarm receiver
private static PowerManager.WakeLock mRecvrWakeLock = null;
private IWiFiGPSLocationService mWiFiGPSServ = null;
private IAccelService mAccelServ = null;
private ILocationChangedCallback mMotionDetectCB= null;
private ServiceConnection mWifiGPSServConn = null;
private ServiceConnection mAccelServConn = null;
//Number of samples collected in the current duty cycle
private int mNSamples = 0;
//Number of initial samples collected when the very first time the
//sampling is started
private int mNInitialSamples = 0;
//Current sleep time before the next duty cycle
private long mCurrSleepTime = 0;
//Speed calculated in the previous duty cycle
private float mPrevSpeed = 0;
//Speed calculated in the current duty cycle
private float mCurrSpeed = 0;
//Current value of the proximity distance
private float mCurrProxDist = 0;
//The latest location update received
private final Location mLastKnownLoc = new Location(LocationManager.GPS_PROVIDER);
//Time stamp of the above location update
private long mLastKnownLocTime = 0;
//Location update received in the previous duty cycle
private final Location mLastKnownLocBackup = new Location(LocationManager.GPS_PROVIDER);
//Time stamp of the above location update
private long mLastKnownLocTimeBackup = 0;
//The last category (place) where the user was known to be in
private int mLatestCateg = CATEG_ID_INVAL;
//Flag to check if a pass-through check is initiated
private boolean mPassThroughChecking = false;
//The category id for which the pass through check is being performed
private int mPassThroughCheckCateg = CATEG_ID_INVAL;
private long mCategPrevTS = LocTrigDB.TIME_STAMP_INVALID;
//Flag to check if the sampling is started at all
private boolean mSamplingStarted = false;
private boolean mGPSStarted = false;
private long mMotionDetectTS = 0;
private WifiManager.WifiLock mWifiLock = null;
//Status of location tracing
private boolean mLocTraceEnabled = false;
//Upload trace even if it is similar to the previous trace
private boolean mLocTraceUploadAlways = false;
//Latest location uploaded
private final Location mLastLocTrace = new Location(LocationManager.GPS_PROVIDER);
//Handler to handle motion detection callback
//It is require to run the handling in the current thread
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
handleWifiGPSLocChange();
}
};
//The list of all locations to watch for
private LinkedList<LocListItem> mLocList;
@Override
public IBinder onBind(Intent arg0) {
return null;
}
@Override
public void onCreate() {
Analytics.service(this, Status.ON);
//Let the service live forever
setKeepAliveAlarm();
//Cache the locations
mLocList = new LinkedList<LocListItem>();
populateLocList();
initState();
PowerManager powerMan = (PowerManager) getSystemService(POWER_SERVICE);
mWakeLock = powerMan.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
if(LocTrigConfig.useNetworkLocation) {
Log.v(TAG, "LocTrigService: Using network location");
WifiManager wifiMan = (WifiManager) getSystemService(WIFI_SERVICE);
mWifiLock = wifiMan.createWifiLock(WifiManager.WIFI_MODE_SCAN_ONLY, TAG);
}
if(LocTrigConfig.useMotionDetection) {
Log.v(TAG, "LocTrigService: Using motion detection");
}
mMotionDetectCB = new ILocationChangedCallback.Stub() {
@Override
public void locationChanged() throws RemoteException {
if(LocTrigConfig.useMotionDetection) {
mHandler.sendMessage(mHandler.obtainMessage());
}
}
};
mSamplingStarted = false;
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
int trigId = intent.getIntExtra(KEY_TRIG_ID, -1);
String trigDesc = intent.getStringExtra(KEY_TRIG_DESC);
if(intent.getAction().equals(ACTION_START_TRIGGER)) {
setTriggerAlwaysAlarm(trigId, trigDesc);
}
else if(intent.getAction().equals(ACTION_REMOVE_TRIGGER)) {
cancelTriggerAlwaysAlarm(trigId);
}
else if(intent.getAction().equals(ACTION_RESET_TRIGGER)) {
setTriggerAlwaysAlarm(trigId, trigDesc);
}
else if(intent.getAction().equals(ACTION_HANDLE_ALARM)) {
handleAlarm(intent.getExtras());
}
else if(intent.getAction().equals(ACTION_UPDATE_LOCATIONS)) {
populateLocList();
}
else if(intent.getAction().equals(ACTION_UPDATE_TRACING_STATUS)) {
updateLocTracingState();
}
updateSamplingStatus();
releaseRecvrWakeLock();
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
Analytics.service(this, Status.OFF);
stopGPS();
mLocList.clear();
disconnectRemoteServices();
releaseWakeLock();
releaseRecvrWakeLock();
super.onDestroy();
}
private void connectToRemoteServices() {
//Connect to WIFIGPS
bindService(new Intent(IWiFiGPSLocationService.class.getName()),
mWifiGPSServConn, Context.BIND_AUTO_CREATE);
//Connect to ACCEL
bindService(new Intent(IAccelService.class.getName()),
mAccelServConn, Context.BIND_AUTO_CREATE);
}
private void disconnectRemoteServices() {
if(mAccelServConn != null && mAccelServ != null) {
unbindService(mAccelServConn);
}
if(mWifiGPSServConn != null && mWiFiGPSServ != null) {
unbindService(mWifiGPSServConn);
}
mAccelServ = null;
mAccelServConn = null;
mWiFiGPSServ = null;
mWifiGPSServConn = null;
}
private void acquireWakeLock() {
if(!mWakeLock.isHeld()) {
mWakeLock.acquire();
}
}
private void releaseWakeLock() {
if(mWakeLock.isHeld()) {
mWakeLock.release();
}
}
private static void acquireRecvrWakeLock(Context context) {
if(mRecvrWakeLock == null) {
PowerManager powerMan = (PowerManager) context.
getSystemService(POWER_SERVICE);
mRecvrWakeLock = powerMan.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, RECR_WAKE_LOCK_TAG);
mRecvrWakeLock.setReferenceCounted(true);
}
if(!mRecvrWakeLock.isHeld()) {
mRecvrWakeLock.acquire();
}
}
private static void releaseRecvrWakeLock() {
if(mRecvrWakeLock == null) {
return;
}
if(mRecvrWakeLock.isHeld()) {
mRecvrWakeLock.release();
}
}
private void handleAlarm(Bundle extras) {
String alm = extras.getString(KEY_ALARM_ACTION);
if(alm.equals(ACTION_ALM_TRIGGER_ALWAYS)) {
handleTriggerAlwaysAlarm(extras.getInt(KEY_TRIG_ID));
}
else if(alm.equals(ACTION_ALRM_GPS_SAMPLE)) {
handleSampleGPSAlarm();
}
else if(alm.equals(ACTION_ALRM_GPS_TIMEOUT)) {
handleGPSTimeoutAlarm();
}
else if(alm.equals(ACTION_ALRM_PASS_THROUGH)) {
handlePassThroughCheckAlarm(extras.getInt(KEY_SAMPLING_ALARM_EXTRA));
}
}
private void setKeepAliveAlarm() {
Intent i = new Intent(ACTION_ALRM_SRV_KEEP_ALIVE);
//set the alarm if not already existing
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_NO_CREATE);
AlarmManager alarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
if(pi != null) {
alarmMan.cancel(pi);
pi.cancel();
}
pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_CANCEL_CURRENT);
alarmMan.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + SERV_KEEP_ALIVE_TIME,
SERV_KEEP_ALIVE_TIME, pi);
}
private Intent createTriggerAlwaysAlarmIntent(int trigId) {
Intent i = new Intent();
i.setAction(ACTION_ALM_TRIGGER_ALWAYS);
i.setData(Uri.parse(DATA_PREFIX_TRIG_ALWAYS_ALM + trigId));
i.putExtra(KEY_TRIG_ID, trigId);
return i;
}
private void cancelTriggerAlwaysAlarm(int trigId) {
Intent i = createTriggerAlwaysAlarmIntent(trigId);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_NO_CREATE);
if(pi != null) {
//cancel the alarm
AlarmManager alarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
alarmMan.cancel(pi);
pi.cancel();
}
}
private void setTriggerAlwaysAlarm(int trigId, String trigDesc) {
cancelTriggerAlwaysAlarm(trigId);
LocTrigDesc desc = new LocTrigDesc();
if(!desc.loadString(trigDesc)) {
return;
}
if(!desc.shouldTriggerAlways()) {
return;
}
Log.v(TAG, "LocTrigService: Setting trigger always alarm(" +
trigId + ", " + trigDesc + ")");
Calendar target = Calendar.getInstance();
target.set(Calendar.HOUR_OF_DAY, desc.getEndTime().getHour());
target.set(Calendar.MINUTE, desc.getEndTime().getMinute());
target.set(Calendar.SECOND, 0);
LocationTrigger locTrig = new LocationTrigger();
if(locTrig.hasTriggeredToday(this, trigId)) {
target.add(Calendar.DAY_OF_YEAR, 1);
}
else if(System.currentTimeMillis() >= target.getTimeInMillis()) {
target.add(Calendar.DAY_OF_YEAR, 1);
}
Intent i = createTriggerAlwaysAlarmIntent(trigId);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
Log.v(TAG, "LocTrigService: Calculated target time: " +
target.getTime().toString());
long alarmTime = target.getTimeInMillis();
/* Convert the alarm time to elapsed real time.
* If we dont do this, a time change in the system might
* set off all the alarms and a trigger might go off before
* we get a chance to cancel it
*/
long elapsedRT = alarmTime - System.currentTimeMillis();
if(elapsedRT <= 0) {
Log.v(TAG, "LocTrigService: negative elapsed realtime - "
+ "alarm not setting: "
+ trigId);
return;
}
Log.v(TAG, "LocTrigService: Setting alarm for " + elapsedRT
+ " millis into the future");
alarmMan.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + elapsedRT, pi);
}
private void updateLocTracingState() {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(this);
mLocTraceEnabled = prefs.getBoolean(
LocTrigTracingSettActivity.PREF_KEY_TRACING_STATUS,
false);
mLocTraceUploadAlways = prefs.getBoolean(
LocTrigTracingSettActivity.PREF_KEY_UPLOAD_ALWAYS,
false);
Log.v(TAG, "LocTrigService: Updating tracing status to: "
+ mLocTraceEnabled + ", Upload always = "
+ mLocTraceUploadAlways);
}
private void initState() {
Log.v(TAG, "LocTrigService: initState");
mCurrSleepTime = 0;
mCurrSpeed = 0;
mPrevSpeed = 0;
mCurrProxDist = 0;
mNInitialSamples = 0;
mLastKnownLoc.reset();
mLastKnownLocBackup.reset();
mLastKnownLocTime = 0;
mLastKnownLocTimeBackup = 0;
mLatestCateg = CATEG_ID_INVAL;
mPassThroughChecking = false;
mPassThroughCheckCateg = CATEG_ID_INVAL;
mGPSStarted = false;
mMotionDetectTS = 0;
mCategPrevTS = LocTrigDB.TIME_STAMP_INVALID;
mLastLocTrace.reset();
mLastLocTrace.setTime(0);
updateLocTracingState();
}
private void startSampling() {
if(mSamplingStarted) {
return;
}
Log.v(TAG, "LocTrigService: Starting sampling");
startGPS();
mSamplingStarted = true;
}
private void stopSampling() {
if(!mSamplingStarted) {
return;
}
Log.v(TAG, "LocTrigService: Stopping sampling");
stopGPS();
//Remove all alarms
cancelAllSamplingAlarms();
mSamplingStarted = false;
releaseWakeLock();
}
private void updateSamplingStatus() {
LinkedList<Integer> actTrigs = new LinkedList<Integer>();
LocationTrigger lt = new LocationTrigger();
DbHelper dbHelper = new DbHelper(this);
for (Campaign c : dbHelper.getReadyCampaigns()) {
actTrigs.addAll(lt.getAllActiveTriggerIds(this, c.mUrn));
}
//Start sampling if there are active surveys
//or if the location tracing is enabled.
if(mLocTraceEnabled ||
(actTrigs.size() > 0 && mLocList.size() > 0)) {
startSampling();
}
else {
stopSampling();
}
}
@SuppressWarnings("unchecked")
private boolean hasUserMoved() {
List<Double> fList = null;
try {
//TODO cast problem
//AccelService must return a typed list
fList = mAccelServ.getLastForce();
} catch (RemoteException e) {
Log.e(TAG, "LocTrigService: Exception while getLastForce", e);
return false;
}
if(fList == null) {
Log.e(TAG, "LocTrigService: AccelService returned null" +
" force list");
return false;
}
if(fList.size() == 0) {
Log.e(TAG, "LocTrigService: AccelService returned empty" +
" force list");
return false;
}
double mean = 0;
for(double force : fList) {
mean += force;
}
mean /= fList.size();
double var = 0;
for(double force : fList) {
var += Math.pow(force - mean, 2);
}
var /= fList.size();
var *= 1000;
Log.v(TAG, "LocTrigService: Variance = " + var);
if(var < MOTION_DETECT_ACCEL_THRESHOLD) {
return false;
}
Log.v(TAG, "LocTrigService: Motion detected");
return true;
}
private void handleWifiGPSLocChange() {
Log.v(TAG, "LocTrigService: WifiGPS loc changed");
long elapsed = SystemClock.elapsedRealtime() - mMotionDetectTS;
if(elapsed < MOTION_DETECT_DELAY ||
mCurrSleepTime < SLEEP_TIME_MIN_MOTION_DETECT) {
Log.v(TAG, "LocTrigService: Too early, ignoring WifiGPS loc change");
return;
}
try {
if(mAccelServ == null || !mAccelServ.isRunning()) {
Log.v(TAG, "Accel service is not running, " +
"stopping motion detection");
stopMotionDetection();
return;
}
} catch (RemoteException e) {
Log.e(TAG, "Error while checking accel status", e);
return;
}
if(hasUserMoved()) {
Log.v(TAG, "LocTrigService: Starting GPS due to movement");
startGPS();
}
}
private void registerMotionDetectionCB(){
Log.v(TAG, "LocTrigService: Registering for WifiGPS CB");
if(mWiFiGPSServ == null || mAccelServ == null) {
Log.v(TAG, "LocTrigService: Not all services are connected yet." +
" Skipping...");
return;
}
Log.v(TAG, "LocTrigService: All services connected, " +
"attempting to register");
//Use the services opportunistically. Do not start
//motion detection if the services are not already
//running
try {
if(!mWiFiGPSServ.isRunning() || !mAccelServ.isRunning()) {
Log.v(TAG, "LocTrigService: Motion detection NOT started " +
"as the services are not already running");
disconnectRemoteServices();
return;
}
mMotionDetectTS = SystemClock.elapsedRealtime();
//Register a location change cb with wifigps
mWiFiGPSServ.registerCallback(REMOTE_CLIENT_NAME, mMotionDetectCB);
/*
* The following line is a hack/work-around. If this is not done,
* the WifiGPS service will continue to run even after all the other
* have stopped. Essentially, the 'registerCallback' function above
* will increase the 'clientCount' inside the service even if we didnt
* start it explicitly. Thus, we need to call stop to nullify the effect.
*/
mWiFiGPSServ.stop(REMOTE_CLIENT_NAME);
} catch (RemoteException e) {
Log.e(TAG, "LocTrigService: Exception while registering wifigps cb", e);
}
}
private void startMotionDetection() {
Log.v(TAG, "LocTrigService: Starting motion detection...");
mWifiGPSServConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v(TAG, "LocTrigService: WifiGPS disconnected");
mWiFiGPSServ = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v(TAG, "LocTrigService: WifiGPS connected");
mWiFiGPSServ = IWiFiGPSLocationService.Stub.asInterface(service);
registerMotionDetectionCB(); }
};
mAccelServConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v(TAG, "LocTrigService: AccelService disconnected");
mAccelServ = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v(TAG, "LocTrigService: AccelService connected");
mAccelServ = IAccelService.Stub.asInterface(service);
registerMotionDetectionCB();
}
};
connectToRemoteServices();
}
private void stopMotionDetection() {
Log.v(TAG, "LocTrigService: Stopping motion detection...");
if(mWiFiGPSServ != null) {
try {
mWiFiGPSServ.unregisterCallback(REMOTE_CLIENT_NAME, mMotionDetectCB);
/*
* A stop call is actually not required. But to be
* on the safer side...
*/
mWiFiGPSServ.stop(REMOTE_CLIENT_NAME);
} catch (RemoteException e) {
Log.e(TAG, "LocTrigService: Exception while stopping motion detection", e);
}
}
disconnectRemoteServices();
}
private void startGPS() {
if(mGPSStarted) {
return;
}
if(LocTrigConfig.useMotionDetection) {
stopMotionDetection();
}
//Get a wake lock for this duty cycle
acquireWakeLock();
mNSamples = 0;
mPrevSpeed = mCurrSpeed;
Log.v(TAG, "LocTrigService: Turning on location updates");
LocationManager locMan = (LocationManager) getSystemService(LOCATION_SERVICE);
locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER,
0, 0, this);
//Use network location as well
if(LocTrigConfig.useNetworkLocation) {
if(mWifiLock != null) {
if(!mWifiLock.isHeld()) {
mWifiLock.acquire();
}
}
locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
0, 0, this);
}
cancelSamplingAlarm(ACTION_ALRM_GPS_SAMPLE);
//Set GPS timeout
setSamplingAlarm(ACTION_ALRM_GPS_TIMEOUT, GPS_TIMEOUT, 0);
mGPSStarted = true;
}
private void stopGPS() {
if(!mGPSStarted) {
return;
}
cancelSamplingAlarm(ACTION_ALRM_GPS_TIMEOUT);
Log.v(TAG, "LocTrigService: Turning off location updates");
LocationManager locMan = (LocationManager) getSystemService(LOCATION_SERVICE);
locMan.removeUpdates(this);
if(LocTrigConfig.useNetworkLocation) {
if(mWifiLock != null) {
if(mWifiLock.isHeld()) {
mWifiLock.release();
}
}
}
mGPSStarted = false;
}
private void uploadLatestLocation() {
final String KEY_LOC_LAT = "latitude";
final String KEY_LOC_LONG = "longitude";
final String KEY_LOC_ACC = "accuracy";
final String KEY_LOC_PROVIDER = "provider";
final String KEY_LOC_TIME = "time";
final String TIME_STAMP_FORMAT = "yyyy-MM-dd HH:mm:ss";
//Check if any location update has been received
if(mLastKnownLocTime == 0 || mLastKnownLoc == null) {
return;
}
//If 'upload always' is not enabled, check for the
//compile time constants and upload only if it is
//allowed
if(!mLocTraceUploadAlways) {
//Check if the user has moved at least the distance specified
if(mLastKnownLoc.distanceTo(mLastLocTrace) <
LocTrigConfig.LOC_TRACE_MIN_DISTANCE_FOR_UPLOAD) {
//Before discarding the trace, check the time stamp
//of the last upload. If it has been longer than the
//specified time, upload.
if(mLastKnownLoc.getTime() - mLastLocTrace.getTime() <
LocTrigConfig.LOC_TRACE_MAX_GAP_BETWEEN_UPLOADS) {
Log.v(TAG, "LocTrigService: Skipping the location" +
" trace upload");
return;
}
}
}
//TODO move loc to JSON conversion to a common place
//The trigger details upload also has the same logic
JSONObject jLoc = new JSONObject();
try {
jLoc.put(KEY_LOC_LAT, mLastKnownLoc.getLatitude());
jLoc.put(KEY_LOC_LONG, mLastKnownLoc.getLongitude());
jLoc.put(KEY_LOC_ACC, mLastKnownLoc.getAccuracy());
jLoc.put(KEY_LOC_PROVIDER, mLastKnownLoc.getProvider());
SimpleDateFormat dateFormat = new SimpleDateFormat(TIME_STAMP_FORMAT);
jLoc.put(KEY_LOC_TIME, dateFormat.format(
new Date(mLastKnownLoc.getTime())));
} catch (JSONException e) {
Log.e(TAG, "LocTrigService: Error while converting " +
" location to JSON for tracing", e);
return;
}
String msg = "Location trace: " + jLoc.toString();
//Upload the trace using LogProbe
Log.v(TAG, "LocTrigService: Upload location trace: " + msg);
//Save this location locally
mLastLocTrace.set(mLastKnownLoc);
}
/* Calculate the sleep time and set the GPS sampling alarm */
private void reScheduleGPS() {
stopGPS();
long sTime = getUpdatedSleepTime();
setSamplingAlarm(ACTION_ALRM_GPS_SAMPLE, sTime, 0);
if(LocTrigConfig.useMotionDetection) {
if(sTime >= SLEEP_TIME_MIN_MOTION_DETECT) {
startMotionDetection();
}
}
//Upload the latest location for location tracing
uploadLatestLocation();
releaseWakeLock();
}
private void handleTriggerAlwaysAlarm(int trigId) {
//Re-confirm that the trigger has not gone off
//today. Just to prevent any issues due to
//async nature of alarms
LocationTrigger locTrig = new LocationTrigger();
if(!locTrig.hasTriggeredToday(this, trigId)) {
locTrig.notifyTrigger(this, trigId);
}
//Set the alarm for the next day
setTriggerAlwaysAlarm(trigId, locTrig.getTrigger(this, trigId));
}
private void handleSampleGPSAlarm() {
Log.v(TAG, "LocTrigService: Handling GPS sample alarm");
if(!mSamplingStarted) {
return;
}
startGPS();
}
private void handleGPSTimeoutAlarm() {
Log.v(TAG, "LocTrigService: Handling GPS timeout");
//If insufficient samples are obtained, timeout the GPS
if(mNSamples < SAMPLES_LIMIT) {
Log.v(TAG, "LocTrigService: Unable to obtain samples. " +
"Timing out");
/* If a pass-through checking is scheduled, notify the user
* anyway. This is because when the GPS times out when a pass
* through check is scheduled, most likely the user has entered
* a building. Thus, it would a best to assume that the user is
* staying at the location of interest where the pass through
* checking has been scheduled.
*/
if(mPassThroughChecking) {
mPassThroughChecking = false;
Log.v(TAG, "LocTrigService: Unable to verify location" +
" after passthrough timer, still notifying");
triggerIfRequired(mPassThroughCheckCateg);
}
//Assume speed = 0 here. This will help the sleep time
//to slowly buildup to maximum value if the user continues
//to remain in a place where it is difficult to get GPS
//samples
Location loc = new Location(LocationManager.GPS_PROVIDER);
loc.setSpeed(0);
recordSpeed(loc);
reScheduleGPS();
}
}
private void handlePassThroughCheckAlarm(int categId) {
Log.v(TAG, "LocTrigService: Handling pass through alarm");
mPassThroughChecking = true;
mPassThroughCheckCateg = categId;
startGPS();
}
private void cancelSamplingAlarm(String action) {
Intent i = new Intent(action);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_NO_CREATE);
if(pi != null) {
AlarmManager alarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
alarmMan.cancel(pi);
pi.cancel();
}
}
private void setSamplingAlarm(String action, long timeOut, int extra) {
Log.v(TAG, "LocTrigService: Setting alarm: " + action);
cancelSamplingAlarm(action);
Intent i = new Intent(action);
i.putExtra(KEY_SAMPLING_ALARM_EXTRA, extra);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
alarmMan.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + timeOut, pi);
}
private void cancelAllSamplingAlarms() {
cancelSamplingAlarm(ACTION_ALRM_GPS_SAMPLE);
cancelSamplingAlarm(ACTION_ALRM_GPS_TIMEOUT);
cancelSamplingAlarm(ACTION_ALRM_PASS_THROUGH);
}
/* Populate the cached list of locations */
private void populateLocList() {
mLocList.clear();
LocTrigDB db = new LocTrigDB(this);
db.open();
Cursor c = db.getAllLocations();
Log.v(TAG, "LocTrigService: populating loc list with "
+ c.getCount() + " locations");
if(c.moveToFirst()) {
do {
int latE6 = c.getInt(c.getColumnIndexOrThrow(LocTrigDB.KEY_LAT));
int longE6 = c.getInt(c.getColumnIndexOrThrow(LocTrigDB.KEY_LONG));
int cId = c.getInt(c.getColumnIndexOrThrow(LocTrigDB.KEY_CATEGORY_ID));
float r = c.getFloat(c.getColumnIndexOrThrow(LocTrigDB.KEY_RADIUS));
Log.v(TAG, "LocTrigService: adding to the list the location: "
+ latE6 + ", " + longE6
+ ", category id = " + cId
+ ", radius = " + r);
mLocList.add(new LocListItem(latE6, longE6, cId, r));
} while(c.moveToNext());
}
c.close();
db.close();
}
/*
* Check if a location coordinate correspond to a category.
* Return the category id in that case.
*/
private int getLocCategory(Location loc) {
for(LocListItem item : mLocList) {
float[] dist = new float[1];
Location.distanceBetween(loc.getLatitude(), loc.getLongitude(),
item.gp.getLatitudeE6() / 1E6,
item.gp.getLongitudeE6() / 1E6,
dist);
//Check if the given location (including its accuracy)
//completely falls inside an existing location (with an
//error threshold)
if((dist[0] + loc.getAccuracy()) <= (item.radius + CATEG_ACCURACY_MARGIN)) {
return item.categoryId;
}
}
return CATEG_ID_INVAL;
}
/*
* Can be optimized by caching
*/
private long getMinCategoryExpirationTime(int categId) {
LocationTrigger locTrig = new LocationTrigger();
LocTrigDB db = new LocTrigDB(this);
db.open();
String categName = db.getCategoryName(categId);
db.close();
int minReentry = -1;
LinkedList<Integer> trigs = new LinkedList<Integer>();
DbHelper dbHelper = new DbHelper(this);
for (Campaign c : dbHelper.getReadyCampaigns()) {
trigs.addAll(locTrig.getAllActiveTriggerIds(this, c.mUrn));
}
for(int trig : trigs) {
LocTrigDesc desc = new LocTrigDesc();
if(!desc.loadString(locTrig.getTrigger(this, trig))) {
continue;
}
if(!desc.getLocation().equalsIgnoreCase(categName)) {
continue;
}
int cur = desc.getMinReentryInterval();
if(minReentry == -1) {
minReentry = cur;
}
else if(cur < minReentry) {
minReentry = cur;
}
}
if(minReentry == -1) {
return -1;
}
return (minReentry * 60 * 1000);
}
private boolean checkIfCategoryExpired(int categId) {
LocTrigDB db = new LocTrigDB(this);
db.open();
boolean expired = false;
long categTS = db.getCategoryTimeStamp(categId);
if(categTS == LocTrigDB.TIME_STAMP_INVALID) {
expired = true;
}
else {
long elapsed = System.currentTimeMillis() - categTS;
if(elapsed >= getMinCategoryExpirationTime(categId)) {
expired = true;
}
}
db.close();
return expired;
}
private float getDistanceToClosestCategory() {
float minDist = -1;
for(LocListItem item : mLocList) {
//Check the distance to all locations to watch for.
//This includes only those locations which have not
//expired.
if(checkIfCategoryExpired(item.categoryId)) {
float[] dist = new float[1];
Location.distanceBetween(mLastKnownLoc.getLatitude(),
mLastKnownLoc.getLongitude(),
item.gp.getLatitudeE6() / 1E6,
item.gp.getLongitudeE6() / 1E6,
dist);
float d = dist[0] - item.radius;
if(d > 0) {
minDist = (minDist == -1) ? d :
Math.min(minDist, d);
}
}
}
//If there is no closest location, and if the tracing
//needs to be done, use a constant factor
if(minDist == -1 && mLocTraceEnabled) {
minDist = LocTrigConfig.LOC_TRACE_DISTANCE_FACTOR;
}
return minDist;
}
/* Reset the current sleep time */
private void resetSleepTime() {
mCurrSleepTime = Math.min(MIN_SLEEP_TIME, mCurrSleepTime);
}
/* Calculate the new sleep time based on the current speed
* and the distance to the closest category.
*/
private long getUpdatedSleepTime() {
long sTime = mCurrSleepTime;
float sleepDist = 0;
float proximityDist = getDistanceToClosestCategory();
Log.v(TAG, "LocTrigService: Proximity dist: " + proximityDist);
//Based on the proximity, calculate the distance (sleepDist)
//which should be covered before the next sampling
if(proximityDist != -1) {
//sampleDist is a proportional to the proximity
sleepDist = proximityDist / SLEEP_DIST_FACTOR;
//If the user has covered half the proximity distance
//reset the sleep time. This will help when the speed
//cannot be measured and thus the sample time increases
//even though the user moves close to a location.
if(proximityDist <= mCurrProxDist / 2 && mCurrSpeed == 0) {
sTime = Math.min(MIN_SLEEP_TIME, sTime);
}
mCurrProxDist = proximityDist;
}
//Check if the user is idle
if(mCurrSpeed == 0) {
//if the phone is idle now and was moving before,
//reset the sleep time.
if(mPrevSpeed != 0) {
sTime = MIN_SLEEP_TIME;
}
else {
//Double interval when the phone maintains
//the idle status
sTime *= 2;
if(sTime == 0) {
sTime = MIN_SLEEP_TIME;
}
}
}
//If the speed is non-zero, calculate the sleep time based on it
else if(sleepDist != 0) {
float roundSpeed = (float) Math.ceil(mCurrSpeed);
sTime = (long) ((sleepDist / roundSpeed) * 1000);
}
else {
sTime = MAX_SLEEP_TIME;
}
//Bound the sleep time
sTime = Math.min(MAX_SLEEP_TIME, sTime);
Log.v(TAG, "LocTrigService: Sleep time updated to: " + sTime);
mCurrSleepTime = sTime;
return sTime;
}
private void recordSpeed(Location loc) {
//Record speed as a running average
if(loc.hasSpeed()) {
mCurrSpeed = (mCurrSpeed + loc.getSpeed()) / 2;
}
else if(mCurrSpeed == 0 && mLastKnownLocTimeBackup != 0 &&
mLastKnownLocBackup.getAccuracy() <= INACCURATE_SAMPLE_THRESHOLD) {
float disp = mLastKnownLoc.distanceTo(mLastKnownLocBackup);
Log.v(TAG, "LocTrigService: displacement: " + disp);
//Calculate the speed only if the user has moved beyond
//a threshold value.
if(disp > SPEED_CALC_MIN_DISPLACEMENT) {
Log.v(TAG, "LocTrigService: Calculating speed...");
long dT = mLastKnownLocTime - mLastKnownLocTimeBackup;
mCurrSpeed = disp / dT;
}
}
//Round off very small values
if(mCurrSpeed < SPEED_MIN_THRESHOLD) {
mCurrSpeed = 0;
}
Log.v(TAG, "LocTrigService: Current speed: " + mCurrSpeed);
}
private void recordLocation(Location loc) {
//Backup the previous location, needed for speed
//calculation.
if(mNSamples == 0) {
mLastKnownLocBackup.set(mLastKnownLoc);
mLastKnownLocTimeBackup = mLastKnownLocTime;
}
mLastKnownLoc.set(loc);
mLastKnownLocTime = SystemClock.elapsedRealtime();
}
/* Save the id of the category last visited */
private void recordLatestCategory(int categId) {
mLatestCateg = categId;
if(categId != CATEG_ID_INVAL) {
LocTrigDB db = new LocTrigDB(this);
db.open();
db.setCategoryTimeStamp(categId, System.currentTimeMillis());
db.close();
}
}
private void triggerIfRequired(int categId) {
LocTrigDB db = new LocTrigDB(this);
db.open();
String categName = db.getCategoryName(categId);
db.close();
LocationTrigger locTrig = new LocationTrigger();
LinkedList<Integer> trigs = new LinkedList<Integer>();
DbHelper dbHelper = new DbHelper(this);
for (Campaign c : dbHelper.getReadyCampaigns()) {
trigs.addAll(locTrig.getAllActiveTriggerIds(this, c.mUrn));
}
for(int trigId : trigs) {
LocTrigDesc desc = new LocTrigDesc();
if(!desc.loadString(locTrig.getTrigger(this, trigId))) {
continue;
}
if(!desc.getLocation().equalsIgnoreCase(categName)) {
continue;
}
if(desc.isRangeEnabled()) {
Log.v(TAG, "LocTrigService: Range enabling. Checking whether" +
" to trigger");
if(locTrig.hasTriggeredToday(this, trigId)) {
Log.v(TAG, "LocTrigService: Has triggered today, skipping");
continue;
}
Calendar cal = Calendar.getInstance();
SimpleTime now = new SimpleTime(cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE));
if(now.isBefore(desc.getStartTime())) {
continue;
}
SimpleTime end = desc.getEndTime();
if(now.isAfter(end)) {
continue;
}
else if(now.equals(end) && cal.get(Calendar.SECOND) > 0){
continue;
}
Log.v(TAG, "LocTrigService: Triggering now");
cancelTriggerAlwaysAlarm(trigId);
locTrig.notifyTrigger(this, trigId);
}
else if(mCategPrevTS == LocTrigDB.TIME_STAMP_INVALID) {
Log.v(TAG, "LocTrigService: Invalid categ timestamp." +
" Triggering...");
locTrig.notifyTrigger(this, trigId);
}
else {
long elapsed = System.currentTimeMillis() - mCategPrevTS;
long minReentry = desc.getMinReentryInterval() * 60 * 1000;
if(elapsed > minReentry) {
Log.v(TAG, "LocTrigService: Beyond minimum re-entry. " +
"Triggering...");
locTrig.notifyTrigger(this, trigId);
}
else {
Log.v(TAG, "LocTrigService: Minimum re-entry has not expired. " +
" Not triggering.");
}
}
}
}
/* Notify the user if pass through check succeeds */
private void handlePassThroughCheckIfRequired(int categId) {
if(mPassThroughChecking) {
if(categId == mPassThroughCheckCateg) {
Log.v(TAG, "LocTrigService: User hasnt changed category, " +
"notifying");
triggerIfRequired(categId);
}
mPassThroughChecking = false;
}
}
/* Cancel the pass through check */
private void cancelPassThroughCheckingIfRequired() {
if(mPassThroughChecking) {
mPassThroughChecking = false;
}
}
@Override
public void onLocationChanged(Location loc) {
Log.v(TAG, "LocTrigService: new location received: " +
loc.getLatitude() + ", " +
loc.getLongitude() + " (" +
loc.getProvider() + "), accuracy = " +
loc.getAccuracy() + ", speed = " +
loc.getSpeed() + ", Time = " +
new Date(loc.getTime()).toString());
if(!mGPSStarted) {
Log.v(TAG, "LocTrigService: Discarding stray location " +
"after disabling locaiton updates");
return;
}
//Discard if a stale location is received (could be possible in network
//location)
if((loc.getTime() + STALE_LOC_TIME) < System.currentTimeMillis()) {
Log.v(TAG, "LocTrigService: Discarding stale location");
return;
}
/* Check if the last known location belongs to a category.
* This is required when the user defines a category on the current
* location. At this time, the surveys must not be
* immediately triggered but only when the user enters this
* category the next time. Thus, at this point, check if the last
* known location has a category. This means that the last known location
* must not be triggered.
*
* There is a corner case where this will not work. Suppose, the user entered
* a location and the GPS sampling hasn't been done yet. So, this location is
* not yet recorded. If the user defines a new category on this location and
* sets triggers, it will be triggered. This can happen only if the user sets
* a trigger with in MAX_SLEEP_TIME after entering a location.
*/
int prevCateg = getLocCategory(mLastKnownLoc);
if(prevCateg != CATEG_ID_INVAL && mLatestCateg == CATEG_ID_INVAL) {
Log.v(TAG, "LocTrigService: Assigning category to the prev loc");
recordLatestCategory(prevCateg);
}
if(!loc.hasAccuracy()) {
Log.v(TAG, "LocTrigService: Discarding loc with no accuracy info");
return;
}
recordLocation(loc);
int locCateg = getLocCategory(loc);
Log.v(TAG, "LocTrigService: Loc category = " + locCateg);
if(locCateg != CATEG_ID_INVAL) {
handlePassThroughCheckIfRequired(locCateg);
//If entering a new category, trigger if necessary
//set the pass through check alarm first
if(locCateg != mLatestCateg) {
//start triggering only after sufficient number of
//initial samples have been collected
if(mNInitialSamples >= SAMPLES_LIMIT) {
//Cache the previous visit time for this category
//as it is going to be updated now
LocTrigDB db = new LocTrigDB(this);
db.open();
mCategPrevTS = db.getCategoryTimeStamp(locCateg);
Log.v(TAG, "LocTrigService: Caching category time stamp: " +
mCategPrevTS);
db.close();
setSamplingAlarm(ACTION_ALRM_PASS_THROUGH,
PASS_THROUGH_TIME, locCateg);
}
}
recordLatestCategory(locCateg);
mNInitialSamples = SAMPLES_LIMIT;
mNSamples = SAMPLES_LIMIT;
mCurrSpeed = 0;
reScheduleGPS();
return;
}
else {
//Discard very inaccurate samples
if(loc.getAccuracy() > INACCURATE_SAMPLE_THRESHOLD) {
/* Refresh the time stamp of the latest category.
* If the user enters a building within a category
* then that category might expire due to inaccurate
* samples. Refreshing the time stamp will prevent
* duplicate triggers from happening at that category
*/
recordLatestCategory(mLatestCateg);
Log.v(TAG, "LocTrigService: Discarding inaccurate sample");
return;
}
//Detect an exit from a category and start aggressive sampling
if(mLatestCateg != CATEG_ID_INVAL) {
resetSleepTime();
}
recordLatestCategory(locCateg);
//Do not turn off updates until sufficient samples with speed info
//are obtained
if(LocTrigConfig.useNetworkLocation) {
if(loc.getProvider().equals(LocationManager.NETWORK_PROVIDER) &&
!loc.hasSpeed()) {
Log.v(TAG, "LocTrigService: Discarding network " +
"location without speed");
return;
}
}
recordSpeed(loc);
//Collect some initial samples when the service/sampling starts for
//the first time. This is to establish the current location.
//This makes sure than the very first trigger set on the current
//location is not triggered immediately.
if(mNInitialSamples < SAMPLES_LIMIT) {
mNInitialSamples++;
Log.v(TAG, "LocTrigService: Collecting initial samples");
return;
}
mNSamples++;
//If the sample count hits the limit, reschedule.
if(mNSamples == SAMPLES_LIMIT) {
cancelPassThroughCheckingIfRequired();
reScheduleGPS();
}
}
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
public void onProviderEnabled(String provider) {
updateSamplingStatus();
}
@Override
public void onStatusChanged(String provider, int status,
Bundle extras) {
}
/************************ INNER CLASSES ************************/
/* Class representing an item in the cached location list */
private class LocListItem {
public GeoPoint gp;
public float radius;
public int categoryId;
public LocListItem(int latE6, int longE6, int cId, float radius) {
this.gp = new GeoPoint(latE6, longE6);
this.categoryId = cId;
this.radius = radius;
}
}
/* Receiver for all the alarms */
public static class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
acquireRecvrWakeLock(context);
Intent i = new Intent(context, LocTrigService.class);
i.setAction(ACTION_HANDLE_ALARM);
i.replaceExtras(intent);
i.putExtra(KEY_ALARM_ACTION, intent.getAction());
context.startService(i);
}
}
}