1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-23 18:02:15 -05:00

Merge pull request #222 from maniac103/jb-notifications

Merge branch 'jb-notifications' of https://github.com/maniac103/k-9

* 'jb-notifications' of https://github.com/maniac103/k-9:
  Strip off signatures from preview.
  Reset list of unseen messages when the user clears the notification.
  In the new-style notification, directly go to message if new message count is 1.
  Cancel notification when viewing message by clicking on notification.
  Directly go to message when clicking on a single-message notification.
  Fix message overflow.
  Incorporate review comments.
  Some fixes to notification behaviour.
  Add German translation for message delete notification action.
  Add delete action to notification.
  Update summarized notification if a message is deleted or read remotely.
  Fixed deprecation warnings.
  Simplify code and beautify pre-jellybean notifications by using bold sender span for those.
  Respect "Show contact names" option when determining sender for notification.
  Fix marking messages as read from notification.
  Add German translation for new strings.
  Add actions to notifications.
  First stab at new notifications.
  Remove own Notification.Builder abstraction.
  Update android support library to latest version.
This commit is contained in:
Andrew Chen 2013-01-07 15:13:51 -08:00
commit 1ff0bb6289
25 changed files with 919 additions and 523 deletions

View File

@ -172,6 +172,14 @@
android:configChanges="locale"
>
</activity>
<activity
android:name=".activity.NotificationDeleteConfirmation"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:launchMode="singleTop"
android:taskAffinity=""
android:excludeFromRecents="true"
>
</activity>
<!-- XXX Note: this activity is hacked to ignore config changes,
since it doesn't currently handle them correctly in code. -->
<activity
@ -360,6 +368,11 @@ otherwise it would make K-9 start at the wrong time
android:enabled="true"
>
</service>
<service
android:name=".service.NotificationActionService"
android:enabled="true"
>
</service>
<service
android:name=".service.PushService"
android:enabled="true"

View File

@ -205,7 +205,13 @@ Um Fehler zu melden, neue Funktionen vorzuschlagen oder Fragen zu stellen, besuc
<string name="recreating_account">Konto \"<xliff:g id="account">%s</xliff:g>\" wiederherstellen</string>
<string name="notification_new_title">Neue Nachricht</string>
<string name="notification_new_messages_title"><xliff:g id="new_message_count">%d</xliff:g> neue Nachrichten</string>
<string name="notification_new_one_account_fmt"><xliff:g id="unread_message_count">%d</xliff:g> Ungelesen (<xliff:g id="account">%s</xliff:g>)</string> <!-- 279 Unread (someone@google.com) -->
<string name="notification_additional_messages">und <xliff:g id="additional_messages">%d</xliff:g> weitere (<xliff:g id="account">%s</xliff:g>)</string>
<string name="notification_action_reply">Antworten</string>
<string name="notification_action_read">Gelesen</string>
<string name="notification_action_delete">Löschen</string>
<string name="notification_bg_sync_ticker">Auf neue Nachrichten prüfen: <xliff:g id="account">%s</xliff:g>:<xliff:g id="folder">%s</xliff:g></string>
<string name="notification_bg_sync_title">Auf neue Nachrichten prüfen</string>
@ -320,12 +326,19 @@ Um Fehler zu melden, neue Funktionen vorzuschlagen oder Fragen zu stellen, besuc
<string name="global_settings_confirm_action_delete">Löschen (nur in Nachrichtenansicht)</string>
<string name="global_settings_confirm_action_delete_starred">Sternmarkierte Löschen (nur in Nachrichtenansicht)</string>
<string name="global_settings_confirm_action_spam">Spam</string>
<string name="global_settings_confirm_action_delete_notif">Löschen (aus Benachrichtigung)</string>
<string name="global_settings_notification_hide_subject_title">Betreff in Benachrichtigungen verbergen</string>
<string name="global_settings_notification_hide_subject_never">Niemals</string>
<string name="global_settings_notification_hide_subject_when_locked">Wenn der Bildschirm gesperrt ist</string>
<string name="global_settings_notification_hide_subject_always">Immer</string>
<string name="global_settings_notification_quick_delete_title">Löschen erlauben</string>
<string name="global_settings_notification_quick_delete_never">Nie</string>
<string name="global_settings_notification_quick_delete_when_single_msg">Für einzelne Nachricht</string>
<string name="global_settings_notification_quick_delete_always">Immer</string>
<string name="global_settings_notification_quick_delete_description">Der Benachrichtigung eine Schaltfläche zum Löschen der Nachrichten hinzufügen</string>
<string name="global_settings_batch_buttons">Gruppenoperationen-Schaltflächen</string>
<string name="global_settings_batch_buttons_summary">Zeige folgende Schaltflächen in der Nachrichtenliste an</string>
<string name="global_settings_mark_read">Als (un)gelesen markieren</string>
@ -945,6 +958,10 @@ Um Fehler zu melden, neue Funktionen vorzuschlagen oder Fragen zu stellen, besuc
<string name="dialog_confirm_delete_title">Löschen bestätigen</string>
<string name="dialog_confirm_delete_message">Wollen Sie diese Nachricht löschen?</string>
<plurals name="dialog_confirm_delete_message">
<item quantity="one">Wollen Sie diese Nachricht wirklich löschen?</item>
<item quantity="other">Wollen Sie wirklich <xliff:g id="message_count">%1$d</xliff:g> Nachrichten löschen?</item>
</plurals>
<string name="dialog_confirm_delete_confirm_button">Löschen</string>
<string name="dialog_confirm_delete_cancel_button">Nicht löschen</string>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TextAppearance.StatusBar.EventContent.Emphasized" parent="@android:style/TextAppearance.StatusBar.EventContent">
<item name="android:textColor">#cccccc</item>
</style>
</resources>

