/*
* Calendula - An assistant for personal medication management.
* Copyright (C) 2016 CITIUS - USC
*
* Calendula is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software. If not, see <http://www.gnu.org/licenses/>.
*/
package es.usc.citius.servando.calendula.scheduling;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import es.usc.citius.servando.calendula.CalendulaApp;
import es.usc.citius.servando.calendula.R;
import es.usc.citius.servando.calendula.activities.ConfirmActivity;
import es.usc.citius.servando.calendula.activities.ReminderNotification;
import es.usc.citius.servando.calendula.database.DB;
import es.usc.citius.servando.calendula.persistence.DailyScheduleItem;
import es.usc.citius.servando.calendula.persistence.Routine;
import es.usc.citius.servando.calendula.persistence.Schedule;
import es.usc.citius.servando.calendula.persistence.ScheduleItem;
/**
*
*/
public class AlarmScheduler {
public static final String EXTRA_PARAMS = "alarm_params";
private static final String TAG = "AlarmScheduler";
private static final AlarmScheduler instance = new AlarmScheduler();
private AlarmScheduler() {
}
// static method to get the AlarmScheduler instance
public static AlarmScheduler instance() {
return instance;
}
public static boolean isWithinDefaultMargins(DateTime t, Context cxt) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(cxt);
String delayMinutesStr = prefs.getString("alarm_reminder_window", "60");
long window = Long.parseLong(delayMinutesStr);
DateTime now = DateTime.now();
return t.isBefore(now) && t.plusMillis((int) window * 60 * 1000).isAfter(now);
}
/*
* Whether an alarm for a specific time can be scheduled or not based on
* the alarm time and the alarm reminder window defined by the user. Alarm time plus
* alarm window must be in the future to allow alarm scheduling
*/
public static boolean canBeScheduled(DateTime t, Context cxt) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(cxt);
String delayMinutesStr = prefs.getString("alarm_reminder_window", "60");
int window = (int) Long.parseLong(delayMinutesStr);
return t.plusMinutes(window).isAfterNow();
}
public static void setAlarmParams(Intent intent, AlarmIntentParams parcelable) {
Bundle b = new Bundle();
b.putParcelable(EXTRA_PARAMS, parcelable);
intent.putExtra(EXTRA_PARAMS, b);
intent.putExtra("date", parcelable.date);
}
public static AlarmIntentParams getAlarmParams(Intent intent) {
// try to get the bundle
Bundle bundleExtra = intent.getBundleExtra(EXTRA_PARAMS);
if (bundleExtra != null) {
return bundleExtra.getParcelable(EXTRA_PARAMS);
} else {
// try to get the parcelable from the intent
return intent.getParcelableExtra(EXTRA_PARAMS);
}
}
private static PendingIntent pendingIntent(Context ctx, Routine routine, LocalDate date, boolean delayed, int actionType){
Intent intent = new Intent(ctx, AlarmReceiver.class);
AlarmIntentParams params = AlarmIntentParams.forRoutine(routine.getId(), date, delayed, actionType);
setAlarmParams(intent, params);
return PendingIntent.getBroadcast(ctx, params.hashCode(), intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
private static PendingIntent pendingIntent(Context ctx, Schedule schedule, LocalTime time, LocalDate date, boolean delayed, int actionType){
Intent intent = new Intent(ctx, AlarmReceiver.class);
AlarmIntentParams params = AlarmIntentParams.forSchedule(schedule.getId(), time, date, delayed,actionType);
setAlarmParams(intent, params);
return PendingIntent.getBroadcast(ctx, params.hashCode(), intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
public void onAlarmReceived(AlarmIntentParams params, Context ctx) {
Routine routine = Routine.findById(params.routineId);
if (routine != null) {
Log.d(TAG, "onAlarmReceived: " + routine.getId() + ", " + routine.name());
if (params.actionType == AlarmIntentParams.USER || isWithinDefaultMargins(routine, params.date(), ctx)) {
Log.d(TAG, "Routine alarm received, is user action: " + (params.actionType == AlarmIntentParams.USER));
onRoutineTime(routine, params, ctx);
} else {
Log.d(TAG, "Routine lost");
onRoutineLost(routine, params, ctx);
}
}
}
public void onHourlyAlarmReceived(AlarmIntentParams params, Context ctx) {
Schedule schedule = Schedule.findById(params.scheduleId);
if (schedule != null) {
DateTime time = params.dateTime();
if (params.actionType == AlarmIntentParams.USER || isWithinDefaultMargins(time, ctx)) {
Log.d(TAG, "Hourly alarm received, is user action: " + (params.actionType == AlarmIntentParams.USER));
onHourlyScheduleTime(schedule, params, ctx);
} else {
Log.d(TAG, "Schedule lest");
onHourlyScheduleLost(schedule, params, ctx);
}
}
}
public void onDelayRoutine(Routine r, LocalDate date, Context ctx) {
if (isWithinDefaultMargins(r, date, ctx)) {
setRepeatAlarm(r, AlarmIntentParams.forRoutine(r.getId(), date, true), ctx, getAlarmRepeatFreq(ctx) * 60 * 1000);
}
}
public void onDelayHourlySchedule(Schedule s, LocalTime t, LocalDate date, Context ctx) {
if (isWithinDefaultMargins(date.toDateTime(t), ctx)) {
setRepeatAlarm(s, AlarmIntentParams.forSchedule(s.getId(), t, date, true), ctx, getAlarmRepeatFreq(ctx) * 60 * 1000);
}
}
public void onUserDelayHourlySchedule(Schedule s, LocalTime t, LocalDate date, Context ctx, long delayMinutes) {
cancelHourlyDelayedAlarm(s, t, date, ctx, AlarmIntentParams.AUTO);
setRepeatAlarm(s, AlarmIntentParams.forSchedule(s.getId(), t, date, true, AlarmIntentParams.USER), ctx, delayMinutes * 60 * 1000);
}
public void onUserDelayRoutine(Routine r, LocalDate date, Context ctx, int delayMinutes) {
cancelDelayedAlarm(r, date, ctx, AlarmIntentParams.AUTO);
setRepeatAlarm(r, AlarmIntentParams.forRoutine(r.getId(), date, true, AlarmIntentParams.USER), ctx, delayMinutes * 60 * 1000);
}
public void updateAllAlarms(Context ctx) {
for (Schedule schedule : Schedule.findAll()) {
setAlarmsIfNeeded(schedule, LocalDate.now(), ctx);
}
}
public boolean isWithinDefaultMargins(Routine r, LocalDate date, Context cxt) {
return isWithinDefaultMargins(date.toDateTime(r.time()), cxt);
}
public void onIntakeCancelled(Routine r, LocalDate date, Context ctx) {
// set time taken
cancelIntake(r, date, ctx);
// cancel alarms
onIntakeCompleted(r, date, ctx);
}
public void onIntakeCancelled(Schedule s, LocalTime t, LocalDate date, Context ctx) {
// set time taken
cancelIntake(s, t, date, ctx);
// cancell all alarms
onIntakeCompleted(s, t, date, ctx);
}
public void onIntakeCompleted(Routine r, LocalDate date, Context ctx) {
// cancel notification
ReminderNotification.cancel(ctx, ReminderNotification.routineNotificationId(r.getId().intValue()));
// cancel all delay alarms
cancelDelayedAlarm(r, date, ctx, AlarmIntentParams.USER);
cancelDelayedAlarm(r, date, ctx, AlarmIntentParams.AUTO);
}
public void onIntakeCompleted(Schedule s, LocalTime t, LocalDate date, Context ctx) {
// cancel notification
ReminderNotification.cancel(ctx, ReminderNotification.scheduleNotificationId(s.getId().intValue()));
// cancel all delay alarms
cancelHourlyDelayedAlarm(s, t, date, ctx, AlarmIntentParams.USER);
cancelHourlyDelayedAlarm(s, t, date, ctx, AlarmIntentParams.AUTO);
}
public void onCreateOrUpdateRoutine(Routine r, Context ctx) {
Log.d(TAG, "onCreateOrUpdateRoutine: " + r.getId() + ", " + r.name());
setFirstAlarm(r, LocalDate.now(), ctx);
}
public void onCreateOrUpdateSchedule(Schedule s, Context ctx) {
Log.d(TAG, "onCreateOrUpdateSchedule: " + s.getId() + ", " + s.medicine().name());
setAlarmsIfNeeded(s, LocalDate.now(), ctx);
}
public void onDeleteRoutine(Routine r, Context ctx) {
Log.d(TAG, "onDeleteRoutine: " + r.getId() + ", " + r.name());
cancelAlarm(r, LocalDate.now(), ctx);
}
private Long getAlarmRepeatFreq(Context context){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String delayMinutesStr = prefs.getString("alarm_repeat_frequency", "15");
return Long.parseLong(delayMinutesStr);
}
/**
* Set an alarm for a routine
*/
private void setFirstAlarm(Routine routine, LocalDate date, Context ctx) {
long timestamp = date.toDateTime(routine.time()).getMillis();
PendingIntent routinePendingIntent = pendingIntent(ctx, routine, date, false, AlarmIntentParams.AUTO);
setExactAlarm(ctx,timestamp,routinePendingIntent);
}
//
// Methods to check if a intake is available according to the user preferences
//
/**
* Set an alarm for a repeating schedule item
*/
private void setFirstAlarm(Schedule schedule, LocalTime time, LocalDate date, Context ctx) {
DateTime dateTime = date.toDateTime(time);
PendingIntent routinePendingIntent = pendingIntent(ctx, schedule, time, date, false,AlarmIntentParams.AUTO);
setExactAlarm(ctx,dateTime.getMillis(),routinePendingIntent);
}
private void setRepeatAlarm(Routine routine, AlarmIntentParams firstParams, Context ctx, long delayMillis) {
PendingIntent routinePendingIntent = pendingIntent(ctx, routine, firstParams.date(), true, firstParams.actionType);
setExactAlarm(ctx,DateTime.now().getMillis() + delayMillis,routinePendingIntent);
}
private void setRepeatAlarm(Schedule schedule, AlarmIntentParams firstParams, Context ctx, long delayMillis) {
PendingIntent schedulePendingIntent = pendingIntent(ctx, schedule, firstParams.scheduleTime(), firstParams.date(), true, firstParams.actionType);
setExactAlarm(ctx,DateTime.now().getMillis() + delayMillis,schedulePendingIntent);
}
private void setExactAlarm(Context ctx, long millis, PendingIntent pendingIntent){
AlarmManager alarmManager = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
if (Build.VERSION.SDK_INT >= 23) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, millis, pendingIntent);
} else if (Build.VERSION.SDK_INT >= 19) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, millis, pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, millis, pendingIntent);
}
}
}
private void cancelAlarm(Routine routine, LocalDate date, Context ctx)
{
PendingIntent routinePendingIntent = pendingIntent(ctx, routine, date, false, AlarmIntentParams.AUTO);
AlarmManager alarmManager = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
alarmManager.cancel(routinePendingIntent);
}
}
private void cancelDelayedAlarm(Routine routine, LocalDate date, Context ctx, int actionType) {
// cancel alarm
// get delay routine pending intent
PendingIntent routinePendingIntent = pendingIntent(ctx, routine, date, true, actionType);
// Get the AlarmManager service
AlarmManager alarmManager = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
alarmManager.cancel(routinePendingIntent);
}
}
private void cancelHourlyDelayedAlarm(Schedule s, LocalTime t, LocalDate date, Context ctx, int actionType) {
DailyScheduleItem ds = DB.dailyScheduleItems().findBy(s,date,t);
if(ds!=null) {
// get hourly delay pending intent
PendingIntent pendingIntent = pendingIntent(ctx, s, t, date, true,actionType);
// Get the AlarmManager service
AlarmManager alarmManager = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
alarmManager.cancel(pendingIntent);
}
}
}
private void setAlarmsIfNeeded(Schedule schedule, LocalDate date, Context ctx) {
if (!schedule.repeatsHourly()) {
for (ScheduleItem scheduleItem : schedule.items()) {
if (scheduleItem.routine() != null && canBeScheduled(scheduleItem.routine().time().toDateTimeToday(),ctx)){
setFirstAlarm(scheduleItem.routine(), date, ctx);
}
}
} else {
List<DateTime> times = schedule.hourlyItemsAt(date.toDateTimeAtStartOfDay());
for (DateTime time : times) {
if(canBeScheduled(time,ctx)) {
setFirstAlarm(schedule, time.toLocalTime(), date, ctx);
}
}
}
}
private void onRoutineTime(Routine routine, AlarmIntentParams firstParams, Context ctx) {
List<ScheduleItem> doses = new ArrayList<>();
List<ScheduleItem> rItems = routine.scheduleItems();
boolean notify = false;
// check if all items have timeTaken (cancelled notifications)
for (ScheduleItem scheduleItem : rItems) {
Log.d(TAG, "Routine schedule items: " + rItems.size());
DailyScheduleItem ds = DB.dailyScheduleItems().findByScheduleItemAndDate(scheduleItem, firstParams.date());
if (ds != null) {
Log.d(TAG, "DailySchedule Item: " + ds.toString());
doses.add(scheduleItem);
if (ds.timeTaken() == null) {
Log.d(TAG, ds.scheduleItem().schedule().medicine().name() + " not checked or cancelled. Notify!");
notify = true;
}
}
}
if (notify) {
final Intent intent = new Intent(ctx, ConfirmActivity.class);
intent.putExtra("routine_id", routine.getId());
intent.putExtra("date", firstParams.date);
intent.putExtra("actionType", firstParams.actionType);
ReminderNotification.notify(ctx, ctx.getResources().getString(R.string.meds_time), routine, doses, firstParams.date(), intent, false);
Log.d(TAG, "Show notification");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
boolean repeatAlarms = prefs.getBoolean("alarm_repeat_enabled", false);
if (repeatAlarms) {
firstParams.actionType = AlarmIntentParams.AUTO;
setRepeatAlarm(routine, firstParams, ctx, getAlarmRepeatFreq(ctx)*60*1000);
}
}
}
//
// Methods called when there are changes in database, to update the alarm status
//
private void onHourlyScheduleTime(Schedule schedule, AlarmIntentParams firstParams, Context ctx) {
boolean notify = false;
// check if this item has timeTaken (cancelled notifications)
DailyScheduleItem ds = DB.dailyScheduleItems().findBy(schedule, firstParams.date(), firstParams.scheduleTime());
if (ds != null && ds.timeTaken() == null)
{
notify = true;
}
if (notify)
{
final Intent intent = new Intent(ctx, ConfirmActivity.class);
intent.putExtra(CalendulaApp.INTENT_EXTRA_SCHEDULE_ID, schedule.getId());
intent.putExtra(CalendulaApp.INTENT_EXTRA_SCHEDULE_TIME, firstParams.scheduleTime);
intent.putExtra("date", firstParams.date);
intent.putExtra("actionType", firstParams.actionType);
String title = ctx.getResources().getString(R.string.meds_time);
ReminderNotification.notify(ctx, title, schedule, firstParams.date(), firstParams.scheduleTime(), intent, false);
// Handle delay if needed
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
boolean repeatAlarms = prefs.getBoolean("alarm_repeat_enabled", false);
if (repeatAlarms)
{
firstParams.actionType = AlarmIntentParams.AUTO;
setRepeatAlarm(schedule, firstParams, ctx, getAlarmRepeatFreq(ctx) * 60 * 1000);
}
}
}
private void onRoutineLost(Routine routine, AlarmIntentParams params, Context ctx) {
// get the schedule items for the current routine, excluding already taken
List<ScheduleItem> doses = ScheduleUtils.getRoutineScheduleItems(routine, params.date());
// cancel intake
cancelIntake(routine, params.date(), ctx);
// cancel alarms
onIntakeCompleted(routine,params.date(),ctx);
// show routine lost notification
final Intent intent = new Intent(ctx, ConfirmActivity.class);
intent.putExtra("routine_id", routine.getId());
intent.putExtra("date", params.date);
String title = ctx.getResources().getString(R.string.meds_time_lost);
ReminderNotification.notify(ctx, title, routine, doses, params.date(), intent, true);
}
private void onHourlyScheduleLost(Schedule schedule, AlarmIntentParams params, Context ctx) {
// cancel intake (set time taken)
cancelIntake(schedule, params.scheduleTime(), params.date(), ctx);
// cancel alarms
onIntakeCompleted(schedule, params.scheduleTime(), params.date(), ctx);
// show schedule lost notification
final Intent intent = new Intent(ctx, ConfirmActivity.class);
intent.putExtra(CalendulaApp.INTENT_EXTRA_SCHEDULE_ID, schedule.getId());
intent.putExtra(CalendulaApp.INTENT_EXTRA_SCHEDULE_TIME, params.scheduleTime);
intent.putExtra("date", params.date);
String title = ctx.getResources().getString(R.string.meds_time_lost);
ReminderNotification.notify(ctx, title, schedule, params.date(), params.scheduleTime(), intent, true);
}
private void cancelIntake(Routine r, LocalDate date, Context ctx) {
for (ScheduleItem scheduleItem : r.scheduleItems()) {
DailyScheduleItem ds = DB.dailyScheduleItems().findByScheduleItemAndDate(scheduleItem,date);
if(ds.timeTaken() == null) {
Log.d(TAG, "Cancelling schedule item");
ds.setTimeTaken(LocalTime.now());
ds.save();
}
}
}
private void cancelIntake(Schedule s, LocalTime t, LocalDate date, Context ctx) {
DailyScheduleItem ds = DB.dailyScheduleItems().findBy(s,date,t);
if(ds!=null && ds.timeTaken()==null) {
Log.d(TAG, "Cancelling schedule item");
ds.setTimeTaken(LocalTime.now());
ds.save();
}
}
}