/*
* Copyright (c) 2015 Jonas Kalderstam.
*
* This program 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 program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nononsenseapps.notepad.data.receiver;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import com.nononsenseapps.notepad.R;
import com.nononsenseapps.notepad.data.model.sql.Task;
import com.nononsenseapps.notepad.util.Log;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
public class NotificationHelper extends BroadcastReceiver {
public static final int NOTIFICATION_ID = 4364;
public static final String NOTIFICATION_CANCEL_ARG = "notification_cancel_arg";
public static final String NOTIFICATION_DELETE_ARG = "notification_delete_arg";
// static final String ARG_MAX_TIME = "maxtime";
// static final String ARG_LISTID = "listid";
static final String ARG_TASKID = "taskid";
private static final String ACTION_COMPLETE = "com.nononsenseapps.notepad.ACTION.COMPLETE";
private static final String ACTION_SNOOZE = "com.nononsenseapps.notepad.ACTION.SNOOZE";
private static final String ACTION_RESCHEDULE = "com.nononsenseapps.notepad.ACTION.RESCHEDULE";
private static final String TAG = "nononsenseapps.NotificationHelper";
private static ContextObserver observer = null;
private static ContextObserver getObserver(final Context context) {
if (observer == null) {
observer = new ContextObserver(context, null);
}
return observer;
}
private static void monitorUri(final Context context) {
context.getContentResolver().unregisterContentObserver(getObserver(context));
context.getContentResolver().registerContentObserver(com.nononsenseapps.notepad.data.model.sql.Notification.URI, true, getObserver(context));
}
public static void clearNotification(@NonNull final Context context, @NonNull final Intent
intent) {
if (intent.getLongExtra(NOTIFICATION_DELETE_ARG, -1) > 0) {
com.nononsenseapps.notepad.data.model.sql.Notification.deleteOrReschedule(context, com.nononsenseapps.notepad.data.model.sql.Notification.getUri(intent.getLongExtra
(NOTIFICATION_DELETE_ARG, -1)));
}
if (intent.getLongExtra(NOTIFICATION_CANCEL_ARG, -1) > 0) {
NotificationHelper.cancelNotification(context, (int) intent.getLongExtra
(NOTIFICATION_CANCEL_ARG, -1));
}
}
/**
* Displays notifications that have a time occurring in the past (and no
* location). If no notifications like that exist, will make sure to cancel
* any notifications showing.
*/
private static void notifyPast(Context context, boolean alertOnce) {
// Get list of past notifications
final Calendar now = Calendar.getInstance();
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications = com.nononsenseapps.notepad.data.model.sql.Notification
.getNotificationsWithTime(context, now.getTimeInMillis(), true);
// Remove duplicates
makeUnique(context, notifications);
final NotificationManager notificationManager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
Log.d(TAG, "Number of notifications: " + notifications.size());
// If empty, cancel
if (notifications.isEmpty()) {
// cancelAll permanent notifications here if/when that is
// implemented. Don't touch others.
// Dont do this, it clears location
// notificationManager.cancelAll();
}
else {
// else, notify
// Fetch sound and vibrate settings
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
// Always use default lights
int lightAndVibrate = Notification.DEFAULT_LIGHTS;
// If vibrate on, use default vibration pattern also
if (prefs.getBoolean(context.getString(R.string.const_preference_vibrate_key), false)
) lightAndVibrate |= Notification.DEFAULT_VIBRATE;
// Need to get a new one because the action buttons will duplicate
// otherwise
NotificationCompat.Builder builder;
// if (false)
// //
// prefs.getBoolean(context.getString(R.string.key_pref_group_on_lists),
// // false))
// {
// // Group together notes contained in the same list.
// // Always use listid
// for (long listId : getRelatedLists(notifications)) {
// builder = getNotificationBuilder(context,
// Integer.parseInt(prefs.getString(
// context.getString(R.string.key_pref_prio),
// "0")), lightAndVibrate,
// Uri.parse(prefs.getString(context
// .getString(R.string.key_pref_ringtone),
// "DEFAULT_NOTIFICATION_URI")), alertOnce);
//
// List<com.nononsenseapps.notepad.data.model.sql.Notification> subList =
// getSubList(
// listId, notifications);
// if (subList.size() == 1) {
// // Notify as single
// notifyBigText(context, notificationManager, builder,
// listId, subList.get(0));
// }
// else {
// notifyInboxStyle(context, notificationManager, builder,
// listId, subList);
// }
// }
// }
// else {
// Notify for each individually
for (com.nononsenseapps.notepad.data.model.sql.Notification note : notifications) {
builder = getNotificationBuilder(
context,
lightAndVibrate, Uri.parse(prefs.getString(context.getString(R.string.const_preference_ringtone_key),
"DEFAULT_NOTIFICATION_URI")), alertOnce);
notifyBigText(context, notificationManager, builder, note);
}
// }
}
}
/**
* Returns a notification builder set with non-item specific properties.
*/
private static NotificationCompat.Builder getNotificationBuilder(final Context context,
final int lightAndVibrate, final Uri ringtone,
final boolean alertOnce) {
final NotificationCompat.Builder builder = new NotificationCompat.Builder(
context)
.setWhen(0)
.setSmallIcon(R.drawable.ic_stat_notification_edit)
.setLargeIcon(
BitmapFactory.decodeResource(context.getResources(), R.drawable.app_icon))
.setPriority(NotificationCompat.PRIORITY_DEFAULT).setDefaults(lightAndVibrate).setAutoCancel(true)
.setOnlyAlertOnce(alertOnce).setSound(ringtone);
return builder;
}
/**
* Remove from the database, and the specified list, duplicate
* notifications. The result is that each note is only associated with ONE
* EXPIRED notification.
*
* @param context
* @param notifications
*/
private static void makeUnique(
final Context context,
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications) {
// get duplicates and iterate over them
for (com.nononsenseapps.notepad.data.model.sql.Notification noti : getLatestOccurence(notifications)) {
// remove all but the first one from database, and big list
for (com.nononsenseapps.notepad.data.model.sql.Notification dupNoti : getDuplicates(
noti, notifications)) {
notifications.remove(dupNoti);
cancelNotification(context, dupNoti);
// Cancelled called in delete
dupNoti.deleteOrReschedule(context);
}
}
}
/**
* Returns the first occurrence of each note's notification. Effectively the
* returned list has unique elements with regard to the note id.
*
* @param notifications
* @return
*/
private static List<com.nononsenseapps.notepad.data.model.sql.Notification> getLatestOccurence(
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications) {
final ArrayList<Long> seenIds = new ArrayList<Long>();
final ArrayList<com.nononsenseapps.notepad.data.model.sql.Notification> firsts = new ArrayList<com.nononsenseapps.notepad.data.model.sql.Notification>();
com.nononsenseapps.notepad.data.model.sql.Notification noti;
for (int i = notifications.size() - 1; i >= 0; i--) {
noti = notifications.get(i);
if (!seenIds.contains(noti.taskID)) {
seenIds.add(noti.taskID);
firsts.add(noti);
}
}
return firsts;
}
private static List<com.nononsenseapps.notepad.data.model.sql.Notification> getDuplicates(
final com.nononsenseapps.notepad.data.model.sql.Notification first,
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications) {
final ArrayList<com.nononsenseapps.notepad.data.model.sql.Notification> dups = new ArrayList<com.nononsenseapps.notepad.data.model.sql.Notification>();
for (com.nononsenseapps.notepad.data.model.sql.Notification noti : notifications) {
if (noti.taskID == first.taskID && noti._id != first._id) {
dups.add(noti);
}
}
return dups;
}
/**
* Needs the builder that contains non-note specific values.
*
*/
private static void notifyBigText(final Context context,
final NotificationManager notificationManager,
final NotificationCompat.Builder builder,
final com.nononsenseapps.notepad.data.model.sql.Notification note) {
final Intent delIntent = new Intent(Intent.ACTION_DELETE, note.getUri());
if (note.repeats != 0) {
delIntent.setAction(ACTION_RESCHEDULE);
}
// Add extra so we don't delete all
// if (note.time != null) {
// delIntent.putExtra(ARG_MAX_TIME, note.time);
// }
delIntent.putExtra(ARG_TASKID, note.taskID);
// Delete it on clear
PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0,
delIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Open intent
final Intent openIntent = new Intent(Intent.ACTION_VIEW, Task.getUri(note.taskID));
// Should create a new instance to avoid fragment problems
openIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
// Delete intent on non-location repeats
// Opening the note should delete/reschedule the notification
openIntent.putExtra(NOTIFICATION_DELETE_ARG, note._id);
// Opening always cancels the notification though
openIntent.putExtra(NOTIFICATION_CANCEL_ARG, note._id);
// Open note on click
PendingIntent clickIntent = PendingIntent.getActivity(context, 0,
openIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
// Action to complete
PendingIntent completeIntent = PendingIntent.getBroadcast(context, 0,
new Intent(ACTION_COMPLETE, note.getUri()).putExtra(ARG_TASKID,
note.taskID), PendingIntent.FLAG_UPDATE_CURRENT);
// Action to snooze
PendingIntent snoozeIntent = PendingIntent.getBroadcast(context, 0,
new Intent(ACTION_SNOOZE, note.getUri()).putExtra(ARG_TASKID,
note.taskID), PendingIntent.FLAG_UPDATE_CURRENT);
// Build notification
builder.setContentTitle(note.taskTitle)
.setContentText(note.taskNote)
.setContentIntent(clickIntent)
.setStyle(
new NotificationCompat.BigTextStyle()
.bigText(note.taskNote));
// Delete intent on non-location repeats
builder.setDeleteIntent(deleteIntent);
// Snooze button only on time non-repeating
if (note.time != null && note.repeats == 0) {
builder.addAction(R.drawable.ic_alarm_24dp_white,
context.getText(R.string.snooze), snoozeIntent);
}
// Complete button only on non-repeating, both time and location
if (note.repeats == 0) {
builder.addAction(R.drawable.ic_check_24dp_white,
context.getText(R.string.completed), completeIntent);
}
final Notification noti = builder.build();
notificationManager.notify((int) note._id, noti);
}
private static long getLatestTime(final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications) {
long latest = 0;
for (com.nononsenseapps.notepad.data.model.sql.Notification noti : notifications) {
if (noti.time > latest)
latest = noti.time;
}
return latest;
}
// private static void notifyInboxStyle(
// final Context context,
// final NotificationManager notificationManager,
// final NotificationCompat.Builder builder,
// final Long idToUse,
// final List<com.nononsenseapps.notepad.data.model.sql.Notification>
// notifications) {
// // Delete intent must delete all notifications
// Intent delint = new Intent(Intent.ACTION_DELETE,
// com.nononsenseapps.notepad.data.model.sql.Notification.URI);
// // Add extra so we don't delete all
// final long maxTime = getLatestTime(notifications);
// delint.putExtra(ARG_MAX_TIME, maxTime);
// delint.putExtra(ARG_LISTID, idToUse);
// PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0,
// delint, PendingIntent.FLAG_UPDATE_CURRENT);
//
// // Open intent should open the list
// PendingIntent clickIntent = PendingIntent.getActivity(context, 0,
// new Intent(Intent.ACTION_VIEW, TaskList.getUri(idToUse),
// context, ActivityMain_.class),
// PendingIntent.FLAG_UPDATE_CURRENT);
//
// final String title = notifications.get(0).listTitle + " ("
// + notifications.size() + ")";
// // Build notification
// builder.setContentTitle(title).setNumber(notifications.size())
// .setContentText(notifications.get(0).taskTitle)
// .setContentIntent(clickIntent).setDeleteIntent(deleteIntent);
//
// // Action to snooze
// PendingIntent snoozeIntent = PendingIntent.getBroadcast(
// context,
// 0,
// new Intent(Intent.ACTION_EDIT, TaskList.getUri(idToUse))
// .putExtra(ARG_SNOOZE, true)
// .putExtra(ARG_LISTID, idToUse)
// .setClass(context, NotificationHelper.class),
// PendingIntent.FLAG_UPDATE_CURRENT);
// builder.addAction(R.drawable.ic_stat_snooze,
// context.getText(R.string.snooze), snoozeIntent);
//
// // Action to complete
// PendingIntent completeIntent = PendingIntent.getBroadcast(
// context,
// 0,
// new Intent(Intent.ACTION_DELETE, TaskList.getUri(idToUse))
// .putExtra(ARG_COMPLETE, true)
// .putExtra(ARG_LISTID, idToUse)
// .putExtra(ARG_MAX_TIME, maxTime)
// .setClass(context, NotificationHelper.class),
// PendingIntent.FLAG_UPDATE_CURRENT);
// builder.addAction(R.drawable.navigation_accept_dark,
// context.getText(R.string.completed), completeIntent);
//
// NotificationCompat.InboxStyle ib = new NotificationCompat.InboxStyle()
// .setBigContentTitle(title);
// if (notifications.size() > 6)
// ib.setSummaryText("+" + (notifications.size() - 6) + " "
// + context.getString(R.string.more));
//
// for (com.nononsenseapps.notepad.data.model.sql.Notification e : notifications)
// {
// ib.addLine(e.taskTitle);
// }
//
// final Notification noti = builder.setStyle(ib).build();
// notificationManager.notify(idToUse.intValue(), noti);
// }
/**
* Schedules to be woken up at the next notification time.
*/
private static void scheduleNext(Context context) {
// Get first future notification
final Calendar now = Calendar.getInstance();
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications = com.nononsenseapps.notepad.data.model.sql.Notification
.getNotificationsWithTime(context, now.getTimeInMillis(), false);
// if not empty, schedule alarm wake up
if (!notifications.isEmpty()) {
// at first's time
// Create a new PendingIntent and add it to the AlarmManager
Intent intent = new Intent(Intent.ACTION_RUN);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager am = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
am.cancel(pendingIntent);
am.set(AlarmManager.RTC_WAKEUP, notifications.get(0).time,
pendingIntent);
}
monitorUri(context);
}
/**
* Schedules coming notifications, and displays expired ones. Only
* notififies once for existing notifications.
*/
public static void schedule(final Context context) {
notifyPast(context, true);
scheduleNext(context);
}
/**
* Updates/Inserts notifications in the database. Immediately notifies and
* schedules next wake up on finish.
*
* @param context
* @param notification
*/
public static void updateNotification(final Context context,
final com.nononsenseapps.notepad.data.model.sql.Notification notification) {
/*
* Only don't insert if update is success This way the editor can update
* a deleted notification and still have it persisted in the database
*/
boolean shouldInsert = true;
// If id is -1, then this should be inserted
if (notification._id > 0) {
// Else it should be updated
int result = notification.save(context);
if (result > 0) shouldInsert = false;
}
if (shouldInsert) {
notification._id = -1;
notification.save(context);
}
notifyPast(context, true);
scheduleNext(context);
}
/**
* Deletes the indicated notification from the notification tray (does not
* touch database)
*
* Called by notification.delete()
*
* @param context
* @param not
*/
public static void cancelNotification(final Context context,
final com.nononsenseapps.notepad.data.model.sql.Notification not) {
if (not != null) {
cancelNotification(context, not.getUri());
}
}
/**
* Does not touch db
*/
public static void cancelNotification(final Context context, final Uri uri) {
if (uri != null)
cancelNotification(context, Integer.parseInt(uri.getLastPathSegment()));
}
/**
* Does not touch db
*/
public static void cancelNotification(final Context context, final int notId) {
final NotificationManager notificationManager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notId);
}
/**
* Given a list of notifications, returns a list of the lists the notes
* belong to.
*/
private static Collection<Long> getRelatedLists(
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications) {
final HashSet<Long> lists = new HashSet<Long>();
for (com.nononsenseapps.notepad.data.model.sql.Notification not : notifications) {
lists.add(not.listID);
}
return lists;
}
/**
* Modifies DB
*/
// public static void deleteNotification(final Context context, long listId,
// long maxTime) {
// com.nononsenseapps.notepad.data.model.sql.Notification.removeWithListId(
// context, listId, maxTime);
//
// final NotificationManager notificationManager = (NotificationManager)
// context
// .getSystemService(Context.NOTIFICATION_SERVICE);
// notificationManager.cancel((int) listId);
// }
/**
* Returns a list of those notifications that are associated to notes in the
* specified list.
*/
private static List<com.nononsenseapps.notepad.data.model.sql.Notification> getSubList(
final long listId,
final List<com.nononsenseapps.notepad.data.model.sql.Notification> notifications) {
final ArrayList<com.nononsenseapps.notepad.data.model.sql.Notification> subList = new ArrayList<com.nononsenseapps.notepad.data.model.sql.Notification>();
for (com.nononsenseapps.notepad.data.model.sql.Notification not : notifications) {
if (not.listID == listId) {
subList.add(not);
}
}
return subList;
}
/**
* Fires notifications that have elapsed and sets an alarm to be woken at
* the next notification.
* <p/>
* If the intent action is ACTION_DELETE, will delete the notification with
* the indicated ID, and cancel it from any active notifications.
*/
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) || Intent.ACTION_RUN.equals
(intent.getAction())) {
// Can't cancel anything. Just schedule and notify at end
} else {
// Always cancel
cancelNotification(context, intent.getData());
if (Intent.ACTION_DELETE.equals(intent.getAction()) || ACTION_RESCHEDULE.equals
(intent.getAction())) {
// Just a notification
com.nononsenseapps.notepad.data.model.sql.Notification.deleteOrReschedule(context,
intent.getData());
} else if (ACTION_SNOOZE.equals(intent.getAction())) {
// msec/sec * sec/min * 30
long delay30min = 1000 * 60 * 30;
final Calendar now = Calendar.getInstance();
com.nononsenseapps.notepad.data.model.sql.Notification.setTime(context, intent.getData
(), delay30min + now.getTimeInMillis());
} else if (ACTION_COMPLETE.equals(intent.getAction())) {
// Complete note
Task.setCompletedSynced(context, true, intent.getLongExtra(ARG_TASKID, -1));
// Delete notifications with the same task id
com.nononsenseapps.notepad.data.model.sql.Notification.removeWithTaskIdsSynced(context, intent.getLongExtra(ARG_TASKID, -1));
}
}
notifyPast(context, true);
scheduleNext(context);
}
private static class ContextObserver extends ContentObserver {
private final Context context;
public ContextObserver(final Context context, Handler h) {
super(h);
this.context = context.getApplicationContext();
}
// Implement the onChange(boolean) method to delegate the
// change notification to the onChange(boolean, Uri) method
// to ensure correct operation on older versions
// of the framework that did not have the onChange(boolean,
// Uri) method.
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
// Implement the onChange(boolean, Uri) method to take
// advantage of the new Uri argument.
@Override
public void onChange(boolean selfChange, Uri uri) {
// Handle change but don't spam
notifyPast(context, true);
}
}
}