You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/astrid/src/main/java/com/todoroo/astrid/reminders/Notifications.java

513 lines
20 KiB
Java

/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.reminders;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.andlib.service.NotificationManager.AndroidNotificationManager;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.FilterWithCustomIntent;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Flags;
import com.todoroo.astrid.voice.VoiceOutputService;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tasks.R;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.tasks.date.DateTimeUtils.currentTimeMillis;
public class Notifications extends BroadcastReceiver {
private static final Logger log = LoggerFactory.getLogger(Notifications.class);
// --- constants
/** task id extra */
public static final String ID_KEY = "id"; //$NON-NLS-1$
/** preference values */
public static final int ICON_SET_PINK = 0;
public static final int ICON_SET_BORING = 1;
public static final int ICON_SET_ASTRID = 2;
/**
* Action name for broadcast intent notifying that task was created from repeating template
*/
public static final String BROADCAST_IN_APP_NOTIFY = Constants.PACKAGE + ".IN_APP_NOTIFY"; //$NON-NLS-1$
public static final String EXTRAS_CUSTOM_INTENT = "intent"; //$NON-NLS-1$
public static final String EXTRAS_NOTIF_ID = "notifId"; //$NON-NLS-1$
/** notification type extra */
public static final String EXTRAS_TYPE = "type"; //$NON-NLS-1$
public static final String EXTRAS_TITLE = "title"; //$NON-NLS-1$
public static final String EXTRAS_TEXT = "text"; //$NON-NLS-1$
public static final String EXTRAS_RING_TIMES = "ringTimes"; //$NON-NLS-1$
// --- instance variables
@Autowired private TaskDao taskDao;
public static NotificationManager notificationManager = null;
private static boolean forceNotificationManager = false;
// --- alarm handling
static {
AstridDependencyInjector.initialize();
}
public Notifications() {
DependencyInjectionService.getInstance().inject(this);
}
@Override
/** Alarm intent */
public void onReceive(Context context, Intent intent) {
ContextManager.setContext(context);
long id = intent.getLongExtra(ID_KEY, 0);
int type = intent.getIntExtra(EXTRAS_TYPE, (byte) 0);
Resources r = context.getResources();
String reminder;
if(type == ReminderService.TYPE_ALARM) {
reminder = getRandomReminder(r.getStringArray(R.array.reminders_alarm));
} else {
reminder = ""; //$NON-NLS-1$
}
synchronized(Notifications.class) {
if(notificationManager == null) {
notificationManager = new AndroidNotificationManager(context);
}
}
if(!showTaskNotification(id, type, reminder)) {
notificationManager.cancel((int)id);
}
try {
VoiceOutputService.getVoiceOutputInstance().onDestroy();
} catch (VerifyError e) {
// unavailable
}
}
// --- notification creation
/** @return a random reminder string */
public static String getRandomReminder(String[] reminders) {
int next = ReminderService.random.nextInt(reminders.length);
return reminders[next];
}
/**
* Show a new notification about the given task. Returns false if there was
* some sort of error or the alarm should be disabled.
*/
public boolean showTaskNotification(long id, int type, String reminder) {
Task task;
try {
task = taskDao.fetch(id, Task.ID, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE,
Task.DUE_DATE, Task.DELETION_DATE, Task.REMINDER_FLAGS, Task.USER_ID);
if(task == null) {
throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
if (!Preferences.getBoolean(R.string.p_rmd_enabled, true)) {
return false;
}
// you're done, or not yours - don't sound, do delete
if(task.isCompleted() || task.isDeleted() || !Task.USER_ID_SELF.equals(task.getUserID())) {
return false;
}
// new task edit in progress
if(TextUtils.isEmpty(task.getTitle())) {
return false;
}
// it's hidden - don't sound, don't delete
if(task.isHidden() && type == ReminderService.TYPE_RANDOM) {
return true;
}
// task due date was changed, but alarm wasn't rescheduled
boolean dueInFuture = task.hasDueTime() && task.getDueDate() > DateUtilities.now() ||
!task.hasDueTime() && task.getDueDate() - DateUtilities.now() > DateUtilities.ONE_DAY;
if((type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE) &&
(!task.hasDueDate() || dueInFuture)) {
return true;
}
// read properties
String taskTitle = task.getTitle();
boolean nonstopMode = task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_MODE_NONSTOP);
boolean ringFiveMode = task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_MODE_FIVE);
int ringTimes = nonstopMode ? -1 : (ringFiveMode ? 5 : 1);
// update last reminder time
task.setReminderLast(DateUtilities.now());
task.setSocialReminder(Task.REMINDER_SOCIAL_UNSEEN);
taskDao.saveExisting(task);
Context context = ContextManager.getContext();
String title = context.getString(R.string.app_name);
String text = reminder + " " + taskTitle; //$NON-NLS-1$
Intent notifyIntent = new Intent(context, TaskListActivity.class);
FilterWithCustomIntent itemFilter = new FilterWithCustomIntent(context.getString(R.string.rmd_NoA_filter),
context.getString(R.string.rmd_NoA_filter),
new QueryTemplate().where(TaskCriteria.byId(id)),
null);
Bundle customExtras = new Bundle();
customExtras.putLong(NotificationFragment.TOKEN_ID, id);
customExtras.putString(EXTRAS_TEXT, text);
itemFilter.customExtras = customExtras;
itemFilter.customTaskList = new ComponentName(context, NotificationFragment.class);
notifyIntent.setAction("NOTIFY" + id); //$NON-NLS-1$
notifyIntent.putExtra(TaskListFragment.TOKEN_FILTER, itemFilter);
notifyIntent.putExtra(NotificationFragment.TOKEN_ID, id);
notifyIntent.putExtra(EXTRAS_TEXT, text);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
notifyIntent.putExtra(TaskListActivity.TOKEN_SOURCE, Constants.SOURCE_NOTIFICATION);
requestNotification((int)id, notifyIntent, type, title, text, ringTimes);
return true;
}
private static void requestNotification(long taskId, Intent intent, int type, String title, String text, int ringTimes) {
Context context = ContextManager.getContext();
Intent inAppNotify = new Intent(BROADCAST_IN_APP_NOTIFY);
inAppNotify.putExtra(EXTRAS_NOTIF_ID, (int)taskId);
inAppNotify.putExtra(NotificationFragment.TOKEN_ID, taskId);
inAppNotify.putExtra(EXTRAS_CUSTOM_INTENT, intent);
inAppNotify.putExtra(EXTRAS_TYPE, type);
inAppNotify.putExtra(EXTRAS_TITLE, title);
inAppNotify.putExtra(EXTRAS_TEXT, text);
inAppNotify.putExtra(EXTRAS_RING_TIMES, ringTimes);
if(forceNotificationManager) {
new ShowNotificationReceiver().onReceive(ContextManager.getContext(), inAppNotify);
} else {
context.sendOrderedBroadcast(inAppNotify, AstridApiConstants.PERMISSION_READ);
}
}
/**
* Receives requests to show an Astrid notification if they were not intercepted and handled
* by the in-app reminders in AstridActivity.
* @author Sam
*
*/
public static class ShowNotificationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notificationId = intent.getIntExtra(EXTRAS_NOTIF_ID, 0);
Intent customIntent = intent.getParcelableExtra(EXTRAS_CUSTOM_INTENT);
int type = intent.getIntExtra(EXTRAS_TYPE, 0);
String title = intent.getStringExtra(EXTRAS_TITLE);
String text = intent.getStringExtra(EXTRAS_TEXT);
int ringTimes = intent.getIntExtra(EXTRAS_RING_TIMES, 1);
showNotification(notificationId, customIntent, type, title, text, ringTimes);
}
}
private static long lastNotificationSound = 0L;
/**
* @return true if notification should sound
*/
private static boolean checkLastNotificationSound() {
long now = DateUtilities.now();
if (now - lastNotificationSound > 10000 || forceNotificationManager) {
lastNotificationSound = now;
return true;
}
return false;
}
/**
* Shows an Astrid notification. Pulls in ring tone and quiet hour settings
* from preferences. You can make it say anything you like.
* @param ringTimes number of times to ring (-1 = nonstop)
*/
public static void showNotification(int notificationId, Intent intent, int type, String title,
String text, int ringTimes) {
Context context = ContextManager.getContext();
if(notificationManager == null) {
notificationManager = new AndroidNotificationManager(context);
}
// don't ring multiple times if random reminder
if(type == ReminderService.TYPE_RANDOM) {
ringTimes = 1;
}
// quiet hours? unless alarm clock
boolean quietHours = (type == ReminderService.TYPE_ALARM || type == ReminderService.TYPE_DUE) ? false : isQuietHours();
PendingIntent pendingIntent = PendingIntent.getActivity(context,
notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// set up properties (name and icon) for the notification
int icon;
switch(Preferences.getIntegerFromString(R.string.p_rmd_icon,
ICON_SET_ASTRID)) {
case ICON_SET_PINK:
icon = R.drawable.notif_pink_alarm;
break;
case ICON_SET_BORING:
icon = R.drawable.notif_boring_alarm;
break;
default:
icon = R.drawable.notif_astrid;
}
// create notification object
final Notification notification = new Notification(
icon, text, System.currentTimeMillis());
notification.setLatestEventInfo(context,
title,
text,
pendingIntent);
notification.flags |= Notification.FLAG_AUTO_CANCEL;
if(Preferences.getBoolean(R.string.p_rmd_persistent, true)) {
notification.flags |= Notification.FLAG_NO_CLEAR |
Notification.FLAG_SHOW_LIGHTS;
notification.ledOffMS = 5000;
notification.ledOnMS = 700;
notification.ledARGB = Color.YELLOW;
}
else {
notification.defaults = Notification.DEFAULT_LIGHTS;
}
AudioManager audioManager = (AudioManager)context.getSystemService(
Context.AUDIO_SERVICE);
// detect call state
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
int callState = tm.getCallState();
boolean voiceReminder = Preferences.getBoolean(R.string.p_voiceRemindersEnabled, false);
// if multi-ring is activated and the setting p_rmd_maxvolume allows it, set up the flags for insistent
// notification, and increase the volume to full volume, so the user
// will actually pay attention to the alarm
boolean maxOutVolumeForMultipleRingReminders = Preferences.getBoolean(R.string.p_rmd_maxvolume, true);
// remember it to set it to the old value after the alarm
int previousAlarmVolume = audioManager.getStreamVolume(AudioManager.STREAM_ALARM);
if (ringTimes != 1 && (type != ReminderService.TYPE_RANDOM)) {
notification.audioStreamType = AudioManager.STREAM_ALARM;
if (maxOutVolumeForMultipleRingReminders) {
audioManager.setStreamVolume(AudioManager.STREAM_ALARM,
audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), 0);
}
// insistent rings until notification is disabled
if(ringTimes < 0) {
notification.flags |= Notification.FLAG_INSISTENT;
voiceReminder = false;
}
} else {
notification.audioStreamType = AudioManager.STREAM_NOTIFICATION;
}
boolean soundIntervalOk = checkLastNotificationSound();
// quiet hours = no sound
if(quietHours || callState != TelephonyManager.CALL_STATE_IDLE) {
notification.sound = null;
voiceReminder = false;
} else {
String notificationPreference = Preferences.getStringValue(R.string.p_rmd_ringtone);
if(audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION) == 0) {
notification.sound = null;
voiceReminder = false;
} else if(notificationPreference != null) {
if(notificationPreference.length() > 0 && soundIntervalOk) {
notification.sound = Uri.parse(notificationPreference);
} else {
notification.sound = null;
}
} else if (soundIntervalOk) {
notification.defaults |= Notification.DEFAULT_SOUND;
}
}
// quiet hours && ! due date or snooze = no vibrate
if(quietHours && !(type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_SNOOZE)) {
notification.vibrate = null;
} else if(callState != TelephonyManager.CALL_STATE_IDLE) {
notification.vibrate = null;
} else {
if (Preferences.getBoolean(R.string.p_rmd_vibrate, true)
&& audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_NOTIFICATION) && soundIntervalOk) {
notification.vibrate = new long[] {0, 1000, 500, 1000, 500, 1000};
} else {
notification.vibrate = null;
}
}
singleThreadVoicePool.submit(new NotificationRunnable(ringTimes, notificationId, notification, voiceReminder,
maxOutVolumeForMultipleRingReminders, audioManager, previousAlarmVolume, text));
if (forceNotificationManager) {
try {
singleThreadVoicePool.awaitTermination(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
//
}
}
}
private static class NotificationRunnable implements Runnable {
private final int ringTimes;
private final int notificationId;
private final Notification notification;
private final boolean voiceReminder;
private final boolean maxOutVolumeForMultipleRingReminders;
private final AudioManager audioManager;
private final int previousAlarmVolume;
private final String text;
public NotificationRunnable(int ringTimes, int notificationId, Notification notification, boolean voiceReminder,
boolean maxOutVolume, AudioManager audioManager, int previousAlarmVolume, String text) {
this.ringTimes = ringTimes;
this.notificationId = notificationId;
this.notification = notification;
this.voiceReminder = voiceReminder;
this.maxOutVolumeForMultipleRingReminders = maxOutVolume;
this.audioManager = audioManager;
this.previousAlarmVolume = previousAlarmVolume;
this.text = text;
}
@Override
public void run() {
for(int i = 0; i < Math.max(ringTimes, 1); i++) {
notificationManager.notify(notificationId, notification);
AndroidUtilities.sleepDeep(500);
}
Flags.set(Flags.REFRESH); // Forces a reload when app launches
if ((voiceReminder || maxOutVolumeForMultipleRingReminders)) {
AndroidUtilities.sleepDeep(2000);
for(int i = 0; i < 50; i++) {
AndroidUtilities.sleepDeep(500);
if(audioManager.getMode() != AudioManager.MODE_RINGTONE) {
break;
}
}
try {
// first reset the Alarm-volume to the value before it was eventually maxed out
if (maxOutVolumeForMultipleRingReminders) {
audioManager.setStreamVolume(AudioManager.STREAM_ALARM, previousAlarmVolume, 0);
}
if (voiceReminder) {
VoiceOutputService.getVoiceOutputInstance().queueSpeak(text);
}
} catch (VerifyError e) {
// unavailable
}
}
}
}
private static ExecutorService singleThreadVoicePool = Executors.newSingleThreadExecutor();
/**
* @return whether we're in quiet hours
*/
public static boolean isQuietHours() {
boolean quietHoursEnabled = Preferences.getBoolean(R.string.p_rmd_enable_quiet, false);
if(quietHoursEnabled) {
long quietHoursStart = new DateTime().withMillisOfDay(Preferences.getInt(R.string.p_rmd_quietStart)).getMillis();
long quietHoursEnd = new DateTime().withMillisOfDay(Preferences.getInt(R.string.p_rmd_quietEnd)).getMillis();
long now = currentTimeMillis();
if(quietHoursStart <= quietHoursEnd) {
if(now >= quietHoursStart && now < quietHoursEnd) {
return true;
}
} else { // wrap across 24/hour boundary
if(now >= quietHoursStart || now < quietHoursEnd) {
return true;
}
}
}
return false;
}
/**
* Schedules alarms for a single task
*/
public static void cancelNotifications(long taskId) {
if(notificationManager == null) {
synchronized (Notifications.class) {
if (notificationManager == null) {
notificationManager = new AndroidNotificationManager(
ContextManager.getContext());
}
}
}
notificationManager.cancel((int)taskId);
}
// --- notification manager
public static void setNotificationManager(
NotificationManager notificationManager) {
Notifications.notificationManager = notificationManager;
}
public static void forceNotificationManager() {
forceNotificationManager = true;
}
}