View File

@ -701,4 +701,16 @@
<item name="3">ALWAYS</item>
</string-array>
<string-array name="global_settings_notification_quick_delete_entries">
<item name="1">@string/global_settings_notification_quick_delete_never</item>
<item name="2">@string/global_settings_notification_quick_delete_when_single_msg</item>
<item name="3">@string/global_settings_notification_quick_delete_always</item>
</string-array>
<string-array name="global_settings_notification_quick_delete_values">
<item name="1">NEVER</item>
<item name="2">FOR_SINGLE_MSG</item>
<item name="3">ALWAYS</item>
</string-array>
</resources>

View File

@ -214,7 +214,13 @@ Please submit bug reports, contribute new features and ask questions at
<string name="recreating_account">Recreating account \"<xliff:g id="account">%s</xliff:g>\"</string>
<string name="notification_new_title">New mail</string>
<string name="notification_new_messages_title"><xliff:g id="new_message_count">%d</xliff:g> new messages</string>
<string name="notification_new_one_account_fmt"><xliff:g id="unread_message_count">%d</xliff:g> Unread (<xliff:g id="account">%s</xliff:g>)</string> <!-- 279 Unread (someone@google.com) -->
<string name="notification_additional_messages">+ <xliff:g id="additional_messages">%d</xliff:g> more on <xliff:g id="account">%s</xliff:g></string>
<string name="notification_action_reply">Reply</string>
<string name="notification_action_read">Read</string>
<string name="notification_action_delete">Delete</string>
<string name="notification_bg_sync_ticker">Checking mail: <xliff:g id="account">%s</xliff:g>:<xliff:g id="folder">%s</xliff:g></string>
<string name="notification_bg_sync_title">Checking mail</string>
@ -329,12 +335,19 @@ Please submit bug reports, contribute new features and ask questions at
<string name="global_settings_confirm_action_delete">Delete (in message view)</string>
<string name="global_settings_confirm_action_delete_starred">Delete Starred (in message view)</string>
<string name="global_settings_confirm_action_spam">Spam</string>
<string name="global_settings_confirm_action_delete_notif">Delete (from notification)</string>
<string name="global_settings_notification_hide_subject_title">Hide subject in notifications</string>
<string name="global_settings_notification_hide_subject_never">Never</string>
<string name="global_settings_notification_hide_subject_when_locked">When device is locked</string>
<string name="global_settings_notification_hide_subject_always">Always</string>
<string name="global_settings_notification_quick_delete_title">Show \'Delete\' button</string>
<string name="global_settings_notification_quick_delete_never">Never</string>
<string name="global_settings_notification_quick_delete_when_single_msg">For single message notification</string>
<string name="global_settings_notification_quick_delete_always">Always</string>
<string name="global_settings_notification_quick_delete_description">Show a button in the notification that allows quick message deletion</string>
<string name="global_settings_batch_buttons">Batch buttons</string>
<string name="global_settings_batch_buttons_summary">Configure message list batch buttons</string>
<string name="global_settings_mark_read">Mark read/unread</string>
@ -957,6 +970,10 @@ Please submit bug reports, contribute new features and ask questions at
<string name="dialog_confirm_delete_title">Confirm deletion</string>
<string name="dialog_confirm_delete_message">Do you want to delete this message?</string>
<plurals name="dialog_confirm_delete_message">
<item quantity="one">Do you really want to delete this message?</item>
<item quantity="other">Do you really want to delete <xliff:g id="message_count">%1$d</xliff:g> messages?</item>
</plurals>
<string name="dialog_confirm_delete_confirm_button">Yes</string>
<string name="dialog_confirm_delete_cancel_button">No</string>

View File

