/*******************************************************************************
* 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.time;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import org.ohmage.logprobe.Analytics;
import org.ohmage.logprobe.Log;
import org.ohmage.logprobe.LogProbe.Status;
import org.ohmage.triggers.utils.SimpleTime;
import java.util.Calendar;
import java.util.Random;
public class TimeTrigService extends Service {
private static final String TAG = "TimeTrigger";
public static final String WAKE_LOCK_NAME =
"org.ohmage.triggers.types.timeTimeTrigService.wake_lock";
public static final String ACTION_HANDLE_TRIGGER = "handle_alarm";
public static final String ACTION_SET_TRIGGER = "set_trigger";
public static final String ACTION_REMOVE_TRIGGER = "remove_trigger";
public static final String ACTION_RESET_TRIGGER = "reset_trigger";
public static final String KEY_TRIG_ID = "trigger_id";
public static final String KEY_TRIG_DESC = "trigger_desc";
private static final String ACTION_TRIG_ALM =
"edu.ucla.cens.triggers.types.time.TimeTriggerAlarm";
private static final String DATA_PREFIX_TRIG_ALM =
"timetrigger://edu.ucla.cens.triggers.types.time/";
private AlarmManager mAlarmMan = null;
private static PowerManager.WakeLock mWakeLock = null;
@Override
public void onCreate() {
super.onCreate();
Analytics.service(this, Status.ON);
mAlarmMan = (AlarmManager) getSystemService(ALARM_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(TAG, "TimeTriggerService: onStart");
String action = intent.getAction();
if(action == null ||
!intent.hasExtra(KEY_TRIG_ID) ||
!intent.hasExtra(KEY_TRIG_DESC)) {
Log.e(TAG, "TimeTriggerService: Started with invalid intent");
releaseWakeLock();
return START_NOT_STICKY;
}
int trigId = intent.getIntExtra(KEY_TRIG_ID, -1);
String trigDesc = intent.getStringExtra(KEY_TRIG_DESC);
if(action.equals(ACTION_HANDLE_TRIGGER)) {
Log.v(TAG, "TimeTriggerService: Handling trigger "
+ trigId);
//Notify user
new TimeTrigger().notifyTrigger(this, trigId);
//repeat the alarm
setTrigger(trigId, trigDesc);
}
else if(action.equals(ACTION_SET_TRIGGER)) {
Log.v(TAG, "TimeTriggerService: Setting trigger "
+ trigId);
setTrigger(trigId, trigDesc);
}
else if(action.equals(ACTION_REMOVE_TRIGGER)) {
Log.v(TAG, "TimeTriggerService: Removing trigger "
+ trigId);
removeTrigger(trigId, trigDesc);
}
else if(action.equals(ACTION_RESET_TRIGGER)) {
Log.v(TAG, "TimeTriggerService: Resetting trigger "
+ trigId);
removeTrigger(trigId, trigDesc);
setTrigger(trigId, trigDesc);
}
releaseWakeLock();
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Analytics.service(this, Status.OFF);
releaseWakeLock();
}
private static void acquireWakeLock(Context context) {
if(mWakeLock == null) {
PowerManager powerMan = (PowerManager) context.
getSystemService(POWER_SERVICE);
mWakeLock = powerMan.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_NAME);
mWakeLock.setReferenceCounted(true);
}
if(!mWakeLock.isHeld()) {
mWakeLock.acquire();
}
}
private static void releaseWakeLock() {
if(mWakeLock == null) {
return;
}
if(mWakeLock.isHeld()) {
mWakeLock.release();
}
}
private Intent createAlarmIntent(int trigId, String trigDesc) {
Intent i = new Intent();
i.setAction(ACTION_TRIG_ALM);
i.setData(Uri.parse(DATA_PREFIX_TRIG_ALM + trigId));
i.putExtra(KEY_TRIG_ID, trigId);
i.putExtra(KEY_TRIG_DESC, trigDesc);
return i;
}
private Calendar getTriggerTimeForToday(int trigId, TimeTrigDesc trigDesc) {
TimeTrigger timeTrig = new TimeTrigger();
if(timeTrig.hasTriggeredToday(this, trigId)) {
return null;
}
Calendar now = Calendar.getInstance();
Calendar target = Calendar.getInstance();
target.set(Calendar.SECOND, 0);
if(!trigDesc.isRandomized()) {
target.set(Calendar.HOUR_OF_DAY, trigDesc.getTriggerTime().getHour());
target.set(Calendar.MINUTE, trigDesc.getTriggerTime().getMinute());
if(now.before(target)) {
return target;
}
}
else { //if randomized, check if there is any more time left in the interval
SimpleTime tCurr = new SimpleTime();
SimpleTime tStart = trigDesc.getRangeStart();
SimpleTime tEnd = trigDesc.getRangeEnd();
if(tCurr.isBefore(tEnd)) {
int diff;
if(tCurr.isAfter(tStart)) {
diff = tCurr.differenceInMinutes(tEnd);
target.set(Calendar.HOUR_OF_DAY, tCurr.getHour());
target.set(Calendar.MINUTE, tCurr.getMinute());
}
else {
diff = tStart.differenceInMinutes(tEnd);
target.set(Calendar.HOUR_OF_DAY, tStart.getHour());
target.set(Calendar.MINUTE, tStart.getMinute());
}
Random rand = new Random();
//Generate a random number (both ranges inclusive)
target.add(Calendar.MINUTE, rand.nextInt(diff + 1));
return target;
}
}
return null;
}
private Calendar getTriggerTimeForDay(int trigId, TimeTrigDesc trigDesc,
int dayOffset) {
Calendar target = Calendar.getInstance();
target.add(Calendar.DAY_OF_YEAR, dayOffset);
String dayStr = TimeTrigDesc.getDayOfWeekString(
target.get(Calendar.DAY_OF_WEEK));
if(!trigDesc.doesRepeatOnDay(dayStr)) {
return null;
}
if(dayOffset == 0) {
return getTriggerTimeForToday(trigId, trigDesc);
}
target.set(Calendar.SECOND, 0);
if(!trigDesc.isRandomized()) {
target.set(Calendar.HOUR_OF_DAY, trigDesc.getTriggerTime().getHour());
target.set(Calendar.MINUTE, trigDesc.getTriggerTime().getMinute());
}
else {
target.set(Calendar.HOUR_OF_DAY, trigDesc.getRangeStart().getHour());
target.set(Calendar.MINUTE, trigDesc.getRangeStart().getMinute());
int diff = trigDesc.getRangeStart()
.differenceInMinutes(trigDesc.getRangeEnd());
Random rand = new Random();
//Generate a random number (both ranges inclusive)
target.add(Calendar.MINUTE, rand.nextInt(diff + 1));
}
return target;
}
private long getAlarmTimeInMillis(int trigId, TimeTrigDesc trigDesc) {
for(int i = 0; i <= 7; i++) {
Calendar target = getTriggerTimeForDay(trigId, trigDesc, i);
if(target != null) {
Log.v(TAG, "TimeTriggerService: Calculated target time: " +
target.getTime().toString());
return target.getTimeInMillis();
}
}
Log.e(TAG, "TimeTriggerService: No valid day of the week found!");
//Must not reach here
return -1;
}
private void cancelAlarm(int trigId, String trigDesc) {
Intent i = createAlarmIntent(trigId, trigDesc);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_NO_CREATE);
if(pi != null) {
//remove the pending intent
Log.v(TAG, "TimeTriggerService: Canceling the pending" +
" intent and alarm for id: " + trigId);
mAlarmMan.cancel(pi);
pi.cancel();
}
}
private void setAlarm(int trigId, TimeTrigDesc desc) {
//Cancel the pending intent and the existing alarm first
cancelAlarm(trigId, desc.toString());
Log.v(TAG, "TimeTriggerService: Attempting to set trigger "
+ trigId);
Intent i = createAlarmIntent(trigId, desc.toString());
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,
PendingIntent.FLAG_CANCEL_CURRENT);
long alarmTime = getAlarmTimeInMillis(trigId, desc);
if(alarmTime == -1) {
Log.v(TAG, "TimeTriggerService: No valid time found for "
+ trigId);
return;
}
/* 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, "TimeTriggerService: negative elapsed realtime - "
+ "alarm not setting: "
+ trigId);
return;
}
Log.v(TAG, "TimeTriggerService: Setting alarm for " + elapsedRT
+ " millis into the future");
mAlarmMan.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + elapsedRT, pi);
}
private void setTrigger(int trigId, String trigDesc) {
Log.v(TAG, "TimeTriggerService: Attempting to set " +
"the trigger: " + trigId);
TimeTrigDesc desc = new TimeTrigDesc();
if(desc.loadString(trigDesc)) {
setAlarm(trigId, desc);
}
else {
Log.e(TAG, "TimeTriggerService: Failed to parse" +
" trigger config: id = " + trigId);
}
}
private void removeTrigger(int trigId, String trigDesc) {
cancelAlarm(trigId, trigDesc);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
/* Receiver for alarms */
public static class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.v(TAG, "TimeTriggerService: Recieved broadcast");
if(intent.getAction().equals(ACTION_TRIG_ALM)) {
Log.v(TAG, "TimeTriggerService: Handling alarm event");
acquireWakeLock(context);
Intent i = new Intent(context, TimeTrigService.class);
i.setAction(ACTION_HANDLE_TRIGGER);
i.replaceExtras(intent);
context.startService(i);
}
}
}
}