1
0
mirror of https://github.com/moparisthebest/k-9 synced 2025-01-08 20:28:34 -05:00

Support for stacked notifications in

#619  "Add android wear support"

No reply with voice yet (as requested in the ticket).
No user-configurable actions yet, just delete+archive+spam
This commit is contained in:
Marcus Wolschon 2015-05-01 19:56:01 +02:00
parent 05934d75d8
commit 2701ffd2ac
2 changed files with 177 additions and 79 deletions

View File

@ -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_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_MARK_ALL_AS_READ = "com.fsck.k9.MessagingController.markAllAsRead";
private static final String PENDING_COMMAND_EXPUNGE = "com.fsck.k9.MessagingController.expunge"; 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<Message> { public static class UidReverseComparator implements Comparator<Message> {
@Override @Override
@ -158,7 +162,7 @@ public class MessagingController implements Runnable {
if (o1 == null || o2 == null || o1.getUid() == null || o2.getUid() == null) { if (o1 == null || o2 == null || o1.getUid() == null || o2.getUid() == null) {
return 0; return 0;
} }
int id1, id2; int id1, id2;
try { try {
id1 = Integer.parseInt(o1.getUid()); id1 = Integer.parseInt(o1.getUid());
id2 = Integer.parseInt(o2.getUid()); id2 = Integer.parseInt(o2.getUid());
@ -4673,6 +4677,7 @@ public class MessagingController implements Runnable {
return null; return null;
} }
private CharSequence getMessageSubject(Context context, Message message) { private CharSequence getMessageSubject(Context context, Message message) {
String subject = message.getSubject(); String subject = message.getSubject();
if (!TextUtils.isEmpty(subject)) { if (!TextUtils.isEmpty(subject)) {
@ -4762,7 +4767,62 @@ public class MessagingController implements Runnable {
// Maximum number of senders to display in a lock screen notification. // Maximum number of senders to display in a lock screen notification.
private static final int NUM_SENDERS_IN_LOCK_SCREEN_NOTIFICATION = 5; 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.
*/
private void addWearActions(final NotificationCompat.WearableExtender wearableExtender, final NotificationCompat.Builder builder, Account account, Message messages) {
ArrayList<MessageReference> subAllRefs = new ArrayList<MessageReference>();
subAllRefs.add(new MessageReference(account.getUuid(), messages.getFolder().getName(), messages.getUid(), messages.getFlags().size()==0?null:messages.getFlags().iterator().next()));
LinkedList<Message> msgList = new LinkedList<Message>();
msgList.add(messages);
addWearActions(wearableExtender, builder, 1, account, subAllRefs, msgList);
}
/**
* Build the specific notification actions for a single or multiple message on Android Wear.
*/
private void addWearActions(final NotificationCompat.WearableExtender wearableExtender, final NotificationCompat.Builder builder, int msgCount, Account account, ArrayList<MessageReference> allRefs, List<? extends Message> messages) {
NotificationQuickDelete deleteOption = K9.getNotificationQuickDeleteBehaviour();
boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS ||
(deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG && msgCount == 1);
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))
.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_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, 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))
.build();
builder.extend(wearableExtender.addAction(wearActionSpam));
}
}
private void notifyAccountWithDataLocked(Context context, final Account account,
LocalMessage message, NotificationData data) { LocalMessage message, NotificationData data) {
boolean updateSilently = false; boolean updateSilently = false;
@ -4815,13 +4875,41 @@ public class MessagingController implements Runnable {
data.supplyAllMessageRefs(allRefs); data.supplyAllMessageRefs(allRefs);
if (platformSupportsExtendedNotifications() && !privacyModeEnabled) { if (platformSupportsExtendedNotifications() && !privacyModeEnabled) {
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender();
if (newMessages > 1) { if (newMessages > 1) {
//TODO: Stacked notifications for Android Wear
// https://developer.android.com/training/wearables/notifications/stacks.html
// multiple messages pending, show inbox style // multiple messages pending, show inbox style
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(builder); NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(builder);
for (Message m : data.messages) { for (Message m : data.messages) {
style.addLine(buildMessageSummary(context, style.addLine(buildMessageSummary(context,
getMessageSender(context, account, m), getMessageSender(context, account, m),
getMessageSubject(context, 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 are the GroupSummary notification
subBuilder.setGroupSummary(false); // this is not the summary
// set content
setNotificationContent(context, m, getMessageSender(context, account, message), getMessageSubject(context, message), subBuilder, accountDescr);
// set actions
addWearActions(wearableExtender, subBuilder, account, m);
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(account.getAccountNumber(), subBuilder.build());
} }
if (!data.droppedMessages.isEmpty()) { if (!data.droppedMessages.isEmpty()) {
style.setSummaryText(context.getString(R.string.notification_additional_messages, style.setSummaryText(context.getString(R.string.notification_additional_messages,
@ -4835,15 +4923,7 @@ public class MessagingController implements Runnable {
builder.setStyle(style); builder.setStyle(style);
} else { } else {
// single message pending, show big text // single message pending, show big text
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(builder); setNotificationContent(context, message, sender, subject, builder, accountDescr);
CharSequence preview = getMessagePreview(context, message);
if (preview != null) {
style.bigText(preview);
}
builder.setContentText(subject);
builder.setSubText(accountDescr);
builder.setContentTitle(sender);
builder.setStyle(style);
builder.addAction( builder.addAction(
platformSupportsLockScreenNotifications() platformSupportsLockScreenNotifications()
@ -4865,53 +4945,29 @@ public class MessagingController implements Runnable {
boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS || boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS ||
(deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG && newMessages == 1); (deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG && newMessages == 1);
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(); // add /different) actions to show on connected Android Wear devices
addWearActions(wearableExtender, builder, newMessages, account, allRefs, data.messages);
if (showDeleteAction) { if (showDeleteAction) {
// we need to pass the action directly to the activity, otherwise the // 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) // status bar won't be pulled up and we won't see the confirmation (if used)
// Delete on phone // Delete on phone
builder.addAction( builder.addAction(
platformSupportsLockScreenNotifications() platformSupportsLockScreenNotifications()
? R.drawable.ic_action_delete_dark_vector ? R.drawable.ic_action_delete_dark_vector
: R.drawable.ic_action_delete_dark, : R.drawable.ic_action_delete_dark,
context.getString(R.string.notification_action_delete), context.getString(R.string.notification_action_delete),
NotificationDeleteConfirmation.getIntent(context, account, allRefs)); 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));
}
} }
if (NotificationActionService.isArchiveAllMessagesWearAvaliable(context, account, data.messages)) { // this may be a summary notification for multiple stacked notifications
// for each individual mail, shown on Android Wear
// The phone will only show the summary as it's the last notification given
// to notifMgr with this account's key
builder.setGroup(NOTIFICATION_GROUP_KEY);
builder.setGroupSummary(true);
// Archive on wear } else { // no extended notifications supported
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 {
String accountNotice = context.getString(R.string.notification_new_one_account_fmt, String accountNotice = context.getString(R.string.notification_new_one_account_fmt,
unreadCount, accountDescr); unreadCount, accountDescr);
builder.setContentTitle(accountNotice); builder.setContentTitle(accountNotice);
@ -4925,6 +4981,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));
// 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<MessageReference> allRefs) {
TaskStackBuilder stack; TaskStackBuilder stack;
boolean treatAsSingleMessageNotification; boolean treatAsSingleMessageNotification;
@ -4953,32 +5052,30 @@ public class MessagingController implements Runnable {
stack = buildMessageListBackStack(context, account, initialFolder); stack = buildMessageListBackStack(context, account, initialFolder);
} }
return stack;
}
builder.setContentIntent(stack.getPendingIntent( /**
account.getAccountNumber(), * Set the content of a notification for a single message.
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT)); * @see #getMessagePreview(android.content.Context, com.fsck.k9.mail.Message)
builder.setDeleteIntent(NotificationActionService.getAcknowledgeIntent(context, account)); * @param context
* @param message
// Only ring or vibrate if we have not done so already on this account and fetch * @param sender
boolean ringAndVibrate = false; * @param subject
if (!updateSilently && !account.isRingNotified()) { * @param builder
account.setRingNotified(true); * @param accountDescr
ringAndVibrate = true; */
private NotificationCompat.Builder setNotificationContent(Context context, /*Local*/Message message, CharSequence sender, CharSequence subject, NotificationCompat.Builder builder, String accountDescr) {
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(builder);
CharSequence preview = getMessagePreview(context, message);
if (preview != null) {
style.bigText(preview);
} }
builder.setContentText(subject);
NotificationSetting n = account.getNotificationSetting(); builder.setSubText(accountDescr);
builder.setContentTitle(sender);
configureLockScreenNotification(builder, context, account, newMessages, unreadCount, accountDescr, sender, data.messages); builder.setStyle(style);
return builder;
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());
} }
private TaskStackBuilder buildAccountsBackStack(Context context) { private TaskStackBuilder buildAccountsBackStack(Context context) {
@ -5155,7 +5252,7 @@ public class MessagingController implements Runnable {
} }
/** Cancel a notification of new email messages */ /** Cancel a notification of new email messages */
public void notifyAccountCancel(Context context, Account account) { public void notifyAccountCancel(final Context context, final Account account) {
NotificationManager notifMgr = NotificationManager notifMgr =
(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notifMgr.cancel(account.getAccountNumber()); notifMgr.cancel(account.getAccountNumber());

View File

@ -12,6 +12,7 @@ import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.activity.MessageReference; import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.LocalMessage;
import android.app.PendingIntent; import android.app.PendingIntent;
@ -80,7 +81,7 @@ public class NotificationActionService extends CoreService {
* @param messages the messages to move to the spam folder (must be synchronized 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 * @return true if the ArchiveAllMessages intent is available for the given messages
*/ */
public static boolean isArchiveAllMessagesWearAvaliable(Context context, final Account account, final LinkedList<LocalMessage> messages) { public static boolean isArchiveAllMessagesWearAvaliable(Context context, final Account account, final List<? extends Message> messages) {
final MessagingController controller = MessagingController.getInstance(context); final MessagingController controller = MessagingController.getInstance(context);
return (account.getArchiveFolderName() != null && !(account.getArchiveFolderName().equals(account.getSpamFolderName()) && K9.confirmSpam()) && isMovePossible(controller, account, account.getSentFolderName(), messages)); return (account.getArchiveFolderName() != null && !(account.getArchiveFolderName().equals(account.getSpamFolderName()) && K9.confirmSpam()) && isMovePossible(controller, account, account.getSentFolderName(), messages));
} }
@ -105,7 +106,7 @@ public class NotificationActionService extends CoreService {
* @param messages the messages to move to the spam folder (must be synchronized 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 * @return true if the SpamAllMessages intent is available for the given messages
*/ */
public static boolean isSpamAllMessagesWearAvaliable(Context context, final Account account, final LinkedList<LocalMessage> messages) { public static boolean isSpamAllMessagesWearAvaliable(Context context, final Account account, final List<? extends Message> messages) {
final MessagingController controller = MessagingController.getInstance(context); final MessagingController controller = MessagingController.getInstance(context);
return (account.getSpamFolderName() != null && !K9.confirmSpam() && isMovePossible(controller, account, account.getSentFolderName(), messages)); return (account.getSpamFolderName() != null && !K9.confirmSpam() && isMovePossible(controller, account, account.getSentFolderName(), messages));
} }
@ -119,14 +120,14 @@ public class NotificationActionService extends CoreService {
return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); return PendingIntent.getService(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT);
} }
private static boolean isMovePossible(MessagingController controller, Account account, String dstFolder, List<LocalMessage> messages) { private static boolean isMovePossible(MessagingController controller, Account account, String dstFolder, List<? extends Message> messages) {
if (!controller.isMoveCapable(account)) { if (!controller.isMoveCapable(account)) {
return false; return false;
} }
if (K9.FOLDER_NONE.equalsIgnoreCase(dstFolder)) { if (K9.FOLDER_NONE.equalsIgnoreCase(dstFolder)) {
return false; return false;
} }
for(LocalMessage messageToMove : messages) { for(Message messageToMove : messages) {
if (!controller.isMoveCapable(messageToMove)) { if (!controller.isMoveCapable(messageToMove)) {
return false; return false;
} }