@ -18,5 +18,8 @@
<item name="android:textColor">@android:color/primary_text_light</item>
</style>
<style name="TextAppearance.StatusBar.EventContent.Emphasized" parent="@android:style/TextAppearance.StatusBar.EventContent">
<item name="android:textStyle">bold</item>
</style>
</resources>

View File

@ -113,4 +113,21 @@
<!-- Prior to Honeycomb we always use the light theme for dialogs -->
<style name="Theme.K9.Dialog.Dark" parent="Theme.K9.Dialog.Light" />
<style name="Theme.K9.Dialog.Translucent.Dark" parent="Theme.Sherlock.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
</style>
<style name="Theme.K9.Dialog.Translucent.Light" parent="Theme.Sherlock.Light.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
</style>
</resources>

View File

@ -294,6 +294,17 @@
android:dialogTitle="@string/quiet_time_ends"
android:title="@string/quiet_time_ends"
/>
<ListPreference
android:key="notification_quick_delete"
android:persistent="false"
android:title="@string/global_settings_notification_quick_delete_title"
android:entries="@array/global_settings_notification_quick_delete_entries"
android:entryValues="@array/global_settings_notification_quick_delete_values"
android:dialogTitle="@string/global_settings_notification_quick_delete_title"
android:summary="@string/global_settings_notification_quick_delete_description"
/>
</PreferenceScreen>
<PreferenceScreen

View File

