diff --git a/k9mail/src/main/java/com/fsck/k9/activity/NotificationDeleteConfirmation.java b/k9mail/src/main/java/com/fsck/k9/activity/NotificationDeleteConfirmation.java index fd6f87db5..bb980b05f 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/NotificationDeleteConfirmation.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/NotificationDeleteConfirmation.java @@ -22,16 +22,38 @@ import com.fsck.k9.service.NotificationActionService; public class NotificationDeleteConfirmation extends Activity { private final static String EXTRA_ACCOUNT = "account"; private final static String EXTRA_MESSAGE_LIST = "messages"; + private final static String EXTRA_NOTIFICATION_ID = NotificationActionService.EXTRA_NOTIFICATION_ID; private final static int DIALOG_CONFIRM = 1; + /** + * The account to delete the messages on. + */ private Account mAccount; + /** + * The messages to delete. + */ private ArrayList mMessageRefs; + /** + * ID of the notification that triggered this Activity. + * To make sure we close the correte notification afterwards because + * there may be multiple of them due to Android Wear stacked notifications. + */ + private int mNotificationID; - public static PendingIntent getIntent(Context context, final Account account, final Serializable refs) { + /** + * + * @param context context to create the PendingIntent. + * @param account The account to delete the messages on. + * @param refs The messages to delete. + * @param notificationID ID of the notification that triggered this Activity. + * @return PendingIntent that either deletes directly or shows a confirm-dialog on the phone (not on the wear device) first. + */ + public static PendingIntent getIntent(final Context context, final Account account, final Serializable refs, final int notificationID) { Intent i = new Intent(context, NotificationDeleteConfirmation.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); return PendingIntent.getActivity(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); @@ -49,11 +71,12 @@ public class NotificationDeleteConfirmation extends Activity { mAccount = preferences.getAccount(intent.getStringExtra(EXTRA_ACCOUNT)); mMessageRefs = intent.getParcelableArrayListExtra(EXTRA_MESSAGE_LIST); + mNotificationID = intent.getIntExtra(EXTRA_NOTIFICATION_ID, mAccount.getAccountNumber()); if (mAccount == null || mMessageRefs == null || mMessageRefs.isEmpty()) { finish(); } else if (!K9.confirmDeleteFromNotification()) { - triggerDelete(); + triggerDelete(mNotificationID); finish(); } else { showDialog(DIALOG_CONFIRM); @@ -71,7 +94,7 @@ public class NotificationDeleteConfirmation extends Activity { new Runnable() { @Override public void run() { - triggerDelete(); + triggerDelete(mNotificationID); finish(); } }, @@ -100,8 +123,8 @@ public class NotificationDeleteConfirmation extends Activity { super.onPrepareDialog(id, d); } - private void triggerDelete() { - Intent i = NotificationActionService.getDeleteAllMessagesIntent(this, mAccount, mMessageRefs); + private void triggerDelete(final int notificationID) { + Intent i = NotificationActionService.getDeleteAllMessagesIntent(this, mAccount, mMessageRefs, notificationID); startService(i); } } diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java index bc2f79108..96e162727 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -151,6 +151,10 @@ public class MessagingController implements Runnable { private static final String PENDING_COMMAND_APPEND = "com.fsck.k9.MessagingController.append"; private static final String PENDING_COMMAND_MARK_ALL_AS_READ = "com.fsck.k9.MessagingController.markAllAsRead"; private static final String PENDING_COMMAND_EXPUNGE = "com.fsck.k9.MessagingController.expunge"; + /** + * Key to group stacked notifications on Android Wear. + */ + private static final String NOTIFICATION_GROUP_KEY = "com.fsck.k9.MessagingController.notificationGroup"; public static class UidReverseComparator implements Comparator { @Override @@ -215,6 +219,10 @@ public class MessagingController implements Runnable { * {@link #removeMatchingMessage(android.content.Context, com.fsck.k9.activity.MessageReference)} instead. */ LinkedList messages; + /** + * Stacked notifications that share this notification as ther summary-notification. + */ + Map stackedNotifications = new HashMap(); /** * List of references for messages that the user is still to be notified of, * but which don't fit into the inbox style anymore. It's sorted from newest @@ -257,9 +265,49 @@ public class MessagingController implements Runnable { messages.addFirst(m); } + /** + * Add a stacked notification that this is a summary notification for. + * @param ref the message to add a stacked notification for + * @param notificationId the id of the stacked notification + */ + public void addStackedChildNotification(final MessageReference ref, final int notificationId) { + stackedNotifications.put(ref.getUid(), new Integer(notificationId)); + } + /** + * Add a stacked notification that this is a summary notification for. + * @param msg the message to add a stacked notification for + * @param notificationId the id of the stacked notification + */ + public void addStackedChildNotification(final Message msg, final int notificationId) { + stackedNotifications.put(msg.getUid(), new Integer(notificationId)); + } + + /** + * @return the IDs of all stacked notifications this is a summary notification for. + */ + public Collection getStackedChildNotifications() { + return stackedNotifications.values(); + } + + /** + * @param ref the message to check for + * @return null or the notification ID of a stacked notification for the given message + */ + public Integer getStackedChildNotification(final MessageReference ref) { + return stackedNotifications.get(ref.getUid()); + } + /** + * @param msg the message to check for + * @return null or the notification ID of a stacked notification for the given message + */ + public Integer getStackedChildNotification(final Message msg) { + return stackedNotifications.get(msg.getUid()); + } + /** * Remove a certain message from the message list. - * + * @see #getStackedChildNotification(com.fsck.k9.activity.MessageReference) for stacked + * notifications you may consider to cancel. * @param context A context. * @param ref Reference of the message to remove * @return true if message was found and removed, false otherwise @@ -307,7 +355,7 @@ public class MessagingController implements Runnable { public int getNewMessageCount() { return messages.size() + droppedMessages.size(); } - }; + } // Key is accountNumber private final ConcurrentMap notificationData = new ConcurrentHashMap(); @@ -1762,7 +1810,18 @@ public class MessagingController implements Runnable { synchronized (data) { MessageReference ref = localMessage.makeMessageReference(); if (data.removeMatchingMessage(context, ref)) { - notifyAccountWithDataLocked(context, account, null, data); + synchronized (data) { + // if we remove a single message from the notification, + // maybe there is a stacked notification active for that one message + Integer childNotification = data.getStackedChildNotification(ref); + if (childNotification != null) { + NotificationManager notificationManager = + (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(childNotification); + } + // update the (summary-) notification + notifyAccountWithDataLocked(context, account, null, data); + } } } } @@ -4605,6 +4664,7 @@ public class MessagingController implements Runnable { return null; } + private CharSequence getMessageSubject(Context context, Message message) { String subject = message.getSubject(); if (!TextUtils.isEmpty(subject)) { @@ -4686,6 +4746,7 @@ public class MessagingController implements Runnable { private void notifyAccount(Context context, Account account, LocalMessage message, int previousUnreadMessageCount) { final NotificationData data = getNotificationData(account, previousUnreadMessageCount); + //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (data) { notifyAccountWithDataLocked(context, account, message, data); } @@ -4694,7 +4755,89 @@ public class MessagingController implements Runnable { // Maximum number of senders to display in a lock screen notification. private static final int NUM_SENDERS_IN_LOCK_SCREEN_NOTIFICATION = 5; - private void notifyAccountWithDataLocked(Context context, Account account, + /** + * Build the specific notification actions for a single message on Android Wear. + * @param builder NotificationBuilder to add actions to + * @param totalMsgCount if this is a stacked notification, how many other messages are there? + * @param account the account we intent to act on + * @param message the single message we intent to act on (in a stacked notification or a summary notification about a single message) + * @param notificationID the id of the future notification. Will be used in the intents, so afterwards the correct notification gets closed. + */ + private void addWearActions(final NotificationCompat.Builder builder, final int totalMsgCount, final Account account, final Message message, final int notificationID) { + ArrayList subAllRefs = new ArrayList(); + subAllRefs.add(new MessageReference(account.getUuid(), message.getFolder().getName(), message.getUid(), message.getFlags().size()==0?null:message.getFlags().iterator().next())); + LinkedList msgList = new LinkedList(); + msgList.add(message); + addWearActions(builder, totalMsgCount, 1, account, subAllRefs, msgList, notificationID); + } + /** + * Build the specific notification actions for a single or multiple message on Android Wear. + * @param builder NotificationBuilder to add actions to + * @param totalMsgCount total message count (may be different from msgCount if this is a stacked notification) + * @param msgCount message count to be handled in this (stacked or summary) notification + * @param account the account we intent to act on + * @param allRefs the messages we intent to act on + * @param messages the messages we intent to act on + * @param notificationID the id of the future notification. Will be used in the intents, so afterwards the correct notification gets closed. + */ + private void addWearActions(final NotificationCompat.Builder builder, final int totalMsgCount, final int msgCount, final Account account, final ArrayList allRefs, final List messages, final int notificationID) { + // we need a new wearableExtender for each notification + final NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(); + + NotificationQuickDelete deleteOption = K9.getNotificationQuickDeleteBehaviour(); + boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS || + (deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG && msgCount == 1); + + // note: while we are limited to 3 actions on the phone, + // this does not seem to be a limit on Android Wear devices. + // Tested on Moto 360, 8 actions seem to be no problem. + + if (showDeleteAction) { + // Delete on wear only if no confirmation is required + // because they would have to be confirmed on the phone, not the wear device + if (!K9.confirmDeleteFromNotification()) { + NotificationCompat.Action wearActionDelete = + new NotificationCompat.Action.Builder( + R.drawable.ic_action_delete_dark, + context.getString(R.string.notification_action_delete), + NotificationDeleteConfirmation.getIntent(context, account, allRefs, notificationID)) + .build(); + builder.extend(wearableExtender.addAction(wearActionDelete)); + } + } + if (NotificationActionService.isArchiveAllMessagesWearAvaliable(context, account, messages)) { + + // Archive on wear + NotificationCompat.Action wearActionArchive = + new NotificationCompat.Action.Builder( + R.drawable.ic_action_archive_dark, + context.getString(R.string.notification_action_archive), + NotificationActionService.getArchiveAllMessagesIntent(context, account, allRefs, totalMsgCount > msgCount, notificationID)) + .build(); + builder.extend(wearableExtender.addAction(wearActionArchive)); + } + if (NotificationActionService.isSpamAllMessagesWearAvaliable(context, account, messages)) { + + // Spam on wear + NotificationCompat.Action wearActionSpam = + new NotificationCompat.Action.Builder( + R.drawable.ic_action_delete_dark, + context.getString(R.string.notification_action_spam), + NotificationActionService.getSpamAllMessagesIntent(context, account, allRefs, totalMsgCount > msgCount, notificationID)) + .build(); + builder.extend(wearableExtender.addAction(wearActionSpam)); + } + } + + /** + * Create/Upate and show notifications about new messages + * or that there suddenly are no longer any new messages on an account + * @param context used to create the notification and it's intents + * @param account the account that has new messages + * @param message the message (if it's just one) + * @param data all the details + */ + private void notifyAccountWithDataLocked(Context context, final Account account, LocalMessage message, NotificationData data) { boolean updateSilently = false; @@ -4748,13 +4891,63 @@ public class MessagingController implements Runnable { if (platformSupportsExtendedNotifications() && !privacyModeEnabled) { if (newMessages > 1) { + + // Stacked notifications for Android Wear + // https://developer.android.com/training/wearables/notifications/stacks.html + // multiple messages pending, show inbox style NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(builder); + int nID = account.getAccountNumber(); for (Message m : data.messages) { style.addLine(buildMessageSummary(context, getMessageSender(context, account, m), getMessageSubject(context, m))); + + // build child-notifications for Android Wear, + // so the grouped notification can be opened to + // reveal the individual messages and their actions. + NotificationCompat.Builder subBuilder = new NotificationCompat.Builder(context); + subBuilder.setSmallIcon(R.drawable.ic_notify_new_mail); + subBuilder.setWhen(System.currentTimeMillis()); + subBuilder.setGroup(NOTIFICATION_GROUP_KEY); // same group as summary + subBuilder.setAutoCancel(true); // summary closes all, stacked only itself + + nID = 1000 + nID; + // reuse existing notification IDs if some of the stacked messages + // are already shown on the wear device. + Integer realnID = data.getStackedChildNotification(m); + if (realnID == null) { + realnID = nID; + } + + // set content + setNotificationContent(context, m, getMessageSender(context, account, m), getMessageSubject(context, m), subBuilder, accountDescr); + + + // set actions + addWearActions(subBuilder, newMessages, account, m, realnID); + if (m.isSet(Flag.FLAGGED)) { + subBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); + } + + // no sound, no vibrate, no LED because these are for the summary notification only + // and depend on quiet time and user settings + + // this must be done before the summary notification + notifMgr.notify(realnID, subBuilder.build()); + data.addStackedChildNotification(m, realnID); } + // go on configuring the summary notification on the phone + // The phone will only show the summary + // the wear device will show the stacked notifications + builder.setGroup(NOTIFICATION_GROUP_KEY); + builder.setGroupSummary(true); + + //do not set summary notification to localOnly. + //Wear devices use the vibrate pattern of the summary + //despite not displaying the summary + builder.setLocalOnly(true); + if (!data.droppedMessages.isEmpty()) { style.setSummaryText(context.getString(R.string.notification_additional_messages, data.droppedMessages.size(), accountDescr)); @@ -4767,22 +4960,19 @@ public class MessagingController implements Runnable { builder.setStyle(style); } else { // single message pending, show big text - NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(builder); - CharSequence preview = getMessagePreview(context, message); - if (preview != null) { - style.bigText(preview); - } - builder.setContentText(subject); - builder.setSubText(accountDescr); - builder.setContentTitle(sender); - builder.setStyle(style); + setNotificationContent(context, message, sender, subject, builder, accountDescr); builder.addAction( platformSupportsLockScreenNotifications() ? R.drawable.ic_action_single_message_options_dark_vector : R.drawable.ic_action_single_message_options_dark, context.getString(R.string.notification_action_reply), - NotificationActionService.getReplyIntent(context, account, message.makeMessageReference())); + NotificationActionService.getReplyIntent(context, account, message.makeMessageReference(), account.getAccountNumber())); + + // add /different) actions to show on connected Android Wear devices + // do not add these to the a summary notification or they will affect all stacked + // notifications + addWearActions(builder, newMessages, newMessages, account, allRefs, data.messages, account.getAccountNumber()); } // Mark Read on phone @@ -4791,59 +4981,27 @@ public class MessagingController implements Runnable { ? R.drawable.ic_action_mark_as_read_dark_vector : R.drawable.ic_action_mark_as_read_dark, context.getString(R.string.notification_action_mark_as_read), - NotificationActionService.getReadAllMessagesIntent(context, account, allRefs)); + NotificationActionService.getReadAllMessagesIntent(context, account, allRefs, account.getAccountNumber())); NotificationQuickDelete deleteOption = K9.getNotificationQuickDeleteBehaviour(); boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS || (deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG && newMessages == 1); - NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(); + if (showDeleteAction) { // we need to pass the action directly to the activity, otherwise the // status bar won't be pulled up and we won't see the confirmation (if used) // Delete on phone builder.addAction( - platformSupportsLockScreenNotifications() - ? R.drawable.ic_action_delete_dark_vector - : R.drawable.ic_action_delete_dark, - context.getString(R.string.notification_action_delete), - NotificationDeleteConfirmation.getIntent(context, account, allRefs)); - - // Delete on wear only if no confirmation is required - if (!K9.confirmDeleteFromNotification()) { - NotificationCompat.Action wearActionDelete = - new NotificationCompat.Action.Builder( - R.drawable.ic_action_delete_dark, - context.getString(R.string.notification_action_delete), - NotificationDeleteConfirmation.getIntent(context, account, allRefs)) - .build(); - builder.extend(wearableExtender.addAction(wearActionDelete)); - } + platformSupportsLockScreenNotifications() + ? R.drawable.ic_action_delete_dark_vector + : R.drawable.ic_action_delete_dark, + context.getString(R.string.notification_action_delete), + NotificationDeleteConfirmation.getIntent(context, account, allRefs, account.getAccountNumber())); } - if (NotificationActionService.isArchiveAllMessagesWearAvaliable(context, account, data.messages)) { - // Archive on wear - NotificationCompat.Action wearActionArchive = - new NotificationCompat.Action.Builder( - R.drawable.ic_action_delete_dark, - context.getString(R.string.notification_action_archive), - NotificationActionService.getArchiveAllMessagesIntent(context, account, allRefs)) - .build(); - builder.extend(wearableExtender.addAction(wearActionArchive)); - } - if (NotificationActionService.isSpamAllMessagesWearAvaliable(context, account, data.messages)) { - - // Archive on wear - NotificationCompat.Action wearActionSpam = - new NotificationCompat.Action.Builder( - R.drawable.ic_action_delete_dark, - context.getString(R.string.notification_action_spam), - NotificationActionService.getSpamAllMessagesIntent(context, account, allRefs)) - .build(); - builder.extend(wearableExtender.addAction(wearActionSpam)); - } - } else { + } else { // no extended notifications supported String accountNotice = context.getString(R.string.notification_new_one_account_fmt, unreadCount, accountDescr); builder.setContentTitle(accountNotice); @@ -4857,6 +5015,49 @@ public class MessagingController implements Runnable { } } + TaskStackBuilder stack = buildNotificationNavigationStack(context, account, message, newMessages, unreadCount, allRefs); + + builder.setContentIntent(stack.getPendingIntent( + account.getAccountNumber(), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT)); + builder.setDeleteIntent(NotificationActionService.getAcknowledgeIntent(context, account, account.getAccountNumber())); + + // Only ring or vibrate if we have not done so already on this account and fetch + boolean ringAndVibrate = false; + if (!updateSilently && !account.isRingNotified()) { + account.setRingNotified(true); + ringAndVibrate = true; + } + + NotificationSetting n = account.getNotificationSetting(); + + configureLockScreenNotification(builder, context, account, newMessages, unreadCount, accountDescr, sender, data.messages); + + configureNotification( + builder, + (n.shouldRing()) ? n.getRingtone() : null, + (n.shouldVibrate()) ? n.getVibration() : null, + (n.isLed()) ? Integer.valueOf(n.getLedColor()) : null, + K9.NOTIFICATION_LED_BLINK_SLOW, + ringAndVibrate); + + notifMgr.notify(account.getAccountNumber(), builder.build()); + } + + + /** + * Builds the TaskStack of a notification using either buildMessageViewBackStack + * or buildUnreadBackStack or buildMessageListBackStack depending on the + * behavior we have on this device generation. + * @param context + * @param account + * @param message (only used if there is only 1 new message) + * @param newMessages (used on newer platforms) + * @param unreadCount (used on platforms that support no extended notifications) + * @param allRefs + * @return + */ + private TaskStackBuilder buildNotificationNavigationStack(Context context, Account account, LocalMessage message, int newMessages, int unreadCount, ArrayList allRefs) { TaskStackBuilder stack; boolean treatAsSingleMessageNotification; @@ -4885,32 +5086,30 @@ public class MessagingController implements Runnable { stack = buildMessageListBackStack(context, account, initialFolder); } + return stack; + } - builder.setContentIntent(stack.getPendingIntent( - account.getAccountNumber(), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT)); - builder.setDeleteIntent(NotificationActionService.getAcknowledgeIntent(context, account)); - - // Only ring or vibrate if we have not done so already on this account and fetch - boolean ringAndVibrate = false; - if (!updateSilently && !account.isRingNotified()) { - account.setRingNotified(true); - ringAndVibrate = true; + /** + * Set the content of a notification for a single message. + * @see #getMessagePreview(android.content.Context, com.fsck.k9.mail.Message) + * @param context + * @param message + * @param sender + * @param subject + * @param builder + * @param accountDescr + */ + private NotificationCompat.Builder setNotificationContent(final Context context, final Message message, final CharSequence sender, final CharSequence subject, final NotificationCompat.Builder builder, final String accountDescr) { + NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(builder); + CharSequence preview = getMessagePreview(context, message); + if (preview != null) { + style.bigText(preview); } - - NotificationSetting n = account.getNotificationSetting(); - - configureLockScreenNotification(builder, context, account, newMessages, unreadCount, accountDescr, sender, data.messages); - - configureNotification( - builder, - (n.shouldRing()) ? n.getRingtone() : null, - (n.shouldVibrate()) ? n.getVibration() : null, - (n.isLed()) ? Integer.valueOf(n.getLedColor()) : null, - K9.NOTIFICATION_LED_BLINK_SLOW, - ringAndVibrate); - - notifMgr.notify(account.getAccountNumber(), builder.build()); + builder.setContentText(subject); + builder.setSubText(accountDescr); + builder.setContentTitle(sender); + builder.setStyle(style); + return builder; } private TaskStackBuilder buildAccountsBackStack(Context context) { @@ -5086,12 +5285,16 @@ public class MessagingController implements Runnable { } } - /** Cancel a notification of new email messages */ - public void notifyAccountCancel(Context context, Account account) { - NotificationManager notifMgr = + /** + * Cancel a notification of new email messages + * @param account all notifications for this account will be canceled and removed + */ + public void notifyAccountCancel(final Context context, final Account account) { + NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); - notifMgr.cancel(account.getAccountNumber()); - notifMgr.cancel(-1000 - account.getAccountNumber()); + notificationManager.cancel(account.getAccountNumber()); + notificationManager.cancel(-1000 - account.getAccountNumber()); + notificationData.remove(account.getAccountNumber()); } diff --git a/k9mail/src/main/java/com/fsck/k9/service/NotificationActionService.java b/k9mail/src/main/java/com/fsck/k9/service/NotificationActionService.java index 3dd050884..966c5121e 100644 --- a/k9mail/src/main/java/com/fsck/k9/service/NotificationActionService.java +++ b/k9mail/src/main/java/com/fsck/k9/service/NotificationActionService.java @@ -2,7 +2,6 @@ package com.fsck.k9.service; import java.io.Serializable; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import com.fsck.k9.Account; @@ -12,8 +11,10 @@ import com.fsck.k9.activity.MessageCompose; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Message; import com.fsck.k9.mailstore.LocalMessage; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -34,37 +35,83 @@ public class NotificationActionService extends CoreService { private final static String EXTRA_ACCOUNT = "account"; private final static String EXTRA_MESSAGE = "message"; private final static String EXTRA_MESSAGE_LIST = "messages"; + private final static String EXTRA_DONTCANCEL = "dontcancel"; + /** + * ID of the notification that triggered an intent. + * Used to cancel exactly that one notification because due to + * Android Wear there may be multiple notifications per account. + */ + public final static String EXTRA_NOTIFICATION_ID = "notificationid"; - public static PendingIntent getReplyIntent(Context context, final Account account, final MessageReference ref) { + /** + * + * @param context context to use for creating the {@link Intent} + * @param account the account we intent to act on + * @param ref the message we intent to act on + * @param notificationID ID of the notification, this intent is for. + * @see #EXTRA_NOTIFICATION_ID + * @return the requested intent. To be used in a Notification. + */ + public static PendingIntent getReplyIntent(Context context, final Account account, final MessageReference ref, final int notificationID) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE, ref); + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setAction(REPLY_ACTION); return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } - public static PendingIntent getReadAllMessagesIntent(Context context, final Account account, final Serializable refs) { + /** + * + * @param context context to use for creating the {@link Intent} + * @param account the account we intent to act on + * @param refs the messages we intent to act on + * @param notificationID ID of the notification, this intent is for. + * @return the requested intent. To be used in a Notification. + * @see #EXTRA_NOTIFICATION_ID + */ + public static PendingIntent getReadAllMessagesIntent(Context context, final Account account, final Serializable refs, final int notificationID) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setAction(READ_ALL_ACTION); return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } - public static PendingIntent getAcknowledgeIntent(Context context, final Account account) { + /** + * + * @param context context to use for creating the {@link Intent} + * @param account the account for the intent to act on + * @param notificationID ID of the notification, this intent is for. + * @return the requested intent. To be used in a Notification. + * @see #EXTRA_NOTIFICATION_ID + */ + public static PendingIntent getAcknowledgeIntent(Context context, final Account account, final int notificationID) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setAction(ACKNOWLEDGE_ACTION); return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } - public static Intent getDeleteAllMessagesIntent(Context context, final Account account, final Serializable refs) { + /** + * + * @param context context to use for creating the {@link Intent} + * @param account the account we intent to act on + * @param refs the messages we intent to act on + * @param notificationID ID of the notification, this intent is for. + * @return the requested intent. To be used in a Notification. + * @see #EXTRA_NOTIFICATION_ID + */ + public static Intent getDeleteAllMessagesIntent(Context context, final Account account, final Serializable refs, final int notificationID) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setAction(DELETE_ALL_ACTION); return i; @@ -74,21 +121,35 @@ public class NotificationActionService extends CoreService { * Check if for the given parameters the ArchiveAllMessages intent is possible for Android Wear. * (No confirmation on the phone required and moving these messages to the spam-folder possible)
* Since we can not show a toast like on the phone screen, we must not offer actions that can not be performed. - * @see #getArchiveAllMessagesIntent(android.content.Context, com.fsck.k9.Account, java.io.Serializable) + * @see #getArchiveAllMessagesIntent(android.content.Context, com.fsck.k9.Account, java.io.Serializable, boolean, int) * @param context the context to get a {@link MessagingController} * @param account the account (must allow moving messages to allow true as a result) * @param messages the messages to move to the spam folder (must be synchronized to allow true as a result) * @return true if the ArchiveAllMessages intent is available for the given messages */ - public static boolean isArchiveAllMessagesWearAvaliable(Context context, final Account account, final LinkedList messages) { + public static boolean isArchiveAllMessagesWearAvaliable(Context context, final Account account, final List messages) { final MessagingController controller = MessagingController.getInstance(context); return (account.getArchiveFolderName() != null && !(account.getArchiveFolderName().equals(account.getSpamFolderName()) && K9.confirmSpam()) && isMovePossible(controller, account, account.getSentFolderName(), messages)); } - public static PendingIntent getArchiveAllMessagesIntent(Context context, final Account account, final Serializable refs) { + /** + * + * @param context context to use for creating the {@link Intent} + * @param account the account we intent to act on + * @param refs the messages we intent to act on + * @param dontCancel if true, after executing the intent, not all notifications for this account are canceled automatically + * @param notificationID ID of the notification, this intent is for. + * @return the requested intent. To be used in a Notification. + * @see #EXTRA_NOTIFICATION_ID + */ + public static PendingIntent getArchiveAllMessagesIntent(Context context, final Account account, final Serializable refs, final boolean dontCancel, final int notificationID) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); + if (dontCancel) { + i.putExtra(EXTRA_DONTCANCEL, true); + } + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setAction(ARCHIVE_ALL_ACTION); return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); @@ -99,34 +160,48 @@ public class NotificationActionService extends CoreService { * Check if for the given parameters the SpamAllMessages intent is possible for Android Wear. * (No confirmation on the phone required and moving these messages to the spam-folder possible)
* Since we can not show a toast like on the phone screen, we must not offer actions that can not be performed. - * @see #getSpamAllMessagesIntent(android.content.Context, com.fsck.k9.Account, java.io.Serializable) + * @see #getSpamAllMessagesIntent(android.content.Context, com.fsck.k9.Account, java.io.Serializable, boolean, int) * @param context the context to get a {@link MessagingController} * @param account the account (must allow moving messages to allow true as a result) * @param messages the messages to move to the spam folder (must be synchronized to allow true as a result) * @return true if the SpamAllMessages intent is available for the given messages */ - public static boolean isSpamAllMessagesWearAvaliable(Context context, final Account account, final LinkedList messages) { + public static boolean isSpamAllMessagesWearAvaliable(Context context, final Account account, final List messages) { final MessagingController controller = MessagingController.getInstance(context); return (account.getSpamFolderName() != null && !K9.confirmSpam() && isMovePossible(controller, account, account.getSentFolderName(), messages)); } - public static PendingIntent getSpamAllMessagesIntent(Context context, final Account account, final Serializable refs) { + /** + * + * @param context context to use for creating the {@link Intent} + * @param account the account we intent to act on + * @param refs the messages we intent to act on + * @param dontCancel if true, after executing the intent, not all notifications for this account are canceled automatically + * @param notificationID ID of the notification, this intent is for. + * @return the requested intent. To be used in a Notification. + * @see #EXTRA_NOTIFICATION_ID + */ + public static PendingIntent getSpamAllMessagesIntent(Context context, final Account account, final Serializable refs, final boolean dontCancel, final int notificationID) { Intent i = new Intent(context, NotificationActionService.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_MESSAGE_LIST, refs); + if (dontCancel) { + i.putExtra(EXTRA_DONTCANCEL, true); + } + i.putExtra(EXTRA_NOTIFICATION_ID, notificationID); i.setAction(SPAM_ALL_ACTION); return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); } - private static boolean isMovePossible(MessagingController controller, Account account, String dstFolder, List messages) { + private static boolean isMovePossible(MessagingController controller, Account account, String dstFolder, List messages) { if (!controller.isMoveCapable(account)) { return false; } if (K9.FOLDER_NONE.equalsIgnoreCase(dstFolder)) { return false; } - for(LocalMessage messageToMove : messages) { + for(Message messageToMove : messages) { if (!controller.isMoveCapable(messageToMove)) { return false; } @@ -237,10 +312,21 @@ public class NotificationActionService extends CoreService { } else if (ACKNOWLEDGE_ACTION.equals(action)) { // nothing to do here, we just want to cancel the notification so the list // of unseen messages is reset + Log.i(K9.LOG_TAG, "notification acknowledged"); } - /* there's no point in keeping the notification after the user clicked on it */ - controller.notifyAccountCancel(this, account); + // if this was a stacked notification on Android Wear, update the summary + // notification and keep the other stacked notifications + if (!intent.hasExtra(EXTRA_DONTCANCEL)) { + // there's no point in keeping the notification after the user clicked on it + if (intent.hasExtra(EXTRA_NOTIFICATION_ID)) { + NotificationManager notificationManager = + (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(intent.getIntExtra(EXTRA_NOTIFICATION_ID, account.getAccountNumber())); + } else { + controller.notifyAccountCancel(this, account); + } + } } else { Log.w(K9.LOG_TAG, "Could not find account for notification action."); }