diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ba6f993a4..a7073c8fe 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -172,6 +172,14 @@ android:configChanges="locale" > + + + + Konto \"%s\" wiederherstellen Neue Nachricht + %d neue Nachrichten %d Ungelesen (%s) + und %d weitere (%s) + + Antworten + Gelesen + Löschen Auf neue Nachrichten prüfen: %s:%s Auf neue Nachrichten prüfen @@ -320,12 +326,19 @@ Um Fehler zu melden, neue Funktionen vorzuschlagen oder Fragen zu stellen, besuc Löschen (nur in Nachrichtenansicht) Sternmarkierte Löschen (nur in Nachrichtenansicht) Spam + Löschen (aus Benachrichtigung) Betreff in Benachrichtigungen verbergen Niemals Wenn der Bildschirm gesperrt ist Immer + Löschen erlauben + Nie + Für einzelne Nachricht + Immer + Der Benachrichtigung eine Schaltfläche zum Löschen der Nachrichten hinzufügen + Gruppenoperationen-Schaltflächen Zeige folgende Schaltflächen in der Nachrichtenliste an Als (un)gelesen markieren @@ -945,6 +958,10 @@ Um Fehler zu melden, neue Funktionen vorzuschlagen oder Fragen zu stellen, besuc Löschen bestätigen Wollen Sie diese Nachricht löschen? + + Wollen Sie diese Nachricht wirklich löschen? + Wollen Sie wirklich %1$d Nachrichten löschen? + Löschen Nicht löschen diff --git a/res/values-v11/styles.xml b/res/values-v11/styles.xml new file mode 100644 index 000000000..9893fa580 --- /dev/null +++ b/res/values-v11/styles.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 03675d8ed..d2b1d226e 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -701,4 +701,16 @@ ALWAYS + + @string/global_settings_notification_quick_delete_never + @string/global_settings_notification_quick_delete_when_single_msg + @string/global_settings_notification_quick_delete_always + + + + NEVER + FOR_SINGLE_MSG + ALWAYS + + diff --git a/res/values/strings.xml b/res/values/strings.xml index a29718797..44e80e7ad 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -214,7 +214,13 @@ Please submit bug reports, contribute new features and ask questions at Recreating account \"%s\" New mail + %d new messages %d Unread (%s) + + %d more on %s + + Reply + Read + Delete Checking mail: %s:%s Checking mail @@ -329,12 +335,19 @@ Please submit bug reports, contribute new features and ask questions at Delete (in message view) Delete Starred (in message view) Spam + Delete (from notification) Hide subject in notifications Never When device is locked Always + Show \'Delete\' button + Never + For single message notification + Always + Show a button in the notification that allows quick message deletion + Batch buttons Configure message list batch buttons Mark read/unread @@ -957,6 +970,10 @@ Please submit bug reports, contribute new features and ask questions at Confirm deletion Do you want to delete this message? + + Do you really want to delete this message? + Do you really want to delete %1$d messages? + Yes No diff --git a/res/values/styles.xml b/res/values/styles.xml index 86a4c2eeb..02012079b 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -18,5 +18,8 @@ @android:color/primary_text_light + diff --git a/res/values/themes.xml b/res/values/themes.xml index 5d4e8ad6b..f92c80518 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -113,4 +113,21 @@ + + diff --git a/res/xml/global_preferences.xml b/res/xml/global_preferences.xml index 51bf29284..75efa5598 100644 --- a/res/xml/global_preferences.xml +++ b/res/xml/global_preferences.xml @@ -294,6 +294,17 @@ android:dialogTitle="@string/quiet_time_ends" android:title="@string/quiet_time_ends" /> + + + CREATOR = new Creator() { @Override public MessageReference createFromParcel(Parcel source) { diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index 760e37fac..1e3e663bf 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -14,6 +14,7 @@ import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.activity.misc.SwipeGestureDetector; import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener; +import com.fsck.k9.controller.MessagingController; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.fragment.MessageViewFragment; import com.fsck.k9.fragment.MessageViewFragment.MessageViewFragmentListener; @@ -41,7 +42,7 @@ public class MessageView extends K9FragmentActivity implements MessageViewFragme private static final String EXTRA_MESSAGE_REFERENCE = "com.fsck.k9.MessageView_messageReference"; private static final String EXTRA_MESSAGE_REFERENCES = "com.fsck.k9.MessageView_messageReferences"; - private static final String EXTRA_MESSAGE_LIST_EXTRAS = "com.fsck.k9.MessageView_messageListExtras"; + private static final String EXTRA_FROM_NOTIFICATION ="com.fsck.k9.MessageView_fromNotification"; /** * @see #mLastDirection @@ -50,16 +51,21 @@ public class MessageView extends K9FragmentActivity implements MessageViewFragme private static final int NEXT = 2; - public static void actionView(Context context, MessageReference messRef, - ArrayList messReferences, Bundle messageListExtras) { + public static Intent actionViewIntent(Context context, MessageReference messRef, + ArrayList messReferences) { Intent i = new Intent(context, MessageView.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - i.putExtra(EXTRA_MESSAGE_LIST_EXTRAS, messageListExtras); i.putExtra(EXTRA_MESSAGE_REFERENCE, messRef); i.putParcelableArrayListExtra(EXTRA_MESSAGE_REFERENCES, messReferences); - context.startActivity(i); + return i; } + public static Intent actionHandleNotificationIntent(Context context, MessageReference ref) { + Intent i = actionViewIntent(context, ref, null); + i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + i.putExtra(EXTRA_FROM_NOTIFICATION, true); + return i; + } private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); private Account mAccount; @@ -165,6 +171,9 @@ public class MessageView extends K9FragmentActivity implements MessageViewFragme onAccountUnavailable(); return; } + if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { + MessagingController.getInstance(getApplication()).notifyAccountCancel(this, mAccount); + } StorageManager.getInstance(getApplication()).addListener(mStorageListener); } @@ -438,7 +447,9 @@ public class MessageView extends K9FragmentActivity implements MessageViewFragme private void showNextMessage() { findSurroundingMessagesUid(); - mMessageReferences.remove(mMessageReference); + if (mMessageReferences == null) { + mMessageReferences.remove(mMessageReference); + } if (mLastDirection == NEXT && mNextMessage != null) { onNext(); } else if (mLastDirection == PREVIOUS && mPreviousMessage != null) { @@ -491,6 +502,10 @@ public class MessageView extends K9FragmentActivity implements MessageViewFragme private void findSurroundingMessagesUid() { mNextMessage = mPreviousMessage = null; + if (mMessageReferences == null) { + return; + } + int i = mMessageReferences.indexOf(mMessageReference); if (i < 0) { return; diff --git a/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java b/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java new file mode 100644 index 000000000..aeca92b8b --- /dev/null +++ b/src/com/fsck/k9/activity/NotificationDeleteConfirmation.java @@ -0,0 +1,103 @@ +package com.fsck.k9.activity; + +import java.util.ArrayList; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +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 int DIALOG_CONFIRM = 1; + + private Account mAccount; + private ArrayList mMessageRefs; + + public static PendingIntent getIntent(Context context, final Account account, final ArrayList refs) { + Intent i = new Intent(context, NotificationDeleteConfirmation.class); + i.putExtra(EXTRA_ACCOUNT, account.getUuid()); + i.putExtra(EXTRA_MESSAGE_LIST, refs); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + return PendingIntent.getActivity(context, account.getAccountNumber(), i, PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setTheme(K9.getK9Theme() == K9.THEME_LIGHT ? + R.style.Theme_K9_Dialog_Translucent_Light : R.style.Theme_K9_Dialog_Translucent_Dark); + + final Preferences preferences = Preferences.getPreferences(this); + final Intent intent = getIntent(); + + mAccount = preferences.getAccount(intent.getStringExtra(EXTRA_ACCOUNT)); + mMessageRefs = (ArrayList) intent.getSerializableExtra(EXTRA_MESSAGE_LIST); + + if (mAccount == null || mMessageRefs == null || mMessageRefs.isEmpty()) { + finish(); + } else if (!K9.confirmDeleteFromNotification()) { + triggerDelete(); + finish(); + } else { + showDialog(DIALOG_CONFIRM); + } + } + + @Override + public Dialog onCreateDialog(int id) { + switch (id) { + case DIALOG_CONFIRM: + return ConfirmationDialog.create(this, id, + R.string.dialog_confirm_delete_title, "", + R.string.dialog_confirm_delete_confirm_button, + R.string.dialog_confirm_delete_cancel_button, + new Runnable() { + @Override + public void run() { + triggerDelete(); + finish(); + } + }, + new Runnable() { + @Override + public void run() { + finish(); + } + }); + } + + return super.onCreateDialog(id); + } + + @Override + public void onPrepareDialog(int id, Dialog d) { + AlertDialog alert = (AlertDialog) d; + switch (id) { + case DIALOG_CONFIRM: + alert.setMessage(getResources().getQuantityString( + R.plurals.dialog_confirm_delete_message, mMessageRefs.size())); + break; + } + + super.onPrepareDialog(id, d); + } + + private void triggerDelete() { + Intent i = NotificationActionService.getDeleteAllMessagesIntent(this, mAccount, mMessageRefs); + startService(i); + } +} diff --git a/src/com/fsck/k9/activity/setup/Prefs.java b/src/com/fsck/k9/activity/setup/Prefs.java index 5c413e6c7..8a760e3dc 100644 --- a/src/com/fsck/k9/activity/setup/Prefs.java +++ b/src/com/fsck/k9/activity/setup/Prefs.java @@ -17,15 +17,18 @@ import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceScreen; import android.widget.Toast; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.K9.NotificationHideSubject; +import com.fsck.k9.K9.NotificationQuickDelete; import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.activity.ColorPickerDialog; import com.fsck.k9.activity.K9PreferenceActivity; +import com.fsck.k9.controller.MessagingController; import com.fsck.k9.helper.DateFormatter; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; @@ -72,6 +75,7 @@ public class Prefs extends K9PreferenceActivity { private static final String PREFERENCE_QUIET_TIME_ENABLED = "quiet_time_enabled"; private static final String PREFERENCE_QUIET_TIME_STARTS = "quiet_time_starts"; private static final String PREFERENCE_QUIET_TIME_ENDS = "quiet_time_ends"; + private static final String PREFERENCE_NOTIF_QUICK_DELETE = "notification_quick_delete"; private static final String PREFERENCE_BATCH_BUTTONS_MARK_READ = "batch_buttons_mark_read"; private static final String PREFERENCE_BATCH_BUTTONS_DELETE = "batch_buttons_delete"; private static final String PREFERENCE_BATCH_BUTTONS_ARCHIVE = "batch_buttons_archive"; @@ -122,6 +126,7 @@ public class Prefs extends K9PreferenceActivity { private CheckBoxPreference mQuietTimeEnabled; private com.fsck.k9.preferences.TimePickerPreference mQuietTimeStarts; private com.fsck.k9.preferences.TimePickerPreference mQuietTimeEnds; + private ListPreference mNotificationQuickDelete; private Preference mAttachmentPathPreference; private CheckBoxPreference mBatchButtonsMarkRead; @@ -196,16 +201,25 @@ public class Prefs extends K9PreferenceActivity { mStartIntegratedInbox.setChecked(K9.startIntegratedInbox()); mConfirmActions = (CheckBoxListPreference) findPreference(PREFERENCE_CONFIRM_ACTIONS); - mConfirmActions.setItems(new CharSequence[] { - getString(R.string.global_settings_confirm_action_delete), - getString(R.string.global_settings_confirm_action_delete_starred), - getString(R.string.global_settings_confirm_action_spam), - }); - mConfirmActions.setCheckedItems(new boolean[] { - K9.confirmDelete(), - K9.confirmDeleteStarred(), - K9.confirmSpam(), - }); + + boolean canDeleteFromNotification = MessagingController.platformSupportsExtendedNotifications(); + CharSequence[] confirmActionEntries = new CharSequence[canDeleteFromNotification ? 4 : 3]; + boolean[] confirmActionValues = new boolean[canDeleteFromNotification ? 4 : 3]; + int index = 0; + + confirmActionEntries[index] = getString(R.string.global_settings_confirm_action_delete); + confirmActionValues[index++] = K9.confirmDelete(); + confirmActionEntries[index] = getString(R.string.global_settings_confirm_action_delete_starred); + confirmActionValues[index++] = K9.confirmDeleteStarred(); + if (canDeleteFromNotification) { + confirmActionEntries[index] = getString(R.string.global_settings_confirm_action_delete_notif); + confirmActionValues[index++] = K9.confirmDeleteFromNotification(); + } + confirmActionEntries[index] = getString(R.string.global_settings_confirm_action_spam); + confirmActionValues[index++] = K9.confirmSpam(); + + mConfirmActions.setItems(confirmActionEntries); + mConfirmActions.setCheckedItems(confirmActionValues); mNotificationHideSubject = setupListPreference(PREFERENCE_NOTIFICATION_HIDE_SUBJECT, K9.getNotificationHideSubject().toString()); @@ -305,8 +319,13 @@ public class Prefs extends K9PreferenceActivity { } }); - - + mNotificationQuickDelete = setupListPreference(PREFERENCE_NOTIF_QUICK_DELETE, + K9.getNotificationQuickDeleteBehaviour().toString()); + if (!MessagingController.platformSupportsExtendedNotifications()) { + PreferenceScreen prefs = (PreferenceScreen) findPreference("notification_preferences"); + prefs.removePreference(mNotificationQuickDelete); + mNotificationQuickDelete = null; + } mBackgroundOps = setupListPreference(PREFERENCE_BACKGROUND_OPS, K9.getBackgroundOps().toString()); // In ICS+ there is no 'background data' setting that apps can chose to ignore anymore. So @@ -420,11 +439,16 @@ public class Prefs extends K9PreferenceActivity { K9.setUseVolumeKeysForNavigation(mVolumeNavigation.getCheckedItems()[0]); K9.setUseVolumeKeysForListNavigation(mVolumeNavigation.getCheckedItems()[1]); K9.setStartIntegratedInbox(!mHideSpecialAccounts.isChecked() && mStartIntegratedInbox.isChecked()); - K9.setConfirmDelete(mConfirmActions.getCheckedItems()[0]); - K9.setConfirmDeleteStarred(mConfirmActions.getCheckedItems()[1]); - K9.setConfirmSpam(mConfirmActions.getCheckedItems()[2]); K9.setNotificationHideSubject(NotificationHideSubject.valueOf(mNotificationHideSubject.getValue())); + int index = 0; + K9.setConfirmDelete(mConfirmActions.getCheckedItems()[index++]); + K9.setConfirmDeleteStarred(mConfirmActions.getCheckedItems()[index++]); + if (MessagingController.platformSupportsExtendedNotifications()) { + K9.setConfirmDeleteFromNotification(mConfirmActions.getCheckedItems()[index++]); + } + K9.setConfirmSpam(mConfirmActions.getCheckedItems()[index++]); + K9.setMeasureAccounts(mMeasureAccounts.isChecked()); K9.setCountSearchMessages(mCountSearch.isChecked()); K9.setHideSpecialAccounts(mHideSpecialAccounts.isChecked()); @@ -445,6 +469,11 @@ public class Prefs extends K9PreferenceActivity { K9.setQuietTimeStarts(mQuietTimeStarts.getTime()); K9.setQuietTimeEnds(mQuietTimeEnds.getTime()); + if (mNotificationQuickDelete != null) { + K9.setNotificationQuickDeleteBehaviour( + NotificationQuickDelete.valueOf(mNotificationQuickDelete.getValue())); + } + K9.setBatchButtonsMarkRead(mBatchButtonsMarkRead.isChecked()); K9.setBatchButtonsDelete(mBatchButtonsDelete.isChecked()); K9.setBatchButtonsArchive(mBatchButtonsArchive.isChecked()); diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 64a56d9b5..febe4b540 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -32,7 +32,10 @@ import android.net.Uri; import android.os.Build; import android.os.PowerManager; import android.os.Process; +import android.support.v4.app.NotificationCompat; +import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; import android.util.Log; import com.fsck.k9.Account; @@ -40,11 +43,17 @@ import com.fsck.k9.AccountStats; import com.fsck.k9.K9; import com.fsck.k9.K9.NotificationHideSubject; import com.fsck.k9.K9.Intents; +import com.fsck.k9.K9.NotificationQuickDelete; import com.fsck.k9.NotificationSetting; import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.activity.FolderList; import com.fsck.k9.activity.MessageList; +import com.fsck.k9.activity.MessageReference; +import com.fsck.k9.activity.MessageView; +import com.fsck.k9.activity.NotificationDeleteConfirmation; +import com.fsck.k9.helper.Contacts; +import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.NotificationBuilder; import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.power.TracingPowerManager; @@ -79,6 +88,7 @@ import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchAccount; import com.fsck.k9.search.SearchSpecification; import com.fsck.k9.search.SqlQueryBuilder; +import com.fsck.k9.service.NotificationActionService; /** @@ -193,6 +203,49 @@ public class MessagingController implements Runnable { // Key is accountUuid:folderName:messageUid , value is unimportant private ConcurrentHashMap deletedUids = new ConcurrentHashMap(); + private static class NotificationData { + int unreadBeforeNotification; + LinkedList messages; // newest one first + LinkedList droppedMessages; // newest one first + + // There's no point in storing more than 5 messages for the notification, as a single notification + // can't display more than that anyway. + private final static int MAX_MESSAGES = 5; + + @SuppressWarnings("serial") + public NotificationData(int unread) { + unreadBeforeNotification = unread; + droppedMessages = new LinkedList(); + messages = new LinkedList() { + @Override + public boolean add(Message m) { + while (size() >= MAX_MESSAGES) { + Message dropped = super.removeLast(); + droppedMessages.add(0, dropped.makeMessageReference()); + } + super.add(0, m); + return true; + } + }; + } + + public ArrayList getAllMessageRefs() { + ArrayList refs = new ArrayList(); + for (Message m : messages) { + refs.add(m.makeMessageReference()); + } + refs.addAll(droppedMessages); + return refs; + } + + public int getNewMessageCount() { + return messages.size() + droppedMessages.size(); + } + }; + + // Key is accountNumber + private ConcurrentHashMap notificationData = new ConcurrentHashMap(); + private static final Flag[] SYNC_FLAGS = new Flag[] { Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED, Flag.FORWARDED }; private String createMessageKey(Account account, String folder, Message message) { @@ -1508,7 +1561,7 @@ public class MessagingController implements Runnable { // Send a notification of this message if (shouldNotifyForMessage(account, localFolder, message)) { - notifyAccount(mApplication, account, message, unreadBeforeStart, newMessages); + notifyAccount(mApplication, account, message, unreadBeforeStart); } } catch (MessagingException me) { @@ -1644,7 +1697,7 @@ public class MessagingController implements Runnable { // Send a notification of this message if (shouldNotifyForMessage(account, localFolder, message)) { - notifyAccount(mApplication, account, message, unreadBeforeStart, newMessages); + notifyAccount(mApplication, account, message, unreadBeforeStart); } }//for large messages @@ -1681,6 +1734,7 @@ public class MessagingController implements Runnable { Message localMessage = localFolder.getMessage(remoteMessage.getUid()); boolean messageChanged = syncFlags(localMessage, remoteMessage); if (messageChanged) { + boolean shouldBeNotifiedOf = false; if (localMessage.isSet(Flag.DELETED) || isMessageSuppressed(account, folder, localMessage)) { for (MessagingListener l : getListeners()) { l.synchronizeMailboxRemovedMessage(account, folder, localMessage); @@ -1689,8 +1743,38 @@ public class MessagingController implements Runnable { for (MessagingListener l : getListeners()) { l.synchronizeMailboxAddOrUpdateMessage(account, folder, localMessage); } + if (shouldNotifyForMessage(account, localFolder, localMessage)) { + shouldBeNotifiedOf = true; + } } + NotificationData data = getNotificationData(account, -1); + if (data != null) { + synchronized (data) { + boolean needUpdateNotification = false; + String uid = localMessage.getUid(); + + for (Message m : data.messages) { + if (uid.equals(m.getUid()) && !shouldBeNotifiedOf) { + data.messages.remove(m); + needUpdateNotification = true; + break; + } + } + if (!needUpdateNotification) { + for (MessageReference dropped : data.droppedMessages) { + if (uid.equals(dropped.uid) && !shouldBeNotifiedOf) { + data.droppedMessages.remove(dropped.uid); + needUpdateNotification = true; + break; + } + } + } + if (needUpdateNotification) { + notifyAccountWithDataLocked(mApplication, account, null, data); + } + } + } } progress.incrementAndGet(); for (MessagingListener l : getListeners()) { @@ -3076,7 +3160,7 @@ public class MessagingController implements Runnable { NotificationManager notifMgr = (NotificationManager) mApplication.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationBuilder builder = NotificationBuilder.createInstance(mApplication); + NotificationCompat.Builder builder = new NotificationCompat.Builder(mApplication); builder.setSmallIcon(R.drawable.ic_menu_refresh); builder.setWhen(System.currentTimeMillis()); builder.setOngoing(true); @@ -3101,7 +3185,7 @@ public class MessagingController implements Runnable { } notifMgr.notify(K9.FETCHING_EMAIL_NOTIFICATION - account.getAccountNumber(), - builder.getNotification()); + builder.build()); } private void notifySendTempFailed(Account account, Exception lastFailure) { @@ -3126,7 +3210,7 @@ public class MessagingController implements Runnable { NotificationManager notifMgr = (NotificationManager) mApplication.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationBuilder builder = NotificationBuilder.createInstance(mApplication); + NotificationCompat.Builder builder = new NotificationCompat.Builder(mApplication); builder.setSmallIcon(R.drawable.stat_notify_email_generic); builder.setWhen(System.currentTimeMillis()); builder.setAutoCancel(true); @@ -3142,7 +3226,7 @@ public class MessagingController implements Runnable { K9.NOTIFICATION_LED_BLINK_FAST, true); notifMgr.notify(K9.SEND_FAILED_NOTIFICATION - account.getAccountNumber(), - builder.getNotification()); + builder.build()); } /** @@ -3161,7 +3245,7 @@ public class MessagingController implements Runnable { final NotificationManager notifMgr = (NotificationManager) mApplication.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationBuilder builder = NotificationBuilder.createInstance(mApplication); + NotificationCompat.Builder builder = new NotificationCompat.Builder(mApplication); builder.setSmallIcon(R.drawable.ic_menu_refresh); builder.setWhen(System.currentTimeMillis()); builder.setOngoing(true); @@ -3187,7 +3271,7 @@ public class MessagingController implements Runnable { } notifMgr.notify(K9.FETCHING_EMAIL_NOTIFICATION - account.getAccountNumber(), - builder.getNotification()); + builder.build()); } private void notifyFetchingMailCancel(final Account account) { @@ -4371,46 +4455,159 @@ public class MessagingController implements Runnable { return true; } + private NotificationData getNotificationData(Account account, int previousUnreadMessageCount) { + NotificationData data; + synchronized (notificationData) { + data = notificationData.get(account.getAccountNumber()); + if (data == null && previousUnreadMessageCount >= 0) { + data = new NotificationData(previousUnreadMessageCount); + notificationData.put(account.getAccountNumber(), data); + } + } + + return data; + } + + private CharSequence getMessageSender(Context context, Account account, Message message) { + try { + boolean isSelf = false; + final Contacts contacts = K9.showContactName() ? Contacts.getInstance(context) : null; + final Address[] fromAddrs = message.getFrom(); + + if (fromAddrs != null) { + isSelf = account.isAnIdentity(fromAddrs); + if (!isSelf && fromAddrs.length > 0) { + return fromAddrs[0].toFriendly(contacts).toString(); + } + } + + if (isSelf) { + // show To: if the message was sent from me + Address[] rcpts = message.getRecipients(Message.RecipientType.TO); + + if (rcpts != null && rcpts.length > 0) { + return context.getString(R.string.message_to_fmt, + rcpts[0].toFriendly(contacts).toString()); + } + + return context.getString(R.string.general_no_sender); + } + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Unable to get sender information for notification.", e); + } + + return null; + } + + private CharSequence getMessageSubject(Context context, Message message) { + String subject = message.getSubject(); + if (!TextUtils.isEmpty(subject)) { + return subject; + } + + return context.getString(R.string.general_no_subject); + } + + private static TextAppearanceSpan sEmphasizedSpan; + private TextAppearanceSpan getEmphasizedSpan(Context context) { + if (sEmphasizedSpan == null) { + sEmphasizedSpan = new TextAppearanceSpan(context, + R.style.TextAppearance_StatusBar_EventContent_Emphasized); + } + return sEmphasizedSpan; + } + + private CharSequence getMessagePreview(Context context, Message message) { + CharSequence subject = getMessageSubject(context, message); + String snippet = message.getPreview(); + + if (TextUtils.isEmpty(subject)) { + return snippet; + } else if (TextUtils.isEmpty(snippet)) { + return subject; + } + + SpannableStringBuilder preview = new SpannableStringBuilder(); + preview.append(subject); + preview.append('\n'); + preview.append(snippet); + + preview.setSpan(getEmphasizedSpan(context), 0, subject.length(), 0); + + return preview; + } + + private CharSequence buildMessageSummary(Context context, CharSequence sender, CharSequence subject) { + if (sender == null) { + return subject; + } + + SpannableStringBuilder summary = new SpannableStringBuilder(); + summary.append(sender); + summary.append(" "); + summary.append(subject); + + summary.setSpan(getEmphasizedSpan(context), 0, sender.length(), 0); + + return summary; + } + + private static final boolean platformShowsNumberInNotification() { + // Honeycomb and newer don't show the number as overlay on the notification icon. + // However, the number will appear in the detailed notification view. + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + } + + public static final boolean platformSupportsExtendedNotifications() { + // supported in Jellybean + // TODO: use constant once target SDK is set to >= 16 + return Build.VERSION.SDK_INT >= 16; + } + + private Message findNewestMessageForNotificationLocked(Context context, + Account account, NotificationData data) { + if (!data.messages.isEmpty()) { + return data.messages.get(0); + } + + if (!data.droppedMessages.isEmpty()) { + return data.droppedMessages.get(0).restoreToLocalMessage(context); + } + + return null; + } /** * Creates a notification of a newly received message. */ - private void notifyAccount(Context context, Account account, Message message, - int previousUnreadMessageCount, AtomicInteger newMessageCount) { - - // If we have a message, set the notification to ": " - StringBuilder messageNotice = new StringBuilder(); - final KeyguardManager keyguardService = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); - try { - if (message.getFrom() != null) { - Address[] fromAddrs = message.getFrom(); - String from = fromAddrs.length > 0 ? fromAddrs[0].toFriendly().toString() : null; - String subject = message.getSubject(); - if (subject == null) { - subject = context.getString(R.string.general_no_subject); - } - - if (from != null) { - // Show From: address by default - if (!account.isAnIdentity(fromAddrs)) { - messageNotice.append(from).append(": ").append(subject); - } - // show To: if the message was sent from me - else { - Address[] rcpts = message.getRecipients(Message.RecipientType.TO); - String to = rcpts.length > 0 ? rcpts[0].toFriendly().toString() : null; - if (to != null) { - messageNotice.append(String.format(context.getString(R.string.message_to_fmt), to)).append(": ").append(subject); - } else { - messageNotice.append(context.getString(R.string.general_no_sender)).append(": ").append(subject); - } - } - } - } - } catch (MessagingException e) { - Log.e(K9.LOG_TAG, "Unable to get message information for notification.", e); + private void notifyAccount(Context context, Account account, + Message message, int previousUnreadMessageCount) { + final NotificationData data = getNotificationData(account, previousUnreadMessageCount); + synchronized (data) { + notifyAccountWithDataLocked(context, account, message, data); } + } + + private void notifyAccountWithDataLocked(Context context, Account account, + Message message, NotificationData data) { + boolean updateSilently = false; + + if (message == null) { + /* this can happen if a message we previously notified for is read or deleted remotely */ + message = findNewestMessageForNotificationLocked(context, account, data); + updateSilently = true; + if (message == null) { + return; + } + } else { + data.messages.add(message); + } + + final KeyguardManager keyguardService = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + final CharSequence sender = getMessageSender(context, account, message); + final CharSequence subject = getMessageSubject(context, message); + CharSequence summary = buildMessageSummary(context, sender, subject); // If privacy mode active and keyguard active // OR @@ -4420,41 +4617,130 @@ public class MessagingController implements Runnable { if ((K9.getNotificationHideSubject() == NotificationHideSubject.WHEN_LOCKED && keyguardService.inKeyguardRestrictedInputMode()) || (K9.getNotificationHideSubject() == NotificationHideSubject.ALWAYS) || - messageNotice.length() == 0) { - messageNotice = new StringBuilder(context.getString(R.string.notification_new_title)); + summary.length() == 0) { + summary = context.getString(R.string.notification_new_title); } NotificationManager notifMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationBuilder builder = NotificationBuilder.createInstance(context); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); builder.setSmallIcon(R.drawable.stat_notify_email_generic); builder.setWhen(System.currentTimeMillis()); - builder.setTicker(messageNotice); + if (!updateSilently) { + builder.setTicker(summary); + } - final int unreadCount = previousUnreadMessageCount + newMessageCount.get(); - if (account.isNotificationShowsUnreadCount() || - // Honeycomb and newer don't show the number as overlay on the notification icon. - // However, the number will appear in the detailed notification view. - Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + final int newMessages = data.getNewMessageCount(); + final int unreadCount = data.unreadBeforeNotification + newMessages; + + if (account.isNotificationShowsUnreadCount() || platformShowsNumberInNotification()) { builder.setNumber(unreadCount); } String accountDescr = (account.getDescription() != null) ? account.getDescription() : account.getEmail(); - String accountNotice = context.getString(R.string.notification_new_one_account_fmt, - unreadCount, accountDescr); - builder.setContentTitle(accountNotice); - builder.setContentText(messageNotice); + final ArrayList allRefs = data.getAllMessageRefs(); - Intent i = FolderList.actionHandleNotification(context, account, - message.getFolder().getName()); - PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); + if (platformSupportsExtendedNotifications()) { + if (newMessages > 1) { + // 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))); + } + if (!data.droppedMessages.isEmpty()) { + style.setSummaryText(context.getString(R.string.notification_additional_messages, + data.droppedMessages.size(), accountDescr)); + } + String title = context.getString(R.string.notification_new_messages_title, newMessages); + style.setBigContentTitle(title); + builder.setContentTitle(title); + builder.setSubText(accountDescr); + 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); + + builder.addAction(R.drawable.ic_action_single_message_options_dark, + context.getString(R.string.notification_action_reply), + NotificationActionService.getReplyIntent(context, account, message.makeMessageReference())); + } + + builder.addAction(R.drawable.ic_action_mark_as_read_dark, + context.getString(R.string.notification_action_read), + NotificationActionService.getReadAllMessagesIntent(context, account, allRefs)); + + NotificationQuickDelete deleteOption = K9.getNotificationQuickDeleteBehaviour(); + boolean showDeleteAction = deleteOption == NotificationQuickDelete.ALWAYS || + (deleteOption == NotificationQuickDelete.FOR_SINGLE_MSG && newMessages == 1); + + 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) + builder.addAction(R.drawable.ic_action_delete_dark, + context.getString(R.string.notification_action_delete), + NotificationDeleteConfirmation.getIntent(context, account, allRefs)); + } + } else { + String accountNotice = context.getString(R.string.notification_new_one_account_fmt, + unreadCount, accountDescr); + builder.setContentTitle(accountNotice); + builder.setContentText(summary); + } + + for (Message m : data.messages) { + if (m.isSet(Flag.FLAGGED)) { + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + break; + } + } + + Intent targetIntent; + boolean treatAsSingleMessageNotification; + + if (platformSupportsExtendedNotifications()) { + // in the new-style notifications, we focus on the new messages, not the unread ones + treatAsSingleMessageNotification = newMessages == 1; + } else { + // in the old-style notifications, we focus on unread messages, as we don't have a + // good way to express the new message count + treatAsSingleMessageNotification = unreadCount == 1; + } + + if (treatAsSingleMessageNotification) { + targetIntent = MessageView.actionHandleNotificationIntent( + context, message.makeMessageReference()); + } else { + String initialFolder = message.getFolder().getName(); + /* only go to folder if all messages are in the same folder, else go to folder list */ + for (MessageReference ref : allRefs) { + if (!TextUtils.equals(initialFolder, ref.folderName)) { + initialFolder = null; + break; + } + } + + targetIntent = FolderList.actionHandleNotification(context, account, initialFolder); + } + + PendingIntent pi = PendingIntent.getActivity(context, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pi); + 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 (!account.isRingNotified()) { + if (!updateSilently && !account.isRingNotified()) { account.setRingNotified(true); ringAndVibrate = true; } @@ -4469,7 +4755,7 @@ public class MessagingController implements Runnable { K9.NOTIFICATION_LED_BLINK_SLOW, ringAndVibrate); - notifMgr.notify(account.getAccountNumber(), builder.getNotification()); + notifMgr.notify(account.getAccountNumber(), builder.build()); } /** @@ -4490,7 +4776,7 @@ public class MessagingController implements Runnable { * @param ringAndVibrate * {@code true}, if ringtone/vibration are allowed. {@code false}, otherwise. */ - private void configureNotification(NotificationBuilder builder, String ringtone, + private void configureNotification(NotificationCompat.Builder builder, String ringtone, long[] vibrationPattern, Integer ledColor, int ledSpeed, boolean ringAndVibrate) { // if it's quiet time, then we shouldn't be ringing, buzzing or flashing @@ -4529,6 +4815,7 @@ public class MessagingController implements Runnable { (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); notifMgr.cancel(account.getAccountNumber()); notifMgr.cancel(-1000 - account.getAccountNumber()); + notificationData.remove(account.getAccountNumber()); } /** diff --git a/src/com/fsck/k9/helper/NotificationBuilder.java b/src/com/fsck/k9/helper/NotificationBuilder.java deleted file mode 100644 index c637cb96d..000000000 --- a/src/com/fsck/k9/helper/NotificationBuilder.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.fsck.k9.helper; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.os.Vibrator; - -/** - * Helper class to create system notifications - * - * @see NotificationBuilderApi1 - * @see NotificationBuilderApi11 - */ -public abstract class NotificationBuilder { - - /** - * Create instance of an API-specific {@code NotificationBuilder} subclass. - * - * @param context - * A {@link Context} instance. - * - * @return Appropriate {@link NotificationBuilder} instance for this device. - */ - public static NotificationBuilder createInstance(Context context) { - Context appContext = context.getApplicationContext(); - - NotificationBuilder instance; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { - instance = new NotificationBuilderApi1(appContext); - } else { - instance = new NotificationBuilderApi11(appContext); - } - - return instance; - } - - - protected Context mContext; - - /** - * Constructor - * - * @param context - * A {@link Context} instance. - */ - protected NotificationBuilder(Context context) { - mContext = context; - } - - /** - * Set the small icon to use in the notification layouts. - * - * @param icon - * A resource ID in the application's package of the drawble to use. - */ - public abstract void setSmallIcon(int icon); - - /** - * Set the time that the event occurred. - * - * @param when - * Timestamp of when the event occurred. - */ - public abstract void setWhen(long when); - - /** - * Set the text that is displayed in the status bar when the notification first arrives. - * - * @param tickerText - * The text to display. - */ - public abstract void setTicker(CharSequence tickerText); - - /** - * Set the title (first row) of the notification, in a standard notification. - * - * @param title - * The text to display as notification title. - */ - public abstract void setContentTitle(CharSequence title); - - /** - * Set the text (second row) of the notification, in a standard notification. - * - * @param text - * The text to display. - */ - public abstract void setContentText(CharSequence text); - - /** - * Supply a PendingIntent to send when the notification is clicked. - * - * @param intent - * The intent that will be sent when the notification was clicked. - */ - public abstract void setContentIntent(PendingIntent intent); - - /** - * Set the large number at the right-hand side of the notification. - * - * @param number - * The number to display in the notification. - */ - public abstract void setNumber(int number); - - /** - * Set whether this is an ongoing notification. - * - * @param ongoing - * {@code true}, if it this is an ongoing notification. {@code false}, otherwise. - */ - public abstract void setOngoing(boolean ongoing); - - /** - * Setting this flag will make it so the notification is automatically canceled when the user - * clicks it in the panel. - * - * @param autoCancel - * {@code true}, if the notification should be automatically cancelled when the user - * clicks on it. {@code false}, otherwise. - */ - public abstract void setAutoCancel(boolean autoCancel); - - /** - * Set the sound to play. - * - * It will play on the notification stream. - * - * @param sound - * The URI of the sound to play. - */ - public abstract void setSound(Uri sound); - - /** - * Set the vibration pattern to use. - * - * @param pattern - * An array of longs of times for which to turn the vibrator on or off. - * - * @see Vibrator#vibrate(long[], int) - */ - public abstract void setVibrate(long[] pattern); - - /** - * Set the color that you would like the LED on the device to blink, as well as the rate. - * - * @param argb - * The color the LED should blink. - * @param onMs - * The number of milliseconds the LED should be on. - * @param offMs - * The number of milliseconds the LED should be off. - */ - public abstract void setLights(int argb, int onMs, int offMs); - - /** - * Combine all of the options that have been set and return a new {@link Notification} object. - * - * @return A new {@code Notification} object configured by this {@link NotificationBuilder}. - */ - public abstract Notification getNotification(); -} diff --git a/src/com/fsck/k9/helper/NotificationBuilderApi1.java b/src/com/fsck/k9/helper/NotificationBuilderApi1.java deleted file mode 100644 index c57a77991..000000000 --- a/src/com/fsck/k9/helper/NotificationBuilderApi1.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.fsck.k9.helper; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.media.AudioManager; -import android.net.Uri; - -/** - * Create notifications using the now deprecated {@link Notification} constructor. - */ -public class NotificationBuilderApi1 extends NotificationBuilder { - private int mSmallIcon; - private long mWhen; - private CharSequence mTickerText; - private CharSequence mContentText; - private CharSequence mContentTitle; - private PendingIntent mContentIntent; - private int mNumber; - private boolean mOngoing; - private boolean mAutoCancel; - private Uri mSoundUri; - private long[] mVibrationPattern; - private int mLedColor; - private int mLedOnMS; - private int mLedOffMS; - private boolean mBlinkLed; - - - protected NotificationBuilderApi1(Context context) { - super(context); - } - - @Override - public void setSmallIcon(int icon) { - mSmallIcon = icon; - } - - @Override - public void setWhen(long when) { - mWhen = when; - } - - @Override - public void setTicker(CharSequence tickerText) { - mTickerText = tickerText; - } - - @Override - public void setContentTitle(CharSequence title) { - mContentTitle = title; - } - - @Override - public void setContentText(CharSequence text) { - mContentText = text; - } - - @Override - public void setContentIntent(PendingIntent intent) { - mContentIntent = intent; - } - - @Override - public void setNumber(int number) { - mNumber = number; - } - - @Override - public void setOngoing(boolean ongoing) { - mOngoing = ongoing; - } - - @Override - public void setAutoCancel(boolean autoCancel) { - mAutoCancel = autoCancel; - } - - @Override - public void setSound(Uri sound) { - mSoundUri = sound; - } - - @Override - public void setVibrate(long[] pattern) { - mVibrationPattern = pattern; - } - - @Override - public void setLights(int argb, int onMs, int offMs) { - mBlinkLed = true; - mLedColor = argb; - mLedOnMS = onMs; - mLedOffMS = offMs; - } - - @SuppressWarnings("deprecation") - @Override - public Notification getNotification() { - Notification notification = new Notification(mSmallIcon, mTickerText, mWhen); - notification.number = mNumber; - notification.setLatestEventInfo(mContext, mContentTitle, mContentText, mContentIntent); - - if (mSoundUri != null) { - notification.sound = mSoundUri; - notification.audioStreamType = AudioManager.STREAM_NOTIFICATION; - } - - if (mVibrationPattern != null) { - notification.vibrate = mVibrationPattern; - } - - if (mBlinkLed) { - notification.flags |= Notification.FLAG_SHOW_LIGHTS; - notification.ledARGB = mLedColor; - notification.ledOnMS = mLedOnMS; - notification.ledOffMS = mLedOffMS; - } - - if (mAutoCancel) { - notification.flags |= Notification.FLAG_AUTO_CANCEL; - } - - if (mOngoing) { - notification.flags |= Notification.FLAG_ONGOING_EVENT; - } - - return notification; - } -} diff --git a/src/com/fsck/k9/helper/NotificationBuilderApi11.java b/src/com/fsck/k9/helper/NotificationBuilderApi11.java deleted file mode 100644 index 369db11ab..000000000 --- a/src/com/fsck/k9/helper/NotificationBuilderApi11.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.fsck.k9.helper; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.media.AudioManager; -import android.net.Uri; - -/** - * Create notifications using the new {@link android.app.Notification.Builder} class. - */ -@TargetApi(11) -public class NotificationBuilderApi11 extends NotificationBuilder { - private Notification.Builder mBuilder; - - - protected NotificationBuilderApi11(Context context) { - super(context); - mBuilder = new Notification.Builder(context); - } - - @Override - public void setSmallIcon(int icon) { - mBuilder.setSmallIcon(icon); - } - - @Override - public void setWhen(long when) { - mBuilder.setWhen(when); - } - - @Override - public void setTicker(CharSequence tickerText) { - mBuilder.setTicker(tickerText); - } - - @Override - public void setContentTitle(CharSequence title) { - mBuilder.setContentTitle(title); - } - - @Override - public void setContentText(CharSequence text) { - mBuilder.setContentText(text); - } - - @Override - public void setContentIntent(PendingIntent intent) { - mBuilder.setContentIntent(intent); - } - - @Override - public void setNumber(int number) { - mBuilder.setNumber(number); - mBuilder.setContentInfo("" + number); - } - - @Override - public void setOngoing(boolean ongoing) { - mBuilder.setOngoing(ongoing); - } - - @Override - public void setAutoCancel(boolean autoCancel) { - mBuilder.setAutoCancel(autoCancel); - } - - @Override - public void setSound(Uri sound) { - mBuilder.setSound(sound, AudioManager.STREAM_NOTIFICATION); - } - - @Override - public void setVibrate(long[] pattern) { - mBuilder.setVibrate(pattern); - } - - @Override - public void setLights(int argb, int onMs, int offMs) { - mBuilder.setLights(argb, onMs, offMs); - } - - @Override - public Notification getNotification() { - return mBuilder.getNotification(); - } -} diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 3d31f0839..6f0a819bc 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -148,7 +148,54 @@ public abstract class Message implements Part, Body { public abstract String getPreview(); public abstract boolean hasAttachments(); + /* + * calculateContentPreview + * Takes a plain text message body as a string. + * Returns a message summary as a string suitable for showing in a message list + * + * A message summary should be about the first 160 characters + * of unique text written by the message sender + * Quoted text, "On $date" and so on will be stripped out. + * All newlines and whitespace will be compressed. + * + */ + public static String calculateContentPreview(String text) { + if (text == null) { + return null; + } + // Only look at the first 8k of a message when calculating + // the preview. This should avoid unnecessary + // memory usage on large messages + if (text.length() > 8192) { + text = text.substring(0, 8192); + } + + // Remove (correctly delimited by '-- \n') signatures + text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", ""); + // try to remove lines of dashes in the preview + text = text.replaceAll("(?m)^----.*?$", ""); + // remove quoted text from the preview + text = text.replaceAll("(?m)^[#>].*$", ""); + // Remove a common quote header from the preview + text = text.replaceAll("(?m)^On .*wrote.?$", ""); + // Remove a more generic quote header from the preview + text = text.replaceAll("(?m)^.*\\w+:$", ""); + // Remove horizontal rules. + text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " "); + + // URLs in the preview should just be shown as "..." - They're not + // clickable and they usually overwhelm the preview + text = text.replaceAll("https?://\\S+", "..."); + // Don't show newlines in the preview + text = text.replaceAll("(\\r|\\n)+", " "); + // Collapse whitespace in the preview + text = text.replaceAll("\\s+", " "); + // Remove any whitespace at the beginning and end of the string. + text = text.trim(); + + return (text.length() <= 512) ? text : text.substring(0, 512); + } public void delete(String trashFolderName) throws MessagingException {} diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 2777420de..004cef186 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -23,6 +23,10 @@ import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; @@ -591,6 +595,32 @@ public class MimeMessage extends Message { } public String getPreview() { + String preview = null; + + try { + Part part = MimeUtility.findFirstPartByMimeType(this, "text/html"); + if (part != null) { + // We successfully found an HTML part; do the necessary character set decoding. + preview = MimeUtility.getTextFromPart(part); + if (preview != null) { + preview = HtmlConverter.htmlToText(preview); + } + } + if (preview == null) { + // no HTML part -> try and get a text part. + part = MimeUtility.findFirstPartByMimeType(this, "text/plain"); + if (part != null) { + preview = MimeUtility.getTextFromPart(part); + } + } + } catch (MessagingException e) { + Log.d(K9.LOG_TAG, "Could not extract message preview", e); + } + + if (preview != null) { + return calculateContentPreview(preview); + } + return ""; } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 6e7a6afc2..4db8edd25 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -2403,7 +2403,7 @@ public class LocalStore extends Store implements Serializable { html = HtmlConverter.convertEmoji2Img(container.html); } - String preview = calculateContentPreview(text); + String preview = Message.calculateContentPreview(text); try { ContentValues cv = new ContentValues(); @@ -2501,7 +2501,7 @@ public class LocalStore extends Store implements Serializable { String text = container.text; String html = HtmlConverter.convertEmoji2Img(container.html); - String preview = calculateContentPreview(text); + String preview = Message.calculateContentPreview(text); try { db.execSQL("UPDATE messages SET " @@ -3010,53 +3010,6 @@ public class LocalStore extends Store implements Serializable { } } - /* - * calculateContentPreview - * Takes a plain text message body as a string. - * Returns a message summary as a string suitable for showing in a message list - * - * A message summary should be about the first 160 characters - * of unique text written by the message sender - * Quoted text, "On $date" and so on will be stripped out. - * All newlines and whitespace will be compressed. - * - */ - public String calculateContentPreview(String text) { - if (text == null) { - return null; - } - - // Only look at the first 8k of a message when calculating - // the preview. This should avoid unnecessary - // memory usage on large messages - if (text.length() > 8192) { - text = text.substring(0, 8192); - } - - // try to remove lines of dashes in the preview - text = text.replaceAll("(?m)^----.*?$", ""); - // remove quoted text from the preview - text = text.replaceAll("(?m)^[#>].*$", ""); - // Remove a common quote header from the preview - text = text.replaceAll("(?m)^On .*wrote.?$", ""); - // Remove a more generic quote header from the preview - text = text.replaceAll("(?m)^.*\\w+:$", ""); - // Remove horizontal rules. - text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " "); - - // URLs in the preview should just be shown as "..." - They're not - // clickable and they usually overwhelm the preview - text = text.replaceAll("https?://\\S+", "..."); - // Don't show newlines in the preview - text = text.replaceAll("(\\r|\\n)+", " "); - // Collapse whitespace in the preview - text = text.replaceAll("\\s+", " "); - // Remove any whitespace at the beginning and end of the string. - text = text.trim(); - - return (text.length() <= 512) ? text : text.substring(0, 512); - } - @Override public boolean isInTopGroup() { return mInTopGroup; diff --git a/src/com/fsck/k9/service/NotificationActionService.java b/src/com/fsck/k9/service/NotificationActionService.java new file mode 100644 index 000000000..698ff53b2 --- /dev/null +++ b/src/com/fsck/k9/service/NotificationActionService.java @@ -0,0 +1,129 @@ +package com.fsck.k9.service; + +import java.util.ArrayList; + +import com.fsck.k9.Account; +import com.fsck.k9.K9; +import com.fsck.k9.Preferences; +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.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class NotificationActionService extends CoreService { + private final static String REPLY_ACTION = "com.fsck.k9.service.NotificationActionService.REPLY_ACTION"; + private final static String READ_ALL_ACTION = "com.fsck.k9.service.NotificationActionService.READ_ALL_ACTION"; + private final static String DELETE_ALL_ACTION = "com.fsck.k9.service.NotificationActionService.DELETE_ALL_ACTION"; + private final static String ACKNOWLEDGE_ACTION = "com.fsck.k9.service.NotificationActionService.ACKNOWLEDGE_ACTION"; + + private final static String EXTRA_ACCOUNT = "account"; + private final static String EXTRA_MESSAGE = "message"; + private final static String EXTRA_MESSAGE_LIST = "messages"; + + public static PendingIntent getReplyIntent(Context context, final Account account, final MessageReference ref) { + Intent i = new Intent(context, NotificationActionService.class); + i.putExtra(EXTRA_ACCOUNT, account.getUuid()); + i.putExtra(EXTRA_MESSAGE, ref); + 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 ArrayList refs) { + Intent i = new Intent(context, NotificationActionService.class); + i.putExtra(EXTRA_ACCOUNT, account.getUuid()); + i.putExtra(EXTRA_MESSAGE_LIST, refs); + 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) { + Intent i = new Intent(context, NotificationActionService.class); + i.putExtra(EXTRA_ACCOUNT, account.getUuid()); + 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 ArrayList refs) { + Intent i = new Intent(context, NotificationActionService.class); + i.putExtra(EXTRA_ACCOUNT, account.getUuid()); + i.putExtra(EXTRA_MESSAGE_LIST, refs); + i.setAction(DELETE_ALL_ACTION); + + return i; + } + + @Override + public int startService(Intent intent, int startId) { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "NotificationActionService started with startId = " + startId); + final Preferences preferences = Preferences.getPreferences(this); + final MessagingController controller = MessagingController.getInstance(getApplication()); + final Account account = preferences.getAccount(intent.getStringExtra(EXTRA_ACCOUNT)); + final String action = intent.getAction(); + + if (account != null) { + if (READ_ALL_ACTION.equals(action)) { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "NotificationActionService marking messages as read"); + + ArrayList refs = (ArrayList) + intent.getSerializableExtra(EXTRA_MESSAGE_LIST); + for (MessageReference ref : refs) { + controller.setFlag(account, ref.folderName, ref.uid, Flag.SEEN, true); + } + } else if (DELETE_ALL_ACTION.equals(action)) { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "NotificationActionService deleting messages"); + + ArrayList refs = (ArrayList) + intent.getSerializableExtra(EXTRA_MESSAGE_LIST); + ArrayList messages = new ArrayList(); + + for (MessageReference ref : refs) { + Message m = ref.restoreToLocalMessage(this); + if (m != null) { + messages.add(m); + } + } + + controller.deleteMessages(messages, null); + } else if (REPLY_ACTION.equals(action)) { + if (K9.DEBUG) + Log.i(K9.LOG_TAG, "NotificationActionService initiating reply"); + + MessageReference ref = (MessageReference) intent.getParcelableExtra(EXTRA_MESSAGE); + Message message = ref.restoreToLocalMessage(this); + if (message != null) { + Intent i = MessageCompose.getActionReplyIntent(this, account, message, false, null); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(i); + } else { + Log.i(K9.LOG_TAG, "Could not execute reply action."); + } + } 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 + } + + /* there's no point in keeping the notification after the user clicked on it */ + controller.notifyAccountCancel(this, account); + } else { + Log.w(K9.LOG_TAG, "Could not find account for notification action."); + } + + return START_NOT_STICKY; + } +}