1
0
mirror of https://github.com/moparisthebest/k-9 synced 2025-01-08 04:08:15 -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_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<Message> {
@Override
@ -158,7 +162,7 @@ public class MessagingController implements Runnable {
if (o1 == null || o2 == null || o1.getUid() == null || o2.getUid() == null) {
return 0;
}
int id1, id2;
int id1, id2;
try {
id1 = Integer.parseInt(o1.getUid());
id2 = Integer.parseInt(o2.getUid());
@ -4673,6 +4677,7 @@ public class MessagingController implements Runnable {
return null;
}
private CharSequence getMessageSubject(Context context, Message message) {
String subject = message.getSubject();
if (!TextUtils.isEmpty(subject)) {
@ -4762,7 +4767,62 @@ 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.
*/
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) {
boolean updateSilently = false;
@ -4815,13 +4875,41 @@ public class MessagingController implements Runnable {
data.supplyAllMessageRefs(allRefs);
if (platformSupportsExtendedNotifications() && !privacyModeEnabled) {
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender();
if (newMessages > 1) {
//TODO: 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);
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 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()) {
style.setSummaryText(context.getString(R.string.notification_additional_messages,
@ -4835,15 +4923,7 @@ 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()
@ -4865,53 +4945,29 @@ public class MessagingController implements Runnable {
boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS ||
(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) {
// 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));
}
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
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);
@ -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;
boolean treatAsSingleMessageNotification;
@ -4953,32 +5052,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(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);
}
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) {
@ -5155,7 +5252,7 @@ public class MessagingController implements Runnable {
}
/** 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)context.getSystemService(Context.NOTIFICATION_SERVICE);
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.controller.MessagingController;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mailstore.LocalMessage;
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)
* @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);
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)
* @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);
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);
}
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)) {
return false;
}
if (K9.FOLDER_NONE.equalsIgnoreCase(dstFolder)) {
return false;
}
for(LocalMessage messageToMove : messages) {
for(Message messageToMove : messages) {
if (!controller.isMoveCapable(messageToMove)) {
return false;
}