@ -186,6 +186,7 @@ public class K9 extends Application {
private static boolean mConfirmDelete = false;
private static boolean mConfirmDeleteStarred = false;
private static boolean mConfirmSpam = false;
private static boolean mConfirmDeleteFromNotification = true;
private static NotificationHideSubject sNotificationHideSubject = NotificationHideSubject.NEVER;
@ -198,6 +199,17 @@ public class K9 extends Application {
NEVER
}
private static NotificationQuickDelete sNotificationQuickDelete = NotificationQuickDelete.NEVER;
/**
* Controls behaviour of delete button in notifications.
*/
public enum NotificationQuickDelete {
ALWAYS,
FOR_SINGLE_MSG,
NEVER
}
private static boolean mMessageListCheckboxes = false;
private static int mMessageListPreviewLines = 2;
@ -503,11 +515,13 @@ public class K9 extends Application {
editor.putBoolean("confirmDelete", mConfirmDelete);
editor.putBoolean("confirmDeleteStarred", mConfirmDeleteStarred);
editor.putBoolean("confirmSpam", mConfirmSpam);
editor.putBoolean("confirmDeleteFromNotification", mConfirmDeleteFromNotification);
editor.putString("sortTypeEnum", mSortType.name());
editor.putBoolean("sortAscending", mSortAscending.get(mSortType));
editor.putString("notificationHideSubject", sNotificationHideSubject.toString());
editor.putString("notificationQuickDelete", sNotificationQuickDelete.toString());
editor.putString("attachmentdefaultpath", mAttachmentDefaultPath);
editor.putBoolean("useBackgroundAsUnreadIndicator", sUseBackgroundAsUnreadIndicator);
@ -691,6 +705,7 @@ public class K9 extends Application {
mConfirmDelete = sprefs.getBoolean("confirmDelete", false);
mConfirmDeleteStarred = sprefs.getBoolean("confirmDeleteStarred", false);
mConfirmSpam = sprefs.getBoolean("confirmSpam", false);
mConfirmDeleteFromNotification = sprefs.getBoolean("confirmDeleteFromNotification", true);
try {
String value = sprefs.getString("sortTypeEnum", Account.DEFAULT_SORT_TYPE.name());
@ -712,6 +727,11 @@ public class K9 extends Application {
sNotificationHideSubject = NotificationHideSubject.valueOf(notificationHideSubject);
}
String notificationQuickDelete = sprefs.getString("notificationQuickDelete", null);
if (notificationQuickDelete != null) {
sNotificationQuickDelete = NotificationQuickDelete.valueOf(notificationQuickDelete);
}
mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString());
sUseBackgroundAsUnreadIndicator = sprefs.getBoolean("useBackgroundAsUnreadIndicator", true);
sThreadedViewEnabled = sprefs.getBoolean("threadedView", true);
@ -1107,6 +1127,14 @@ public class K9 extends Application {
mConfirmSpam = confirm;
}
public static boolean confirmDeleteFromNotification() {
return mConfirmDeleteFromNotification;
}
public static void setConfirmDeleteFromNotification(final boolean confirm) {
mConfirmDeleteFromNotification = confirm;
}
public static NotificationHideSubject getNotificationHideSubject() {
return sNotificationHideSubject;
}
@ -1115,6 +1143,14 @@ public class K9 extends Application {
sNotificationHideSubject = mode;
}
public static NotificationQuickDelete getNotificationQuickDeleteBehaviour() {
return sNotificationQuickDelete;
}
public static void setNotificationQuickDeleteBehaviour(final NotificationQuickDelete mode) {
sNotificationQuickDelete = mode;
}
public static boolean batchButtonsMarkRead() {
return mBatchButtonsMarkRead;
}

View File

@ -389,6 +389,32 @@ public class MessageCompose extends K9Activity implements OnClickListener {
context.startActivity(i);
}
/**
* Get intent for composing a new message as a reply to the given message. If replyAll is true
* the function is reply all instead of simply reply.
* @param context
* @param account
* @param message
* @param replyAll
* @param messageBody optional, for decrypted messages, null if it should be grabbed from the given message
*/
public static Intent getActionReplyIntent(
Context context,
Account account,
Message message,
boolean replyAll,
String messageBody) {
Intent i = new Intent(context, MessageCompose.class);
i.putExtra(EXTRA_MESSAGE_BODY, messageBody);
i.putExtra(EXTRA_MESSAGE_REFERENCE, message.makeMessageReference());
if (replyAll) {
i.setAction(ACTION_REPLY_ALL);
} else {
i.setAction(ACTION_REPLY);
}
return i;
}
/**
* Compose a new message as a reply to the given message. If replyAll is true the function
* is reply all instead of simply reply.
@ -404,15 +430,7 @@ public class MessageCompose extends K9Activity implements OnClickListener {
Message message,
boolean replyAll,
String messageBody) {
Intent i = new Intent(context, MessageCompose.class);
i.putExtra(EXTRA_MESSAGE_BODY, messageBody);
i.putExtra(EXTRA_MESSAGE_REFERENCE, message.makeMessageReference());
if (replyAll) {
i.setAction(ACTION_REPLY_ALL);
} else {
i.setAction(ACTION_REPLY);
}
context.startActivity(i);
context.startActivity(getActionReplyIntent(context, account, message, replyAll, messageBody));
}
/**

View File

@ -586,7 +586,8 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
Log.i(K9.LOG_TAG, "MessageList sending message " + messageReference);
MessageView.actionView(this, messageReference, messageRefs, getIntent().getExtras());
Intent i = MessageView.actionViewIntent(this, messageReference, messageRefs);
startActivity(i);
}
/*

View File

@ -1,11 +1,17 @@
package com.fsck.k9.activity;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.helper.Utility;
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 java.util.StringTokenizer;
@ -122,6 +128,31 @@ public class MessageReference implements Parcelable {
'}';
}
public Message restoreToLocalMessage(Context context) {
try {
Account account = Preferences.getPreferences(context).getAccount(accountUuid);
if (account != null) {
Folder folder = account.getLocalStore().getFolder(folderName);
if (folder != null) {
Message message = folder.getMessage(uid);
if (message != null) {
return message;
} else {
Log.d(K9.LOG_TAG, "Could not restore message, uid " + uid + " is unknown.");
}
} else {
Log.d(K9.LOG_TAG, "Could not restore message, folder " + folderName + " is unknown.");
}
} else {
Log.d(K9.LOG_TAG, "Could not restore message, account " + accountUuid + " is unknown.");
}
} catch (MessagingException e) {
Log.w(K9.LOG_TAG, "Could not retrieve message for reference.", e);
}
return null;
}
public static final Creator<MessageReference> CREATOR = new Creator<MessageReference>() {
@Override
public MessageReference createFromParcel(Parcel source) {

View File

@ -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<MessageReference> messReferences, Bundle messageListExtras) {
public static Intent actionViewIntent(Context context, MessageReference messRef,
ArrayList<MessageReference> 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;

View File

@ -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<MessageReference> mMessageRefs;
public static PendingIntent getIntent(Context context, final Account account, final ArrayList<MessageReference> 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<MessageReference>) 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);
}
}

View File

@ -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());

View File

@ -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<String, String> deletedUids = new ConcurrentHashMap<String, String>();
private static class NotificationData {
int unreadBeforeNotification;
LinkedList<Message> messages; // newest one first
LinkedList<MessageReference> 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<MessageReference>();
messages = new LinkedList<Message>() {
@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<MessageReference> getAllMessageRefs() {
ArrayList<MessageReference> refs = new ArrayList<MessageReference>();
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<Integer, NotificationData> notificationData = new ConcurrentHashMap<Integer, NotificationData>();
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 "<From>: <Subject>"
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<MessageReference> 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());
}
/**

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -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 "";
}

View File

@ -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;

View File

@ -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<MessageReference> 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<MessageReference> 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<MessageReference> refs = (ArrayList<MessageReference>)
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<MessageReference> refs = (ArrayList<MessageReference>)
intent.getSerializableExtra(EXTRA_MESSAGE_LIST);
ArrayList<Message> messages = new ArrayList<Message>();
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;
}
}